diff --git a/py2pack/__init__.py b/py2pack/__init__.py index 5c0dc81..a43bb09 100644 --- a/py2pack/__init__.py +++ b/py2pack/__init__.py @@ -35,7 +35,8 @@ import py2pack.requires from py2pack import version as py2pack_version from py2pack.utils import (_get_archive_filelist, get_pyproject_table, - parse_pyproject, get_setuptools_scripts) + parse_pyproject, get_setuptools_scripts, + get_metadata) from email import parser @@ -188,6 +189,7 @@ def _canonicalize_setup_data(data): tests_require = ( get_pyproject_table(data, "project.optional-dependencies.test") or get_pyproject_table(data, "tool.flit.metadata.requires-extra.test") or + get_pyproject_table(data, "tool.poetry.group.test.dependencies") or data.get("tests_require", None)) if tests_require: # Setuptools: tests_require may be a string, convert to list of strings: @@ -226,6 +228,7 @@ def _canonicalize_setup_data(data): console_scripts += list(get_pyproject_table(data, "project.scripts", notfound={}).keys()) console_scripts += list(get_pyproject_table(data, "project.gui-scripts", notfound={}).keys()) console_scripts += list(get_pyproject_table(data, "tool.flit.scripts", notfound={}).keys()) + console_scripts += list(get_pyproject_table(data, "tool.poetry.scripts", notfound={}).keys()) if console_scripts: # remove duplicates, preserver order data["console_scripts"] = list(dict.fromkeys(console_scripts)) @@ -263,6 +266,7 @@ def _augment_data_from_tarball(args, filename, data): except KeyError: # No build system specified in pyproject.toml: legacy setuptools buildrequires = ['setuptools'] + if any(['setuptools' in br for br in buildrequires]): try: data_archive = meta_utils.from_archive(filename) @@ -271,6 +275,14 @@ def _augment_data_from_tarball(args, filename, data): warnings.warn("Could not get setuptools information from tarball {}: {}. " "Valuable information for the generation might be missing." .format(filename, exc)) + else: + try: + mdata = get_metadata(filename) + data.update(mdata) + except Exception as exc: + warnings.warn("Could not get metadata information from tarball {}: {}. " + "Valuable information for the generation might be missing." + .format(filename, exc)) names = _get_archive_filelist(filename) _canonicalize_setup_data(data) diff --git a/py2pack/utils.py b/py2pack/utils.py index 9fcbc36..3f24e3e 100644 --- a/py2pack/utils.py +++ b/py2pack/utils.py @@ -17,6 +17,12 @@ """Module containing utility functions that fit nowhere else.""" +import os +import tempfile +import shutil +from contextlib import contextmanager +from build.util import project_wheel_metadata + from typing import List # noqa: F401, pylint: disable=unused-import try: import tomllib as toml @@ -136,3 +142,56 @@ def get_setuptools_scripts(data): scripts = (list(eps.select(group="console_scripts").names) + list(eps.select(group="gui_scripts").names)) return scripts + + +@contextmanager +def _extract_to_tempdir(archive_filename): + """extract the given tarball or zipfile to a tempdir and change + the cwd to the new tempdir. Delete the tempdir at the end""" + if not os.path.exists(archive_filename): + raise Exception("Archive '%s' does not exist" % (archive_filename)) + + tempdir = tempfile.mkdtemp(prefix="py2pack_") + current_cwd = os.getcwd() + try: + if tarfile.is_tarfile(archive_filename): + with tarfile.open(archive_filename) as f: + f.extractall(tempdir) + elif zipfile.is_zipfile(archive_filename): + with zipfile.ZipFile(archive_filename) as f: + f.extractall(tempdir) + else: + raise Exception("Can not extract '%s'. " + "Not a tar or zip file" % archive_filename) + os.chdir(tempdir) + yield tempdir + finally: + os.chdir(current_cwd) + shutil.rmtree(tempdir) + + +def get_metadata(filename): + """ + Extracts metadata from the archive filename + """ + data = {} + + with _extract_to_tempdir(filename) as root_dir: + dir_list, *_ = os.listdir(root_dir) + path = os.path.join(root_dir, dir_list) + mdata = project_wheel_metadata(path, isolated=True) + + data['home_page'] = mdata.get('Home-page') + data['name'] = mdata.get('Name') + data['version'] = mdata.get('Version') + data['description'] = mdata.get('Description') + data['summary'] = mdata.get('Summary') + data['license'] = mdata.get('License') + data['keywords'] = mdata.get('Keywords') + data['author'] = mdata.get('Author') + data['author_email'] = mdata.get('Author-email') + data['maintainer'] = mdata.get('Maintainer') + data['maintainer_email'] = mdata.get('Maintainer-email') + data['install_requires'] = mdata.get_all('Requires-Dist') + + return data diff --git a/pyproject.toml b/pyproject.toml index 09ccd44..fd70b49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ classifiers = [ dependencies = [ "Jinja2", "backports.entry_points_selectable", + "build", "setuptools", "metaextract", "packaging", diff --git a/test/examples/poetry-opensuse-augmented.spec b/test/examples/poetry-opensuse-augmented.spec new file mode 100644 index 0000000..f709636 --- /dev/null +++ b/test/examples/poetry-opensuse-augmented.spec @@ -0,0 +1,239 @@ +# +# spec file for package python-poetry +# +# Copyright (c) 2023 SUSE LLC +# +# All modifications and additions to the file contributed by third parties +# remain the property of their copyright owners, unless otherwise agreed +# upon. The license for this file, and modifications and additions to the +# file, is the same license as for the pristine package itself (unless the +# license for the pristine package is not an Open Source License, in which +# case the license is the MIT License). An "Open Source License" is a +# license that conforms to the Open Source Definition (Version 1.9) +# published by the Open Source Initiative. + +# Please submit bugfixes or comments via https://bugs.opensuse.org/ +# + + +Name: python-poetry +Version: 1.5.1 +Release: 0 +Summary: Python dependency management and packaging made easy +License: MIT +URL: https://python-poetry.org/ +Source: https://files.pythonhosted.org/packages/source/p/poetry/poetry-%{version}.tar.gz +BuildRequires: python-rpm-macros +BuildRequires: %{python_module pip} +BuildRequires: %{python_module poetry-core >= 1.5.0} +# SECTION test requirements +BuildRequires: %{python_module build >= 0.10.0} +BuildRequires: %{python_module cachecontrol >= 0.12.9} +BuildRequires: %{python_module cleo >= 2.0.0} +BuildRequires: %{python_module crashtest >= 0.4.1} +BuildRequires: %{python_module dulwich >= 0.21.2} +BuildRequires: %{python_module filelock >= 3.8.0} +BuildRequires: %{python_module html5lib >= 1.0} +BuildRequires: %{python_module installer >= 0.7.0} +BuildRequires: %{python_module jsonschema >= 4.10.0} +BuildRequires: %{python_module keyring >= 23.9.0} +BuildRequires: %{python_module lockfile >= 0.12.2} +BuildRequires: %{python_module packaging >= 20.4} +BuildRequires: %{python_module pexpect >= 4.7.0} +BuildRequires: %{python_module pkginfo >= 1.9.4} +BuildRequires: %{python_module platformdirs >= 3.0.0} +BuildRequires: %{python_module poetry-core == 1.6.1} +BuildRequires: %{python_module poetry-plugin-export >= 1.4.0} +BuildRequires: %{python_module pyproject-hooks >= 1.0.0} +BuildRequires: %{python_module requests >= 2.18} +BuildRequires: %{python_module requests-toolbelt >= 0.9.1} +BuildRequires: %{python_module shellingham >= 1.5} +BuildRequires: %{python_module tomlkit >= 0.11.4} +BuildRequires: %{python_module trove-classifiers >= 2022.5.19} +BuildRequires: %{python_module urllib3 >= 1.26.0} +BuildRequires: %{python_module virtualenv >= 20.22.0} +BuildRequires: %{python_module cachy} +BuildRequires: %{python_module deepdiff} +BuildRequires: %{python_module httpretty} +BuildRequires: %{python_module pytest} +BuildRequires: %{python_module pytest-cov} +BuildRequires: %{python_module pytest-mock} +BuildRequires: %{python_module pytest-randomly} +BuildRequires: %{python_module pytest-xdist} +BuildRequires: %{python_module zipp} +# /SECTION +BuildRequires: fdupes +Requires: python-build >= 0.10.0 +Requires: python-cachecontrol >= 0.12.9 +Requires: python-cleo >= 2.0.0 +Requires: python-crashtest >= 0.4.1 +Requires: python-dulwich >= 0.21.2 +Requires: python-filelock >= 3.8.0 +Requires: python-html5lib >= 1.0 +Requires: python-installer >= 0.7.0 +Requires: python-jsonschema >= 4.10.0 +Requires: python-keyring >= 23.9.0 +Requires: python-lockfile >= 0.12.2 +Requires: python-packaging >= 20.4 +Requires: python-pexpect >= 4.7.0 +Requires: python-pkginfo >= 1.9.4 +Requires: python-platformdirs >= 3.0.0 +Requires: python-poetry-core == 1.6.1 +Requires: python-poetry-plugin-export >= 1.4.0 +Requires: python-pyproject-hooks >= 1.0.0 +Requires: python-requests >= 2.18 +Requires: python-requests-toolbelt >= 0.9.1 +Requires: python-shellingham >= 1.5 +Requires: python-tomlkit >= 0.11.4 +Requires: python-trove-classifiers >= 2022.5.19 +Requires: python-urllib3 >= 1.26.0 +Requires: python-virtualenv >= 20.22.0 +BuildArch: noarch +%python_subpackages + +%description +# Poetry: Python packaging and dependency management made easy + +[![Stable Version](https://img.shields.io/pypi/v/poetry?label=stable)][PyPI Releases] +[![Pre-release Version](https://img.shields.io/github/v/release/python-poetry/poetry?label=pre-release&include_prereleases&sort=semver)][PyPI Releases] +[![Python Versions](https://img.shields.io/pypi/pyversions/poetry)][PyPI] +[![Download Stats](https://img.shields.io/pypi/dm/poetry)](https://pypistats.org/packages/poetry) +[![Discord](https://img.shields.io/discord/487711540787675139?logo=discord)][Discord] + +Poetry helps you declare, manage and install dependencies of Python projects, +ensuring you have the right stack everywhere. + +![Poetry Install](https://raw.githubusercontent.com/python-poetry/poetry/master/assets/install.gif) + +Poetry replaces `setup.py`, `requirements.txt`, `setup.cfg`, `MANIFEST.in` and `Pipfile` with a simple `pyproject.toml` +based project format. + +```toml +[tool.poetry] +name = "my-package" +version = "0.1.0" +description = "The description of the package" + +license = "MIT" + +authors = [ + "Sébastien Eustace " +] + +repository = "https://github.com/python-poetry/poetry" +homepage = "https://python-poetry.org" + +# README file(s) are used as the package description +readme = ["README.md", "LICENSE"] + +# Keywords (translated to tags on the package index) +keywords = ["packaging", "poetry"] + +[tool.poetry.dependencies] +# Compatible Python versions +python = ">=3.8" +# Standard dependency with semver constraints +aiohttp = "^3.8.1" +# Dependency with extras +requests = { version = "^2.28", extras = ["security"] } +# Version-specific dependencies with prereleases allowed +tomli = { version = "^2.0.1", python = "<3.11", allow-prereleases = true } +# Git dependencies +cleo = { git = "https://github.com/python-poetry/cleo.git", branch = "master" } +# Optional dependencies (installed by extras) +pendulum = { version = "^2.1.2", optional = true } + +# Dependency groups are supported for organizing your dependencies +[tool.poetry.group.dev.dependencies] +pytest = "^7.1.2" +pytest-cov = "^3.0" + +# ...and can be installed only when explicitly requested +[tool.poetry.group.docs] +optional = true +[tool.poetry.group.docs.dependencies] +Sphinx = "^5.1.1" + +# Python-style entrypoints and scripts are easily expressed +[tool.poetry.scripts] +my-script = "my_package:main" +``` + +## Installation + +Poetry supports multiple installation methods, including a simple script found at [install.python-poetry.org]. For full +installation instructions, including advanced usage of the script, alternate install methods, and CI best practices, see +the full [installation documentation]. + +## Documentation + +[Documentation] for the current version of Poetry (as well as the development branch and recently out of support +versions) is available from the [official website]. + +## Contribute + +Poetry is a large, complex project always in need of contributors. For those new to the project, a list of +[suggested issues] to work on in Poetry and poetry-core is available. The full [contributing documentation] also +provides helpful guidance. + +## Resources + +* [Releases][PyPI Releases] +* [Official Website] +* [Documentation] +* [Issue Tracker] +* [Discord] + + [PyPI]: https://pypi.org/project/poetry/ + [PyPI Releases]: https://pypi.org/project/poetry/#history + [Official Website]: https://python-poetry.org + [Documentation]: https://python-poetry.org/docs/ + [Issue Tracker]: https://github.com/python-poetry/poetry/issues + [Suggested Issues]: https://github.com/python-poetry/poetry/contribute + [Contributing Documentation]: https://python-poetry.org/docs/contributing + [Discord]: https://discord.com/invite/awxPgve + [install.python-poetry.org]: https://install.python-poetry.org + [Installation Documentation]: https://python-poetry.org/docs/#installation + +## Related Projects + +* [poetry-core](https://github.com/python-poetry/poetry-core): PEP 517 build-system for Poetry projects, and +dependency-free core functionality of the Poetry frontend +* [poetry-plugin-export](https://github.com/python-poetry/poetry-plugin-export): Export Poetry projects/lock files to +foreign formats like requirements.txt +* [poetry-plugin-bundle](https://github.com/python-poetry/poetry-plugin-bundle): Install Poetry projects/lock files to +external formats like virtual environments +* [install.python-poetry.org](https://github.com/python-poetry/install.python-poetry.org): The official Poetry +installation script +* [website](https://github.com/python-poetry/website): The official Poetry website and blog + + + +%prep +%autosetup -p1 -n poetry-%{version} + +%build +%pyproject_wheel + +%install +%pyproject_install +%python_clone -a %{buildroot}%{_bindir}/poetry +%python_expand %fdupes %{buildroot}%{$python_sitelib} + +%check +CHOOSE: %pytest OR %pyunittest -v OR CUSTOM + +%post +%python_install_alternative poetry + +%postun +%python_uninstall_alternative poetry + +%files %{python_files} +%doc README.md +%license LICENSE +%python_alternative %{_bindir}/poetry +%{python_sitelib}/poetry +%{python_sitelib}/poetry-%{version}.dist-info + +%changelog diff --git a/test/test_template.py b/test/test_template.py index c08ea40..9557eab 100644 --- a/test/test_template.py +++ b/test/test_template.py @@ -20,6 +20,7 @@ import os import os.path import pwd +import sys import pytest @@ -52,9 +53,11 @@ class Args(object): ('opensuse.spec', True)]) @pytest.mark.parametrize('project, version', [('py2pack', '0.8.5'), # legacy setup.py sdist without pyproject.toml - ('sampleproject', '3.0.0')]) # PEP517 only sdist without setup.py + ('sampleproject', '3.0.0'), # PEP517 only sdist without setup.py + ('poetry', '1.5.1')]) # poetry build system def test_template(tmpdir, template, fetch_tarball, project, version): """ Test if generated specfile equals to stored one. """ + args = Args() args.template = template base, ext = template.split(".") @@ -64,6 +67,9 @@ def test_template(tmpdir, template, fetch_tarball, project, version): args.name = project args.version = version reference = os.path.join(compare_dir, f'{args.name}-{filename}') + if project == 'poetry' and sys.version_info < (3, 11): + pytest.xfail("Different requirements for python < 3.11") + if not os.path.exists(reference): pytest.xfail("No reference template available") with tmpdir.as_cwd():