Skip to content

typerdrive Base Modules

typerdrive.config

Provide controls for the configuration of typerdrive itself.

Note

This is distinct from the functionality provided in the settings module. The settings feature is for configuring the app built with typerdrive. Usually, settings reflect behavior specific to the app. The config feature is for configuring typerdrive internal behavior. The config features should never be exposed to end-users or used to manage behavior of the app built on top of typerdrive.

Attributes

Classes

TyperdriveConfig

Bases: BaseModel

Define the configurable attributes of typerdrive.

Source code in src/typerdrive/config.py
class TyperdriveConfig(BaseModel):
    """
    Define the configurable attributes of `typerdrive`.
    """

    app_name: str = sys.argv[0].split("/")[-1]
    """
    The name of the app built on `typerdrive`. Use this setting if you want to override the default behavior of using
    `sys.argv[0]`.
    """

    log_file_rotation: FileRotationSpec = "1 week"
    """
    Define the how often logs should be rotated. See the
    [`loguru` docs](https://loguru.readthedocs.io/en/stable/overview.html#easier-file-logging-with-rotation-retention-compression)
    for more information.
    """

    log_file_retention: FileRetentionSpec = "1 month"
    """
    Define the how soon logs should be deleted. See the
    [`loguru` docs](https://loguru.readthedocs.io/en/stable/overview.html#easier-file-logging-with-rotation-retention-compression)
    for more information.
    """

    log_file_compression: FileCompressionSpec = "tar.gz"
    """
    Define whether rotated logs should be compressed. See the
    [`loguru` docs](https://loguru.readthedocs.io/en/stable/overview.html#easier-file-logging-with-rotation-retention-compression)
    for more information.
    """

    log_file_name: str = "app.log"
    """ The name of the file where logs will be written. This file will be created in the `log_dir` directory. """

    console_width: int | None = None
    """ Set the width of the console for `rich.console`. Will override automatic resizing behavior. """

    console_ascii_only: bool = False
    """ If set, only use ascii characters in the console. """

    @computed_field  # type: ignore[prop-decorator]
    @property
    def log_dir(self) -> Path:
        """
        Retrieve the directory where logs will be stored.
        """
        return Path.home() / ".local/share" / self.app_name / "logs"

    @computed_field  # type: ignore[prop-decorator]
    @property
    def settings_path(self) -> Path:
        """
        Retrieve the file where settings will be stored.
        """
        return Path.home() / ".local/share" / self.app_name / "settings.json"

    @computed_field  # type: ignore[prop-decorator]
    @property
    def cache_dir(self) -> Path:
        """
        Retrieve the directory where the cache data will be stored.
        """
        return Path.home() / ".cache" / self.app_name
Attributes
app_name class-attribute instance-attribute
app_name: str = split('/')[-1]

The name of the app built on typerdrive. Use this setting if you want to override the default behavior of using sys.argv[0].

cache_dir property
cache_dir: Path

Retrieve the directory where the cache data will be stored.

console_ascii_only class-attribute instance-attribute
console_ascii_only: bool = False

If set, only use ascii characters in the console.

console_width class-attribute instance-attribute
console_width: int | None = None

Set the width of the console for rich.console. Will override automatic resizing behavior.

log_dir property
log_dir: Path

Retrieve the directory where logs will be stored.

log_file_compression class-attribute instance-attribute
log_file_compression: FileCompressionSpec = 'tar.gz'

Define whether rotated logs should be compressed. See the loguru docs for more information.

log_file_name class-attribute instance-attribute
log_file_name: str = 'app.log'

The name of the file where logs will be written. This file will be created in the log_dir directory.

log_file_retention class-attribute instance-attribute
log_file_retention: FileRetentionSpec = '1 month'

Define the how soon logs should be deleted. See the loguru docs for more information.

log_file_rotation class-attribute instance-attribute
log_file_rotation: FileRotationSpec = '1 week'

Define the how often logs should be rotated. See the loguru docs for more information.

settings_path property
settings_path: Path

Retrieve the file where settings will be stored.

Functions

get_typerdrive_config

