Skip to content

Core Classes

Internal Click subclasses and configuration objects. Exposed publicly for advanced use cases such as subclassing or type-checking.

PyclifOption

Extends click.Option with is_global and env-var binding support.

pyclif.PyclifOption

Bases: StoreInMetaMixin, Option

Custom Click Option that can be marked as global for propagation.

Source code in src/pyclif/core/classes.py
class PyclifOption(StoreInMetaMixin, click_extra.Option):
    """Custom Click Option that can be marked as global for propagation."""

    def __init__(self, *args: Any, is_global: bool = False, **kwargs: Any) -> None:
        """Initialize the option.

        Args:
            *args: Positional arguments for click.Option.
            is_global: If True, this option will be propagated to subcommands.
            **kwargs: Keyword arguments for click.Option.
        """
        self.is_global = is_global
        super().__init__(*args, **kwargs)

__init__(*args, is_global=False, **kwargs)

Initialize the option.

Parameters:

Name Type Description Default
*args Any

Positional arguments for click.Option.

()
is_global bool

If True, this option will be propagated to subcommands.

False
**kwargs Any

Keyword arguments for click.Option.

{}
Source code in src/pyclif/core/classes.py
def __init__(self, *args: Any, is_global: bool = False, **kwargs: Any) -> None:
    """Initialize the option.

    Args:
        *args: Positional arguments for click.Option.
        is_global: If True, this option will be propagated to subcommands.
        **kwargs: Keyword arguments for click.Option.
    """
    self.is_global = is_global
    super().__init__(*args, **kwargs)

PyclifGroup

Base Click group class used by app_group and group. Composes HandleResponseMixin + GlobalOptionsMixin.

pyclif.PyclifGroup = PyclifExtraGroup module-attribute


CustomConfigOption

Extends click-extra's config option with multi-location Linux config file search (/etc/<app>/, ~/.config/<app>/, etc.).

pyclif.CustomConfigOption

Bases: StoreInMetaMixin, ConfigOption

Custom ConfigOption to add support for /etc/ on Linux systems.

This class extends the default click-extra ConfigOption to include system-wide configuration directories following Linux conventions while maintaining cross-platform compatibility.

