commit:     8e70727b745e8259fe0b9e5821b01947143367f5
Author:     Michał Górny <mgorny <AT> gentoo <DOT> org>
AuthorDate: Tue Feb  7 17:36:15 2023 +0000
Commit:     Arthur Zamarin <arthurzam <AT> gentoo <DOT> org>
CommitDate: Wed Feb 15 17:49:28 2023 +0000
URL:        
https://gitweb.gentoo.org/proj/pkgcore/pkgcheck.git/commit/?id=8e70727b

checks: Add a check for PyPI URL replacement with pypi.eclass

Signed-off-by: Michał Górny <mgorny <AT> gentoo.org>
Closes: https://github.com/pkgcore/pkgcheck/pull/543
Signed-off-by: Arthur Zamarin <arthurzam <AT> gentoo.org>

 src/pkgcheck/checks/python.py                      | 153 ++++++++++-
 .../PythonInlinePyPIURI/expected.json              |  20 ++
 .../PythonInlinePyPIURI/fix.patch                  | 304 +++++++++++++++++++++
 .../PythonInlinePyPIURL/Manifest                   |  10 +
 .../PythonInlinePyPIURL-0.ebuild                   |  15 +
 .../PythonInlinePyPIURL-1_alpha1-r1.ebuild         |  16 ++
 .../PythonInlinePyPIURL-1_alpha1.ebuild            |  13 +
 .../PythonInlinePyPIURL-2_p4-r1.ebuild             |  16 ++
 .../PythonInlinePyPIURL-2_p4.ebuild                |  13 +
 .../PythonInlinePyPIURL-3-r1.ebuild                |  16 ++
 .../PythonInlinePyPIURL-3.ebuild                   |  21 ++
 .../PythonInlinePyPIURL-4.ebuild                   |  16 ++
 .../PythonInlinePyPIURL/metadata.xml               |   7 +
 .../python-inline-pypi-url/Manifest                |   5 +
 .../python-inline-pypi-url/metadata.xml            |   7 +
 .../python-inline-pypi-url-0.ebuild                |  12 +
 .../python-inline-pypi-url-1.ebuild                |  12 +
 .../python-inline-pypi-url-100.ebuild              |  12 +
 .../python-inline-pypi-url-1000-r1.ebuild          |  12 +
 .../python-inline-pypi-url-1000-r100.ebuild        |  12 +
 .../python-inline-pypi-url-1000-r101.ebuild        |  12 +
 .../python-inline-pypi-url-1000-r200.ebuild        |  12 +
 .../python-inline-pypi-url-1000.ebuild             |  12 +
 .../python-inline-pypi-url-200.ebuild              |  12 +
 .../repos/python/app-arch/unzip/unzip-0.ebuild     |   4 +
 testdata/repos/python/eclass/pypi.eclass           | 107 ++++++++
 testdata/repos/python/metadata/stubs               |   1 +
 testdata/repos/python/profiles/thirdpartymirrors   |   1 +
 28 files changed, 851 insertions(+), 2 deletions(-)

diff --git a/src/pkgcheck/checks/python.py b/src/pkgcheck/checks/python.py
index 8cd00782..291a56b4 100644
--- a/src/pkgcheck/checks/python.py
+++ b/src/pkgcheck/checks/python.py
@@ -1,5 +1,6 @@
 import itertools
 import re
+import typing
 from collections import defaultdict
 from operator import attrgetter
 
@@ -23,6 +24,15 @@ IUSE_PREFIX_S = "python_single_target_"
 
 GITHUB_ARCHIVE_RE = re.compile(r"^https://github\.com/[^/]+/[^/]+/archive/";)
 SNAPSHOT_RE = re.compile(r"[a-fA-F0-9]{40}\.tar\.gz$")
+PYPI_URI_PREFIX = "https://files.pythonhosted.org/packages/";
+PYPI_SDIST_URI_RE = re.compile(
+    re.escape(PYPI_URI_PREFIX) + r"source/[^/]/(?P<package>[^/]+)/"
+    
r"(?P<fn_package>(?P=package)|[^/-]+)-(?P<version>[^/]+)(?P<suffix>\.tar\.gz|\.zip)$"
+)
+PYPI_WHEEL_URI_RE = re.compile(
+    re.escape(PYPI_URI_PREFIX) + r"(?P<pytag>[^/]+)/[^/]/(?P<package>[^/]+)/"
+    
r"(?P<fn_package>[^/-]+)-(?P<version>[^/-]+)-(?P=pytag)-(?P<abitag>[^/]+)\.whl$"
+)
 USE_FLAGS_PYTHON_USEDEP = re.compile(r"\[(.+,)?\$\{PYTHON_USEDEP\}(,.+)?\]$")
 
 PROJECT_SYMBOL_NORMALIZE_RE = re.compile(r"[-_.]+")
@@ -687,11 +697,44 @@ class PythonGHDistfileSuffix(results.VersionResult, 
results.Warning):
         )
 
 