get_typerdrive_config() -> TyperdriveConfig

Retrieve the current TyperdriveConfig.

Source code in src/typerdrive/config.py
def get_typerdrive_config() -> TyperdriveConfig:
    """
    Retrieve the current `TyperdriveConfig`.
    """
    return _typerdrive_config.model_copy()

set_typerdrive_config

set_typerdrive_config(**kwargs: Any)

Update the TyperdriveConfig with the provided key/values.

Source code in src/typerdrive/config.py
def set_typerdrive_config(**kwargs: Any):
    """
    Update the `TyperdriveConfig` with the provided key/values.
    """
    global _typerdrive_config
    combined_config = {**_typerdrive_config.model_dump(), **kwargs}

    # TyperdriveConfigError?
    with TyperdriveError.handle_errors("Invalid typerdrive config provided"):
        _typerdrive_config = TyperdriveConfig(**combined_config)

typerdrive.constants

Provide some constants for the project.

Classes

ExitCode

Bases: IntEnum

Maps exit codes for the application.

Source code in src/typerdrive/constants.py
class ExitCode(IntEnum):
    """
    Maps exit codes for the application.
    """

    SUCCESS = 0
    GENERAL_ERROR = 1
    CANNOT_EXECUTE = 126
    INTERNAL_ERROR = 128
Attributes
CANNOT_EXECUTE class-attribute instance-attribute
CANNOT_EXECUTE = 126
GENERAL_ERROR class-attribute instance-attribute
GENERAL_ERROR = 1
INTERNAL_ERROR class-attribute instance-attribute
INTERNAL_ERROR = 128
SUCCESS class-attribute instance-attribute
SUCCESS = 0

Validation

Bases: Flag

Defines when validation of settings should happen in the @attach_settings context manager.

Source code in src/typerdrive/constants.py
class Validation(Flag):
    """
    Defines when validation of settings should happen in the `@attach_settings` context manager.
    """

    BEFORE = auto()
    AFTER = auto()
    BOTH = BEFORE | AFTER
    NEVER = 0
Attributes
AFTER class-attribute instance-attribute
AFTER = auto()
BEFORE class-attribute instance-attribute
BEFORE = auto()
BOTH class-attribute instance-attribute
BOTH = BEFORE | AFTER
NEVER class-attribute instance-attribute
NEVER = 0

typerdrive.context

Provide tools for working with the TyperContext and the TyperdriveContext.

Classes

TyperdriveContext dataclass

Define the typerdrive context that is attached to the Typer.Context as obj.

Source code in src/typerdrive/context.py
@dataclass
class TyperdriveContext:
    """
    Define the `typerdrive` context that is attached to the `Typer.Context` as `obj`.
    """
    settings_manager: SettingsManager | None = None
    cache_manager: CacheManager | None = None
    client_manager: ClientManager | None = None
    logging_manager: LoggingManager | None = None
Attributes
cache_manager class-attribute instance-attribute
cache_manager: CacheManager | None = None
client_manager class-attribute instance-attribute
client_manager: ClientManager | None = None
logging_manager class-attribute instance-attribute
logging_manager: LoggingManager | None = None
settings_manager class-attribute instance-attribute
settings_manager: SettingsManager | None = None
Functions
__init__
__init__(
    settings_manager: SettingsManager | None = None,
    cache_manager: CacheManager | None = None,
    client_manager: ClientManager | None = None,
    logging_manager: LoggingManager | None = None,
) -> None

Functions

from_context

from_context(ctx: Context, name: str) -> TyperdriveManager

Retrieve a TyperdriveManager from the TyperdriveContext at the given name.

Source code in src/typerdrive/context.py
def from_context(ctx: typer.Context, name: str) -> TyperdriveManager:
    """
    Retrieve a `TyperdriveManager` from the `TyperdriveContext` at the given `name`.
    """
    user_context = get_user_context(ctx)
    return ContextError.enforce_defined(getattr(user_context, name), f"{name} is not bound to context")

get_user_context

get_user_context(ctx: Context)

Retrieve the user context from the typer.Context.