Source code in src/pyclif/core/classes.py
class CustomConfigOption(StoreInMetaMixin, ConfigOption):
    """Custom ConfigOption to add support for /etc/<cli_name> on Linux systems.

    This class extends the default click-extra ConfigOption to include system-wide
    configuration directories following Linux conventions while maintaining
    cross-platform compatibility.
    """

    def __init__(self, *args: Any, is_global: bool = False, **kwargs: Any) -> None:
        """Initialize the custom config option.

        Args:
            *args: Positional arguments.
            is_global: If True, this option will be propagated to subcommands.
            **kwargs: Keyword arguments.
        """
        self.is_global = is_global
        super().__init__(*args, **kwargs)

    def get_default(self, ctx, call=True):
        """Override get_default to fix rich-click help rendering.

        rich-click fetches the default with call=False during help generation,
        which returns the raw bound method. We intercept this and force
        evaluation to display the actual path cleanly.
        """
        default = super().get_default(ctx, call=call)
        # If call=False returned a callable (our bound method), evaluate it anyway
        if not call and callable(default):
            # noinspection PyBroadException
            try:
                return default()
            except Exception:
                pass
        return default

    def default_pattern(self) -> str:
        """Generate the default configuration search pattern.

        Creates search patterns for configuration files, prioritizing Linux system
        directories when running on Linux platforms. Falls back to standard
        user configuration directories on other platforms.

        Patterns are joined with "|" so that wcmatch's SPLIT flag (active on
        click-extra's ConfigOption) treats each path as a separate glob target.

        Returns:
            The pipe-separated glob pattern covering all config locations.

        Raises:
            RuntimeError: If no click, context is available to determine CLI name.
        """
        all_patterns = self._get_all_config_patterns()

        if not all_patterns:
            return self._get_fallback_pattern()

        return "|".join(all_patterns)

    def _get_extension_pattern(self) -> str:
        """Build a file extension pattern from supported formats.

        Creates a glob-compatible extension pattern from the configured
        formats, using either single extension or brace notation for
        multiple extensions.

        Returns:
            str: Extension pattern for glob matching (e.g., 'toml' or '{toml,yaml,json}').
        """
        # Build file extension pattern from supported formats
        extensions = []

        if self.file_format_patterns:
            patterns = unique(flatten(self.file_format_patterns.values()))
            # Keep only generic extensions (e.g., "*.toml" -> "toml")
            # and ignore specific file patterns like "pyproject.toml"
            extensions.extend(pat[2:] for pat in patterns if pat.startswith("*."))

        extensions = unique([ext for ext in extensions if ext])

        if not extensions:
            return "*"

        # Create an extension pattern for glob matching
        if len(extensions) == 1:
            return extensions[0]
        else:
            # Use brace notation for multiple extension matching
            return f"{{{','.join(extensions)}}}"

    def _get_all_config_patterns(self) -> list[str]:
        """Get all configuration search patterns in priority order.

        Constructs file search patterns for different configuration locations
        based on the current platform and supported file formats.

        Returns:
            List of glob patterns for configuration file search, ordered by
            priority (system-wide first, then user-specific).

        Raises:
            RuntimeError: If no click, context is available to determine CLI name.
        """
        patterns = []

        # Get extension pattern
        ext_pattern = self._get_extension_pattern()

        # Get CLI name from click context
        try:
            ctx = get_current_context()
            cli_name = ctx.find_root().info_name
        except RuntimeError:
            # No click context available - this can happen during testing
            # or when called outside a click command context
            return []

        if not cli_name:
            return []

        # 1. Add a system-wide configuration path (Linux only)
        if is_linux():
            system_config_dir = Path(f"/etc/{cli_name}")
            system_pattern = str(system_config_dir / f"*.{ext_pattern}")
            patterns.append(system_pattern)

        # 2. Add a user configuration directory (all platforms)
        try:
            roaming = getattr(self, "roaming", False)
            force_posix = getattr(self, "force_posix", False)
            app_dir = Path(
                get_app_dir(cli_name, roaming=roaming, force_posix=force_posix)
            ).resolve()
            user_pattern = str(app_dir / f"*.{ext_pattern}")
            patterns.append(user_pattern)
        except (OSError, ValueError, TypeError) as e:
            # Handle specific exceptions that can be raised by get_app_dir or Path operations:
            # - OSError: File system-related errors (permissions, path issues, etc.)
            # - ValueError: Invalid arguments passed to get_app_dir or Path
            # - TypeError: Type-related issues with arguments
            import logging

            # noinspection PyUnresolvedReferences
            logger = logging.getLogger(__name__)
            logger.debug(f"Failed to get user config directory: {e}")
            # Continue without user config pattern - system config may still work

        return patterns

    def _get_fallback_pattern(self) -> str:
        """Get a fallback configuration pattern when normal detection fails.

        Provides a basic configuration search pattern for cases where
        the click context is not available or CLI name cannot be determined.

        Returns:
            str: A basic configuration file search pattern.
        """
        # Basic fallback pattern in the current directory
        ext_pattern = self._get_extension_pattern()
        return f"*.{ext_pattern}"

__init__(*args, is_global=False, **kwargs)

Initialize the custom config option.

Parameters:

Name Type Description Default
*args Any

Positional arguments.

()
is_global bool

If True, this option will be propagated to subcommands.

False
**kwargs Any

Keyword arguments.

{}
Source code in src/pyclif/core/classes.py
def __init__(self, *args: Any, is_global: bool = False, **kwargs: Any) -> None:
    """Initialize the custom config option.

    Args:
        *args: Positional arguments.
        is_global: If True, this option will be propagated to subcommands.
        **kwargs: Keyword arguments.
    """
    self.is_global = is_global
    super().__init__(*args, **kwargs)

get_default(ctx, call=True)

Override get_default to fix rich-click help rendering.

rich-click fetches the default with call=False during help generation, which returns the raw bound method. We intercept this and force evaluation to display the actual path cleanly.

