mafw.devtools.toolchain.base

Abstract base class and data models for the toolchain management system.

This module defines the ToolChainTool interface that every managed development tool must implement, the ProjectTool intermediate base class for tools managed via pyproject.toml, the HostTool intermediate base class for pipx-managed host tools, and supporting data models used by the CLI commands to aggregate and report results.

Module Attributes

LOWER_BOUND_RE

Regex to extract the version portion from a >= specifier.

Category

Tool category discriminator.

Classes

HostTool()

Intermediate base for tools installed system-wide via pipx.

Issue(tool_name, description)

A configuration inconsistency detected during verification.

ProjectTool([project_root])

Intermediate base for tools managed via pyproject.toml.

ToolChainTool()

Abstract interface that every managed development tool must implement.

ToolCheckResult(tool_name, category, ...[, ...])

Result of checking a single tool's version status.

ToolUpdateResult(tool_name, updated[, ...])

Result of processing a single tool during the update command.

class mafw.devtools.toolchain.base.HostTool[source]

Bases: ToolChainTool, ABC

Intermediate base for tools installed system-wide via pipx.

Concrete subclasses need only define pipx_package_name as a class-level attribute. This base class provides working default implementations of all abstract methods from ToolChainTool.

Subclasses may override individual methods for tool-specific behaviour (e.g. UvTool overrides update() and verify() to additionally synchronize pyproject.toml).

_ensure_pipx_available() str[source]

Locate the pipx executable on the system PATH.

Returns:

Absolute path to the pipx executable.

Raises:

DevtoolsError – If pipx is not found on the system PATH.

_get_pipx_version() Version | None[source]

Parse the installed version from pipx list --json output.

Queries pipx for its list of managed packages and extracts the version string for pipx_package_name.

Returns:

The installed Version, or None if the package is not present in pipx.

Raises:

DevtoolsError – If the pipx command fails or the JSON output has an unexpected structure.

bootstrap() bool[source]

Install the tool via pipx install.

Returns:

True if installation was performed successfully.

Raises:

DevtoolsError – If pipx is not available or installation fails.

detect_current_version() Version | None[source]

Detect the installed version from pipx metadata.

Runs pipx list --json and parses the JSON output to extract the version of this package.

Returns:

The installed Version, or None if the package is not installed via pipx.

Raises:

DevtoolsError – If pipx is not available or the JSON output cannot be parsed.

detect_latest_version() Version[source]

Query PyPI for the latest stable release.

Returns:

The latest available Version.

Raises:

DevtoolsError – If the PyPI query fails.

update() bool[source]

Upgrade the tool via pipx upgrade.

Compares the installed version before and after the upgrade to determine whether a change occurred.

Returns:

True if the version changed, False if the tool was already at the latest version.

Raises:

DevtoolsError – If pipx is not available or the upgrade fails.

verify() list[Issue][source]

Verify configuration consistency.

Host tools that do not modify repository files have no cross-file consistency checks. Override in subclasses that need verification (e.g. uv checks its pyproject.toml specifier).

Returns:

An empty list (no issues to report).

property category: Literal['host', 'project']

Tool category: 'host' (pipx-managed).

property name: str

Human-readable tool identifier.

Defaults to pipx_package_name. Override in subclasses that need a different display name.

abstract property pipx_package_name: str

The package name as known to pipx (e.g. 'hatch', 'uv').

class mafw.devtools.toolchain.base.Issue(tool_name: str, description: str)[source]

Bases: object

A configuration inconsistency detected during verification.

Each instance captures a single problem found by a tool’s ToolChainTool.verify() method.

Parameters:
  • tool_name – Name of the tool that reported the issue.

  • description – Human-readable explanation of the inconsistency.

class mafw.devtools.toolchain.base.ProjectTool(project_root: Path | None = None)[source]

Bases: ToolChainTool, ABC

Intermediate base for tools managed via pyproject.toml.

Concrete subclasses need only define package_name and section_path as class-level attributes. The base class provides working default implementations of all abstract methods from ToolChainTool.

Subclasses override only what differs (e.g. custom update logic for ruff, post_update hooks for pytest/mypy/sphinx/pre-commit).