If a user context has not been established yet, it will be initialized.

Read the 'click' docs to learn more.

Source code in src/typerdrive/context.py
def get_user_context(ctx: typer.Context):
    """
    Retrieve the user context from the `typer.Context`.

    If a user context has not been established yet, it will be initialized.

    Read the ['click' docs](https://click.palletsprojects.com/en/stable/complex/#contexts) to learn more.
    """
    if not ctx.obj:
        ctx.obj = TyperdriveContext()
    return ctx.obj

to_context

to_context(
    ctx: Context, name: str, val: TyperdriveManager
) -> None

Attach a TyperdriveManager object to the TyperdriveContext at the given name.

Source code in src/typerdrive/context.py
def to_context(ctx: typer.Context, name: str, val: TyperdriveManager) -> None:
    """
    Attach a `TyperdriveManager` object to the `TyperdriveContext` at the given `name`.
    """
    user_context = get_user_context(ctx)
    field_type = TyperdriveContext.__dataclass_fields__[name].type

    if get_origin(field_type) is UnionType:
        defined_types = [t for t in get_args(field_type) if t is not NoneType]
        require_condition(
            len(defined_types) == 1,
            "PANIC! TyperdriveContext fields must only have one type or None.",
            raise_exc_class=RuntimeError,
        )
        field_type = defined_types[0]

    # TODO: Get the type hinting on the next line right.
    ContextError.ensure_type(val, field_type, "Value is not of type any of the union types")  # type: ignore[arg-type]

    setattr(user_context, name, val)

typerdrive.dirs

Provide methods for working with directories.

Classes

DirInfo dataclass

Describes directory info needed for the render_directory() function.

Source code in src/typerdrive/dirs.py
@dataclass
class DirInfo:
    """
    Describes directory info needed for the `render_directory()` function.
    """

    tree: Tree
    file_count: int
    total_size: int
Attributes
file_count instance-attribute
file_count: int
total_size instance-attribute
total_size: int
tree instance-attribute
tree: Tree
Functions
__init__
__init__(
    tree: Tree, file_count: int, total_size: int
) -> None

Functions

clear_directory

clear_directory(path: Path) -> int

Delete all files and directories in the directory at the given path.

If the target path is not a directory, an exception will be raised. If the target path does not exist, an exception will be raised.

Source code in src/typerdrive/dirs.py
def clear_directory(path: Path) -> int:
    """
    Delete all files and directories in the directory at the given path.

    If the target path is not a directory, an exception will be raised.
    If the target path does not exist, an exception will be raised.
    """
    TyperdriveError.require_condition(path.exists(), f"Target {path=} does not exist")
    TyperdriveError.require_condition(path.is_dir(), f"Target {path=} is not a directory")
    with TyperdriveError.handle_errors(f"Failed to clear directory at {path=}"):
        return _clear_dir(path)

is_child

is_child(path: Path, parent: Path) -> bool

Return true if the given path is a child of the given parent.

Source code in src/typerdrive/dirs.py
def is_child(path: Path, parent: Path) -> bool:
    """
    Return true if the given path is a child of the given parent.
    """
    root_path = Path(path.parts[0])
    temp_path = path
    while temp_path != root_path:
        if temp_path == parent:
            return True
        temp_path = temp_path.parent
    return False

render_directory

render_directory(
    path: Path, is_root: bool = True
) -> DirInfo

Render a visualization of the directory at the given path using rich.tree.

This is a recursive function. If calling this function, you should call it with is_root=True.

