mafw.devtools.dependencies

Dependency management utilities for MAFw development tools.

This package provides business logic for compiling, freezing, unfreezing, comparing, and auditing project dependencies.

Functions

collect_compiled_dependency_versions(...)

Compile dependency lockfiles and collect the resolved versions they report.

commit_dependency_unfreeze(dry_run)

Commit the unfreezing of dependency upper bounds to main.

compare_packages(latest_packages, ...)

Compare two package dictionaries and classify differences.

compile_dependency_lockfile(python_version, ...)

Compile a dependency lockfile for a specific CPython version.

compile_python_selector(python_version)

Build the Python selector used by uv for dependency compilation.

compute_upper_bound(lower_bound)

Compute an upper bound for a dependency based on PEP 440 compatible-release philosophy.

ensure_mafw_project_root()

Ensure the current working directory is the MAFw project root.

freeze_dependencies(*, dry_run)

Freeze dependencies by adding computed upper bounds in pyproject.toml.

freeze_pyproject_toml(toml_text, *[, ...])

Freeze dependencies in a pyproject.toml payload by adding upper bounds.

freeze_requirement(requirement_text, *[, ...])

Add a computed upper bound to a requirement string, if eligible.

get_expected_lower_bounds()

Read pyproject.toml and extract dependencies with their expected lower bounds.

load_pylock_packages(pylock_path)

Parse a pylock TOML file into a dictionary keyed by lowercase package name.

load_pyproject_doc(toml_text)

Parse a pyproject.toml payload once and return the TOML document.

parse_python_version(version)

Parse a Python version string in major.minor form.

project_python_versions()

Read the supported CPython versions from tool.mafw.supported-python list in pyproject.toml.

project_python_versions_from_doc(doc)

Extract supported CPython versions from a parsed pyproject.toml document.

python_versions_between(min_python_ver, ...)

Build the inclusive list of Python versions between two bounds.

read_compiled_dependency_versions(pylock_text)

Read resolved dependency versions from a compiled lockfile payload.

render_comparison_json(results)

Render comparison results as a JSON document string.

render_comparison_markdown(results)

Render comparison results as a markdown document string.

render_comparison_rich(results, console)

Render comparison results to the terminal using Rich panels and tables.

resolve_output_format(explicit_format, ...)

Determine the effective output format based on CLI arguments.

run_pip_audit(req_file, output_file, ...)

Run pip-audit on a requirements file and save the output.

unfreeze_dependencies(...)

Unfreeze dependencies by removing computed upper bounds in pyproject.toml.

unfreeze_pyproject_toml(toml_text, *[, ...])

Unfreeze dependencies in a pyproject.toml payload by removing computed upper bounds.

unfreeze_requirement(requirement_text)

Remove a computed upper bound from a requirement string, if it matches the computed rule.

update_requirements_and_readme(*, dry_run)

Update the requirements RST files and README.rst.

verify_lowest_resolution(env_name, ...)

Verify that the specified environment has the expected lowest dependency versions.

Classes

PackageChange(package_name, change_type, ...)

A single dependency change between reference and latest lockfiles.

VersionComparisonResult(python_version, changes)

Comparison result for a single Python version.

class mafw.devtools.dependencies.PackageChange(package_name: str, change_type: Literal['ADDED', 'REMOVED', 'UPDATED'], reference_version: str | None, new_version: str | None)

Bases: object

A single dependency change between reference and latest lockfiles.

Each instance represents one package that was added, removed, or updated between the reference and the latest resolved dependency set.

Parameters:
  • package_name – Normalized (lowercase) package name.

  • change_type – Classification of the change (ADDED, REMOVED, UPDATED).

  • reference_version – Version in the reference file, None for ADDED entries.

  • new_version – Version in the latest file, None for REMOVED entries.

class mafw.devtools.dependencies.VersionComparisonResult(python_version: str, changes: list[~mafw.devtools.dependencies.PackageChange] = <factory>)

