From 05ea1af35508f90327dc81b2b5dc0bcb2d81de45 Mon Sep 17 00:00:00 2001 From: Frank Schreiner Date: Fri, 17 Nov 2023 12:33:19 +0100 Subject: [PATCH] convert '~=' requirements specifier As described in #183 '~=' is not a valid version specifier for rpm and deb packages. This patch converts the specifier to valid version specifiers for rpm/deb. Examples: `abc ~= 1.1.1` * in rpm: ``` BuildRequires: %{python_module abc >= 1.1.1} # Only for suse Requires: python-abc >= 1.1.1, python-abc < 1.2 ``` * in deb: ``` Depends: python-abc (>= 1.1.1), python-abc (<< 1.2) ``` FIXES: #183 An schould also fix FIXES: #93 --- py2pack/__init__.py | 60 +++++++++++++++++++++++--- py2pack/requires.py | 18 +++++++- py2pack/templates/fedora.spec | 27 ++++-------- py2pack/templates/mageia.spec | 12 +++--- py2pack/templates/opensuse-legacy.spec | 8 ++-- py2pack/templates/opensuse.dsc | 6 +-- py2pack/templates/opensuse.spec | 20 ++++----- py2pack/utils.py | 1 + test/test_py2pack.py | 48 ++++++++++----------- test/test_requires.py | 6 +-- 10 files changed, 131 insertions(+), 75 deletions(-) diff --git a/py2pack/__init__.py b/py2pack/__init__.py index f4d7a07..675899c 100755 --- a/py2pack/__init__.py +++ b/py2pack/__init__.py @@ -149,6 +149,12 @@ def fetch(args): def _canonicalize_setup_data(data): + def _search_requires(req, pat): + for arr in req: + if arr[0] == pat: + return 1 + return 0 + if data.get('build-system', None): # PEP 518: 'requires' field is mandatory data['build_requires'] = py2pack.requires._requirements_sanitize( @@ -160,14 +166,13 @@ def _canonicalize_setup_data(data): if isinstance(setup_requires, str): setup_requires = setup_requires.splitlines() # canonicalize to build_requires - data["build_requires"] = ['setuptools', 'wheel'] + \ + data["build_requires"] = [['setuptools'], ['wheel']] + \ py2pack.requires._requirements_sanitize(setup_requires) else: # no build_requires means most probably legacy setuptools - data["build_requires"] = ['setuptools'] - if 'setuptools' in data['build_requires'] and 'wheel' not in data['build_requires']: - data['build_requires'] += ['wheel'] - + data["build_requires"] = [['setuptools']] + if _search_requires(data['build_requires'], 'setuptools') and not _search_requires(data['build_requires'], 'wheel'): + data['build_requires'] += [['wheel']] install_requires = ( get_pyproject_table(data, "project.dependencies") or get_pyproject_table(data, "tool.flit.metadata.requires") or @@ -336,10 +341,51 @@ def _normalize_license(data): def _prepare_template_env(template_dir): # setup jinja2 environment with custom filters env = jinja2.Environment(loader=jinja2.FileSystemLoader(template_dir)) + + def _rpm_format_requires(req): + req[0] = "python-" + req[0] + part1 = " ".join(req[0:3]) + part2 = "" + if len(req) > 3: + req[3] = "python-" + req[3] + part2 = ", " + " ".join(req[3:]) + return part1 + part2 + + def _parenthesize_version(req): + print(req) + ret = req[0].lower() + ver = [] + if len(req) > 1: + print(req[1:2]) + if req[1] == '<': + req[1] = '<<' + if req[1] == '>': + req[1] = '>>' + ver.append(" (" + " ".join(req[1:3]) + ")") + if len(req) > 3: + if req[4] == '<': + req[4] = '<<' + if req[4] == '>': + req[4] = '>>' + ver.append(req[3].lower() + " (" + " ".join(req[4:6]) + ")") + + if ver: + print(ver) + ret += ", ".join(ver) + return ret + env.filters['parenthesize_version'] = \ - lambda s: re.sub('([=<>]+)(.+)', r' (\1 \2)', s) + lambda s: _parenthesize_version(s) env.filters['basename'] = \ lambda s: s[s.rfind('/') + 1:] + env.filters['rpm_format_buildrequires'] = \ + lambda s: " ".join(s[0:3]) + env.filters['rpm_format_requires'] = \ + lambda s: _rpm_format_requires(s) + env.filters['sort_requires'] = \ + lambda s: sorted(s, key=lambda k: k[0].lower()) + env.filters['reject_pkg'] = \ + lambda s, req: s if not any(s[0] in sl for sl in req) else None return env @@ -361,7 +407,7 @@ def generate(args): args.template = file_template_list()[0] if not args.filename: args.filename = "python-" + args.name + '.' + args.template.rsplit('.', 1)[1] # take template file ending - print('generating spec file for {0}...'.format(args.name)) + print('generating spec file for {0} using {1}...'.format(args.name, args.template)) data = args.fetched_data['info'] durl = newest_download_url(args) source_url = data['source_url'] = (args.source_url or (durl and durl['url'])) diff --git a/py2pack/requires.py b/py2pack/requires.py index f960794..0839189 100644 --- a/py2pack/requires.py +++ b/py2pack/requires.py @@ -102,4 +102,20 @@ def _requirements_sanitize(req_list): (Requirement(s.split("#", maxsplit=1)[0]) for s in req_list) if _requirement_filter_by_marker(req) ) - return [" ".join(req) for req in filtered_req_list] + out_list = [] + for req in filtered_req_list: + # Convert '~=' operator to something rpm and deb can understand + # abc ~= 1.1.1 + # should be converted into something like (for rpm) + # + # Requires: python-abc >= 1.1.1, python-abc < 1.2 + # BuildRequires: %{python_module abc >= 1.1.1} + if len(req) > 1 and req[1] == '~=': + req[1] = '>=' + v = req[2].split('.') + v.pop() + v[-1] = str(int(v[-1]) + 1) + req += [req[0], '<', ".".join(v)] + out_list.append(req) + + return out_list diff --git a/py2pack/templates/fedora.spec b/py2pack/templates/fedora.spec index 611cbe8..b5bf2a3 100644 --- a/py2pack/templates/fedora.spec +++ b/py2pack/templates/fedora.spec @@ -10,24 +10,15 @@ Summary: {{ summary }} License: {{ license }} URL: {{ home_page }} Source: {{ source_url|replace(version, '%{version}') }} - -BuildRequires: pyproject-rpm-macros -BuildRequires: python-devel -%if %{undefined python_module} -%define python_module() python3dist(%1) -%endif - -{%- set build_requires_plus_pip = ((build_requires if build_requires and build_requires is not none else []) + - ['pip']) %} -{%- for req in build_requires_plus_pip |sort %} -BuildRequires: %{python_module {{ req }}} +BuildRoot: %{_tmppath}/%{name}-%{version}-build +BuildRequires: python-devel {%- if requires_python %} {{ requires_python }} {% endif %} +{%- for req in requires %} +BuildRequires: {{ req|rpm_format_requires }} +Requires: {{ req|rpm_format_requires }} {%- endfor %} -{%- if (install_requires and install_requires is not none) or (tests_require and tests_require is not none) %} -# SECTION test requirements -%if %{with test} -{%- if install_requires and install_requires is not none %} -{%- for req in install_requires|reject("in",build_requires)|sort %} -BuildRequires: %{python_module {{ req }}} +{%- for req in install_requires %} +BuildRequires: {{ req|rpm_format_requires }} +Requires: {{ req|rpm_format_requires }} }} {%- endfor %} {%- endif %} {%- if tests_require and tests_require is not none %} @@ -50,7 +41,7 @@ Requires: %{python_module {{ req }}} {%- if extras_require and extras_require is not none %} {%- for reqlist in extras_require.values() %} {%- for req in reqlist %} -Suggests: %{python_module {{ req }}} +Suggests: {{ req|rpm_format_requires }} {%- endfor %} {%- endfor %} {%- endif %} diff --git a/py2pack/templates/mageia.spec b/py2pack/templates/mageia.spec index 5c2140d..3586013 100644 --- a/py2pack/templates/mageia.spec +++ b/py2pack/templates/mageia.spec @@ -11,17 +11,17 @@ Source: {{ source_url|replace(version, '%{version}') }} BuildRoot: %{_tmppath}/%{name}-%{version}-buildroot BuildRequires: python-devel {%- for req in requires %} -BuildRequires: python-{{ req|lower }} -Requires: pyhton-{{ req|lower }} +BuildRequires: {{ req|rpm_format_requires|lower }} +Requires: {{ req|rpm_format_requires|lower }} {%- endfor %} {%- for req in install_requires %} -BuildRequires: python-{{ req|lower }} -Requires: python-{{ req|lower }} +BuildRequires: {{ req|rpm_format_requires|lower }} +Requires: {{ req|rpm_format_requires|lower }} {%- endfor %} {%- if extras_require %} {%- for reqlist in extras_require.values() %} {%- for req in reqlist %} -Suggests: python-{{ req|lower }} +Suggests: {{ req|rpm_format_requires|lower }} {%- endfor %} {%- endfor %} {%- endif %} @@ -51,8 +51,10 @@ rm -rf %{buildroot} {%- if doc_files %} %doc {{ doc_files|join(" ") }} {%- endif %} +{%- if scripts %} {%- for script in scripts %} %{_bindir}/{{ script }} {%- endfor %} +{%- endif %} %{python_sitelib}/* diff --git a/py2pack/templates/opensuse-legacy.spec b/py2pack/templates/opensuse-legacy.spec index 349e50d..2afe7f9 100644 --- a/py2pack/templates/opensuse-legacy.spec +++ b/py2pack/templates/opensuse-legacy.spec @@ -25,13 +25,13 @@ Source: {{ source_url|replace(version, '%{version}') }} BuildRequires: python-setuptools {%- if install_requires and install_requires is not none %} {%- for req in install_requires|sort %} -BuildRequires: python-{{ req }} +BuildRequires: {{ req|rpm_format_requires}} {%- endfor %} {%- endif %} {%- if tests_require and tests_require is not none %} # test requirements {%- for req in tests_require|sort %} -BuildRequires: python-{{ req }} +BuildRequires: {{ req|rpm_format_requires}} {%- endfor %} {%- endif %} {%- if source_url.endswith('.zip') %} @@ -39,13 +39,13 @@ BuildRequires: unzip {%- endif %} {%- if install_requires and install_requires is not none %} {%- for req in install_requires|sort %} -Requires: python-{{ req }} +Requires: {{ req|rpm_format_requires}} {%- endfor %} {%- endif %} {%- if extras_require and extras_require is not none %} {%- for reqlist in extras_require.values() %} {%- for req in reqlist %} -Suggests: python-{{ req }} +Suggests: {{ req|rpm_format_requires}} {%- endfor %} {%- endfor %} {%- endif %} diff --git a/py2pack/templates/opensuse.dsc b/py2pack/templates/opensuse.dsc index 2561507..ca972df 100644 --- a/py2pack/templates/opensuse.dsc +++ b/py2pack/templates/opensuse.dsc @@ -5,11 +5,11 @@ Binary: python-{{ name|lower }} Maintainer: {{ user_name }} Architecture: any Standards-Version: 3.7.1 -Build-Depends: debhelper (>= 4.0.0), python-dev{% for req in requires %}, python-{{ req|lower|parenthesize_version }}{% endfor %}{% for req in install_requires %}, python-{{ req|lower|parenthesize_version }}{% endfor %} +Build-Depends: debhelper (>= 4.0.0), python-dev{% for req in requires %}, python-{{ req|parenthesize_version }}{% endfor %}{% for req in install_requires %}, python-{{ req|parenthesize_version }}{% endfor %} {%- if requires or install_requires %} -Depends: {% for req in requires %}python-{{ req|lower|parenthesize_version }}{{ ', ' if not loop.last or install_requires }}{% endfor %}{% for req in install_requires %}python-{{ req|lower|parenthesize_version }}{{ ', ' if not loop.last }}{% endfor %} +Depends: {% for req in requires %}python-{{ req|parenthesize_version }}{{ ', ' if not loop.last or install_requires }}{% endfor %}{% for req in install_requires %}python-{{ req|parenthesize_version }}{{ ', ' if not loop.last }}{% endfor %} {%- endif %} {%- if extras_require %} -Suggests: {% for reqlist in extras_require.values() %}{% for req in reqlist %}python-{{ req|lower|parenthesize_version }}{{ ', ' if not loop.last }}{%- endfor %}{{ ', ' if not loop.last }}{%- endfor %} +Suggests: {% for reqlist in extras_require.values() %}{% for req in reqlist %}python-{{ req|parenthesize_version }}{{ ', ' if not loop.last }}{%- endfor %}{{ ', ' if not loop.last }}{%- endfor %} {%- endif %} diff --git a/py2pack/templates/opensuse.spec b/py2pack/templates/opensuse.spec index 0d7286a..c5cb41a 100644 --- a/py2pack/templates/opensuse.spec +++ b/py2pack/templates/opensuse.spec @@ -25,20 +25,20 @@ URL: {{ home_page }} Source: {{ source_url|replace(version, '%{version}') }} BuildRequires: python-rpm-macros {%- set build_requires_plus_pip = ((build_requires if build_requires and build_requires is not none else []) + - ['pip']) %} -{%- for req in build_requires_plus_pip |sort %} -BuildRequires: %{python_module {{ req }}} + [['pip']]) %} +{%- for req in build_requires_plus_pip|sort_requires %} +BuildRequires: %{python_module {{ req|rpm_format_buildrequires }}} {%- endfor %} {%- if (install_requires and install_requires is not none) or (tests_require and tests_require is not none) %} # SECTION test requirements {%- if install_requires and install_requires is not none %} -{%- for req in install_requires|reject("in",build_requires)|sort %} -BuildRequires: %{python_module {{ req }}} +{%- for req in install_requires|reject("in",build_requires)|sort_requires %} +BuildRequires: %{python_module {{ req|rpm_format_buildrequires }}} {%- endfor %} {%- endif %} {%- if tests_require and tests_require is not none %} -{%- for req in tests_require|sort|reject("in",build_requires|sort) %} -BuildRequires: %{python_module {{ req }}} +{%- for req in tests_require|sort_requires|reject_pkg(build_requires) %} +BuildRequires: %{python_module {{ req|rpm_format_buildrequires }}} {%- endfor %} {%- endif %} # /SECTION @@ -48,14 +48,14 @@ BuildRequires: unzip {%- endif %} BuildRequires: fdupes {%- if install_requires and install_requires is not none %} -{%- for req in install_requires|sort %} -Requires: python-{{ req }} +{%- for req in install_requires|sort_requires %} +Requires: {{ req|rpm_format_requires }} {%- endfor %} {%- endif %} {%- if extras_require and extras_require is not none %} {%- for reqlist in extras_require.values() %} {%- for req in reqlist %} -Suggests: python-{{ req }} +Suggests: {{ req|rpm_format_requires }} {%- endfor %} {%- endfor %} {%- endif %} diff --git a/py2pack/utils.py b/py2pack/utils.py index 7f85191..15b48bd 100644 --- a/py2pack/utils.py +++ b/py2pack/utils.py @@ -34,6 +34,7 @@ from backports.entry_points_selectable import EntryPoint, EntryPoints + def _get_archive_filelist(filename): # type: (str) -> List[str] """Extract the list of files from a tar or zip archive. diff --git a/test/test_py2pack.py b/test/test_py2pack.py index 9726a6a..c682b67 100644 --- a/test/test_py2pack.py +++ b/test/test_py2pack.py @@ -114,39 +114,39 @@ def test__prepare_template_env(self): @data( ( {'install_requires': ["pywin32>=1.0;sys_platform=='win32'", 'monotonic>=0.1 #comment']}, - {'build_requires': ['setuptools', 'wheel'], - 'install_requires': ['monotonic >= 0.1']}, + {'build_requires': [['setuptools'], ['wheel']], + 'install_requires': [['monotonic', '>=', '0.1']]}, ), ( {'install_requires': 'six >=1.9,!=1.0 # comment\nfoobar>=0.1,>=0.5'}, - {'build_requires': ['setuptools', 'wheel'], - 'install_requires': ['six >= 1.9', 'foobar >= 0.1']} + {'build_requires': [['setuptools'], ['wheel']], + 'install_requires': [['six', '>=', '1.9'], ['foobar', '>=', '0.1']]} ), ( {'setup_requires': 'six >=1.9,!=1.0 # comment\nfoobar>=0.1,>=0.5'}, - {'build_requires': ['setuptools', 'wheel', 'six >= 1.9', 'foobar >= 0.1']} + {'build_requires': [['setuptools'], ['wheel'], ['six', '>=', '1.9'], ['foobar', '>=', '0.1']]} ), ( {'tests_require': ['six >=1.9', 'foobar>=0.1,>=0.5']}, - {'build_requires': ['setuptools', 'wheel'], - 'tests_require': ['six >= 1.9', 'foobar >= 0.1']} + {'build_requires': [['setuptools'], ['wheel']], + 'tests_require': [['six', '>=', '1.9'], ['foobar', '>=', '0.1']]} ), ( {'tests_require': 'six >=1.9\nfoobar>=0.1,>=0.5'}, - {'build_requires': ['setuptools', 'wheel'], - 'tests_require': ['six >= 1.9', 'foobar >= 0.1']} + {'build_requires': [['setuptools'], ['wheel']], + 'tests_require': [['six', '>=', '1.9'], ['foobar', '>=', '0.1']]} ), ( {'extras_require': {'extra1': ['foobar<=3.0, >= 2.1']}}, - {'build_requires': ['setuptools', 'wheel'], - 'extras_require': {'extra1': ['foobar >= 2.1']}} + {'build_requires': [['setuptools'], ['wheel']], + 'extras_require': {'extra1': [['foobar', '>=', '2.1']]}} ), ( {'extras_require': {'extra1': 'foobar<=3.0, >= 2.1\ntest1 # comment', 'extra2': ['test2']}}, - {'build_requires': ['setuptools', 'wheel'], - 'extras_require': {'extra1': ['foobar >= 2.1', 'test1'], - 'extra2': ['test2']}} + {'build_requires': [['setuptools'], ['wheel']], + 'extras_require': {'extra1': [['foobar', '>=', '2.1'], ['test1']], + 'extra2': [['test2']]}} ), ( {'build-system': {'requires': ['setuptools']}, @@ -155,10 +155,10 @@ def test__prepare_template_env(self): 'test': ['pytest']}, 'scripts': {'cmd1': 'foo:main', 'cmd2': 'bar:cmd2'}, 'gui-scripts': {'gui': 'foo:mainview'}}}, - {'build_requires': ['setuptools', 'wheel'], - 'install_requires': ['foo', 'bar >= 1', 'foobar > 2'], - 'extras_require': {'extra': ['extra1', 'extra2 > 2']}, - 'tests_require': ['pytest'], + {'build_requires': [['setuptools'], ['wheel']], + 'install_requires': [['foo'], ['bar', '>=', '1'], ['foobar', '>', '2']], + 'extras_require': {'extra': [['extra1'], ['extra2', '>', '2']]}, + 'tests_require': [['pytest']], 'console_scripts': ['cmd1', 'cmd2', 'gui']} ), ( @@ -169,17 +169,17 @@ def test__prepare_template_env(self): 'requires-extra': {'extra': ['extra1', 'extra2>2'], 'test': ['pytest']}}, 'scripts': {'cmd': 'foo:main'}}}}, - {'build_requires': ['flit_core'], - 'install_requires': ['foo', 'bar >= 1', 'foobar > 2'], - 'extras_require': {'extra': ['extra1', 'extra2 > 2']}, - 'tests_require': ['pytest'], + {'build_requires': [['flit_core']], + 'install_requires': [['foo'], ['bar', '>=', '1'], ['foobar', '>', '2']], + 'extras_require': {'extra': [['extra1'], ['extra2', '>', '2']]}, + 'tests_require': [['pytest']], 'console_scripts': ['cmd']} ), ( {'build-system': {'requires': ['hatchling']}, 'project': {'dependencies': ['foo', 'bar>=1', 'foobar>2,<3']}}, - {'build_requires': ['hatchling'], - 'install_requires': ['foo', 'bar >= 1', 'foobar > 2']} + {'build_requires': [['hatchling']], + 'install_requires': [['foo'], ['bar', '>=', '1'], ['foobar', '>', '2']]} ), ) @unpack diff --git a/test/test_requires.py b/test/test_requires.py index c303103..abf51fc 100644 --- a/test/test_requires.py +++ b/test/test_requires.py @@ -66,9 +66,9 @@ def test__requirement_find_lowest_possible(self, req, expected): self.assertEqual(list(py2pack.requires._requirement_find_lowest_possible(pkg)), expected) @data( - (["six", "monotonic>=0.1"], ["six", "monotonic >= 0.1"]), - (["monotonic>=1.0,>0.1"], ["monotonic > 0.1"]), - (["pywin32>=1.0;sys_platform=='win32' # PSF", "foobar>3"], ["foobar > 3"]) + (["six", "monotonic>=0.1"], [["six"], ['monotonic', '>=', '0.1']]), + (["monotonic>=1.0,>0.1"], [['monotonic', '>', '0.1']]), + (["pywin32>=1.0;sys_platform=='win32' # PSF", "foobar>3"], [['foobar', '>', '3']]) ) @unpack def test__requirements_sanitize(self, req_list, expected):