Source code in src/typerdrive/dirs.py
def render_directory(path: Path, is_root: bool = True) -> DirInfo:
    """
    Render a visualization of the directory at the given path using `rich.tree`.

    This is a recursive function. If calling this function, you should call it with `is_root=True`.
    """
    root_label: str
    if is_root:
        root_label = str(path)
        color = "[bold yellow]"
    else:
        root_label = escape(path.name)
        color = "[bold blue]"

    dir_info = DirInfo(
        tree=Tree(f"{color}📂 {root_label}"),
        file_count=0,
        total_size=0,
    )

    child_paths = sorted(
        path.iterdir(),
        key=lambda p: (
            p.is_file(),
            p.name.lower(),
        ),
    )
    for child_path in child_paths:
        if child_path.is_dir():
            child_info = render_directory(child_path, is_root=False)
            dir_info.tree
            dir_info.tree.children.append(child_info.tree)
            dir_info.file_count += child_info.file_count
            dir_info.total_size += child_info.total_size
        else:
            file_size = child_path.stat().st_size
            icon = Text("📄 ")
            label = Text(escape(child_path.name), "green")
            info = Text(f" ({humanize.naturalsize(file_size)})", "blue")
            dir_info.tree.add(icon + label + info)
            dir_info.file_count += 1
            dir_info.total_size += file_size
    return dir_info

show_directory

show_directory(path: Path, subject: str | None = None)

Print the visualization of a target directory retrieved with render_directory() using terminal_message.

Source code in src/typerdrive/dirs.py
def show_directory(path: Path, subject: str | None = None):
    """
    Print the visualization of a target directory retrieved with `render_directory()` using `terminal_message`.
    """
    dir_info = render_directory(path)
    human_size = humanize.naturalsize(dir_info.total_size)
    terminal_message(
        dir_info.tree,
        subject=subject or f"Showing {path}",
        footer=f"Storing {human_size} in {dir_info.file_count} files",
    )

typerdrive.env

Provide utility functions for working with the environment.

Functions

tweak_env

tweak_env(**kwargs: str)

Temporarily alter environment variables for the lifetime of the context manager.

All items in kwargs will be set in the environment. If there was exiting value in the given key, it will be restored after the context manager exits. If there was no existing value in the given key, it will have no value after the context manager exits.

Source code in src/typerdrive/env.py
@contextmanager
def tweak_env(**kwargs: str):
    """
    Temporarily alter environment variables for the lifetime of the context manager.

    All items in `kwargs` will be set in the environment.
    If there was exiting value in the given key, it will be restored after the context manager exits.
    If there was no existing value in the given key, it will have no value after the context manager exits.
    """
    old_vals: dict[str, str] = {}
    del_vals: list[str] = []
    for key, val in kwargs.items():
        old_val = os.environ.get(key, None)
        if old_val:
            old_vals[key] = old_val
        else:
            del_vals.append(key)
        os.environ[key] = val

    yield

    for key, val in old_vals.items():
        os.environ[key] = val
    for key in del_vals:
        del os.environ[key]

typerdrive.exceptions

Provide exception types for typerdrive.

All exception types derived from TyperdriveError will, by default, be handled by the @handle_errors decorator.

Classes

BuildCommandError

Bases: TyperdriveError

Indicates that there was a problem building a command with typer-repyt.

Source code in src/typerdrive/exceptions.py
class BuildCommandError(TyperdriveError):
    """
    Indicates that there was a problem building a command with `typer-repyt`.
    """
    exit_code: ExitCode = ExitCode.INTERNAL_ERROR
Attributes
exit_code class-attribute instance-attribute
exit_code: ExitCode = INTERNAL_ERROR

ContextError

Bases: TyperdriveError

Indicates that there was a problem interacting with the Typer.Context.

Source code in src/typerdrive/exceptions.py
class ContextError(TyperdriveError):
    """
    Indicates that there was a problem interacting with the `Typer.Context`.
    """
    exit_code: ExitCode = ExitCode.INTERNAL_ERROR
Attributes
exit_code class-attribute instance-attribute
exit_code: ExitCode = INTERNAL_ERROR

DisplayError

Bases: TyperdriveError

Indicates that there was a problem displaying data to the user.

Source code in src/typerdrive/exceptions.py
class DisplayError(TyperdriveError):
    """
    Indicates that there was a problem displaying data to the user.
    """
    exit_code: ExitCode = ExitCode.INTERNAL_ERROR
Attributes
exit_code class-attribute instance-attribute
exit_code: ExitCode = INTERNAL_ERROR

TyperdriveError

Bases: Buzz

Base exception class for all typerdrive errors.