Bases: object

Comparison result for a single Python version.

Holds all detected dependency changes for a given Python interpreter version.

Parameters:
  • python_version – Python version string (e.g. "3.12").

  • changes – Sorted list of package changes for this version.

property has_changes: bool

Return True if there is at least one dependency change.

mafw.devtools.dependencies.collect_compiled_dependency_versions(python_versions: list[str]) dict[str, Version]

Compile dependency lockfiles and collect the resolved versions they report.

The command mirrors the dependency verification workflow by invoking uv pip compile for each supported CPython version. A temporary lockfile is generated for each version and removed afterwards.

Parameters:

python_versions (list[str]) – Supported Python versions to compile.

Returns:

Mapping of package name to highest resolved version across the compiled lockfiles.

Return type:

dict[str, Version]

mafw.devtools.dependencies.commit_dependency_unfreeze(dry_run: bool) None

Commit the unfreezing of dependency upper bounds to main.

Parameters:

dry_run (bool) – Whether command execution is disabled.

mafw.devtools.dependencies.compare_packages(latest_packages: dict[str, dict[str, Any]], reference_packages: dict[str, dict[str, Any]]) list[PackageChange]

Compare two package dictionaries and classify differences.

Both dictionaries are expected to be keyed by lowercase package name. The function classifies each difference into one of three categories:

  • ADDED: package present in latest but absent from reference.

  • REMOVED: package present in reference but absent from latest.

  • UPDATED: package present in both but with a different version or marker.

Parameters:
  • latest_packages (dict[str, dict[str, Any]]) – Packages from the freshly compiled lockfile, keyed by lowercase name.

  • reference_packages (dict[str, dict[str, Any]]) – Packages from the reference lockfile, keyed by lowercase name.

Returns:

List of PackageChange entries sorted alphabetically by package name.

Return type:

list[PackageChange]

mafw.devtools.dependencies.compile_dependency_lockfile(python_version: str, pylock_file: Path, extras: list[str], resolution: str | None = None, output_format: str | None = None, with_hashes: bool = False) None

Compile a dependency lockfile for a specific CPython version.

Parameters:
  • python_version (str) – CPython version used for the uv pip compile run.

  • pylock_file (Path) – Output path for the generated lockfile.

  • extras (list[str]) – Project extras requested during compilation.

  • resolution (str | None) – Optional UV resolution strategy (e.g. ‘lowest-direct’, ‘highest’).

  • output_format (str | None) – Optional output format (e.g. ‘requirements.txt’).

  • with_hashes (bool) – Whether to generate hashes for the compiled requirements.

mafw.devtools.dependencies.compile_python_selector(python_version: str) str

Build the Python selector used by uv for dependency compilation.

For Python 3.14 and newer, request the GIL-enabled variant explicitly so free-threaded interpreters do not leak into dependency resolution.

This distinction is still needed because of the psycopg

Parameters:

python_version (str) – Base Python version in major.minor form.

Returns:

Python selector string passed to uv.

Return type:

str

mafw.devtools.dependencies.compute_upper_bound(lower_bound: str) str

Compute an upper bound for a dependency based on PEP 440 compatible-release philosophy.

The rule implemented here is purposely conservative and mirrors the intent of compatible release clauses while remaining explicit:

  • For major-versioned releases (X.* with X > 0), freeze to <(X + 1).

  • For 0.* releases, freeze to <0.(minor + 1) (rolling compatibility during pre-1.0).

Parameters:

lower_bound (str) – Version string used as the starting point for the freeze rule.

Returns:

Upper-bound version string without operator.

Return type:

str

Raises:

DevtoolsError – If the version cannot be parsed.

mafw.devtools.dependencies.ensure_mafw_project_root() list[str]

Ensure the current working directory is the MAFw project root.

Returns:

Validated supported Python versions from tool.mafw.supported-python.