Source code in src/pyclif/core/classes.py
def get_default(self, ctx, call=True):
    """Override get_default to fix rich-click help rendering.

    rich-click fetches the default with call=False during help generation,
    which returns the raw bound method. We intercept this and force
    evaluation to display the actual path cleanly.
    """
    default = super().get_default(ctx, call=call)
    # If call=False returned a callable (our bound method), evaluate it anyway
    if not call and callable(default):
        # noinspection PyBroadException
        try:
            return default()
        except Exception:
            pass
    return default

default_pattern()

Generate the default configuration search pattern.

Creates search patterns for configuration files, prioritizing Linux system directories when running on Linux platforms. Falls back to standard user configuration directories on other platforms.

Patterns are joined with "|" so that wcmatch's SPLIT flag (active on click-extra's ConfigOption) treats each path as a separate glob target.

Returns:

Type Description
str

The pipe-separated glob pattern covering all config locations.

Raises:

Type Description
RuntimeError

If no click, context is available to determine CLI name.

Source code in src/pyclif/core/classes.py
def default_pattern(self) -> str:
    """Generate the default configuration search pattern.

    Creates search patterns for configuration files, prioritizing Linux system
    directories when running on Linux platforms. Falls back to standard
    user configuration directories on other platforms.

    Patterns are joined with "|" so that wcmatch's SPLIT flag (active on
    click-extra's ConfigOption) treats each path as a separate glob target.

    Returns:
        The pipe-separated glob pattern covering all config locations.

    Raises:
        RuntimeError: If no click, context is available to determine CLI name.
    """
    all_patterns = self._get_all_config_patterns()

    if not all_patterns:
        return self._get_fallback_pattern()

    return "|".join(all_patterns)

PyclifTimerOption

Internal option class powering timer=True on @app_group. Subclasses click-extra's TimerOption to integrate with pyclif's output format: suppresses the text echo in json/yaml mode and injects timing fields into Response.data via returns_response.

pyclif.core.classes.PyclifTimerOption

Bases: TimerOption

TimerOption integrated with pyclif output format.

Skips the text echo in json/yaml mode — timing data is injected directly into the Response by returns_response instead.

Source code in src/pyclif/core/classes.py
class PyclifTimerOption(TimerOption):
    """TimerOption integrated with pyclif output format.

    Skips the text echo in json/yaml mode — timing data is injected directly
    into the Response by returns_response instead.
    """

    # noinspection PyAttributeOutsideInit
    def register_timer_on_close(
        self, ctx: click_extra.Context, param: click_extra.Parameter, value: bool
    ) -> None:
        """Register the timer and store the context for deferred format check."""
        if not value:
            return
        self.start_time = perf_counter()
        self._close_ctx = ctx
        ctx.meta["click_extra.start_time"] = self.start_time
        ctx.call_on_close(self.print_timer)

    def print_timer(self) -> None:
        """Print elapsed time unless the output format is json or yaml.

        Output format is read at close time so that eager option processing
        order does not matter — meta is fully populated by then.
        """
        output_format = self._close_ctx.find_root().meta.get("pyclif.output_format", "table")
        if output_format in ("json", "yaml"):
            return
        elapsed = perf_counter() - self.start_time
        click_extra.echo(f"Execution time: {elapsed:.3f} seconds.")

register_timer_on_close(ctx, param, value)

Register the timer and store the context for deferred format check.

Source code in src/pyclif/core/classes.py
def register_timer_on_close(
    self, ctx: click_extra.Context, param: click_extra.Parameter, value: bool
) -> None:
    """Register the timer and store the context for deferred format check."""
    if not value:
        return
    self.start_time = perf_counter()
    self._close_ctx = ctx
    ctx.meta["click_extra.start_time"] = self.start_time
    ctx.call_on_close(self.print_timer)

print_timer()

Print elapsed time unless the output format is json or yaml.

Output format is read at close time so that eager option processing order does not matter — meta is fully populated by then.

Source code in src/pyclif/core/classes.py
def print_timer(self) -> None:
    """Print elapsed time unless the output format is json or yaml.

    Output format is read at close time so that eager option processing
    order does not matter — meta is fully populated by then.
    """
    output_format = self._close_ctx.find_root().meta.get("pyclif.output_format", "table")
    if output_format in ("json", "yaml"):
        return
    elapsed = perf_counter() - self.start_time
    click_extra.echo(f"Execution time: {elapsed:.3f} seconds.")