Derives from the buzz.Buzz base class. See the py-buzz docs to learn more.

Source code in src/typerdrive/exceptions.py
class TyperdriveError(Buzz):
    """
    Base exception class for all `typerdrive` errors.

    Derives from the `buzz.Buzz` base class. See the
    [`py-buzz` docs](https://dusktreader.github.io/py-buzz/features/#the-buzz-base-class) to learn more.
    """

    subject: str | None = None
    """ The subject to show on user-facing messages when this error is handled by `@handle_errors()` """

    footer: str | None = None
    """ The footer to show on user-facing messages when this error is handled by `@handle_errors()` """

    exit_code: ExitCode = ExitCode.GENERAL_ERROR
    """ The exit code that should be used after handling the error and terminating the app """

    details: Any | None = None
    """ Any additional details that should be included in the error as well """

    def __init__(
        self,
        *args: Any,
        subject: str | None = None,
        footer: str | None = None,
        details: Any | None = None,
        exit_code: ExitCode | None = None,
        **kwargs: Any,
    ):
        super().__init__(*args, **kwargs)
        if subject:
            self.subject = subject
        if footer:
            self.footer = footer
        if details:
            self.details = details
        if exit_code:
            self.exit_code = exit_code
Attributes
details class-attribute instance-attribute
details: Any | None = None

Any additional details that should be included in the error as well

exit_code class-attribute instance-attribute
exit_code: ExitCode = GENERAL_ERROR

The exit code that should be used after handling the error and terminating the app

footer class-attribute instance-attribute
footer: str | None = None

The footer to show on user-facing messages when this error is handled by @handle_errors()

subject class-attribute instance-attribute
subject: str | None = None

The subject to show on user-facing messages when this error is handled by @handle_errors()

Functions
__init__
__init__(
    *args: Any,
    subject: str | None = None,
    footer: str | None = None,
    details: Any | None = None,
    exit_code: ExitCode | None = None,
    **kwargs: Any,
)
Source code in src/typerdrive/exceptions.py
def __init__(
    self,
    *args: Any,
    subject: str | None = None,
    footer: str | None = None,
    details: Any | None = None,
    exit_code: ExitCode | None = None,
    **kwargs: Any,
):
    super().__init__(*args, **kwargs)
    if subject:
        self.subject = subject
    if footer:
        self.footer = footer
    if details:
        self.details = details
    if exit_code:
        self.exit_code = exit_code

typerdrive.format

Provide methods for formatted output for the user.

Classes

Functions

pretty_field_info

pretty_field_info(field_info: FieldInfo) -> str

Format a pydantic FieldInfo.

Source code in src/typerdrive/format.py
def pretty_field_info(field_info: FieldInfo) -> str:
    """
    Format a pydantic FieldInfo.
    """
    annotation = DisplayError.enforce_defined(field_info.annotation, f"The field info annotation for {field_info} was not defined!")
    if issubclass(annotation, BaseModel):
        return pretty_model(annotation)
    else:
        return annotation.__name__

pretty_model

pretty_model(model_class: type[BaseModel]) -> str

Format a pydantic model class.

Source code in src/typerdrive/format.py
def pretty_model(model_class: type[BaseModel]) -> str:
    """
    Format a pydantic model class.
    """
    parts: list[str] = [
        f"{fn}={pretty_field_info(fi)}" for (fn, fi) in model_class.model_fields.items()
    ]
    return f"{model_class.__name__}({' '.join(parts)})"

simple_message

simple_message(
    message: str,
    indent: bool = False,
    markdown: bool = False,
    error: bool = False,
    to_clipboard: bool = False,
)

Show a simple, non-decorated message to the user.

Parameters:

Name Type Description Default
message str

The message to show to the user; May not be a rich.RenderableType

required
indent bool

If True, indent all lines of the message by 2 spaces

False
markdown bool

If True, render the text with rich.markdown

False
error bool

If True, print the output to stderr instead of stdout

False
to_clipboard bool

If True, attempt to copy the output to the user's clipboard