+class PythonInlinePyPIURI(results.VersionResult, results.Warning):
+    """PyPI URI used inline instead of via pypi.eclass"""
+
+    def __init__(
+        self,
+        url: str,
+        replacement: typing.Optional[tuple[str, ...]] = None,
+        normalize: typing.Optional[bool] = None,
+        append: typing.Optional[bool] = None,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+        self.url = url
+        self.replacement = tuple(replacement) if replacement is not None else 
None
+        self.normalize = normalize
+        self.append = append
+
+    @property
+    def desc(self) -> str:
+        if self.replacement is None:
+            no_norm = "" if self.normalize else "set PYPI_NO_NORMALIZE=1, "
+            final = "use SRC_URI+= for other URIs" if self.append else "remove 
SRC_URI"
+            return (
+                "inline PyPI URI found matching pypi.eclass default, inherit 
the eclass, "
+                f"{no_norm}and {final} instead"
+            )
+        else:
+            return (
+                f"inline PyPI URI found: {self.url}, inherit pypi.eclass and 
replace with "
+                f"$({' '.join(self.replacement)})"
+            )
+
+
 class PythonFetchableCheck(Check):
-    """Perform Python-specific checks on fetchables."""
+    """Perform Python-specific checks to fetchables."""
 
     required_addons = (addons.UseAddon,)
-    known_results = frozenset({PythonGHDistfileSuffix})
+    known_results = frozenset({PythonGHDistfileSuffix, PythonInlinePyPIURI})
 
     def __init__(self, *args, use_addon):
         super().__init__(*args)
@@ -718,6 +761,111 @@ class PythonFetchableCheck(Check):
                     yield PythonGHDistfileSuffix(f.filename, uri, pkg=pkg)
                     break
 
+    @staticmethod
+    def simplify_pn_pv(pn: str, pv: str, pkg, allow_none: bool) -> tuple[str, 
str]:
+        if pv == pkg.version:
+            pv = None if allow_none else '"${PV}"'
+
+        if pn == pkg.package:
+            pn = None if pv is None else '"${PN}"'
+        # check for common PN transforms that conform to naming policy
+        elif pn == pkg.package.replace("-", ".", 1):
+            pn = '"${PN/-/.}"'
+        elif pn == pkg.package.replace("-", "."):
+            pn = '"${PN//-/.}"'
+        elif pn == pkg.package.replace("-", "_", 1):
+            pn = '"${PN/-/_}"'
+        elif pn == pkg.package.replace("-", "_"):
+            pn = '"${PN//-/_}"'
+        # .title() is not exactly the same as ^
+        elif pn == f"{pkg.package[:1].upper()}{pkg.package[1:]}":
+            pn = '"${PN^}"'
+
+        return pn, pv
+
+    @staticmethod
+    def normalize_distribution_name(name: str) -> str:
+        """Normalize the distribution according to sdist/wheel spec"""
+        return PROJECT_SYMBOL_NORMALIZE_RE.sub("_", name).lower()
+
+    @staticmethod
+    def translate_version(version: str) -> str:
+        """Translate Gentoo version into PEP 440 version"""
+        return (
+            version.replace("_alpha", "a")
+            .replace("_beta", "b")
+            .replace("_rc", "rc")
+            .replace("_p", ".post")
+        )
+
+    def check_pypi_mirror(self, pkg, fetchables):
+        # consider only packages that don't inherit pypi.eclass already
+        if "pypi" in pkg.inherited:
+            return
+
+        uris = [(uri, f.filename) for f in fetchables for uri in f.uri]
+        # check if we have any mirror://pypi URLs in the first place
+        pypi_uris = [uri for uri in uris if uri[0].startswith(PYPI_URI_PREFIX)]
+        if not pypi_uris:
+            return
+
+        # if there's exactly one PyPI URI, perhaps inheriting the eclass will 
suffice
+        if len(pypi_uris) == 1:
+            uri, filename = pypi_uris[0]
+
+            def matches_fn(expected_fn: str) -> bool:
+                expected = 
f"{PYPI_URI_PREFIX}source/{pkg.package[0]}/{pkg.package}/{expected_fn}"
+                return uri == expected and filename == expected_fn
+
+            version = self.translate_version(pkg.version)
+            append = len(uris) > 1
+            if 
matches_fn(f"{self.normalize_distribution_name(pkg.package)}-{version}.tar.gz"):
+                yield PythonInlinePyPIURI(uri, normalize=True, append=append, 
pkg=pkg)
+                return
+            if matches_fn(f"{pkg.package}-{version}.tar.gz"):
+                yield PythonInlinePyPIURI(uri, normalize=False, append=append, 
pkg=pkg)
+                return
+
+        # otherwise, yield result for every URL, with suggested replacement
+        for uri, dist_filename in pypi_uris:
+            if source_match := PYPI_SDIST_URI_RE.match(uri):
+                pn, filename_pn, pv, suffix = source_match.groups()
+
+                if filename_pn == self.normalize_distribution_name(pn):
+                    no_normalize_arg = None
+                elif filename_pn == pn:
+                    no_normalize_arg = "--no-normalize"
+                else:  # incorrect project name?
+                    continue
+                if suffix == ".tar.gz":
+                    suffix = None
+                pn, pv = self.simplify_pn_pv(pn, pv, pkg, suffix is None)
+
+                args = tuple(filter(None, ("pypi_sdist_url", no_normalize_arg, 
pn, pv, suffix)))
+                yield PythonInlinePyPIURI(uri, args, pkg=pkg)
+                continue
+
+            if wheel_match := PYPI_WHEEL_URI_RE.match(uri):
+                pytag, pn, filename_pn, pv, abitag = wheel_match.groups()
+                unpack_arg = None
+
+                # only normalized wheel names are supported
+                if filename_pn != self.normalize_distribution_name(pn):
+                    return
+                if dist_filename in (
+                    f"{filename_pn}-{pv}-{pytag}-{abitag}.whl.zip",
+                    f"{filename_pn}-{pv}-{pytag}-{abitag}.zip",
+                ):
+                    unpack_arg = "--unpack"
+                if abitag == "none-any":
+                    abitag = None
+                if pytag == "py3" and abitag is None:
+                    pytag = None
+                pn, pv = self.simplify_pn_pv(pn, pv, pkg, abitag is None)
+
+                args = tuple(filter(None, ("pypi_wheel_url", unpack_arg, pn, 
pv, pytag, abitag)))
+                yield PythonInlinePyPIURI(uri, args, pkg=pkg)
+
     def feed(self, pkg):
         fetchables, _ = self.iuse_filter(
             (fetch.fetchable,),
@@ -728,6 +876,7 @@ class PythonFetchableCheck(Check):
         )
 
         yield from self.check_gh_suffix(pkg, fetchables)
+        yield from self.check_pypi_mirror(pkg, fetchables)
 
 
 class PythonMismatchedPackageName(results.PackageResult, results.Info):

diff --git 
a/testdata/data/repos/python/PythonFetchableCheck/PythonInlinePyPIURI/expected.json
 
b/testdata/data/repos/python/PythonFetchableCheck/PythonInlinePyPIURI/expected.json
new file mode 100644
index 00000000..e8c01812
--- /dev/null
+++ 
b/testdata/data/repos/python/PythonFetchableCheck/PythonInlinePyPIURI/expected.json
@@ -0,0 +1,20 @@
+{"__class__": "PythonInlinePyPIURI", "category": "PythonFetchableCheck", 
"package": "PythonInlinePyPIURL", "version": "0", "url": 
"https://files.pythonhosted.org/packages/source/P/PythonInlinePyPIURL/PythonInlinePyPIURL-0.zip";,
 "replacement": ["pypi_sdist_url", "--no-normalize", "\"${PN}\"", "\"${PV}\"", 
".zip"], "normalize": null, "append": null}
+{"__class__": "PythonInlinePyPIURI", "category": "PythonFetchableCheck", 
"package": "PythonInlinePyPIURL", "version": "1_alpha1", "url": 
"https://files.pythonhosted.org/packages/source/P/PythonInlinePyPIURL/PythonInlinePyPIURL-1a1.tar.gz";,
 "replacement": null, "normalize": false, "append": false}
+{"__class__": "PythonInlinePyPIURI", "category": "PythonFetchableCheck", 
"package": "PythonInlinePyPIURL", "version": "1_alpha1-r1", "url": 
"https://files.pythonhosted.org/packages/source/P/PythonInlinePyPIURL/PythonInlinePyPIURL-1a1.tar.gz";,
 "replacement": null, "normalize": false, "append": true}
+{"__class__": "PythonInlinePyPIURI", "category": "PythonFetchableCheck", 
"package": "PythonInlinePyPIURL", "version": "2_p4", "url": 
"https://files.pythonhosted.org/packages/source/P/PythonInlinePyPIURL/pythoninlinepypiurl-2.post4.tar.gz";,
 "replacement": null, "normalize": true, "append": false}
+{"__class__": "PythonInlinePyPIURI", "category": "PythonFetchableCheck", 
"package": "PythonInlinePyPIURL", "version": "2_p4-r1", "url": 
"https://files.pythonhosted.org/packages/source/P/PythonInlinePyPIURL/pythoninlinepypiurl-2.post4.tar.gz";,
 "replacement": null, "normalize": true, "append": true}
+{"__class__": "PythonInlinePyPIURI", "category": "PythonFetchableCheck", 
"package": "PythonInlinePyPIURL", "version": "3", "url": 
"https://files.pythonhosted.org/packages/py3/p/pypi-url/pypi_url-3-py3-none-any.whl";,
 "replacement": ["pypi_wheel_url", "--unpack", "pypi-url"], "normalize": null, 
"append": null}
+{"__class__": "PythonInlinePyPIURI", "category": "PythonFetchableCheck", 
"package": "PythonInlinePyPIURL", "version": "3", "url": 
"https://files.pythonhosted.org/packages/source/p/pypi-url/pypi-url-3.zip";, 
"replacement": ["pypi_sdist_url", "--no-normalize", "pypi-url", "\"${PV}\"", 
".zip"], "normalize": null, "append": null}
+{"__class__": "PythonInlinePyPIURI", "category": "PythonFetchableCheck", 
"package": "PythonInlinePyPIURL", "version": "3-r1", "url": 
"https://files.pythonhosted.org/packages/py3/p/pypi-url/pypi_url-3-py3-none-any.whl";,
 "replacement": ["pypi_wheel_url", "pypi-url"], "normalize": null, "append": 
null}
+{"__class__": "PythonInlinePyPIURI", "category": "PythonFetchableCheck", 
"package": "PythonInlinePyPIURL", "version": "3-r1", "url": 
"https://files.pythonhosted.org/packages/source/p/pypi-url/pypi-url-3.tar.gz";, 
"replacement": ["pypi_sdist_url", "--no-normalize", "pypi-url"], "normalize": 
null, "append": null}
+{"__class__": "PythonInlinePyPIURI", "category": "PythonFetchableCheck", 
"package": "PythonInlinePyPIURL", "version": "4", "url": 
"https://files.pythonhosted.org/packages/cp310/P/PythonInlinePyPIURL/pythoninlinepypiurl-4-cp310-cp310-linux_x86_64.whl";,
 "replacement": ["pypi_wheel_url", "\"${PN}\"", "\"${PV}\"", "cp310", 
"cp310-linux_x86_64"], "normalize": null, "append": null}
+{"__class__": "PythonInlinePyPIURI", "category": "PythonFetchableCheck", 
"package": "PythonInlinePyPIURL", "version": "4", "url": 
"https://files.pythonhosted.org/packages/source/p/pypi-url/pypi_url-4.tar.gz";, 
"replacement": ["pypi_sdist_url", "pypi-url"], "normalize": null, "append": 
null}
+{"__class__": "PythonInlinePyPIURI", "category": "PythonFetchableCheck", 
"package": "python-inline-pypi-url", "version": "0", "url": 
"https://files.pythonhosted.org/packages/source/p/python.inline-pypi-url/python.inline-pypi-url-0.tar.gz";,
 "replacement": ["pypi_sdist_url", "--no-normalize", "\"${PN/-/.}\""], 
"normalize": null, "append": null}
+{"__class__": "PythonInlinePyPIURI", "category": "PythonFetchableCheck", 
"package": "python-inline-pypi-url", "version": "1", "url": 
"https://files.pythonhosted.org/packages/source/p/python.inline.pypi.url/python.inline.pypi.url-1.tar.gz";,
 "replacement": ["pypi_sdist_url", "--no-normalize", "\"${PN//-/.}\""], 
"normalize": null, "append": null}
+{"__class__": "PythonInlinePyPIURI", "category": "PythonFetchableCheck", 
"package": "python-inline-pypi-url", "version": "100", "url": 
"https://files.pythonhosted.org/packages/source/p/python_inline-pypi-url/python_inline-pypi-url-100.tar.gz";,
 "replacement": ["pypi_sdist_url", "--no-normalize", "\"${PN/-/_}\""], 
"normalize": null, "append": null}
+{"__class__": "PythonInlinePyPIURI", "category": "PythonFetchableCheck", 
"package": "python-inline-pypi-url", "version": "200", "url": 
"https://files.pythonhosted.org/packages/source/P/Python-inline-pypi-url/Python-inline-pypi-url-200.tar.gz";,
 "replacement": ["pypi_sdist_url", "--no-normalize", "\"${PN^}\""], 
"normalize": null, "append": null}
+{"__class__": "PythonInlinePyPIURI", "category": "PythonFetchableCheck", 
"package": "python-inline-pypi-url", "version": "1000", "url": 
"https://files.pythonhosted.org/packages/source/p/python.inline-pypi-url/python_inline_pypi_url-1000.tar.gz";,
 "replacement": ["pypi_sdist_url", "\"${PN/-/.}\""], "normalize": null, 
"append": null}
+{"__class__": "PythonInlinePyPIURI", "category": "PythonFetchableCheck", 
"package": "python-inline-pypi-url", "version": "1000-r1", "url": 
"https://files.pythonhosted.org/packages/source/p/python.inline.pypi.url/python_inline_pypi_url-1000.tar.gz";,
 "replacement": ["pypi_sdist_url", "\"${PN//-/.}\""], "normalize": null, 
"append": null}
+{"__class__": "PythonInlinePyPIURI", "category": "PythonFetchableCheck", 
"package": "python-inline-pypi-url", "version": "1000-r100", "url": 
"https://files.pythonhosted.org/packages/source/p/python_inline-pypi-url/python_inline_pypi_url-1000.tar.gz";,
 "replacement": ["pypi_sdist_url", "\"${PN/-/_}\""], "normalize": null, 
"append": null}
+{"__class__": "PythonInlinePyPIURI", "category": "PythonFetchableCheck", 
"package": "python-inline-pypi-url", "version": "1000-r101", "url": 
"https://files.pythonhosted.org/packages/source/p/python_inline_pypi_url/python_inline_pypi_url-1000.tar.gz";,
 "replacement": ["pypi_sdist_url", "\"${PN//-/_}\""], "normalize": null, 
"append": null}
+{"__class__": "PythonInlinePyPIURI", "category": "PythonFetchableCheck", 
"package": "python-inline-pypi-url", "version": "1000-r200", "url": 
"https://files.pythonhosted.org/packages/source/P/Python-inline-pypi-url/python_inline_pypi_url-1000.tar.gz";,
 "replacement": ["pypi_sdist_url", "\"${PN^}\""], "normalize": null, "append": 
null}

diff --git 
a/testdata/data/repos/python/PythonFetchableCheck/PythonInlinePyPIURI/fix.patch 
b/testdata/data/repos/python/PythonFetchableCheck/PythonInlinePyPIURI/fix.patch
new file mode 100644
index 00000000..e30ff89e
--- /dev/null
+++ 
b/testdata/data/repos/python/PythonFetchableCheck/PythonInlinePyPIURI/fix.patch
@@ -0,0 +1,304 @@
+diff -Naur 
python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-0.ebuild
 
fixed/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-0.ebuild
+--- 
python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-0.ebuild
 2023-02-13 18:02:07.141313182 +0100
++++ 
fixed/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-0.ebuild
  2023-02-13 18:16:47.391898323 +0100
+@@ -3,9 +3,11 @@
+ 
+ EAPI=8
+ 
++inherit pypi
++
+ DESCRIPTION="Ebuild with PyPI URL"
+ HOMEPAGE="https://example.com";
+-SRC_URI="mirror://pypi/${PN::1}/${PN/-/.}/${P/-/.}.tar.gz"
++SRC_URI="$(pypi_sdist_url --no-normalize "${PN/-/.}")"
+ S=${WORKDIR}/${P/-/.}
+ 
+ LICENSE="BSD"
+diff -Naur 
python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000.ebuild
 
fixed/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000.ebuild
+--- 
python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000.ebuild
      2023-02-13 18:02:37.101867819 +0100
++++ 
fixed/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000.ebuild
       2023-02-13 18:16:52.222022177 +0100
+@@ -3,10 +3,11 @@
+ 
+ EAPI=8
+ 
++inherit pypi
++
+ DESCRIPTION="Ebuild with PyPI URL"
+ HOMEPAGE="https://example.com";
+-SRC_URI="mirror://pypi/${PN::1}/${PN/-/.}/${PN//-/_}-${PV}.tar.gz"
+-S=${WORKDIR}/${PN//-/_}-${PV}
++SRC_URI="$(pypi_sdist_url "${PN/-/.}")"
+ 
+ LICENSE="BSD"
+ SLOT="0"
+diff -Naur 
python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000-r100.ebuild
 
fixed/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000-r100.ebuild
+--- 
python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000-r100.ebuild
 2023-02-13 18:05:32.871560794 +0100
++++ 
fixed/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000-r100.ebuild
  2023-02-13 18:16:55.365435891 +0100
+@@ -3,10 +3,11 @@
+ 
+ EAPI=8
+ 
++inherit pypi
++
+ DESCRIPTION="Ebuild with PyPI URL"
+ HOMEPAGE="https://example.com";
+-SRC_URI="mirror://pypi/${PN::1}/${PN/-/_}/${PN//-/_}-${PV}.tar.gz"
+-S=${WORKDIR}/${PN//-/_}-${PV}
++SRC_URI="$(pypi_sdist_url "${PN/-/_}")"
+ 
+ LICENSE="BSD"
+ SLOT="0"
+diff -Naur 
python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000-r101.ebuild
 
fixed/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000-r101.ebuild
+--- 
python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000-r101.ebuild
 2023-02-13 18:05:45.775104791 +0100
++++ 
fixed/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000-r101.ebuild
  2023-02-13 18:16:57.982169331 +0100
+@@ -3,10 +3,11 @@
+ 
+ EAPI=8
+ 
++inherit pypi
++
+ DESCRIPTION="Ebuild with PyPI URL"
+ HOMEPAGE="https://example.com";
+-SRC_URI="mirror://pypi/${PN::1}/${PN//-/_}/${PN//-/_}-${PV}.tar.gz"
+-S=${WORKDIR}/${PN//-/_}-${PV}
++SRC_URI="$(pypi_sdist_url "${PN//-/_}")"
+ 
+ LICENSE="BSD"
+ SLOT="0"
+diff -Naur 
python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000-r1.ebuild
 
fixed/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000-r1.ebuild
+--- 
python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000-r1.ebuild
   2023-02-13 18:04:32.533891351 +0100
++++ 
fixed/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000-r1.ebuild
    2023-02-13 18:17:01.165583737 +0100
+@@ -3,10 +3,11 @@
+ 
+ EAPI=8
+ 
++inherit pypi
++
+ DESCRIPTION="Ebuild with PyPI URL"
+ HOMEPAGE="https://example.com";
+-SRC_URI="mirror://pypi/${PN::1}/${PN//-/.}/${PN//-/_}-${PV}.tar.gz"
+-S=${WORKDIR}/${PN//-/_}-${PV}
++SRC_URI="$(pypi_sdist_url "${PN//-/.}")"
+ 
+ LICENSE="BSD"
+ SLOT="0"
+diff -Naur 
python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000-r200.ebuild
 
fixed/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000-r200.ebuild
+--- 
python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000-r200.ebuild
 2023-02-13 18:06:26.842434235 +0100
++++ 
fixed/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000-r200.ebuild
  2023-02-13 18:17:04.392332387 +0100
+@@ -3,10 +3,11 @@
+ 
+ EAPI=8
+ 
++inherit pypi
++
+ DESCRIPTION="Ebuild with PyPI URL"
+ HOMEPAGE="https://example.com";
+-SRC_URI="mirror://pypi/P/${PN^}/${PN//-/_}-${PV}.tar.gz"
+-S=${WORKDIR}/${PN//-/_}-${PV}
++SRC_URI="$(pypi_sdist_url "${PN^}")"
+ 
+ LICENSE="BSD"
+ SLOT="0"
+diff -Naur 
python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-100.ebuild
 
fixed/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-100.ebuild
+--- 
python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-100.ebuild
       2023-02-13 18:10:56.409921917 +0100
++++ 
fixed/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-100.ebuild
        2023-02-13 18:17:19.662717926 +0100
+@@ -3,9 +3,11 @@
+ 
+ EAPI=8
+ 
++inherit pypi
++
+ DESCRIPTION="Ebuild with PyPI URL"
+ HOMEPAGE="https://example.com";
+-SRC_URI="mirror://pypi/${PN::1}/${PN/-/_}/${P/-/_}.tar.gz"
++SRC_URI="$(pypi_sdist_url --no-normalize "${PN/-/_}")"
+ S=${WORKDIR}/${P/-/_}
+ 
+ LICENSE="BSD"
+diff -Naur 
python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1.ebuild
 
fixed/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1.ebuild
+--- 
python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1.ebuild
 2023-02-13 18:10:03.499125550 +0100
++++ 
fixed/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1.ebuild
  2023-02-13 18:17:19.666051344 +0100
+@@ -3,9 +3,11 @@
+ 
+ EAPI=8
+ 
++inherit pypi
++
+ DESCRIPTION="Ebuild with PyPI URL"
+ HOMEPAGE="https://example.com";
+-SRC_URI="mirror://pypi/${PN::1}/${PN//-/.}/${PN//-/.}-${PV}.tar.gz"
++SRC_URI="$(pypi_sdist_url --no-normalize "${PN//-/.}")"
+ S=${WORKDIR}/${PN//-/.}-${PV}
+ 
+ LICENSE="BSD"
+diff -Naur 
python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-200.ebuild
 
fixed/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-200.ebuild
+--- 
python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-200.ebuild
       2023-02-13 18:12:05.330950479 +0100
++++ 
fixed/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-200.ebuild
        2023-02-13 18:17:19.666051344 +0100
+@@ -3,9 +3,11 @@
+ 
+ EAPI=8
+ 
++inherit pypi
++
+ DESCRIPTION="Ebuild with PyPI URL"
+ HOMEPAGE="https://example.com";
+-SRC_URI="mirror://pypi/P/${PN^}/${P^}.tar.gz"
++SRC_URI="$(pypi_sdist_url --no-normalize "${PN^}")"
+ S=${WORKDIR}/${P^}
+ 
+ LICENSE="BSD"
+diff -Naur 
python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-0.ebuild 
fixed/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-0.ebuild
+--- 
python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-0.ebuild    
   2023-02-13 13:53:39.474144315 +0100
++++ 
fixed/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-0.ebuild     
   2023-02-13 18:13:52.757100032 +0100
+@@ -3,9 +3,11 @@
+ 
+ EAPI=8
+ 
++inherit pypi
++
+ DESCRIPTION="Ebuild with PyPI URL"
+ HOMEPAGE="https://example.com";
+-SRC_URI="mirror://pypi/${PN::1}/${PN}/${P}.zip"
++SRC_URI="$(pypi_sdist_url --no-normalize "${PN}" "${PV}" .zip)"
+ 
+ LICENSE="BSD"
+ SLOT="0"
+diff -Naur 
python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-1_alpha1.ebuild
 
fixed/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-1_alpha1.ebuild
+--- 
python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-1_alpha1.ebuild
        2023-02-13 13:45:21.784711215 +0100
++++ 
fixed/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-1_alpha1.ebuild
 2023-02-13 18:13:52.757100032 +0100
+@@ -3,11 +3,11 @@
+ 
+ EAPI=8
+ 
+-MY_P=${P/_alpha/a}
++PYPI_NO_NORMALIZE=1
++inherit pypi
++
+ DESCRIPTION="Ebuild with PyPI URL"
+ HOMEPAGE="https://example.com";
+-SRC_URI="mirror://pypi/${PN::1}/${PN}/${MY_P}.tar.gz"
+-S=${WORKDIR}/${MY_P}
+ 
+ LICENSE="BSD"
+ SLOT="0"
+diff -Naur 
python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-1_alpha1-r1.ebuild
 
fixed/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-1_alpha1-r1.ebuild
+--- 
python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-1_alpha1-r1.ebuild
     2023-02-13 13:45:46.604987058 +0100
++++ 
fixed/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-1_alpha1-r1.ebuild
      2023-02-13 18:13:52.757100032 +0100
+@@ -3,14 +3,14 @@
+ 
+ EAPI=8
+ 
+-MY_P=${P/_alpha/a}
++PYPI_NO_NORMALIZE=1
++inherit pypi
++
+ DESCRIPTION="Ebuild with PyPI URL"
+ HOMEPAGE="https://example.com";
+-SRC_URI="
+-      mirror://pypi/${PN::1}/${PN}/${MY_P}.tar.gz
++SRC_URI+="
+       https://example.com/foo.patch
+ "
+-S=${WORKDIR}/${MY_P}
+ 
+ LICENSE="BSD"
+ SLOT="0"
+diff -Naur 
python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-2_p4.ebuild 
fixed/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-2_p4.ebuild
+--- 
python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-2_p4.ebuild 
   2023-02-13 13:46:34.949608190 +0100
++++ 
fixed/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-2_p4.ebuild  
   2023-02-13 18:13:52.757100032 +0100
+@@ -3,11 +3,10 @@
+ 
+ EAPI=8
+ 
+-MY_P=${PN,,}-${PV/_p/.post}
++inherit pypi
++
+ DESCRIPTION="Ebuild with PyPI URL"
+ HOMEPAGE="https://example.com";
+-SRC_URI="mirror://pypi/${PN::1}/${PN}/${MY_P}.tar.gz"
+-S=${WORKDIR}/${MY_P}
+ 
+ LICENSE="BSD"
+ SLOT="0"
+diff -Naur 
python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-2_p4-r1.ebuild
 
fixed/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-2_p4-r1.ebuild
+--- 
python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-2_p4-r1.ebuild
 2023-02-13 13:46:34.949608190 +0100
++++ 
fixed/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-2_p4-r1.ebuild
  2023-02-13 18:13:52.757100032 +0100
+@@ -3,14 +3,13 @@
+ 
+ EAPI=8
+ 
+-MY_P=${PN,,}-${PV/_p/.post}
++inherit pypi
++
+ DESCRIPTION="Ebuild with PyPI URL"
+ HOMEPAGE="https://example.com";
+-SRC_URI="
+-      mirror://pypi/${PN::1}/${PN}/${MY_P}.tar.gz
++SRC_URI+="
+       https://example.com/foo.patch
+ "
+-S=${WORKDIR}/${MY_P}
+ 
+ LICENSE="BSD"
+ SLOT="0"
+diff -Naur 
python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-3.ebuild 
fixed/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-3.ebuild
+--- 
python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-3.ebuild    
   2023-02-13 15:30:06.802489789 +0100
++++ 
fixed/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-3.ebuild     
   2023-02-13 18:13:52.757100032 +0100
+@@ -3,13 +3,14 @@
+ 
+ EAPI=8
+ 
++inherit pypi
++
+ MY_P=pypi-url-${PV}
+ DESCRIPTION="Ebuild with PyPI URL"
+ HOMEPAGE="https://example.com";
+ SRC_URI="
+-      mirror://pypi/p/pypi-url/${MY_P}.zip
+-      
https://files.pythonhosted.org/packages/py3/p/pypi-url/pypi_url-${PV}-py3-none-any.whl
+-              -> pypi_url-${PV}-py3-none-any.whl.zip
++      $(pypi_sdist_url --no-normalize pypi-url "${PV}" .zip)
++      $(pypi_wheel_url --unpack pypi-url)
+ "
+ S=${WORKDIR}/${MY_P}
+ 
+diff -Naur 
python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-3-r1.ebuild 
fixed/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-3-r1.ebuild
+--- 
python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-3-r1.ebuild 
   2023-02-13 15:24:31.819467907 +0100
++++ 
fixed/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-3-r1.ebuild  
   2023-02-13 18:13:52.757100032 +0100
+@@ -3,12 +3,14 @@
+ 
+ EAPI=8
+ 
++inherit pypi
++
+ MY_P=pypi-url-${PV}
+ DESCRIPTION="Ebuild with PyPI URL"
+ HOMEPAGE="https://example.com";
+ SRC_URI="
+-      mirror://pypi/p/pypi-url/${MY_P}.tar.gz
+-      
https://files.pythonhosted.org/packages/py3/p/pypi-url/pypi_url-3-py3-none-any.whl
++      $(pypi_sdist_url --no-normalize pypi-url)
++      $(pypi_wheel_url pypi-url)
+ "
+ S=${WORKDIR}/${MY_P}
+ 
+diff -Naur 
python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-4.ebuild 
fixed/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-4.ebuild
+--- 
python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-4.ebuild    
   2023-02-13 15:30:54.598707954 +0100
++++ 
fixed/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-4.ebuild     
   2023-02-13 18:13:52.757100032 +0100
+@@ -3,12 +3,14 @@
+ 
+ EAPI=8
+ 
++inherit pypi
++
+ MY_P=pypi_url-${PV}
+ DESCRIPTION="Ebuild with PyPI URL"
+ HOMEPAGE="https://example.com";
+ SRC_URI="
+-      mirror://pypi/p/pypi-url/${MY_P}.tar.gz
+-      
https://files.pythonhosted.org/packages/cp310/${PN::1}/${PN}/${P,,}-cp310-cp310-linux_x86_64.whl
++      $(pypi_sdist_url pypi-url)
++      $(pypi_wheel_url "${PN}" "${PV}" cp310 cp310-linux_x86_64)
+ "
+ S=${WORKDIR}/${MY_P}
+ 

diff --git 
a/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/Manifest 
b/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/Manifest
new file mode 100644
index 00000000..39d6377a
--- /dev/null
+++ b/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/Manifest
@@ -0,0 +1,10 @@
+DIST PythonInlinePyPIURL-0.zip 153310 BLAKE2B 
b7484cd9bebe912f9c8877c0f09df059130c2dc5c4da8c926f8df7945bcb7b255cdf810ce8cd16a987fb5bca3d1e71c088cd894968641db5dfae1c4c059df836
 SHA512 
86ff9e1c4b9353b1fbb475c7bb9d2a97bd9db8421ea5190b5a84832930b34cb5b79f8c3da68a5eb8db334f06851ec129cc6611a371e47b7c5de7a615feec5e05
+DIST PythonInlinePyPIURL-1a1.tar.gz 153310 BLAKE2B 
b7484cd9bebe912f9c8877c0f09df059130c2dc5c4da8c926f8df7945bcb7b255cdf810ce8cd16a987fb5bca3d1e71c088cd894968641db5dfae1c4c059df836
 SHA512 
86ff9e1c4b9353b1fbb475c7bb9d2a97bd9db8421ea5190b5a84832930b34cb5b79f8c3da68a5eb8db334f06851ec129cc6611a371e47b7c5de7a615feec5e05
+DIST foo.patch 153310 BLAKE2B 
b7484cd9bebe912f9c8877c0f09df059130c2dc5c4da8c926f8df7945bcb7b255cdf810ce8cd16a987fb5bca3d1e71c088cd894968641db5dfae1c4c059df836
 SHA512 
86ff9e1c4b9353b1fbb475c7bb9d2a97bd9db8421ea5190b5a84832930b34cb5b79f8c3da68a5eb8db334f06851ec129cc6611a371e47b7c5de7a615feec5e05
+DIST pypi-url-3.tar.gz 153310 BLAKE2B 
b7484cd9bebe912f9c8877c0f09df059130c2dc5c4da8c926f8df7945bcb7b255cdf810ce8cd16a987fb5bca3d1e71c088cd894968641db5dfae1c4c059df836
 SHA512 
86ff9e1c4b9353b1fbb475c7bb9d2a97bd9db8421ea5190b5a84832930b34cb5b79f8c3da68a5eb8db334f06851ec129cc6611a371e47b7c5de7a615feec5e05
+DIST pypi-url-3.zip 153310 BLAKE2B 
b7484cd9bebe912f9c8877c0f09df059130c2dc5c4da8c926f8df7945bcb7b255cdf810ce8cd16a987fb5bca3d1e71c088cd894968641db5dfae1c4c059df836
 SHA512 
86ff9e1c4b9353b1fbb475c7bb9d2a97bd9db8421ea5190b5a84832930b34cb5b79f8c3da68a5eb8db334f06851ec129cc6611a371e47b7c5de7a615feec5e05
+DIST pypi_url-3-py3-none-any.whl 153310 BLAKE2B 
b7484cd9bebe912f9c8877c0f09df059130c2dc5c4da8c926f8df7945bcb7b255cdf810ce8cd16a987fb5bca3d1e71c088cd894968641db5dfae1c4c059df836
 SHA512 
86ff9e1c4b9353b1fbb475c7bb9d2a97bd9db8421ea5190b5a84832930b34cb5b79f8c3da68a5eb8db334f06851ec129cc6611a371e47b7c5de7a615feec5e05
+DIST pypi_url-3-py3-none-any.whl.zip 153310 BLAKE2B 
b7484cd9bebe912f9c8877c0f09df059130c2dc5c4da8c926f8df7945bcb7b255cdf810ce8cd16a987fb5bca3d1e71c088cd894968641db5dfae1c4c059df836
 SHA512 
86ff9e1c4b9353b1fbb475c7bb9d2a97bd9db8421ea5190b5a84832930b34cb5b79f8c3da68a5eb8db334f06851ec129cc6611a371e47b7c5de7a615feec5e05
+DIST pythoninlinepypiurl-4-cp310-cp310-linux_x86_64.whl 153310 BLAKE2B 
b7484cd9bebe912f9c8877c0f09df059130c2dc5c4da8c926f8df7945bcb7b255cdf810ce8cd16a987fb5bca3d1e71c088cd894968641db5dfae1c4c059df836
 SHA512 
86ff9e1c4b9353b1fbb475c7bb9d2a97bd9db8421ea5190b5a84832930b34cb5b79f8c3da68a5eb8db334f06851ec129cc6611a371e47b7c5de7a615feec5e05
+DIST pypi_url-4.tar.gz 153310 BLAKE2B 
b7484cd9bebe912f9c8877c0f09df059130c2dc5c4da8c926f8df7945bcb7b255cdf810ce8cd16a987fb5bca3d1e71c088cd894968641db5dfae1c4c059df836
 SHA512 
86ff9e1c4b9353b1fbb475c7bb9d2a97bd9db8421ea5190b5a84832930b34cb5b79f8c3da68a5eb8db334f06851ec129cc6611a371e47b7c5de7a615feec5e05
+DIST pythoninlinepypiurl-2.post4.tar.gz 153310 BLAKE2B 
b7484cd9bebe912f9c8877c0f09df059130c2dc5c4da8c926f8df7945bcb7b255cdf810ce8cd16a987fb5bca3d1e71c088cd894968641db5dfae1c4c059df836
 SHA512 
86ff9e1c4b9353b1fbb475c7bb9d2a97bd9db8421ea5190b5a84832930b34cb5b79f8c3da68a5eb8db334f06851ec129cc6611a371e47b7c5de7a615feec5e05

diff --git 
a/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-0.ebuild
 
b/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-0.ebuild
new file mode 100644
index 00000000..e37d2061
--- /dev/null
+++ 
b/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-0.ebuild
@@ -0,0 +1,15 @@
+# Copyright 2023 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+EAPI=8
+
+DESCRIPTION="Ebuild with PyPI URL"
+HOMEPAGE="https://example.com";
+SRC_URI="mirror://pypi/${PN::1}/${PN}/${P}.zip"
+
+LICENSE="BSD"
+SLOT="0"
+
+BDEPEND="
+       app-arch/unzip
+"

diff --git 
a/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-1_alpha1-r1.ebuild
 
b/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-1_alpha1-r1.ebuild
new file mode 100644
index 00000000..ef9e9a25
--- /dev/null
+++ 
b/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-1_alpha1-r1.ebuild
@@ -0,0 +1,16 @@
+# Copyright 2023 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+EAPI=8
+
+MY_P=${P/_alpha/a}
+DESCRIPTION="Ebuild with PyPI URL"
+HOMEPAGE="https://example.com";
+SRC_URI="
+       mirror://pypi/${PN::1}/${PN}/${MY_P}.tar.gz
+       https://example.com/foo.patch
+"
+S=${WORKDIR}/${MY_P}
+
+LICENSE="BSD"
+SLOT="0"

diff --git 
a/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-1_alpha1.ebuild
 
b/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-1_alpha1.ebuild
new file mode 100644
index 00000000..ed737716
--- /dev/null
+++ 
b/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-1_alpha1.ebuild
@@ -0,0 +1,13 @@
+# Copyright 2023 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+EAPI=8
+
+MY_P=${P/_alpha/a}
+DESCRIPTION="Ebuild with PyPI URL"
+HOMEPAGE="https://example.com";
+SRC_URI="mirror://pypi/${PN::1}/${PN}/${MY_P}.tar.gz"
+S=${WORKDIR}/${MY_P}
+
+LICENSE="BSD"
+SLOT="0"

diff --git 
a/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-2_p4-r1.ebuild
 
b/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-2_p4-r1.ebuild
new file mode 100644
index 00000000..88b8ab43
--- /dev/null
+++ 
b/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-2_p4-r1.ebuild
@@ -0,0 +1,16 @@
+# Copyright 2023 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+EAPI=8
+
+MY_P=${PN,,}-${PV/_p/.post}
+DESCRIPTION="Ebuild with PyPI URL"
+HOMEPAGE="https://example.com";
+SRC_URI="
+       mirror://pypi/${PN::1}/${PN}/${MY_P}.tar.gz
+       https://example.com/foo.patch
+"
+S=${WORKDIR}/${MY_P}
+
+LICENSE="BSD"
+SLOT="0"

diff --git 
a/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-2_p4.ebuild
 
b/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-2_p4.ebuild
new file mode 100644
index 00000000..3ff2d8bf
--- /dev/null
+++ 
b/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-2_p4.ebuild
@@ -0,0 +1,13 @@
+# Copyright 2023 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+EAPI=8
+
+MY_P=${PN,,}-${PV/_p/.post}
+DESCRIPTION="Ebuild with PyPI URL"
+HOMEPAGE="https://example.com";
+SRC_URI="mirror://pypi/${PN::1}/${PN}/${MY_P}.tar.gz"
+S=${WORKDIR}/${MY_P}
+
+LICENSE="BSD"
+SLOT="0"

diff --git 
a/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-3-r1.ebuild
 
b/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-3-r1.ebuild
new file mode 100644
index 00000000..b6232cee
--- /dev/null
+++ 
b/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-3-r1.ebuild
@@ -0,0 +1,16 @@
+# Copyright 2023 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+EAPI=8
+
+MY_P=pypi-url-${PV}
+DESCRIPTION="Ebuild with PyPI URL"
+HOMEPAGE="https://example.com";
+SRC_URI="
+       mirror://pypi/p/pypi-url/${MY_P}.tar.gz
+       
https://files.pythonhosted.org/packages/py3/p/pypi-url/pypi_url-3-py3-none-any.whl
+"
+S=${WORKDIR}/${MY_P}
+
+LICENSE="BSD"
+SLOT="0"

diff --git 
a/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-3.ebuild
 
b/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-3.ebuild
new file mode 100644
index 00000000..ffa836be
--- /dev/null
+++ 
b/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-3.ebuild
@@ -0,0 +1,21 @@
+# Copyright 2023 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+EAPI=8
+
+MY_P=pypi-url-${PV}
+DESCRIPTION="Ebuild with PyPI URL"
+HOMEPAGE="https://example.com";
+SRC_URI="
+       mirror://pypi/p/pypi-url/${MY_P}.zip
+       
https://files.pythonhosted.org/packages/py3/p/pypi-url/pypi_url-${PV}-py3-none-any.whl
+               -> pypi_url-${PV}-py3-none-any.whl.zip
+"
+S=${WORKDIR}/${MY_P}
+
+LICENSE="BSD"
+SLOT="0"
+
+BDEPEND="
+       app-arch/unzip
+"

diff --git 
a/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-4.ebuild
 
b/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-4.ebuild
new file mode 100644
index 00000000..606bb4bc
--- /dev/null
+++ 
b/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/PythonInlinePyPIURL-4.ebuild
@@ -0,0 +1,16 @@
+# Copyright 2023 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+EAPI=8
+
+MY_P=pypi_url-${PV}
+DESCRIPTION="Ebuild with PyPI URL"
+HOMEPAGE="https://example.com";
+SRC_URI="
+       mirror://pypi/p/pypi-url/${MY_P}.tar.gz
+       
https://files.pythonhosted.org/packages/cp310/${PN::1}/${PN}/${P,,}-cp310-cp310-linux_x86_64.whl
+"
+S=${WORKDIR}/${MY_P}
+
+LICENSE="BSD"
+SLOT="0"

diff --git 
a/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/metadata.xml 
b/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/metadata.xml
new file mode 100644
index 00000000..74fae2ac
--- /dev/null
+++ 
b/testdata/repos/python/PythonFetchableCheck/PythonInlinePyPIURL/metadata.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE pkgmetadata SYSTEM "https://www.gentoo.org/dtd/metadata.dtd";>
+<pkgmetadata>
+       <upstream>
+               <remote-id type="pypi">frobnicate</remote-id>
+       </upstream>
+</pkgmetadata>

diff --git 
a/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/Manifest 
b/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/Manifest
new file mode 100644
index 00000000..d541566b
--- /dev/null
+++ b/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/Manifest
@@ -0,0 +1,5 @@
+DIST python.inline-pypi-url-0.tar.gz 153310 BLAKE2B 
b7484cd9bebe912f9c8877c0f09df059130c2dc5c4da8c926f8df7945bcb7b255cdf810ce8cd16a987fb5bca3d1e71c088cd894968641db5dfae1c4c059df836
 SHA512 
86ff9e1c4b9353b1fbb475c7bb9d2a97bd9db8421ea5190b5a84832930b34cb5b79f8c3da68a5eb8db334f06851ec129cc6611a371e47b7c5de7a615feec5e05
+DIST python.inline.pypi.url-1.tar.gz 153310 BLAKE2B 
b7484cd9bebe912f9c8877c0f09df059130c2dc5c4da8c926f8df7945bcb7b255cdf810ce8cd16a987fb5bca3d1e71c088cd894968641db5dfae1c4c059df836
 SHA512 
86ff9e1c4b9353b1fbb475c7bb9d2a97bd9db8421ea5190b5a84832930b34cb5b79f8c3da68a5eb8db334f06851ec129cc6611a371e47b7c5de7a615feec5e05
+DIST python_inline-pypi-url-100.tar.gz 153310 BLAKE2B 
b7484cd9bebe912f9c8877c0f09df059130c2dc5c4da8c926f8df7945bcb7b255cdf810ce8cd16a987fb5bca3d1e71c088cd894968641db5dfae1c4c059df836
 SHA512 
86ff9e1c4b9353b1fbb475c7bb9d2a97bd9db8421ea5190b5a84832930b34cb5b79f8c3da68a5eb8db334f06851ec129cc6611a371e47b7c5de7a615feec5e05
+DIST Python-inline-pypi-url-200.tar.gz 153310 BLAKE2B 
b7484cd9bebe912f9c8877c0f09df059130c2dc5c4da8c926f8df7945bcb7b255cdf810ce8cd16a987fb5bca3d1e71c088cd894968641db5dfae1c4c059df836
 SHA512 
86ff9e1c4b9353b1fbb475c7bb9d2a97bd9db8421ea5190b5a84832930b34cb5b79f8c3da68a5eb8db334f06851ec129cc6611a371e47b7c5de7a615feec5e05
+DIST python_inline_pypi_url-1000.tar.gz 153310 BLAKE2B 
b7484cd9bebe912f9c8877c0f09df059130c2dc5c4da8c926f8df7945bcb7b255cdf810ce8cd16a987fb5bca3d1e71c088cd894968641db5dfae1c4c059df836
 SHA512 
86ff9e1c4b9353b1fbb475c7bb9d2a97bd9db8421ea5190b5a84832930b34cb5b79f8c3da68a5eb8db334f06851ec129cc6611a371e47b7c5de7a615feec5e05

diff --git 
a/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/metadata.xml
 
b/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/metadata.xml
new file mode 100644
index 00000000..74fae2ac
--- /dev/null
+++ 
b/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/metadata.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE pkgmetadata SYSTEM "https://www.gentoo.org/dtd/metadata.dtd";>
+<pkgmetadata>
+       <upstream>
+               <remote-id type="pypi">frobnicate</remote-id>
+       </upstream>
+</pkgmetadata>

diff --git 
a/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-0.ebuild
 
b/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-0.ebuild
new file mode 100644
index 00000000..2fb8d24e
--- /dev/null
+++ 
b/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-0.ebuild
@@ -0,0 +1,12 @@
+# Copyright 2023 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+EAPI=8
+
+DESCRIPTION="Ebuild with PyPI URL"
+HOMEPAGE="https://example.com";
+SRC_URI="mirror://pypi/${PN::1}/${PN/-/.}/${P/-/.}.tar.gz"
+S=${WORKDIR}/${P/-/.}
+
+LICENSE="BSD"
+SLOT="0"

diff --git 
a/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1.ebuild
 
b/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1.ebuild
new file mode 100644
index 00000000..983d264f
--- /dev/null
+++ 
b/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1.ebuild
@@ -0,0 +1,12 @@
+# Copyright 2023 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+EAPI=8
+
+DESCRIPTION="Ebuild with PyPI URL"
+HOMEPAGE="https://example.com";
+SRC_URI="mirror://pypi/${PN::1}/${PN//-/.}/${PN//-/.}-${PV}.tar.gz"
+S=${WORKDIR}/${PN//-/.}-${PV}
+
+LICENSE="BSD"
+SLOT="0"

diff --git 
a/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-100.ebuild
 
b/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-100.ebuild
new file mode 100644
index 00000000..34189990
--- /dev/null
+++ 
b/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-100.ebuild
@@ -0,0 +1,12 @@
+# Copyright 2023 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+EAPI=8
+
+DESCRIPTION="Ebuild with PyPI URL"
+HOMEPAGE="https://example.com";
+SRC_URI="mirror://pypi/${PN::1}/${PN/-/_}/${P/-/_}.tar.gz"
+S=${WORKDIR}/${P/-/_}
+
+LICENSE="BSD"
+SLOT="0"

diff --git 
a/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000-r1.ebuild
 
b/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000-r1.ebuild
new file mode 100644
index 00000000..4528e886
--- /dev/null
+++ 
b/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000-r1.ebuild
@@ -0,0 +1,12 @@
+# Copyright 2023 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+EAPI=8
+
+DESCRIPTION="Ebuild with PyPI URL"
+HOMEPAGE="https://example.com";
+SRC_URI="mirror://pypi/${PN::1}/${PN//-/.}/${PN//-/_}-${PV}.tar.gz"
+S=${WORKDIR}/${PN//-/_}-${PV}
+
+LICENSE="BSD"
+SLOT="0"

diff --git 
a/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000-r100.ebuild
 
b/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000-r100.ebuild
new file mode 100644
index 00000000..3b9ef524
--- /dev/null
+++ 
b/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000-r100.ebuild
@@ -0,0 +1,12 @@
+# Copyright 2023 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+EAPI=8
+
+DESCRIPTION="Ebuild with PyPI URL"
+HOMEPAGE="https://example.com";
+SRC_URI="mirror://pypi/${PN::1}/${PN/-/_}/${PN//-/_}-${PV}.tar.gz"
+S=${WORKDIR}/${PN//-/_}-${PV}
+
+LICENSE="BSD"
+SLOT="0"

diff --git 
a/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000-r101.ebuild
 
b/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000-r101.ebuild
new file mode 100644
index 00000000..24c7fa9d
--- /dev/null
+++ 
b/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000-r101.ebuild
@@ -0,0 +1,12 @@
+# Copyright 2023 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+EAPI=8
+
+DESCRIPTION="Ebuild with PyPI URL"
+HOMEPAGE="https://example.com";
+SRC_URI="mirror://pypi/${PN::1}/${PN//-/_}/${PN//-/_}-${PV}.tar.gz"
+S=${WORKDIR}/${PN//-/_}-${PV}
+
+LICENSE="BSD"
+SLOT="0"

diff --git 
a/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000-r200.ebuild
 
b/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000-r200.ebuild
new file mode 100644
index 00000000..543eda6e
--- /dev/null
+++ 
b/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000-r200.ebuild
@@ -0,0 +1,12 @@
+# Copyright 2023 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+EAPI=8
+
+DESCRIPTION="Ebuild with PyPI URL"
+HOMEPAGE="https://example.com";
+SRC_URI="mirror://pypi/P/${PN^}/${PN//-/_}-${PV}.tar.gz"
+S=${WORKDIR}/${PN//-/_}-${PV}
+
+LICENSE="BSD"
+SLOT="0"

diff --git 
a/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000.ebuild
 
b/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000.ebuild
new file mode 100644
index 00000000..07e3a6dd
--- /dev/null
+++ 
b/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-1000.ebuild
@@ -0,0 +1,12 @@
+# Copyright 2023 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+EAPI=8
+
+DESCRIPTION="Ebuild with PyPI URL"
+HOMEPAGE="https://example.com";
+SRC_URI="mirror://pypi/${PN::1}/${PN/-/.}/${PN//-/_}-${PV}.tar.gz"
+S=${WORKDIR}/${PN//-/_}-${PV}
+
+LICENSE="BSD"
+SLOT="0"

diff --git 
a/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-200.ebuild
 
b/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-200.ebuild
new file mode 100644
index 00000000..e2a5eeb8
--- /dev/null
+++ 
b/testdata/repos/python/PythonFetchableCheck/python-inline-pypi-url/python-inline-pypi-url-200.ebuild
@@ -0,0 +1,12 @@
+# Copyright 2023 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+EAPI=8
+
+DESCRIPTION="Ebuild with PyPI URL"
+HOMEPAGE="https://example.com";
+SRC_URI="mirror://pypi/P/${PN^}/${P^}.tar.gz"
+S=${WORKDIR}/${P^}
+
+LICENSE="BSD"
+SLOT="0"

diff --git a/testdata/repos/python/app-arch/unzip/unzip-0.ebuild 
b/testdata/repos/python/app-arch/unzip/unzip-0.ebuild
new file mode 100644
index 00000000..0b7766f5
--- /dev/null
+++ b/testdata/repos/python/app-arch/unzip/unzip-0.ebuild
@@ -0,0 +1,4 @@
+DESCRIPTION="Stub unzip package to suppress NonexistentDeps results"
+HOMEPAGE="https://github.com/pkgcore/pkgcheck";
+SLOT="0"
+LICENSE="BSD"

diff --git a/testdata/repos/python/eclass/pypi.eclass 
b/testdata/repos/python/eclass/pypi.eclass
new file mode 100644
index 00000000..af21dc7f
--- /dev/null
+++ b/testdata/repos/python/eclass/pypi.eclass
@@ -0,0 +1,107 @@
+# Copyright 2023 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+# @ECLASS: pypi.eclass
+# @MAINTAINER:
+# Michał Górny <[email protected]>
+# @AUTHOR:
+# Michał Górny <[email protected]>
+# @SUPPORTED_EAPIS: 7 8
+
+# @ECLASS_VARIABLE: PYPI_NO_NORMALIZE
+# @PRE_INHERIT
+# @DEFAULT_UNSET
+# @DESCRIPTION:
+
+# @FUNCTION: pypi_normalize_name
+# @USAGE: <name>
+# @DESCRIPTION:
+# Normalize the project name according to sdist/wheel normalization
+# rules.  That is, convert to lowercase and replace runs of [._-]
+# with a single underscore.
+#
+# Based on the spec, as of 2023-02-10:
+# 
https://packaging.python.org/en/latest/specifications/#package-distribution-file-formats
+pypi_normalize_name() {
+       local name=${1}
+       shopt -s extglob
+       name=${name//+([._-])/_}
+       echo "${name,,}"
+}
+
+# @FUNCTION: pypi_translate_version
+# @USAGE: <version>
+# @DESCRIPTION:
+# Translate the specified Gentoo version into the usual Python
+# counterpart.  Assumes PEP 440 versions.
+#
+# Note that we do not have clear counterparts for the epoch segment,
+# nor for development release segment.
+pypi_translate_version() {
+       local version=${1}
+       version=${version/_alpha/a}
+       version=${version/_beta/b}
+       version=${version/_rc/rc}
+       version=${version/_p/.post}
+       echo "${version}"
+}
+
+# @FUNCTION: pypi_sdist_url
+# @USAGE: [--no-normalize] [<project> [<version> [<suffix>]]]
+# @DESCRIPTION:
+pypi_sdist_url() {
+       local normalize=1
+       if [[ ${1} == --no-normalize ]]; then
+               normalize=
+               shift
+       fi
+
+       local project=${1-"${PN}"}
+       local version=${2-"$(pypi_translate_version "${PV}")"}
+       local suffix=${3-.tar.gz}
+       local fn_project=${project}
+       [[ ${normalize} ]] && fn_project=$(pypi_normalize_name "${project}")
+       printf "https://files.pythonhosted.org/packages/source/%s"; \
+               "${project::1}/${project}/${fn_project}-${version}${suffix}"
+}
+
+# @FUNCTION: pypi_wheel_name
+# @USAGE: [<project> [<version> [<python-tag> [<abi-platform-tag>]]]]
+# @DESCRIPTION:
+pypi_wheel_name() {
+       local project=$(pypi_normalize_name "${1-"${PN}"}")
+       local version=${2-"$(pypi_translate_version "${PV}")"}
+       local pytag=${3-py3}
+       local abitag=${4-none-any}
+       echo "${project}-${version}-${pytag}-${abitag}.whl"
+}
+
+# @FUNCTION: pypi_wheel_url
+# @USAGE: [--unpack] [<project> [<version> [<python-tag> 
[<abi-platform-tag>]]]]
+# @DESCRIPTION:
+pypi_wheel_url() {
+       local unpack=
+       if [[ ${1} == --unpack ]]; then
+               unpack=1
+               shift
+       fi
+
+       local filename=$(pypi_wheel_name "${@}")
+       local project=${1-"${PN}"}
+       local version=${2-"$(pypi_translate_version "${PV}")"}
+       local pytag=${3-py3}
+       printf "https://files.pythonhosted.org/packages/%s"; \
+               "${pytag}/${project::1}/${project}/${filename}"
+
+       if [[ ${unpack} ]]; then
+               echo " -> ${filename}.zip"
+       fi
+}
+
+if [[ ${PYPI_NO_NORMALIZE} ]]; then
+       SRC_URI="$(pypi_sdist_url --no-normalize)"
+       S="${WORKDIR}/${PN}-$(pypi_translate_version "${PV}")"
+else
+       SRC_URI="$(pypi_sdist_url)"
+       S="${WORKDIR}/$(pypi_normalize_name "${PN}")-$(pypi_translate_version 
"${PV}")"
+fi

diff --git a/testdata/repos/python/metadata/stubs 
b/testdata/repos/python/metadata/stubs
index 466e30c2..7a9e32b7 100644
--- a/testdata/repos/python/metadata/stubs
+++ b/testdata/repos/python/metadata/stubs
@@ -1 +1,2 @@
+app-arch/unzip
 dev-lang/python

diff --git a/testdata/repos/python/profiles/thirdpartymirrors 
b/testdata/repos/python/profiles/thirdpartymirrors
new file mode 100644
index 00000000..1605d348
--- /dev/null
+++ b/testdata/repos/python/profiles/thirdpartymirrors
@@ -0,0 +1 @@
+pypi           https://files.pythonhosted.org/packages/source

Reply via email to