diff --git a/py2pack/__init__.py b/py2pack/__init__.py index f4d7a07..f9d3bad 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,56 @@ 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, pfx): + req[0] = pfx + req[0] + part1 = " ".join(req[0:3]) + part2 = "" + if len(req) > 3: + req[3] = pfx + req[3] + part2 = ", " + " ".join(req[3:]) + return part1 + part2 + + def _parenthesize_version(req): + ret = req[0].lower() + ver = [] + if len(req) > 1: + 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: + ret += ", ".join(ver) + return ret + + def reject_pkg(s, req): + nms = list(map(lambda n: n[0], req)) + ret = [] + for n in s: + if n[0] not in nms: + ret.append(n) + 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, p: _rpm_format_requires(s, p) + env.filters['sort_requires'] = \ + lambda s: sorted(s, key=lambda k: k[0].lower()) + env.filters['reject_pkg'] = reject_pkg + return env @@ -361,7 +412,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..fd5fc52 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("python-") }} +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("python-") }} +Requires: {{ req|rpm_format_requires("python-") }} {%- 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("python-") }} {%- endfor %} {%- endfor %} {%- endif %} diff --git a/py2pack/templates/mageia.spec b/py2pack/templates/mageia.spec index 5c2140d..60b174c 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("python-")|lower }} +Requires: {{ req|rpm_format_requires("python-")|lower }} {%- endfor %} {%- for req in install_requires %} -BuildRequires: python-{{ req|lower }} -Requires: python-{{ req|lower }} +BuildRequires: {{ req|rpm_format_requires("python-")|lower }} +Requires: {{ req|rpm_format_requires("python-")|lower }} {%- endfor %} {%- if extras_require %} {%- for reqlist in extras_require.values() %} {%- for req in reqlist %} -Suggests: python-{{ req|lower }} +Suggests: {{ req|rpm_format_requires("python-")|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..87bb615 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("python-") }} {%- 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("python-") }} {%- 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("python-") }} {%- 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("python-") }} {%- endfor %} {%- endfor %} {%- endif %} diff --git a/py2pack/templates/opensuse.dsc b/py2pack/templates/opensuse.dsc index 2561507..b2015e6 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|reject_pkg(build_requires) %}, python-{{ req|parenthesize_version }}{% endfor %}{% for req in build_requires %}, python-{{ req|parenthesize_version }}{% endfor %}{% for req in tests_require|reject_pkg(build_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..d006787 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_pkg(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|reject_pkg(build_requires)|sort_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("python-") }} {%- 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("python-") }} {%- endfor %} {%- endfor %} {%- endif %} diff --git a/py2pack/utils.py b/py2pack/utils.py index 3f24e3e..15b48bd 100644 --- a/py2pack/utils.py +++ b/py2pack/utils.py @@ -129,10 +129,11 @@ def get_setuptools_scripts(data): Returns: list of script names """ - entry_points = data.get('entry_points', None) - if isinstance(entry_points, str): - eps = EntryPoints(EntryPoints._from_text(entry_points)) - elif isinstance(entry_points, dict): + if "entry_points" not in data or not data['entry_points']: + return [] + if isinstance(data["entry_points"], str): + eps = EntryPoints(EntryPoints._from_text(data["entry_points"])) + elif isinstance(data["entry_points"], dict): eps = EntryPoints([EntryPoint(*map(str.strip, entry.split("=", 1)), groupname) for groupname, group in entry_points.items() for entry in group diff --git a/test/examples/sampleproject-fedora.spec b/test/examples/sampleproject-fedora.spec new file mode 100644 index 0000000..83e3d40 --- /dev/null +++ b/test/examples/sampleproject-fedora.spec @@ -0,0 +1,75 @@ +# +# spec file for package python-sampleproject +# +# Copyright (c) __YEAR__ __USER__. +# + +Name: python-sampleproject +Version: 3.0.0 +Release: 0 +Summary: A sample Python project +License: Copyright (c) 2016 The Python Packaging Authority (PyPA) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. (FIXME:No SPDX) +URL: +Source: https://files.pythonhosted.org/packages/source/s/sampleproject/sampleproject-%{version}.tar.gz +BuildRoot: %{_tmppath}/%{name}-%{version}-build +BuildRequires: python-devel >=3.7 + +%description +# A sample Python project + +![Python Logo](https://www.python.org/static/community_logos/python-logo.png "Sample inline image") + +A sample project that exists as an aid to the [Python Packaging User +Guide][packaging guide]'s [Tutorial on Packaging and Distributing +Projects][distribution tutorial]. + +This project does not aim to cover best practices for Python project +development as a whole. For example, it does not provide guidance or tool +recommendations for version control, documentation, or testing. + +[The source for this project is available here][src]. + +The metadata for a Python project is defined in the `pyproject.toml` file, +an example of which is included in this project. You should edit this file +accordingly to adapt this sample project to your needs. + +---- + +This is the README file for the project. + +The file should use UTF-8 encoding and can be written using +[reStructuredText][rst] or [markdown][md use] with the appropriate [key set][md +use]. It will be used to generate the project webpage on PyPI and will be +displayed as the project homepage on common code-hosting services, and should be +written for that purpose. + +Typical contents for this file would include an overview of the project, basic +usage examples, etc. Generally, including the project changelog in here is not a +good idea, although a simple “What's New” section for the most recent version +may be appropriate. + +[packaging guide]: https://packaging.python.org +[distribution tutorial]: https://packaging.python.org/tutorials/packaging-projects/ +[src]: https://github.com/pypa/sampleproject +[rst]: http://docutils.sourceforge.net/rst.html +[md]: https://tools.ietf.org/html/rfc7764#section-3.5 "CommonMark variant" +[md use]: https://packaging.python.org/specifications/core-metadata/#description-content-type-optional + + +%prep +%setup -q -n sampleproject-%{version} + +%build +python setup.py build + +%install +python setup.py install --prefix=%{_prefix} --root=%{buildroot} + +%clean +rm -rf %{buildroot} + +%files +%defattr(-,root,root,-) +%{python_sitelib}/* + +%changelog 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):