Return type:

list[str]

Raises:

DevtoolsError – If pyproject.toml is missing or does not identify MAFw.

mafw.devtools.dependencies.freeze_dependencies(*, dry_run: bool) str

Freeze dependencies by adding computed upper bounds in pyproject.toml.

Parameters:

dry_run (bool) – Whether command execution is disabled.

Returns:

Original pyproject.toml content captured before freeze.

Return type:

str

mafw.devtools.dependencies.freeze_pyproject_toml(toml_text: str, *, resolved_versions: dict[str, Version] | None = None, doc: TOMLDocument | None = None) tuple[str, list[str]]

Freeze dependencies in a pyproject.toml payload by adding upper bounds.

The TOML structure is preserved via tomlkit; only dependency strings may be normalized.

When resolved_versions is provided, the function uses those compiled versions as the basis for upper-bound computation. Otherwise the declared lower bounds are used as a fallback.

Parameters:
  • toml_text (str) – Raw TOML file content.

  • resolved_versions (dict[str, Version] | None) – Optional mapping of dependency names to resolved versions.

  • doc (tomlkit.TOMLDocument | None) – Optional pre-parsed TOML document to reuse when available.

Returns:

Updated TOML plus warnings.

Return type:

tuple[str, list[str]]

Raises:

DevtoolsError – If TOML parsing fails.

mafw.devtools.dependencies.freeze_requirement(requirement_text: str, *, resolved_version: str | None = None) tuple[str, list[str]]

Add a computed upper bound to a requirement string, if eligible.

The function is intentionally conservative:

  • URL-based requirements are skipped (cannot be version constrained).

  • Requirements already containing <, <=, ~=, == or === are skipped.

  • Requirements without a lower bound emit a warning and are left unchanged.

  • When a resolved version is provided, the upper bound is computed from that version instead of the declared lower bound.

Parameters:

requirement_text (str) – Raw PEP 508 requirement string.

Returns:

Updated requirement text plus warnings.

Return type:

tuple[str, list[str]]

Raises:

DevtoolsError – If parsing fails.

mafw.devtools.dependencies.get_expected_lower_bounds() dict[str, Requirement]

Read pyproject.toml and extract dependencies with their expected lower bounds.

Returns:

A dictionary mapping lowercase package names to their parsed Requirement objects.

Return type:

dict[str, Requirement]

Raises:

DevtoolsError – If the pyproject.toml cannot be parsed.

mafw.devtools.dependencies.load_pylock_packages(pylock_path: Path) dict[str, dict[str, Any]]

Parse a pylock TOML file into a dictionary keyed by lowercase package name.

Each value in the returned dictionary contains at minimum the name, version, and optionally marker fields from the original TOML entry.

Parameters:

pylock_path (Path) – Path to the pylock TOML file.

Returns:

Dictionary mapping lowercase package names to their package metadata.

Return type:

dict[str, dict[str, Any]]

Raises:
  • FileNotFoundError – If pylock_path does not exist.

  • tomllib.TOMLDecodeError – If the file is not valid TOML.

mafw.devtools.dependencies.load_pyproject_doc(toml_text: str) TOMLDocument

Parse a pyproject.toml payload once and return the TOML document.

Parameters:

toml_text (str) – Raw TOML payload.

Returns:

Parsed TOML document.

Return type:

tomlkit.TOMLDocument

Raises:

DevtoolsError – If the TOML payload cannot be parsed.

mafw.devtools.dependencies.parse_python_version(version: str) tuple[int, int]

Parse a Python version string in major.minor form.

Parameters:

version (str) – Python version string.

Returns:

Major/minor version tuple.

Return type:

tuple[int, int]

Raises:

DevtoolsError – If the version is invalid.

mafw.devtools.dependencies.project_python_versions() list[str]

Read the supported CPython versions from tool.mafw.supported-python list in pyproject.toml.

