mirror of
https://github.com/danieldemus/pyp2spec.git
synced 2026-07-27 21:00:37 +02:00
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:
committed by
Karolina Surma
parent
fc17319634
commit
2cca4d3844
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
@@ -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"
|
||||
@@ -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
|
||||
|
||||
@@ -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"])
|
||||
|
||||
Reference in New Issue
Block a user