mafw.devtools.toolchain.pyproject_modifier

Pyproject.toml modifier for toolchain dependency management.

This module provides the PyprojectModifier helper that reads, modifies, and writes dependency specifiers in pyproject.toml while preserving all existing comments, formatting, and key ordering via tomlkit.

Multiple concrete ToolChainTool implementations delegate their update logic to this helper so that version-specifier manipulation is centralized and tested once.

Supported section paths:

  • project.dependencies

  • project.optional-dependencies.<group>

  • tool.hatch.envs.<env>.extra-dependencies

Classes

PyprojectModifier(path)

Read, modify, and write dependency specifiers in pyproject.toml.

class mafw.devtools.toolchain.pyproject_modifier.PyprojectModifier(path: Path)[source]

Bases: object

Read, modify, and write dependency specifiers in pyproject.toml.

The modifier preserves all TOML comments, formatting, and key ordering by using tomlkit for parsing and serialization.

Usage:

modifier = PyprojectModifier(Path('pyproject.toml'))
modifier.read()
specifier = modifier.get_dependency_specifier(
    'pytest', 'project.optional-dependencies.test'
)
modifier.update_lower_bound(
    'pytest', '9.1.0', 'project.optional-dependencies.test'
)
modifier.write()
Parameters:

path (Path) – Path to the pyproject.toml file.

static _split_section_path(section_path: str) list[str][source]

Split a section path into individual keys for document traversal.

Handles the special case of optional-dependencies and extra-dependencies where the key itself contains a hyphen and must not be split on it.

Known patterns: - project.dependencies["project", "dependencies"] - project.optional-dependencies.test["project", "optional-dependencies", "test"] - tool.hatch.envs.types.extra-dependencies["tool", "hatch", "envs", "types", "extra-dependencies"]

Parameters:

section_path (str) – The dot-separated section path.

Returns:

List of keys to traverse.

Return type:

list[str]

_resolve_section(section_path: str) Array[source]

Navigate the TOML document to the dependency array at section_path.

Supported path formats:

  • project.dependencies

  • project.optional-dependencies.<group>

  • tool.hatch.envs.<env>.extra-dependencies

Parameters:

section_path (str) – Dot-separated path to the dependency array.

Returns:

The tomlkit Array object containing the dependency strings.

Raises:

DevtoolsError – If the section path does not exist in the document.

get_dependency_specifier(dependency_name: str, section_path: str) str[source]

Return the raw PEP 508 requirement string for a named dependency.

The dependency is located using PEP 503–normalized name comparison (case-insensitive, hyphens ≡ underscores ≡ dots).

Parameters:
  • dependency_name (str) – The package name to look up (e.g. "ruff").

  • section_path (str) – Dot-separated path to the dependency array (e.g. "project.optional-dependencies.dev").

Returns:

The raw requirement string as stored in the TOML array.

Return type:

str

Raises:

DevtoolsError – If the section does not exist or the dependency is not found within it.

read() None[source]

Load pyproject.toml from disk preserving formatting.

Raises:

DevtoolsError – If the file does not exist or cannot be parsed.

update_lower_bound(dependency_name: str, new_version: str, section_path: str) None[source]

Replace the >= lower-bound version for a named dependency.

Only the version portion of the first >= specifier is replaced; all surrounding whitespace, additional specifiers, extras, and environment markers are preserved.

Parameters:
  • dependency_name (str) – The package name whose bound to update.

  • new_version (str) – The new PEP 440 version string (e.g. "9.1.0").

  • section_path (str) – Dot-separated path to the dependency array.

Raises:

DevtoolsError – If the section does not exist, the dependency is not found, or the dependency does not contain a >= specifier.

write() None[source]

Write the (possibly modified) document back to disk.

Preserves all comments, formatting, and key ordering for portions of the file that were not explicitly modified.

Raises:

DevtoolsError – If no document has been loaded via read().

mafw.devtools.toolchain.pyproject_modifier._extract_package_name(specifier: str) str[source]

Extract the distribution name from a PEP 508 requirement string.

Handles extras (e.g. "pandas[hdf5]>=2.2.3"), environment markers (e.g. '; python_version >= "3.14"'), and bare names.

Parameters:

specifier (str) – A PEP 508 requirement string.

Returns:

The distribution name portion.

Return type:

str

mafw.devtools.toolchain.pyproject_modifier._normalize_name(name: str) str[source]

Normalize a Python package name per PEP 503.

Converts to lowercase and replaces runs of hyphens, underscores, and dots with a single hyphen, making name comparison canonical.

Parameters:

name (str) – The raw package name (e.g. "ruamel.yaml").

Returns:

Normalized form (e.g. "ruamel-yaml").

Return type:

str

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

Regex matching a >= specifier and its version portion.

Group 1 captures the >= operator. Group 2 captures the version string to be replaced.

mafw.devtools.toolchain.pyproject_modifier._PEP503_NORMALIZE_RE: Final[Pattern[str]] = re.compile('[-_.]+')

Regex used for PEP 503 package-name normalization.