The field is expected to be a list of strings representing CPython major.minor versions. The helper validates each entry and returns a sorted, de-duplicated list so downstream callers have deterministic ordering.

Returns:

Sorted list of supported CPython versions.

Return type:

list[str]

Raises:

DevtoolsError – If pyproject.toml cannot be parsed or the field contains unsupported values.

mafw.devtools.dependencies.project_python_versions_from_doc(doc: TOMLDocument) list[str]

Extract supported CPython versions from a parsed pyproject.toml document.

Parameters:

doc (tomlkit.TOMLDocument) – Parsed TOML document.

Returns:

Sorted list of supported CPython versions.

Return type:

list[str]

Raises:

DevtoolsError – If the tool.mafw.supported-python field is invalid.

mafw.devtools.dependencies.python_versions_between(min_python_ver: str, max_python_ver: str, supported_versions: list[str]) list[str]

Build the inclusive list of Python versions between two bounds.

The returned versions must also be present in supported_versions.

Parameters:
  • min_python_ver (str) – Minimum Python version.

  • max_python_ver (str) – Maximum Python version.

  • supported_versions (list[str]) – List of supported Python versions from project metadata.

Returns:

Ordered list of version strings.

Return type:

list[str]

mafw.devtools.dependencies.read_compiled_dependency_versions(pylock_text: str) dict[str, Version]

Read resolved dependency versions from a compiled lockfile payload.

The returned mapping stores the highest resolved version seen for each dependency name, normalized to lowercase for stable lookups.

Parameters:

pylock_text (str) – Raw pylock.pyX.Y.toml content.

Returns:

Mapping of package name to highest resolved version.

Return type:

dict[str, Version]

Raises:

DevtoolsError – If the TOML payload cannot be parsed.

mafw.devtools.dependencies.render_comparison_json(results: list[VersionComparisonResult]) str

Render comparison results as a JSON document string.

Produces a JSON object with:

  • timestamp: ISO 8601 generation time.

  • python_versions: ordered list of all Python versions tested.

  • results: dictionary keyed by Python version, each value being a list of change entries (empty list when no differences exist for that version).

Each change entry contains package_name, change_type, reference_version (null for ADDED), and new_version (null for REMOVED).

Parameters:

results (list[VersionComparisonResult]) – Comparison results for each Python version.

Returns:

Formatted JSON string with 2-space indentation.

Return type:

str

mafw.devtools.dependencies.render_comparison_markdown(results: list[VersionComparisonResult]) str

Render comparison results as a markdown document string.

Produces a structured markdown report with a title, an ISO 8601 timestamp, and per-version sections containing ADDED, REMOVED, and UPDATED tables. Versions with no dependency changes are omitted from the output.

Parameters:

results (list[VersionComparisonResult]) – List of comparison results, one per Python version.

Returns:

Complete markdown document as a string.

Return type:

str

mafw.devtools.dependencies.render_comparison_rich(results: list[VersionComparisonResult], console: Console) None

Render comparison results to the terminal using Rich panels and tables.

Displays a title panel, followed by a section per Python version containing ADDED, REMOVED, and UPDATED subsections in that fixed order. Within each subsection, entries are sorted alphabetically by package name.

When no changes are detected across all Python versions, a single informational message is printed instead.

Parameters:
  • results (list[VersionComparisonResult]) – Comparison results for each Python version.

  • console (Console) – Rich Console instance for output.

mafw.devtools.dependencies.resolve_output_format(explicit_format: str | None, output_file: Path | None) str

Determine the effective output format based on CLI arguments.

The resolution follows a priority order that avoids ambiguity between an explicitly requested format and the file extension of the output path:

  1. If output_file has a recognized extension and explicit_format is set:

    • Match (e.g., json + .json): return the format.

    • Conflict (e.g., json + .md): raise click.UsageError.

  2. If output_file has a recognized extension and explicit_format is None: infer the format from the extension.

  3. If output_file has an unrecognized extension and explicit_format is set: return the explicit format.

  4. Otherwise: return "markdown" as the default.