False
Source code in src/typerdrive/format.py
def simple_message(
    message: str,
    indent: bool = False,
    markdown: bool = False,
    error: bool = False,
    to_clipboard: bool = False,
):
    """
    Show a simple, non-decorated message to the user.

    Parameters:
        message:      The message to show to the user; May not be a `rich.RenderableType`
        indent:       If `True`, indent all lines of the message by 2 spaces
        markdown:     If `True`, render the text with `rich.markdown`
        error:        If `True`, print the output to stderr instead of stdout
        to_clipboard: If `True`, attempt to copy the output to the user's clipboard
    """
    text: str = snick.dedent(message)

    if to_clipboard:
        _to_clipboard(text)

    if indent:
        text = snick.indent(text, prefix="  ")

    content: str | Markdown = text

    if markdown:
        content = Markdown(text)

    console = Console(stderr=error)
    console.print()
    console.print(content)
    console.print()

strip_rich_style

strip_rich_style(text: str | Text) -> str

Strip all rich styling (color, emphasis, etc.) from the text.

Source code in src/typerdrive/format.py
def strip_rich_style(text: str | Text) -> str:
    """
    Strip all rich styling (color, emphasis, etc.) from the text.
    """
    if isinstance(text, str):
        text = Text.from_markup(text)
    return text.plain

terminal_message

terminal_message(
    message: RenderableType,
    subject: str | None = None,
    subject_align: Literal[
        "left", "right", "center"
    ] = "left",
    subject_color: str = "green",
    footer: str | None = None,
    footer_align: Literal[
        "left", "right", "center"
    ] = "left",
    indent: bool = True,
    markdown: bool = False,
    error: bool = False,
    to_clipboard: bool = False,
)

Show a formatted message to the user in a rich Panel.

Parameters:

Name Type Description Default
message RenderableType

The message to show to the user. May be a string or any other rich.RenderableType

required
subject str | None

An optional heading for the panel to show to the user

None
subject_align Literal['left', 'right', 'center']

The alignment for the subject

'left'
subject_color str

The color to render the subject in

'green'
footer str | None

An optional footer to show to the user. If to_clipboard results in a successful copy, this will include a message indicating that the message was copied to the clipboard.

None
footer_align Literal['left', 'right', 'center']

The alignment for the footer

'left'
indent bool

If True, indent all lines of the message by 2 spaces

True
markdown bool

If True, render the text with rich.markdown

False
error bool

If True, print the output to stderr instead of stdout

False
to_clipboard bool

If True, attempt to copy the output to the user's clipboard. The text copied to the clipboard will not include the rich.panel decoration including the header and footer.

False
Source code in src/typerdrive/format.py
def terminal_message(
    message: RenderableType,
    subject: str | None = None,
    subject_align: Literal["left", "right", "center"] = "left",
    subject_color: str = "green",
    footer: str | None = None,
    footer_align: Literal["left", "right", "center"] = "left",
    indent: bool = True,
    markdown: bool = False,
    error: bool = False,
    to_clipboard: bool = False,
):
    """
    Show a formatted message to the user in a `rich` Panel.

    Parameters:
        message:       The message to show to the user. May be a string or any other `rich.RenderableType`
        subject:       An optional heading for the panel to show to the user
        subject_align: The alignment for the `subject`
        subject_color: The color to render the `subject` in
        footer:        An optional footer to show to the user. If `to_clipboard` results in a successful copy, this will
                       include a message indicating that the message was copied to the clipboard.
        footer_align:  The alignment for the `footer`
        indent:        If `True`, indent all lines of the message by 2 spaces
        markdown:      If `True`, render the text with `rich.markdown`
        error:         If `True`, print the output to stderr instead of stdout
        to_clipboard:  If `True`, attempt to copy the output to the user's clipboard. The text copied to the clipboard
                       will not include the `rich.panel` decoration including the header and footer.
    """
    if to_clipboard:
        if _to_clipboard(message):
            if not footer:
                footer = "Copied to clipboard!"
            else:
                footer = f"{footer} -- Copied to clipboard!"

    panel_kwargs: dict[str, Any] = dict(padding=1, title_align=subject_align, subtitle_align=footer_align)

    if subject is not None:
        panel_kwargs["title"] = f"[{subject_color}]{subject}"

    if footer is not None:
        panel_kwargs["subtitle"] = f"[dim italic]{footer}[/dim italic]"

    if isinstance(message, str):
        message = snick.dedent(message)
        if indent:
            message = snick.indent(message, prefix="  ")
        if markdown:
            message = Markdown(message)

    config = get_typerdrive_config()
    console_kwargs: dict[str, Any] = dict(stderr=error)

    if config.console_width:
        console_kwargs["width"] = config.console_width

    if config.console_ascii_only:
        #console_kwargs["force_terminal"] = False
        panel_kwargs["box"] = box.ASCII

    console = Console(
        stderr=error,

    )
    console.print()
    console.print(Panel(message, **panel_kwargs))
    console.print()

