Support compat packages (Closes #15)

When specified on command line, a compat package for a given version
will be created.
pyp2spec will create a specfile for the latest version matching the
compat one, e.g. for `pytest --compat 7` it will create specfile for
version 7.4.4; for `pytest --compat 7.2` - 7.2.2.
It is possible to specify both compat and specific version, e.g.:
`pytest --compat 7 --version 7.3.0`
This commit is contained in:
Karolina Surma
2025-02-24 14:39:55 +01:00
committed by Karolina Surma
parent fc17319634
commit 2cca4d3844
15 changed files with 278 additions and 12 deletions
+6
View File
@@ -1,5 +1,11 @@
# Changelog
# Unreleased
### Added
- Support for compat packages of various granularity (eg. '7', '7.2').
Invoke with `--compat <compat-version-string>`
# [0.11.1] - 2024-11-21
### Changed
- Bring back the processing of the legacy sdist filenames (removed in v0.11.0).
+2
View File
@@ -125,6 +125,8 @@ Configuration data is stored in a TOML file.
| automode | create buildable spec files that don't have to fully comply with Fedora Guidelines; useful for automatic build environments | bool |
| license_files_present | `License-File` field was detected in the package metadata | bool |
| archive_name | filename of the sdist | string |
| compat | package compatible with the given version line | string |
### Example config file generated by pyp2spec
+5 -1
View File
@@ -182,6 +182,7 @@ def fill_in_template(config: ConfigFile) -> str:
archful=config.get_bool("archful"),
archive_name=archive_basename(config, pypi_version),
automode=config.get_bool("automode"),
compat=config.get_string("compat"),
extras=",".join(config.get_list("extras")),
license=license,
license_notice=license_notice,
@@ -207,7 +208,10 @@ def save_spec_file(config: ConfigFile, output: str | None) -> str:
result = fill_in_template(config)
if output is None:
output = config.get_string("python_name") + ".spec"
output = config.get_string("python_name")
if compat := config.get_string("compat"):
output += compat
output += ".spec"
with open(output, "w", encoding="utf-8") as spec_file:
spec_file.write(result)
yay(f"Spec file was saved successfully to '{output}'")
+20 -4
View File
@@ -33,6 +33,7 @@ class PackageInfo:
# These may or may not be ever set
python_alt_version: str | None = field(default=None)
automode: bool | None = field(default=None)
compat: str | None = field(default=None)
def is_package_name(package: str) -> bool:
@@ -77,13 +78,15 @@ def gather_package_info(core_metadata: RawMetadata | None, pypi_package_data: di
def create_package_from_source(
package: str,
version: str | None,
compat: str | None,
session: Session | None
) -> PackageInfo:
"""Determine the best source for the given package name and create a PackageInfo instance.
"""
if is_package_name(package):
# explicit `session` argument is needed for testing
pypi_pkg_data = load_from_pypi(package, version=version, session=session)
pypi_pkg_data = load_from_pypi(package, version=version,
compat=compat, session=session)
try:
core_metadata = load_core_metadata_from_pypi(pypi_pkg_data, session=session)
# if no core metadata found, we will fall back to PyPI API
@@ -102,16 +105,22 @@ def create_config_contents(
compliant: bool = False,
python_alt_version: str | None = None,
automode: bool = False,
compat: str | None = None,
) -> dict:
"""Use `package` and provided kwargs to create the whole config contents.
Return pkg_info dictionary.
"""
pkg_info = create_package_from_source(package, version, session)
pkg_info = create_package_from_source(package, version, compat, session)
pkg_info.python_name = prepend_name_with_python(pkg_info.pypi_name, python_alt_version)
if compat is not None:
inform(f"Creating a compat package for version: '{compat}'")
pkg_info.compat = compat
if version is None:
inform(f"Assuming the latest version found on PyPI: '{pkg_info.pypi_version}'")
inform(f"Assuming the version found on PyPI: '{pkg_info.pypi_version}'")
if pkg_info.archful:
caution("Package contains compiled extensions - you may need to specify additional build requirements")
@@ -151,6 +160,8 @@ def save_config(contents: dict, output: str | None = None) -> str:
"""
if not output:
package = contents["python_name"]
if compat := contents.get("compat"):
package += compat
output = f"{package}.conf"
with open(output, "wb") as f:
tomli_w.dump(contents, f, multiline_strings=True)
@@ -166,7 +177,8 @@ def create_config(options: dict) -> str:
version=options["version"],
compliant=options["fedora_compliant"],
python_alt_version=options["python_alt_version"],
automode=options["automode"]
automode=options["automode"],
compat=options["compat"]
)
return save_config(contents, options["config_output"])
@@ -193,6 +205,10 @@ def pypconf_args(func): # noqa
"--python-alt-version", "-p",
help="Provide specific Python version to build for, e.g 3.11",
)
@click.option(
"--compat",
help="Create a compat package for a given version",
)
@wraps(func)
def wrapper(*args, **kwargs): # noqa
return func(*args, **kwargs)
+28 -2
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
from typing import Any
from packaging.metadata import parse_email, RawMetadata
from packaging.version import Version
from requests import Response, Session
from pyp2spec.utils import Pyp2specError
@@ -18,6 +19,10 @@ class CoreMetadataNotFoundError(Pyp2specError):
"""Raised when there's no Metadata file available on PyPI API"""
class CompatibleVersionNotFoundError(Pyp2specError):
"""Raised when project doesn't have a version compatible with the requested one"""
def _get_from_url(url: str, error_str: str, session: Session | None = None) -> Response:
_session = session or Session()
response = _session.get(url)
@@ -55,15 +60,36 @@ def _get_metadata_file(pypi_pkg_data: dict[Any, Any], session: Session | None =
raise CoreMetadataNotFoundError(error_str)
def _find_available_versions(project_releases: dict) -> list[str]:
available_versions = []
for release in project_releases:
available_versions.append(release)
return available_versions
def _find_compatible_version(compat: str, available_versions: list[str]) -> str | None:
compatible_versions = [v for v in available_versions if v.startswith(compat)]
if not compatible_versions:
raise CompatibleVersionNotFoundError(f"There's no version compatible with the requested: `{compat}`")
return max(compatible_versions, key=Version)
def load_from_pypi(
package: str, *,
version: str | None = None,
compat: str | None = None,
session: Session | None= None
) -> dict[Any, Any]:
# Looking for the latest version
if version is None:
pypi_project_data = _get_pypi_package_project_data(package, session=session)
version = pypi_project_data["info"]["version"]
# Looking for the latest version
if compat is None:
version = pypi_project_data["info"]["version"]
# Looking for the latest version of the compat version line
else:
available_versions = _find_available_versions(pypi_project_data["releases"])
version = _find_compatible_version(compat, available_versions)
return _get_versioned_pypi_package_data(package, version=version, session=session)
+7 -4
View File
@@ -2,7 +2,7 @@
%global python3_pkgversion {{ python_alt_version }}
{% endif -%}
Name: {{python_name}}
Name: {{python_name}}{{compat}}
Version: {{version}}
Release: %autorelease
# Fill in the actual package summary to submit package to Fedora
@@ -27,9 +27,12 @@ This is package '{{name}}' generated automatically by pyp2spec.}
%description %_description
{% if not python_alt_version -%}
%package -n python{{python3_pkgversion}}-{{name}}
%package -n python{{python3_pkgversion}}-{{name}}{{compat}}
Summary: %{summary}
{% if compat %}
Conflicts: python{{python3_pkgversion}}-{{name}}
Provides: deprecated()
{% endif %}
%description -n python{{python3_pkgversion}}-{{name}} %_description
{% endif -%}
@@ -74,7 +77,7 @@ Summary: %{summary}
{%- endif %}
%files -n python{{python3_pkgversion}}-{{name}} -f %{pyproject_files}
%files -n python{{python3_pkgversion}}-{{name}}{{compat}} -f %{pyproject_files}
%changelog
@@ -0,0 +1,63 @@
Name: python-pytest7.2
Version: 7.2.1
Release: %autorelease
# Fill in the actual package summary to submit package to Fedora
Summary: pytest: simple powerful testing with Python
# Check if the automatically generated License and its spelling is correct for Fedora
# https://docs.fedoraproject.org/en-US/packaging-guidelines/LicensingGuidelines/
License: MIT
URL: https://github.com/pytest-dev/pytest
Source: %{pypi_source pytest}
BuildArch: noarch
BuildRequires: python3-devel
# Fill in the actual package description to submit package to Fedora
%global _description %{expand:
This is package 'pytest' generated automatically by pyp2spec.}
%description %_description
%package -n python3-pytest7.2
Summary: %{summary}
Conflicts: python3-pytest
Provides: deprecated()
%description -n python3-pytest %_description
# For official Fedora packages, review which extras should be actually packaged
# See: https://docs.fedoraproject.org/en-US/packaging-guidelines/Python/#Extras
%pyproject_extras_subpkg -n python3-pytest testing
%prep
%autosetup -p1 -n pytest-%{version}
%generate_buildrequires
# Keep only those extras which you actually want to package or use during tests
%pyproject_buildrequires -x testing
%build
%pyproject_wheel
%install
%pyproject_install
# Add top-level Python module names here as arguments, you can use globs
%pyproject_save_files -l ...
%check
%pyproject_check_import
%files -n python3-pytest7.2 -f %{pyproject_files}
%changelog
%autochangelog
@@ -0,0 +1,63 @@
Name: python-pytest7
Version: 7.4.4
Release: %autorelease
# Fill in the actual package summary to submit package to Fedora
Summary: pytest: simple powerful testing with Python
# Check if the automatically generated License and its spelling is correct for Fedora
# https://docs.fedoraproject.org/en-US/packaging-guidelines/LicensingGuidelines/
License: MIT
URL: https://github.com/pytest-dev/pytest
Source: %{pypi_source pytest}
BuildArch: noarch
BuildRequires: python3-devel
# Fill in the actual package description to submit package to Fedora
%global _description %{expand:
This is package 'pytest' generated automatically by pyp2spec.}
%description %_description
%package -n python3-pytest7
Summary: %{summary}
Conflicts: python3-pytest
Provides: deprecated()
%description -n python3-pytest %_description
# For official Fedora packages, review which extras should be actually packaged
# See: https://docs.fedoraproject.org/en-US/packaging-guidelines/Python/#Extras
%pyproject_extras_subpkg -n python3-pytest testing
%prep
%autosetup -p1 -n pytest-%{version}
%generate_buildrequires
# Keep only those extras which you actually want to package or use during tests
%pyproject_buildrequires -x testing
%build
%pyproject_wheel
%install
%pyproject_install
# Add top-level Python module names here as arguments, you can use globs
%pyproject_save_files -l ...
%check
%pyproject_check_import
%files -n python3-pytest7 -f %{pyproject_files}
%changelog
%autochangelog
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
View File
@@ -48,6 +48,8 @@ def test_archful_flag_is_loaded(config_dir, conf, expected):
"customized_python-sphinx.conf", # contains extras
"default_python-numpy.conf", # archful
"default_python3.9-pello.conf", # custom Python version
"default_python-pytest7.2.conf", # compat version
"default_python-pytest7.conf", # compat version - lower granularity
]
)
def test_default_generated_specfile(file_regression, config_dir, conf):
@@ -0,0 +1,14 @@
archful = false
archive_name = "pytest-7.2.1.tar.gz"
compat = "7.2"
extras = [
"testing",
]
license = "MIT"
license_files_present = true
pypi_name = "pytest"
pypi_version = "7.2.1"
python_name = "python-pytest"
source = "PyPI"
summary = "pytest: simple powerful testing with Python"
url = "https://github.com/pytest-dev/pytest"
@@ -0,0 +1,14 @@
archful = false
archive_name = "pytest-7.4.4.tar.gz"
compat = "7"
extras = [
"testing",
]
license = "MIT"
license_files_present = true
pypi_name = "pytest"
pypi_version = "7.4.4"
python_name = "python-pytest"
source = "PyPI"
summary = "pytest: simple powerful testing with Python"
url = "https://github.com/pytest-dev/pytest"
+22
View File
@@ -62,6 +62,28 @@ def test_automatically_generated_config_with_alt_python_is_valid(
assert config == loaded_contents
@pytest.mark.parametrize("package, compat, version",
[
("pytest", "7", None),
("pytest", "7.2", "7.2.1") # not the first, nor latest
]
)
def test_automatically_generated_compat_config_is_valid(
betamax_parametrized_session, package, compat, version
):
config = create_config_contents(
package=package,
version=version,
compat=compat,
session=betamax_parametrized_session,
)
with open(f"tests/test_configs/default_python-{package}{compat}.conf", "rb") as config_file:
loaded_contents = tomllib.load(config_file)
assert config["compat"] == compat
assert config == loaded_contents
def test_config_with_customization_is_valid(betamax_session):
"""Get the upstream metadata and modify some fields to get the custom config file.
This also tests the compliance with Fedora Legal data by making
+30 -1
View File
@@ -6,7 +6,8 @@ to prevent loading from the internet on each request.
import pytest
from pyp2spec.pypi_loaders import load_from_pypi, load_core_metadata_from_pypi
from pyp2spec.pypi_loaders import PackageNotFoundError
from pyp2spec.pypi_loaders import PackageNotFoundError, CompatibleVersionNotFoundError
from pyp2spec.pypi_loaders import _find_available_versions, _find_compatible_version
def test_load_from_pypi_no_version_given(betamax_session):
@@ -31,3 +32,31 @@ def test_load_core_metadata(betamax_session):
assert result["name"] == "Sphinx"
assert result["version"] == "8.1.3"
assert result["provides_extra"] == ["docs", "lint", "test"]
{"2.0.0":["..."],"2.0.1":["..."],"3.0.0":["..."],"6.0.1":["..."],"2.1.0":["..."],"2.1.1":["..."]}
def test_find_available_versions():
releases = {
"2.0.0":["..."],"2.0.1":["..."],"3.0.0":["..."],
"6.0.1":["..."],"2.1.0":["..."],"2.1.1":["..."]
}
expected = ["2.0.0", "2.0.1", "3.0.0", "6.0.1", "2.1.0", "2.1.1"]
assert _find_available_versions(releases) == expected
@pytest.mark.parametrize(
("compat", "available_versions", "expected"), [
("7", ["6.0.0", "7.0.0", "7.9.1"], "7.9.1"),
("7.4", ["7.4.2", "6.0.0", "7.4.3", "7.0.0", "7.0.1"], "7.4.3"),
("7.2", ["7.4.2", "7.2.3", "7.4.3", "6.0.0", "7.0.1"], "7.2.3"),
("7.4.2", ["7.4.2", "7.2.3", "7.4.3", "6.0.0", "7.0.1"], "7.4.2"),
]
)
def test_find_compatible_version(compat, available_versions, expected):
assert _find_compatible_version(compat, available_versions) == expected
def test_no_compatible_version_available():
with pytest.raises(CompatibleVersionNotFoundError):
_find_compatible_version("9", ["6.0.0", "7.0.0", "7.9.1"])