Create spec files for alternative Python versions (closes #16)

This feature is useful when creating packages for Centos Stream and RHEL
which contain multiple Python stacks and make it possible to build packages for
them.
This commit is contained in:
Karolina Surma
2024-04-11 12:40:05 +02:00
committed by Karolina Surma
parent b52e5da264
commit ce099be238
9 changed files with 177 additions and 48 deletions
+3 -2
View File
@@ -80,7 +80,7 @@ Configuration data is stored in a TOML file.
| Field | Description | Type |
| -------- | -------- | -------- |
| pypi_name | package name as stored in PyPI | string |
| python_name | pypi_name prepended with `python-` | string |
| python_name | pypi_name prepended with `python-` and alternative Python version, if `python_alt_version` is defined| string |
| archive_name | source tarball name, stripped of version and file extension | string |
| version | package version to create spec file for (RPM format) | string |
| pypi_version | package version string as in PyPI, '%{version}' if the same as version | string
@@ -90,7 +90,8 @@ Configuration data is stored in a TOML file.
| source | %{pypi_source} macro with optional arguments (tarball URL can be used instead) | string |
| description | long package description | multiline string |
| extras | extra subpackages names | list of strings |
| archful | package contains compiled extensions, implies not using `BuildArch: noarch` and adding `BuildRequires: gcc` | bool
| archful | package contains compiled extensions, implies not using `BuildArch: noarch` and adding `BuildRequires: gcc` | bool |
| python_alt_version | specific Python version to create the spec file for, e.g. 3.9, 3.10, 3.12 | string |
### Optional fields
+14 -14
View File
@@ -133,7 +133,12 @@ def wrap_description(config):
break_long_words=False
)
def fill_in_template(config, python_version):
def python3_pkgversion_or_3(config):
return "%{python3_pkgversion}" if config.get_string("python_alt_version") else "3"
def fill_in_template(config):
"""Return template rendered with data from config file."""
with (files("pyp2spec") / TEMPLATE_FILENAME).open("r", encoding="utf-8") as f:
@@ -152,11 +157,12 @@ def fill_in_template(config, python_version):
name=config.get_string("pypi_name"),
python_name=config.get_string("python_name"),
pypi_version=config.get_string("pypi_version"),
python_version=python_version,
python_alt_version=config.get_string("python_alt_version"),
source=config.get_string("source"),
summary=config.get_string("summary"),
test_method=generate_check(config),
test_top_level=config.get_bool("test_top_level"),
python3_pkgversion=python3_pkgversion_or_3(config),
url=config.get_string("url"),
version=config.get_string("version"),
)
@@ -164,11 +170,11 @@ def fill_in_template(config, python_version):
return result
def save_spec_file(config, output, python_version):
def save_spec_file(config, output):
"""Save the spec file in the current directory if custom output is not set.
Return the saved file name."""
result = fill_in_template(config, python_version)
result = fill_in_template(config)
if output is None:
output = config.get_string("python_name") + ".spec"
with open(output, "w", encoding="utf-8") as spec_file:
@@ -177,10 +183,10 @@ def save_spec_file(config, output, python_version):
return output
def create_spec_file(config_file, spec_output=None, python_version=None):
def create_spec_file(config_file, spec_output=None):
"""Create and save the generate spec file."""
config = ConfigFile(config_file)
return save_spec_file(config, spec_output, python_version)
return save_spec_file(config, spec_output)
@click.command()
@@ -190,15 +196,9 @@ def create_spec_file(config_file, spec_output=None, python_version=None):
"-o",
help="Provide custom output for spec file",
)
@click.option(
"--python-version",
"-p",
help="Specify specific python version to build for, e.g 3.11",
)
def main(config, spec_output, python_version):
def main(config, spec_output):
try:
create_spec_file(config, spec_output, python_version)
create_spec_file(config, spec_output)
except (Pyp2specError, NotImplementedError) as exc:
click.secho(f"Fatal exception occurred: {exc}", fg="red")
sys.exit(1)
+34 -5
View File
@@ -87,11 +87,30 @@ class PypiPackage:
error_str = f"Package `{self.package_name}` or version `{self.version}` was not found on PyPI"
return self._get_from_url(pkg_index, error_str)
def python_name(self):
def python_name(self, *, python_alt_version=None):
"""Create a component name for the specfile.
Prepend the name with 'python' (unless it already starts with it).
Add the Python alternative version, if it's defined.
Valid outcomes:
package name: foo, python_alt_version: 3.11
-> python3.11-foo
package name: foo, python_alt_version: None
-> python-foo
package name: python-foo, python_alt_version: 3.12
-> python3.12-foo
package name: python-foo, python_alt_version: None
-> python-foo
"""
alt_version = "" if python_alt_version is None else python_alt_version
if self.pypi_name.startswith("python"):
return self.pypi_name
else:
return f"python-{self.pypi_name}"
return self.pypi_name.replace("python", f"python{alt_version}")
return f"python{alt_version}-{self.pypi_name}"
def source(self):
"""Return valid pypi_source RPM macro.
@@ -335,6 +354,7 @@ def create_config_contents(
license=None,
compliant=False,
top_level=False,
python_alt_version=None,
):
"""Use `package` and provided kwargs to create the whole config contents.
Return contents dictionary.
@@ -372,6 +392,10 @@ def create_config_contents(
if archful := pkg.is_archful():
click.secho("Package is archful - you may need to specify additional build requirements", fg="magenta")
if python_alt_version is not None:
click.secho(f"Assuming build for Python: {python_alt_version}", fg="yellow")
contents["python_alt_version"] = python_alt_version
contents["archful"] = archful
contents["description"] = description
contents["summary"] = summary
@@ -379,7 +403,7 @@ def create_config_contents(
contents["pypi_version"] = pkg.pypi_version_or_macro()
contents["license"] = license
contents["pypi_name"] = pkg.pypi_name
contents["python_name"] = pkg.python_name()
contents["python_name"] = pkg.python_name(python_alt_version=python_alt_version)
contents["url"] = pkg.project_url()
contents["source"] = pkg.source()
contents["archive_name"] = pkg.archive_name()
@@ -414,6 +438,7 @@ def create_config(options):
license=options["license"],
compliant=options["fedora_compliant"],
top_level=options["top_level"],
python_alt_version=options["python_alt_version"]
)
return save_config(contents, options["config_output"])
@@ -448,6 +473,10 @@ def pypconf_args(func):
"--top-level", "-t", is_flag=True,
help="Test only top-level modules in %check",
)
@click.option(
"--python-alt-version", "-p",
help="Provide specific Python version to build for, e.g 3.11",
)
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
+12 -26
View File
@@ -1,5 +1,5 @@
{% if python_version %}
%global python3_pkgversion {{ python_version }}
{% if python_alt_version -%}
%global python3_pkgversion {{ python_alt_version }}
{% endif -%}
Name: {{python_name}}
@@ -15,11 +15,7 @@ Source: {{source}}
{% if not archful %}
BuildArch: noarch
{%- endif %}
{% if python_version %}
BuildRequires: python%{python3_pkgversion}-devel
{% else -%}
BuildRequires: python3-devel
{% endif -%}
BuildRequires: python{{python3_pkgversion}}-devel
{% for br in additional_build_requires -%}
BuildRequires: {{br}}
{% endfor %}
@@ -29,25 +25,18 @@ BuildRequires: {{br}}
{{description}}}
%description %_description
{% if python_version %}
%package -n python%{python3_pkgversion}-{{name}}
{% else %}
%package -n python3-{{name}}
{% endif -%}
{% if not python_alt_version -%}
%package -n python{{python3_pkgversion}}-{{name}}
Summary: %{summary}
{% if python_version %}
%description -n python%{python3_pkgversion}-{{name}} %_description
{% else %}
%description -n python3-{{name}} %_description
%description -n python{{python3_pkgversion}}-{{name}} %_description
{% endif -%}
{% if extras %}
# For official Fedora packages, review which extras should be actually packaged
# See: https://docs.fedoraproject.org/en-US/packaging-guidelines/Python/#Extras
{%- if python_version %}
%pyproject_extras_subpkg -n python%{python3_pkgversion}-{{name}} {{extras}}
{% else %}
%pyproject_extras_subpkg -n python3-{{name}} {{extras}}
{% endif -%}
%pyproject_extras_subpkg -n python{{python3_pkgversion}}-{{name}} {{extras}}
{% endif %}
%prep
@@ -77,11 +66,8 @@ Summary: %{summary}
{% if test_method -%}
{{test_method}}
{% endif %}
{% if python_version %}
%files -n python%{python3_pkgversion}-{{name}} -f %{pyproject_files}
{% else %}
%files -n python3-{{name}} -f %{pyproject_files}
{% endif -%}
%files -n python{{python3_pkgversion}}-{{name}} -f %{pyproject_files}
{% if doc_files -%}
%doc {{doc_files}}
{% endif -%}
@@ -0,0 +1,58 @@
%global python3_pkgversion 3.9
Name: python3.9-pello
Version: 1.0.4
Release: %autorelease
Summary: An example Python Hello World package
# Check if the automatically generated License and its spelling is correct for Fedora
# https://docs.fedoraproject.org/en-US/packaging-guidelines/LicensingGuidelines/
License: MIT-0
URL: https://github.com/fedora-python/Pello
Source: %{pypi_source Pello}
BuildArch: noarch
BuildRequires: python%{python3_pkgversion}-devel
# Fill in the actual package description to submit package to Fedora
%global _description %{expand:
This is package 'Pello' generated automatically by pyp2spec.}
%description %_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 python%{python3_pkgversion}-pello color
%prep
%autosetup -p1 -n Pello-%{version}
%generate_buildrequires
# Keep only those extras which you actually want to package or use during tests
%pyproject_buildrequires -x color
%build
%pyproject_wheel
%install
%pyproject_install
# For official Fedora packages, including files with '*' +auto is not allowed
# Replace it with a list of relevant Python modules/globs and list extra files in %%files
%pyproject_save_files '*' +auto
%check
%pyproject_check_import
%files -n python%{python3_pkgversion}-pello -f %{pyproject_files}
%changelog
%autochangelog
+2 -1
View File
@@ -121,7 +121,8 @@ def test_archful_flag_is_loaded(config_dir, conf, expected):
"default_python-click.conf",
"customized_markdown-it-py.conf",
"customized_python-sphinx.conf",
"default_python-numpy.conf"
"default_python-numpy.conf",
"default_python3.9-pello.conf",
]
)
def test_default_generated_specfile(file_regression, config_dir, conf):
@@ -0,0 +1,15 @@
python_alt_version = "3.9"
archful = false
description = "This is package 'Pello' generated automatically by pyp2spec."
summary = "An example Python Hello World package"
version = "1.0.4"
pypi_version = "%{version}"
license = "MIT-0"
pypi_name = "pello"
python_name = "python3.9-pello"
url = "https://github.com/fedora-python/Pello"
source = "%{pypi_source Pello}"
archive_name = "Pello"
extras = [
"color",
]
+38
View File
@@ -38,6 +38,28 @@ def test_automatically_generated_config_is_valid(betamax_parametrized_session, p
assert config == loaded_contents
@pytest.mark.parametrize("package, version, alt_python",
[
("Pello", "1.0.4", "3.9"),
]
)
def test_automatically_generated_config_with_alt_python_is_valid(
betamax_parametrized_session, package, version, alt_python
):
"""Run the config rendering in fully automated mode and compare the results"""
config = create_config_contents(
package=package,
version=version,
python_alt_version=alt_python,
session=betamax_parametrized_session,
)
with open(f"tests/test_configs/default_python{alt_python}-{package.lower()}.conf", "rb") as config_file:
loaded_contents = tomllib.load(config_file)
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
@@ -224,6 +246,22 @@ def test_capitalized_underscored_pypi_name_is_normalized():
assert pkg.python_name() == "python-awesome-testpkg"
@pytest.mark.parametrize(
("pypi_name", "alt_version", "expected"), [
("foo", "3.10", "python3.10-foo"),
("python-foo", "3.9", "python3.9-foo"),
("python_foo", "3.12", "python3.12-foo"),
("foo", None, "python-foo"),
("python-foo", None, "python-foo"),
("python_foo", None, "python-foo"),
]
)
def test_python_name(pypi_name, alt_version, expected):
fake_pkg_data = {"info": {"name": pypi_name}}
pkg = PypiPackage(pypi_name, version="1.2.3", package_metadata=fake_pkg_data)
assert pkg.python_name(python_alt_version=alt_version) == expected
@pytest.mark.parametrize(
("pypi_version", "rpm_version"), [
("1.1.0", "1.1.0"),