Management of Python dependencies sources

Материал из ALT Linux Wiki

Management of dependencies sources

Overview

This feature allows to manage runtime and/or buildtime dependencies of Python RPM package in Python-ecosystem manner for the purpose of automation (e.g. such dependencies can be synced, verified or evaluated). In particular, %pyproject_deps_resync-based macros collect PEP508 requirements from different sources and store them in %_sourcedir/pyproject_deps.json while %pyproject_builddeps and %pyproject_runtimedeps evaluate dependencies specified by this config file in current Python environment and generate BuildRequires and Requires RPM specfile’s entries respectively. Sources of dependencies can be either well known standard formats like core metadata, PEP518, PEP517 or tool-specific formats like pip reqfile, tox deps, etc.

Typical RPM specfile for Python package using this feature looks like:

...
Source1: %pyproject_deps_config_name
%pyproject_runtimedeps_metadata
BuildRequires(pre): rpm-build-pyproject
%pyproject_builddeps_build
%if_with check
%pyproject_builddeps_metadata
%endif
...
%prep
%setup
%pyproject_deps_resync_build
%pyproject_deps_resync_metadata

%pyproject_deps-based RPM macros are shipped in rpm-build-pyproject RPM package, that can be used as drop-in replacement for rpm-build-python3 in RPM specfile.

BUILD

Build dependencies format is standardized by PEP518 and PEP517.

  • add %pyproject_builddeps_build macro into RPM specfile that:

    • eval dependencies of PEP518 and PEP517 sources (they will be defined later) in Python environment
    • map dependencies to distro names
    • generate RPM BuildRequires tags
  • append %pyproject_deps_resync_build macro to RPM %prep section. This macro:

    • configure PEP518 source of dependencies if it doesn’t exist yet
    • verify PEP518 source and resync it if needed
    • configure PEP517 source of dependencies if it doesn’t exist yet
    • verify PEP517 source and resync it if needed
  • run build. The RPM build may fail on verification of PEP518 dependencies (if any). Update stored config with produced one if needed. For example,

    cp ~/hasher/chroot/usr/src/RPM/SOURCES/pyproject_deps.json .gear/
    
  • %pyproject_deps-based RPM macros expect to find its configuration in RPM sources directory by default. Hence,

    • add copy: .gear/pyproject_deps.json to .gear/rules
    • add Source1: %pyproject_deps_config_name to RPM specfile (the source number doesn’t matter)
  • exclude filters can be applied on evaluation of dependencies. For example, filter out dependencies having normalized names that match types-:

    %set_pyproject_deps_build_filter types-

    or

    %add_pyproject_deps_build_filter types-
  • rerun build. The RPM build may fail on verification of PEP517 dependencies (if any). Update stored config with produced one if needed. For example,

    cp ~/hasher/chroot/usr/src/RPM/SOURCES/pyproject_deps.json .gear/
    

This is enough to manage Python build dependencies of vast of majority of Python projects.

RUNTIME

By default, runtime dependencies of Python projects in ALTLinux are generated automatically by parsing source code and fetching of all of external imports (this feature is known as autoreq). This may or may not work well. For example:

  • conditional or optional dependencies can’t be automatically accounted
  • installation tools like pip may expect all installed dependencies of projects

Downstream maintainers have to manually handle such cases.

Runtime dependencies format is standardized by core metadata and it’s usually generated by build backend from some tool-specific format. Produced list of dependencies is not necessary and sufficient. But it may complement auto-generated dependencies to cover unhandled cases.

  • add %pyproject_runtimedeps_metadata macro into RPM specfile that:

    • eval dependencies of metadata source (it will be defined later) in Python environment
    • map dependencies to distro names
    • generate RPM Requires tags
  • append %pyproject_deps_resync_metadata macro to RPM %prep section. This macro:

    • configure core metadata source of dependencies if it doesn’t exist yet
    • verify core metadata source and resync it if needed
  • rerun build. The RPM build may fail on verification of metadata dependencies (if any). Update stored config with produced one if needed. For example,

    cp ~/hasher/chroot/usr/src/RPM/SOURCES/pyproject_deps.json .gear/
    
  • exclude filters can be applied on evaluation of dependencies: For example, filter out dependencies having normalized names that match types-:

    %set_pyproject_deps_runtime_filter types-

    or

    %add_pyproject_deps_runtime_filter types-

CHECK

Tests dependencies format is not standardized, but pyproject_installer supports most common of them like:

Usually tests require all runtime dependencies of project and additional packages for test environment (e.g. pytest). Thus, configuration of several sources may be required (e.g. metadata + pip_reqfile).

check filter

There can be many of redundant dependencies like:

  • code style checkers that are really helpful in development but they often report different results from version to version.
  • coverage measurement, it’s pointless overhead to do this for RPM packaging.
  • publishing tools
  • documentation tools

Hence, there is a predefined filter of check dependencies %pyproject_deps_check_filter. This filter is applied automatically on %pyproject_builddeps_check or %pyproject_builddeps_metadata macro usage.