Parameters:

project_root (Path | None) – Path to the project root directory containing pyproject.toml. Defaults to the current working directory.

static _parse_resolution_failure(stderr: str) str | None[source]

Extract the conflicting package name from resolver error output.

Parses hatch/uv resolver error messages to identify which package caused a dependency resolution failure. Returns the package name if found, or None if the error cannot be parsed.

Handles common patterns such as:

  • "Because only <package><=X.Y is available ..."

  • "Because <package>>=X.Y depends on ..."

  • "package <package> has no version that satisfies ..."

  • "Could not find a version that satisfies the requirement <package>"

  • "No matching distribution found for <package>"

Package names may contain letters, digits, hyphens, underscores, and dots (e.g. sphinxcontrib-external-links, ruamel.yaml).

Parameters:

stderr – The standard error output from a failed resolver command.

Returns:

The conflicting package name, or None if not parseable.

Return type:

str | None

_get_highest_python_version() str[source]

Return the highest supported Python version (cached after first call).

On the first invocation, reads tool.mafw.supported-python from pyproject.toml and caches the result. Subsequent calls return the cached value without re-reading the file, avoiding repeated TOML parsing across multiple tools.

Returns:

The highest supported Python version as a dotted string (e.g. "3.14").

Return type:

str

Raises:

DevtoolsError – If tool.mafw.supported-python is missing or empty in pyproject.toml.

_get_modifier() PyprojectModifier[source]

Create a PyprojectModifier for this project’s pyproject.toml.

Returns:

A fresh modifier instance (not yet loaded).

Return type:

PyprojectModifier

_recreate_environment(env_name: str, py_version: str | None = None) None[source]

Validate dependency resolution via a temporary clone environment.

Instead of removing and recreating the real environment (which may be the active environment running devtools), this method creates a temporary clone environment in pyproject.toml, attempts to create it via hatch env create, and removes it afterwards. If the clone creation fails due to a dependency resolution conflict, the pyproject.toml is reverted (removing the clone declaration) and a descriptive error is raised.

The temporary environment inherits the template of the target environment, so it resolves the same dependency graph. Its name is _toolchain_verify to avoid collisions with real environments.

Parameters:
  • env_name – The hatch environment base name to validate (e.g. 'dev', 'types', 'hatch-test').

  • py_version – Python version for matrix slot targeting (e.g. '3.14'). When provided, the clone is created for that specific Python version.

Raises:

DevtoolsError – If the temporary environment creation fails (indicating a resolution conflict with the updated dependencies).

_revert() None[source]

Restore pyproject.toml to the content saved before update().

If no content was saved (update was not called or already reverted), this method is a no-op.

bootstrap() bool[source]

Project tools cannot be bootstrapped independently.

They are installed automatically when syncing the appropriate Hatch environment.

Raises:

DevtoolsError – Always, since project tools do not support standalone bootstrapping.

detect_current_version() Version | None[source]

Parse the >= lower bound from pyproject.toml.

Reads the dependency specifier for package_name from the section at section_path and extracts the version from its >= constraint using LOWER_BOUND_RE.

Returns:

The currently configured lower-bound version, or None if the dependency cannot be found or lacks a >= specifier.

Return type:

Version | None

detect_latest_version() Version[source]

Query PyPI for the latest stable release.

Delegates to fetch_latest_version() using package_name.

Returns:

The latest non-pre-release version on PyPI.

Return type:

Version

Raises:

DevtoolsError – If the PyPI query fails.

update() bool[source]

Update the lower bound in pyproject.toml.

Compares the current lower bound with the latest PyPI version and updates the specifier if necessary. Saves the original file content before modification so that subclasses with post_update() hooks can revert on failure via _revert().

Returns:

True if the version was changed, False if already up to date.

Raises:

DevtoolsError – If the update process fails.

verify() list[Issue][source]

Return an empty list — default for single-location tools.

Most project tools are referenced in a single pyproject.toml section, so there is no cross-file consistency to check. Tools with multiple locations (e.g. ruff) override this method.

Returns:

An empty list of issues.

Return type:

list[Issue]

property category: Literal['host', 'project']

Tool category.

Returns:

"project" — this tool is managed via pyproject.toml.

Return type:

Category

abstract property env_name: str

Hatch environment base name where this tool lives.

This is the environment that gets recreated (via a temporary clone) during post-update validation. Examples: 'dev', 'types', 'hatch-test'.

property name: str

Human-readable tool identifier.

Defaults to package_name. Override in subclasses that need a different display name.

abstract property package_name: str

PyPI package name (e.g. 'pytest', 'git-cliff').

abstract property section_path: str

Dot-separated TOML path to the dependency array.

Examples: 'project.optional-dependencies.dev', 'tool.hatch.envs.types.extra-dependencies'.

class mafw.devtools.toolchain.base.ToolChainTool[source]

Bases: ABC

Abstract interface that every managed development tool must implement.

Concrete subclasses represent individual tools (e.g. ruff, pytest, hatch) and provide the logic for bootstrapping, version detection, updating, and configuration verification.

abstractmethod bootstrap() bool[source]

Install the tool if not present.

Returns:

True if installation was performed.

Raises:

Exception – If the tool is already installed or installation fails.

abstractmethod detect_current_version() Version | None[source]

Return the currently installed or configured version.

Returns:

The current Version, or None if the tool is not installed.

abstractmethod detect_latest_version() Version[source]

Return the latest available version from PyPI or the canonical source.

Returns:

The latest Version.

Raises:

Exception – If the version cannot be determined (e.g. network error).

post_update() None[source]

Hook executed after a successful update.

Override in subclasses to run validation (e.g. test suites, type checks) after the tool’s version has been bumped. The default implementation is a no-op.

abstractmethod update() bool[source]

Update the tool to its latest version.

Returns:

True if a configuration change was made, False if already up to date.

Raises:

Exception – If the update process fails.

abstractmethod verify() list[Issue][source]

Check configuration consistency and return any detected issues.

Returns:

A list of Issue objects describing inconsistencies, or an empty list when the tool’s configuration is consistent.

abstract property category: Literal['host', 'project']

Tool category: 'host' (pipx-managed) or 'project' (pyproject-managed).

abstract property name: str

Human-readable tool identifier (lowercase, e.g. 'ruff').

class mafw.devtools.toolchain.base.ToolCheckResult(tool_name: str, category: str, current_version: Version | None, latest_version: Version | None, current_error: str | None = None, latest_error: str | None = None)[source]

Bases: object

Result of checking a single tool’s version status.

Captures the current and latest versions along with any errors encountered during detection.

Parameters:
  • tool_name – Name of the tool that was checked.

  • category – The tool’s category ("host" or "project").

  • current_version – Currently installed/configured version, or None if detection failed.

  • latest_version – Latest available version from the canonical source, or None if detection failed.

  • current_error – Error message if current version detection failed.

  • latest_error – Error message if latest version detection failed.

property in_sync: bool

Tool is in sync when both versions are known and equal.

Returns False if either version detection encountered an error or if the two versions differ.

class mafw.devtools.toolchain.base.ToolUpdateResult(tool_name: str, updated: bool, error: str | None = None, hook_error: str | None = None, revert_failed: bool = False)[source]

Bases: object

Result of processing a single tool during the update command.

Aggregates the outcome of calling ToolChainTool.update() and the optional ToolChainTool.post_update() hook.

Parameters:
  • tool_name – Name of the tool that was processed.

  • updated – Whether the tool’s configuration was changed.

  • error – Error message if the update method raised an exception.

  • hook_error – Error message if the post_update hook failed.

  • revert_failed – Whether the rollback attempt after a hook failure also failed.

mafw.devtools.toolchain.base.Category

Tool category discriminator.

  • "host": tools installed system-wide via pipx (e.g. hatch, uv).

  • "project": tools whose version is managed through pyproject.toml.

alias of Literal[‘host’, ‘project’]

mafw.devtools.toolchain.base.LOWER_BOUND_RE: Final[Pattern[str]] = re.compile('>=\\s*([A-Za-z0-9.*!+]+)')

Regex to extract the version portion from a >= specifier.

Shared across all project tools that parse pyproject.toml dependency specifiers. Defined here to avoid duplication in each tool module.