typerdrive.handle_errors

Provide an error handler that can be attached to a command through a decorator.

Classes

Functions

handle_errors

handle_errors(
    base_message: str,
    *,
    handle_exc_class: type[Exception]
    | tuple[type[Exception], ...] = TyperdriveError,
    ignore_exc_class: type[Exception]
    | tuple[type[Exception], ...]
    | None = None,
    do_except: Callable[[DoExceptParams], None]
    | None = None,
    do_else: Callable[[], None] | None = None,
    do_finally: Callable[[], None] | None = None,
    unwrap_message: bool = True,
    debug: bool = False,
) -> Callable[
    [WrappedFunction[P, T]], WrappedFunction[P, T]
]

Handle errors raised by the decorated command function and show a user-friendly message in the terminal.

The behavior of this function is very similar to the py-buzz handle_errors() context manager. See the py-buzz docs for more context.

Parameters:

Name Type Description Default
base_message str

The "base" message to be used for the error. This will be the "subject" of the message shown to the user when an error is handled.

required
handle_exc_class type[Exception] | tuple[type[Exception], ...]

The exception class that will be handled. Exception types that inherit from this will also be handled as well.

TyperdriveError
ignore_exc_class type[Exception] | tuple[type[Exception], ...] | None

An exception class that will not be handled even if it inherits from the handle_exc_class.

None
do_except Callable[[DoExceptParams], None] | None

A function that will be called if an exception is handled. This is most useful for logging the details of the error.

None
do_else Callable[[], None] | None

A function that will be called if no exceptions were handled.

None
do_finally Callable[[], None] | None

A function that will always be called, even if an exception was handled.

None
unwrap_message bool

If true, "unwrap" the message so that newline characters are removed.

True
debug bool

If true, the message shown in the terminal_message will include the string representation of the handled error. Not as suitable for end-users, and should only be used for debugging.

