Skip to content

Core Classes

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

PycliferOption

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

pyclifer.PycliferOption

Bases: StoreInMetaMixin, Option

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

Source code in src/pyclifer/core/classes.py
class PycliferOption(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/pyclifer/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)

PycliferGroup

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

pyclifer.PycliferGroup = PycliferExtraGroup module-attribute


CustomConfigOption

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

pyclifer.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/pyclifer/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 not call and callable(default):
            # noinspection PyBroadException
            try:
                return default()
            except Exception:  # rich-click may pass an unresolvable callable — swallow silently
                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}').
        """
        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 "*"

        if len(extensions) == 1:
            return extensions[0]
        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 = []

        ext_pattern = self._get_extension_pattern()

        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 []

        if is_linux():
            system_config_dir = Path(f"/etc/{cli_name}")
            system_pattern = str(system_config_dir / f"*.{ext_pattern}")
            patterns.append(system_pattern)

        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
            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.
        """
        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/pyclifer/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/pyclifer/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 not call and callable(default):
        # noinspection PyBroadException
        try:
            return default()
        except Exception:  # rich-click may pass an unresolvable callable — swallow silently
            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/pyclifer/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)

PycliferTimerOption

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

pyclifer.core.classes.PycliferTimerOption

Bases: TimerOption

TimerOption integrated with pyclifer 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/pyclifer/core/classes.py
class PycliferTimerOption(TimerOption):
    """TimerOption integrated with pyclifer 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 init_timer(
        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("pyclifer.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.")

init_timer(ctx, param, value)

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

Source code in src/pyclifer/core/classes.py
def init_timer(
    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/pyclifer/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("pyclifer.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.")