Today’s default filter includes:

  • %{?pypi_name:‘%{pep503_name %pypi_name}$’}
  • flake8 and its plugins
  • black and its plugins
  • coverage and its plugins
  • codecov and its plugins
  • coveralls and its plugins
  • mypy and its plugins
  • isort and its plugins
  • pre-commit and its plugins
  • pytest-checkdocs
  • twine and its plugins
  • ruff and its plugins
  • typing stubs
  • pyright and its plugins
  • Sphinx and its plugins

This filter can be changed like:

%set_pyproject_deps_check_filter foo bar

or

%add_pyproject_deps_check_filter foo bar

Note: this is exposed as deps –exclude option that accepts regex patterns and excludes requirement having PEP503-normalized name which matches one of these patterns. Thus, if only the name should be excluded then this name should be normalized first:

%add_pyproject_deps_check_filter '%{pep503_name Foo-Project.my}$'

core metadata

Specification:

For example, upstream use core metadata as source of tests dependencies and mark them with testing extra (this means that all runtime dependencies of current package and any dependencies that are listed in the testing extra of current package will be installed, see extras). Usually this is exposed as extras = testing in tox.ini or pip install .[testing] in CI config.

  • add macro to RPM specfile

    %if_with check
    %pyproject_builddeps_metadata_extra testing
    %endif

    This macro:

    • eval dependencies of metadata source (it’s already defined as source of runtime dependencies, otherwise it should be defined manually) in Python environment with testing extra
    • filter out dependencies according to %pyproject_deps_check_filter
    • map dependencies to distro names
    • generate RPM BuildRequires tags
  • filter out extra dependencies with check-filter

pip reqfile

Specification:

Limitations:

  • supported only PEP508 requirements
  • line continuations are not supported
  • pip’s inline options are not supported

For example, upstream use pip’s reqfile as source of tests dependencies and it’s saved as test-requirements.txt.

  • add macro to RPM specfile

    %if_with check
    %pyproject_builddeps_check
    %endif

    This macro:

    • eval dependencies of check source (it will be defined later) in Python environment
    • filter out dependencies according to %pyproject_deps_check_filter
    • map dependencies to distro names
    • generate RPM BuildRequires tags
  • configure check source of dependencies by appending to RPM %prep section:

    %if_with check
    %pyproject_deps_resync_check_pipreqfile test-requirements.txt
    %endif

    or source having arbitrary name

    %if_with check
    %pyproject_deps_resync check pip_reqfile test-requirements.txt
    %endif
  • rerun build. The RPM build may fail on verification of check dependencies (if any). Update stored config with produced one if needed. For example,

    cp ~/hasher/chroot/usr/src/RPM/SOURCES/pyproject_deps.json .gear/
    
  • filter out extra dependencies with check-filter

poetry group

Specification:

Limitations:

  • versions are not supported (poetry version format is not compatible with PEP440)
  • python field is not supported

For example, upstream uses poetry dependencies group dev as source of tests dependencies. By default, poetry installs dependencies of all non-optional groups including dependencies of project. Hence, runtime should be configured first.

  • add macro to RPM specfile

    %if_with check
    %pyproject_builddeps_metadata
    %pyproject_builddeps_check
    %endif

    These macros:

    • eval dependencies of metadata and check sources (metadata is already defined as source of runtime dependencies, otherwise it should be defined manually) in Python environment
    • filter out dependencies according to %pyproject_deps_check_filter
    • map dependencies to distro names
    • generate RPM BuildRequires tags
  • configure check source of dependencies by appending to RPM %prep section:

    %if_with check
    %pyproject_deps_resync_check_poetry dev
    %endif

    or source having arbitrary name

    %if_with check
    %pyproject_deps_resync check poetry dev
    %endif
  • rerun build. The RPM build may fail on verification of check dependencies (if any). Update stored config with produced one if needed. For example,

    cp ~/hasher/chroot/usr/src/RPM/SOURCES/pyproject_deps.json .gear/
    
  • filter out extra dependencies with check-filter

tox env

Specification:

Limitations:

  • supported only PEP508 requirements
  • config substitutions are not supported
  • testenv inheritance is not supported
  • pip’s req files or options are not supported

Note: if deps includes pip’s requirements file then use pip’s reqfile instead. If testenv has extras then use core metadata instead.

For example, upstream uses tox’ deps of testenv section of tox.ini config as source of tests dependencies. By default, tox installs these dependencies prior to package installation and then installs package and its runtime dependencies. Hence, runtime should be configured first.

  • add macro to RPM specfile

    %if_with check
    %pyproject_builddeps_metadata
    %pyproject_builddeps_check
    %endif

    These macros:

    • eval dependencies of metadata and check sources (metadata is already defined as source of runtime dependencies, otherwise it should be defined manually) in Python environment
    • filter out dependencies according to %pyproject_deps_check_filter
    • map dependencies to distro names
    • generate RPM BuildRequires tags
  • configure check source of dependencies by appending to RPM %prep section:

    %if_with check
    %pyproject_deps_resync_check_tox tox.ini testenv
    %endif

    or source having arbitrary name

    %if_with check
    %pyproject_deps_resync check tox tox.ini testenv
    %endif
  • rerun build. The RPM build may fail on verification of check dependencies (if any). Update stored config with produced one if needed. For example,

    cp ~/hasher/chroot/usr/src/RPM/SOURCES/pyproject_deps.json .gear/
    
  • filter out extra dependencies with check-filter

