Skip to content

Commit

Permalink
Use build.project_wheel_metadata to extract metadata
Browse files Browse the repository at this point in the history
This patch uses build.project_wheel_metadata to extract more metadata
when it's not using setuptools and the metaextract package is not
useful.

This patch also includes some support for detecting scripts and test
dependencies for projects using poetry.

See ##177
  • Loading branch information
danigm authored and mcepl committed May 18, 2024
1 parent ae596e3 commit 99ee644
Show file tree
Hide file tree
Showing 5 changed files with 319 additions and 2 deletions.
14 changes: 13 additions & 1 deletion py2pack/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
59 changes: 59 additions & 0 deletions py2pack/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ classifiers = [
dependencies = [
"Jinja2",
"backports.entry_points_selectable",
"build",
"setuptools",
"metaextract",
"packaging",
Expand Down
239 changes: 239 additions & 0 deletions test/examples/poetry-opensuse-augmented.spec
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>"
]

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
8 changes: 7 additions & 1 deletion test/test_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import os
import os.path
import pwd
import sys

import pytest

Expand Down Expand Up @@ -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(".")
Expand All @@ -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():
Expand Down

0 comments on commit 99ee644

Please sign in to comment.