Skip to content

Commit

Permalink
convert '~=' requirements specifier
Browse files Browse the repository at this point in the history
As described in openSUSE#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: openSUSE#183
An schould also fix
FIXES: openSUSE#93
  • Loading branch information
M0ses committed Aug 8, 2024
1 parent c73c5bf commit 05ea1af
Show file tree
Hide file tree
Showing 10 changed files with 131 additions and 75 deletions.
60 changes: 53 additions & 7 deletions py2pack/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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


Expand All @@ -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']))
Expand Down
18 changes: 17 additions & 1 deletion py2pack/requires.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
27 changes: 9 additions & 18 deletions py2pack/templates/fedora.spec
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
Expand All @@ -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 %}
Expand Down
12 changes: 7 additions & 5 deletions py2pack/templates/mageia.spec
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
Expand Down Expand Up @@ -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}/*

8 changes: 4 additions & 4 deletions py2pack/templates/opensuse-legacy.spec
Original file line number Diff line number Diff line change
Expand Up @@ -25,27 +25,27 @@ 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') %}
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 %}
Expand Down
6 changes: 3 additions & 3 deletions py2pack/templates/opensuse.dsc
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}

20 changes: 10 additions & 10 deletions py2pack/templates/opensuse.spec
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 %}
Expand Down
1 change: 1 addition & 0 deletions py2pack/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
48 changes: 24 additions & 24 deletions test/test_py2pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']},
Expand All @@ -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']}
),
(
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions test/test_requires.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down

0 comments on commit 05ea1af

Please sign in to comment.