Parameters:
  • explicit_format (str | None) – The value of --format if explicitly provided by the user, or None when omitted.

  • output_file (Path | None) – The value of --output-file, or None when omitted.

Returns:

The resolved format string ("markdown" or "json").

Return type:

str

Raises:

click.UsageError – When explicit_format conflicts with the format inferred from the file extension.

mafw.devtools.dependencies.run_pip_audit(req_file: Path, output_file: Path, output_format: str) CompletedProcess[Any]

Run pip-audit on a requirements file and save the output.

Parameters:
  • req_file (Path) – Requirements file to audit.

  • output_file (Path) – Path to the output report.

  • output_format (str) – Format of the report (e.g. ‘markdown’, ‘json’).

Returns:

Completed process produced by the command execution.

Return type:

subprocess.CompletedProcess[Any]

mafw.devtools.dependencies.unfreeze_dependencies(original_pyproject_toml: str, *, dry_run: bool) None

Unfreeze dependencies by removing computed upper bounds in pyproject.toml.

Parameters:

dry_run (bool) – Whether command execution is disabled.

mafw.devtools.dependencies.unfreeze_pyproject_toml(toml_text: str, *, baseline_toml_text: str | None = None, doc: TOMLDocument | None = None, baseline_doc: TOMLDocument | None = None) tuple[str, list[str]]

Unfreeze dependencies in a pyproject.toml payload by removing computed upper bounds.

Only upper bounds matching the computed rule are removed; existing manual constraints remain.

Parameters:
  • toml_text (str) – Raw TOML file content.

  • baseline_toml_text (str | None) – Optional original TOML text captured before freezing. When provided, unfreezing is computed against the baseline to avoid altering dependencies that were already frozen before release.

  • doc (tomlkit.TOMLDocument | None) – Optional pre-parsed TOML document to reuse when available.

  • baseline_doc (tomlkit.TOMLDocument | None) – Optional pre-parsed baseline TOML document to reuse when available.

Returns:

Updated TOML plus warnings.

Return type:

tuple[str, list[str]]

Raises:

DevtoolsError – If TOML parsing fails.

mafw.devtools.dependencies.unfreeze_requirement(requirement_text: str) tuple[str, list[str]]

Remove a computed upper bound from a requirement string, if it matches the computed rule.

Only the auto-generated upper bound <upper is removed; existing manual upper bounds are preserved.

Parameters:

requirement_text (str) – Raw PEP 508 requirement string.

Returns:

Updated requirement text plus warnings.

Return type:

tuple[str, list[str]]

Raises:

DevtoolsError – If parsing fails.

mafw.devtools.dependencies.update_requirements_and_readme(*, dry_run: bool) None

Update the requirements RST files and README.rst.

Parameters:

dry_run (bool) – Whether command execution is disabled.

mafw.devtools.dependencies.verify_lowest_resolution(env_name: str, python_version: str, expected_bounds: dict[str, Requirement]) None

Verify that the specified environment has the expected lowest dependency versions.

This function executes uv pip list in the given environment, parses the output, and compares installed versions against the expected lower bounds extracted from pyproject.toml.

Parameters:
  • env_name (str) – The name of the environment (e.g., ‘hatch-test’, ‘types’).

  • python_version (str) – The Python version string (e.g., ‘3.11’).

  • expected_bounds (dict[str, Requirement]) – Mapping of package names to their Requirement objects.

Raises:

DevtoolsError – If a required dependency is missing.

Modules

audit

Dependency auditing utilities for MAFw.

compare

Dependency comparison engine for MAFw lockfiles.

compile

Dependency compilation utilities for MAFw.

freeze

Dependency freezing and unfreezing utilities for MAFw.

verify

Dependency resolution verification utilities for MAFw.