False
Source code in src/typerdrive/handle_errors.py
def handle_errors(
    base_message: str,
    *,
    handle_exc_class: type[Exception] | tuple[type[Exception], ...] = TyperdriveError,
    ignore_exc_class: type[Exception] | tuple[type[Exception], ...] | None = None,
    do_except: Callable[[DoExceptParams], None] | None = None,
    do_else: Callable[[], None] | None = None,
    do_finally: Callable[[], None] | None = None,
    unwrap_message: bool = True,
    debug: bool = False,
) -> Callable[[WrappedFunction[P, T]], WrappedFunction[P, T]]:
    """
    Handle errors raised by the decorated command function and show a user-friendly message in the terminal.

    The behavior of this function is _very_ similar to the `py-buzz` `handle_errors()` context manager. See the
    [py-buzz docs](https://dusktreader.github.io/py-buzz/features/#exception-handling-context-manager) for more context.

    Parameters:
        base_message:     The "base" message to be used for the error. This will be the "subject" of the message shown
                          to the user when an error is handled.
        handle_exc_class: The exception class that will be handled. Exception types that inherit from this will also be
                          handled as well.
        ignore_exc_class: An exception class that will _not_ be handled even if it inherits from the `handle_exc_class`.
        do_except:        A function that will be called if an exception is handled. This is most useful for logging
                          the details of the error.
        do_else:          A function that will be called if no exceptions were handled.
        do_finally:       A function that will always be called, even if an exception was handled.
        unwrap_message:   If true, "unwrap" the message so that newline characters are removed.
        debug:            If true, the message shown in the `terminal_message` will include the string representation of
                          the handled error. Not as suitable for end-users, and should only be used for debugging.
    """

    class _DefaultIgnoreException(Exception):
        """
        Define a special exception class to use for the default ignore behavior.

        Basically, this exception type can't be extracted from this method (easily), and thus could never actually
        be raised in any other context. This is only created here to preserve the `try/except/except/else/finally`
        structure.
        """

    ignore_exc_class = _DefaultIgnoreException if ignore_exc_class is None else ignore_exc_class

    def _decorate(func: WrappedFunction[P, T]) -> WrappedFunction[P, T]:
        @wraps(func)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
            return_value: T | None = None
            try:
                return_value = func(*args, **kwargs)
            except ignore_exc_class:
                raise
            except handle_exc_class as err:
                try:
                    final_message = reformat_exception(base_message, err)
                except Exception as msg_err:
                    raise RuntimeError(f"Failed while formatting message: {repr(msg_err)}")

                trace = get_traceback()

                if do_except:
                    do_except(
                        DoExceptParams(
                            err=err,
                            base_message=base_message,
                            final_message=final_message,
                            trace=trace,
                        )
                    )

                subject: str | None = base_message
                footer: str | None = None
                message: str

                exit_code: int = ExitCode.GENERAL_ERROR
                if isinstance(err, TyperdriveError):
                    if err.subject:
                        subject = err.subject
                    if err.footer:
                        footer = err.footer
                    if debug:
                        message = err.message
                    else:
                        message = err.base_message or err.message
                    if unwrap_message:
                        message = snick.unwrap(message)
                    exit_code = err.exit_code
                else:
                    message = str(err)

                terminal_message(
                    message,
                    subject=f"[red]{subject}[/red]",
                    footer=footer,
                    error=True,
                )

                raise typer.Exit(code=exit_code)

            else:
                if do_else:
                    do_else()
                return return_value

            finally:
                if do_finally:
                    do_finally()

        return wrapper

    return _decorate

typerdrive.types

Provide some useful type definitions for the project.

The File* types align with loguru file sink rotation and retention spec. Refer to the Loguru documentation for more information:

Attributes

FileCompressionSpec module-attribute

FileCompressionSpec = str | Callable[[str], None]

FileRetentionSpec module-attribute

FileRetentionSpec = (
    str | int | timedelta | Callable[[list[str]], None]
)

FileRotationSpec module-attribute

FileRotationSpec = (
    str
    | int
    | timedelta
    | time
    | Callable[[str, TextIO], bool]
)

typerdrive.version

Provide methods for getting the project version.

Attributes

__version__ module-attribute

__version__ = get_version()

Functions

get_version

get_version() -> str

Try to get version from metadata, then fallback to pyproject.toml, then fallback to "unknown".

Source code in src/typerdrive/version.py
def get_version() -> str:
    """
    Try to get version from metadata, then fallback to `pyproject.toml`, then fallback to "unknown".
    """
    try:
        return get_version_from_metadata()
    except metadata.PackageNotFoundError:
        try:
            return get_version_from_pyproject()
        except (FileNotFoundError, KeyError):
            return "unknown"

get_version_from_metadata

get_version_from_metadata() -> str

Retrieve version from package metadata.

Source code in src/typerdrive/version.py
def get_version_from_metadata() -> str:
    """
    Retrieve version from package metadata.
    """
    return metadata.version(__package__ or __name__)

get_version_from_pyproject

get_version_from_pyproject() -> str

Retrieve version from the pyproject.toml file.

Source code in src/typerdrive/version.py
def get_version_from_pyproject() -> str:
    """
    Retrieve version from the `pyproject.toml` file.
    """
    with open("pyproject.toml", "rb") as file:
        return tomllib.load(file)["project"]["version"]