Skip to content

Commit

Permalink
Merge branch 'main' into error_no_urls
Browse files Browse the repository at this point in the history
  • Loading branch information
johannesjh committed Jul 7, 2024
2 parents acdf642 + 2497299 commit 744ab1b
Show file tree
Hide file tree
Showing 9 changed files with 383 additions and 408 deletions.
27 changes: 27 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// DevContainer Definition for req2flatpak development

// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/python
{
"name": "Python 3",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/python:3.8",

// Features to add to the dev container. More info: https://containers.dev/features.
"features": {
"ghcr.io/devcontainers-contrib/features/poetry:2": {},
"ghcr.io/devcontainers-contrib/features/pre-commit:2": {}
}

// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],

// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "pip3 install --user -r requirements.txt",

// Configure tool-specific properties.
// "customizations": {},

// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/python-poetry/poetry
rev: "f631c22f715e0712d99a7493caab3f8088a582ab" # frozen: 1.6.0
rev: "c85477da8a610a87133299f996f8d8a593aa7bff" # frozen: 1.8.0
hooks:
- id: poetry-check
- id: poetry-lock
Expand All @@ -22,7 +22,7 @@ repos:
name: isort (python)

- repo: https://github.com/psf/black
rev: e87737140f32d3cd7c44ede75f02dcd58e55820e # frozen: 23.9.1
rev: 3702ba224ecffbcec30af640c149f231d90aebdb # frozen: 24.4.2
hooks:
- id: black

Expand Down
11 changes: 8 additions & 3 deletions docs/source/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,13 @@ And it is possible to specify specific branches, commits and tags,
see `pip's documentation on VCS support <https://pip.pypa.io/en/stable/topics/vcs-support/>`_.


Simply Downloading and Copying the Script
-----------------------------------------
Downloading the (almost) Standalone Script
------------------------------------------

You can download the ``req2flatpak.py`` script to your computer and run it.
Simple as that.
You will need Python installed and the additional ``packaging`` package:

.. code-block:: bash
pip install packaging
./req2flatpak.py --help
572 changes: 301 additions & 271 deletions poetry.lock

Large diffs are not rendered by default.

9 changes: 4 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,11 @@ req2flatpak = 'req2flatpak:main'

[tool.poetry.dependencies]
python = "^3.7.2"
packaging = { version = "^21.3", optional = true }
pyyaml = { version = "^6.0", optional = true }
packaging = { version = "^21.3" }
pyyaml = { version = "^6.0.1", optional = true }

[tool.poetry.extras]
packaging = ["packaging"]
yaml = ["pyyaml"]
yaml = ["pyyaml"]

[tool.poetry.group.lint.dependencies]
pylama = { extras = [
Expand All @@ -29,7 +28,7 @@ pylama = { extras = [
"eradicate",
"toml",
], version = "^8.4.1" }
bandit = { extras = ["toml"], version = "^1.7.4" }
bandit = { extras = ["toml"], version = "^1.7.5" }

# type stubs for mypy linting
types-setuptools = "^65.5.0.2"
Expand Down
112 changes: 31 additions & 81 deletions req2flatpak.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@
import urllib.request
from contextlib import nullcontext, suppress
from dataclasses import asdict, dataclass, field
from importlib import metadata
from itertools import product
from typing import (
Any,
Dict,
FrozenSet,
Generator,
Hashable,
Iterable,
Expand All @@ -60,7 +60,9 @@
)
from urllib.parse import urlparse

import pkg_resources
import packaging.requirements as packaging_reqs
import packaging.tags
from packaging.utils import parse_wheel_filename

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -109,64 +111,14 @@ class InvalidWheelFilename(Exception):
"""An invalid wheel filename was found, users should refer to PEP 427."""


try:
# use packaging.tags functionality if available
from packaging.utils import parse_wheel_filename

def tags_from_wheel_filename(filename: str) -> Set[str]:
"""
Parses a wheel filename into a list of compatible platform tags.
Implemented using functionality from ``packaging.utils.parse_wheel_filename``.
"""
_, _, _, tags = parse_wheel_filename(filename)
return {str(tag) for tag in tags}

except ModuleNotFoundError:
# fall back to a local implementation
# that is heavily inspired by / almost vendored from the `packaging` package:
def tags_from_wheel_filename(filename: str) -> Set[str]:
"""
Parses a wheel filename into a list of compatible platform tags.
Implemented as (semi-)vendored functionality in req2flatpak.
"""
Tag = Tuple[str, str, str]

# the following code is based on packaging.tags.parse_tag,
# it is needed for the parse_wheel_filename function:
def parse_tag(tag: str) -> FrozenSet[Tag]:
tags: Set[Tag] = set()
interpreters, abis, platforms = tag.split("-")
for interpreter in interpreters.split("."):
for abi in abis.split("."):
for platform_ in platforms.split("."):
tags.add((interpreter, abi, platform_))
return frozenset(tags)

# the following code is based on packaging.utils.parse_wheel_filename:
# pylint: disable=redefined-outer-name
def parse_wheel_filename(
wheel_filename: str,
) -> Iterable[Tag]:
if not wheel_filename.endswith(".whl"):
raise InvalidWheelFilename(
"Error parsing wheel filename: "
"Invalid wheel filename (extension must be '.whl'): "
f"{wheel_filename}"
)
wheel_filename = wheel_filename[:-4]
dashes = wheel_filename.count("-")
if dashes not in (4, 5):
raise InvalidWheelFilename(
"Error parsing wheel filename: "
"Invalid wheel filename (wrong number of parts): "
f"{wheel_filename}"
)
parts = wheel_filename.split("-", dashes - 2)
return parse_tag(parts[-1])
def tags_from_wheel_filename(filename: str) -> Set[str]:
"""
Parses a wheel filename into a list of compatible platform tags.
return {"-".join(tag_tuple) for tag_tuple in parse_wheel_filename(filename)}
Implemented using functionality from ``packaging.utils.parse_wheel_filename``.
"""
_, _, _, tags = parse_wheel_filename(filename)
return {str(tag) for tag in tags}


# =============================================================================
Expand Down Expand Up @@ -271,17 +223,8 @@ def _get_current_python_version() -> List[str]:

@staticmethod
def _get_current_python_tags() -> List[str]:
try:
# pylint: disable=import-outside-toplevel
import packaging.tags

tags = [str(tag) for tag in packaging.tags.sys_tags()]
return tags
except ModuleNotFoundError as e:
logger.warning(
'Error trying to import the "packaging" package.', exc_info=e
)
return []
tags = [str(tag) for tag in packaging.tags.sys_tags()]
return tags

@classmethod
def from_current_interpreter(cls) -> Platform:
Expand Down Expand Up @@ -431,27 +374,32 @@ class RequirementsParser:
resolve dependencies.
"""

# based on: https://stackoverflow.com/a/59971236
# using functionality from pkg_resources.parse_requirements

@classmethod
def parse_string(cls, requirements_txt: str) -> List[Requirement]:
"""Parses requirements.txt string content into a list of Requirement objects."""

def validate_requirement(req: pkg_resources.Requirement) -> None:
def validate_requirement(req: packaging_reqs.Requirement) -> None:
assert (
len(req.specs) == 1
len(req.specifier) == 1
), "Error parsing requirements: A single version number must be specified."
assert (
req.specs[0][0] == "=="
list(req.specifier)[0].operator == "=="
), "Error parsing requirements: The exact version must specified as 'package==version'."

def make_requirement(req: pkg_resources.Requirement) -> Requirement:
def make_requirement(req: packaging_reqs.Requirement) -> Requirement:
validate_requirement(req)
return Requirement(package=req.project_name, version=req.specs[0][1])
return Requirement(package=req.name, version=list(req.specifier)[0].version)

reqs = pkg_resources.parse_requirements(requirements_txt)
return [make_requirement(req) for req in reqs]
requirements = []
for line in requirements_txt.splitlines():
if not (line := line.strip()):
continue
if line.startswith("#"):
continue
req = packaging_reqs.Requirement(line)
requirements.append(make_requirement(req))

return requirements

@classmethod
def parse_file(cls, file) -> List[Requirement]:
Expand Down Expand Up @@ -761,7 +709,9 @@ def main(): # pylint: disable=too-many-branches
# print installed packages if requested, and exit
if options.installed_packages:
# pylint: disable=not-an-iterable
pkgs = {p.key: p.version for p in pkg_resources.working_set}
pkgs = {
p.metadata["Name"]: p.metadata["Version"] for p in metadata.distributions()
}
for pkg, version in pkgs.items():
print(f"{pkg}=={version}", file=output_stream)
parser.exit()
Expand Down
1 change: 1 addition & 0 deletions tests/test_pypi_client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Automated tests for :class:req2flatpak.PypiClient."""

import contextlib
import unittest
from pathlib import Path
Expand Down
1 change: 1 addition & 0 deletions tests/test_req2flatpak.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Tests for req2flatpak's commandline interface."""

import json
import subprocess
import tempfile
Expand Down
54 changes: 8 additions & 46 deletions tests/test_tags_from_wheel_filename.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
"""Test for the parsing of tags in wheel filenames."""

import unittest
from importlib import reload
from typing import Callable, Dict, Optional, Set
from unittest import mock
from typing import Dict, Set

import req2flatpak

Expand All @@ -12,24 +10,10 @@ class TestTagsFromWheelFilename(unittest.TestCase):
"""
Compatibility test for py:meth:`~req2flatpak.tags_from_wheel_filename`.
The need for this test comes from the fact that req2flatpak contains two
alternative implementations of ``tags_from_wheel_filename``:
A vendored implementation that does not need the packaging package.
And another implementation that relies on functionality from the packaging
package.
Both implementations of ``tags_from_wheel_filename`` are tested using the
same testdata to ensure that their behavior is correct and equal.
The test thus serves as regression test for the vendored implementation.
And it also serves as future-compatibility test to safeguard against changes
This tests serves as future-compatibility test to safeguard against changes
in the packaging package.
"""

implementations: Dict[str, Optional[Callable]] = {
"with_packaging": None,
"without_packaging": None,
}

data: Dict[str, Set[str]] = {
"pandas-1.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl": {
"cp310-cp310-manylinux_2_17_aarch64",
Expand All @@ -43,34 +27,12 @@ class TestTagsFromWheelFilename(unittest.TestCase):
},
}

@classmethod
def setUpClass(cls):
"""Retrieves the two implementations of ``tags_from_wheel_filename``."""

# get the implementation that does not use the ``packaging`` package
# related work:
# - how to mock a ModuleNotFoundError: https://stackoverflow.com/a/67884737
# - how to ignore certain imports: https://stackoverflow.com/a/63353431
with mock.patch.dict("sys.modules", {"packaging.utils": None}):
reload(req2flatpak)
cls.implementations[
"without_packaging"
] = req2flatpak.tags_from_wheel_filename

# get the implementation that uses the ``packaging`` package
reload(req2flatpak)
cls.implementations["with_packaging"] = req2flatpak.tags_from_wheel_filename

# ensure that we got two different implementations
assert (
cls.implementations["without_packaging"]
!= cls.implementations["with_packaging"]
)

def test(self):
"""Tests the behavior of ``tags_from_wheel_filename``."""
for filename, expected_tags in self.data.items():
for description, func in self.implementations.items():
with self.subTest(filename=filename, impl=description):
parsed_tags = func(filename) # pylint: disable=not-callable
self.assertEqual(parsed_tags, expected_tags)
with self.subTest(filename=filename):
parsed_tags = req2flatpak.tags_from_wheel_filename(
filename
) # pylint: disable=not-callable
self.assertEqual(parsed_tags, expected_tags)
self.assertEqual(parsed_tags, expected_tags)

0 comments on commit 744ab1b

Please sign in to comment.