hatch env

Specification:

Limitations:

  • supported only PEP508 requirements
  • env inheritance is not supported
  • context formatting is not supported
  • features(extra) is not supported

Note: if env has features then use core metadata with extra instead.

For example, upstream uses hatch’s dependencies of test env in hatch.toml config as source of tests dependencies. By default, hatch installs these dependencies in addition to the runtime ones. Hence, runtime should be configured first.

  • add macro to RPM specfile

    %if_with check
    %pyproject_builddeps_metadata
    %pyproject_builddeps_check
    %endif

    These macros:

    • eval dependencies of metadata and check sources (metadata is already defined as source of runtime dependencies, otherwise it should be defined manually) in Python environment
    • filter out dependencies according to %pyproject_deps_check_filter
    • map dependencies to distro names
    • generate RPM BuildRequires tags
  • configure check source of dependencies by appending to RPM %prep section:

    %if_with check
    %pyproject_deps_resync_check_hatch hatch.toml test
    %endif

    or source having arbitrary name

    %if_with check
    %pyproject_deps_resync YOURSOURCENAME hatch hatch.toml test
    %endif
  • rerun build. The RPM build may fail on verification of check dependencies (if any). Update stored config with produced one if needed. For example,

    cp ~/hasher/chroot/usr/src/RPM/SOURCES/pyproject_deps.json .gear/
    
  • filter out extra dependencies with check-filter

pdm group

Specification:

Limitations:

  • supported only PEP508 requirements

For example, upstream uses pdm dev dependencies’ group test as source of tests dependencies. By default, pdm installs dependencies of all non-optional groups including runtime dependencies of project. Hence, runtime should be configured first.

  • add macro to RPM specfile

    %if_with check
    %pyproject_builddeps_metadata
    %pyproject_builddeps_check
    %endif

    These macros:

    • eval dependencies of metadata and check sources (metadata is already defined as source of runtime dependencies, otherwise it should be defined manually) in Python environment
    • filter out dependencies according to %pyproject_deps_check_filter
    • map dependencies to distro names
    • generate RPM BuildRequires tags
  • configure check source of dependencies by appending to RPM %prep section:

    %if_with check
    %pyproject_deps_resync_check_pdm test
    %endif

    or source having arbitrary name

    %if_with check
    %pyproject_deps_resync YOURSOURCENAME pdm test
    %endif
  • rerun build. The RPM build may fail on verification of check dependencies (if any). Update stored config with produced one if needed. For example,

    cp ~/hasher/chroot/usr/src/RPM/SOURCES/pyproject_deps.json .gear/
    
  • filter out extra dependencies with check-filter

pipenv category

Specification:

Limitations:

For example, upstream uses pipenv dev dependencies’ category dev-packages as a source of tests dependencies. By default, pipenv installs dependencies of packages category and runtime dependencies should be configured first.

  • add macro to RPM specfile

    %if_with check
    %pyproject_builddeps_metadata
    %pyproject_builddeps_check
    %endif

    These macros:

    • eval dependencies of metadata and check sources (metadata is already defined as source of runtime dependencies, otherwise it should be defined manually) in Python environment
    • filter out dependencies according to %pyproject_deps_check_filter
    • map dependencies to distro names
    • generate RPM BuildRequires tags
  • configure check source of dependencies by appending to RPM %prep section:

    %if_with check
    %pyproject_deps_resync_check_pipenv Pipfile dev-packages
    %endif

    or source having arbitrary name

    %if_with check
    %pyproject_deps_resync YOURSOURCENAME pipenv Pipfile dev-packages
    %endif
  • rerun build. The RPM build may fail on verification of check dependencies (if any). Update stored config with produced one if needed. For example,

    cp ~/hasher/chroot/usr/src/RPM/SOURCES/pyproject_deps.json .gear/
    
  • filter out extra dependencies with check-filter

Mapping project names to distro names

Python ecosystem uses PEP503 normalization rules for names comparison:

Mapping rule in ALTLinux:

project_name => python3-module-%{pep503_name project_name}

This almost always works out of the box, but distro packages having unnormalized names must provide such the name:

Provides: python3-module-%{pep503_name %pypi_name} = %EVR

The following naming rule for new Python distro packages is strongly encouraged:

Name: python3-module-%{pypi_nname}

where %pypi_nname must be equal to the result of evaluation of %{pep503_name %pypi_name} macro.