This commit is contained in:
lz_db
2026-10-16 22:48:30 +08:00
parent 190de72571
commit 0b36fd3383
3161 changed files with 12787 additions and 77274 deletions
@@ -1,52 +1,34 @@
"""PyPI and direct package downloading."""
from __future__ import annotations
import base64
import configparser
import hashlib
import html
import http.client
import io
import itertools
"""PyPI and direct package downloading"""
import sys
import os
import re
import io
import shutil
import socket
import subprocess
import sys
import urllib.error
import base64
import hashlib
import itertools
import warnings
import configparser
import html
import http.client
import urllib.parse
import urllib.request
from fnmatch import translate
import urllib.error
from functools import wraps
from typing import NamedTuple
from more_itertools import unique_everseen
import setuptools
from pkg_resources import (
BINARY_DIST,
CHECKOUT_DIST,
DEVELOP_DIST,
EGG_DIST,
SOURCE_DIST,
Distribution,
Environment,
Requirement,
find_distributions,
normalize_path,
parse_version,
safe_name,
safe_version,
to_filename,
CHECKOUT_DIST, Distribution, BINARY_DIST, normalize_path, SOURCE_DIST,
Environment, find_distributions, safe_name, safe_version,
to_filename, Requirement, DEVELOP_DIST, EGG_DIST, parse_version,
)
from setuptools.wheel import Wheel
from .unicode_utils import _cfg_read_utf8_with_fallback, _read_utf8_with_fallback
from distutils import log
from distutils.errors import DistutilsError
from fnmatch import translate
from setuptools.wheel import Wheel
from setuptools.extern.more_itertools import unique_everseen
EGG_FRAGMENT = re.compile(r'^egg=([-A-Za-z0-9_.+!]+)$')
HREF = re.compile(r"""href\s*=\s*['"]?([^'"> ]+)""", re.I)
@@ -58,15 +40,15 @@ URL_SCHEME = re.compile('([-+.a-z0-9]{2,}):', re.I).match
EXTENSIONS = ".tar.gz .tar.bz2 .tar .zip .tgz".split()
__all__ = [
'PackageIndex',
'distros_for_url',
'parse_bdist_wininst',
'PackageIndex', 'distros_for_url', 'parse_bdist_wininst',
'interpret_distro_name',
]
_SOCKET_TIMEOUT = 15
user_agent = f"setuptools/{setuptools.__version__} Python-urllib/{sys.version_info.major}.{sys.version_info.minor}"
_tmpl = "setuptools/{setuptools.__version__} Python-urllib/{py_major}"
user_agent = _tmpl.format(
py_major='{}.{}'.format(*sys.version_info), setuptools=setuptools)
def parse_requirement_arg(spec):
@@ -74,7 +56,7 @@ def parse_requirement_arg(spec):
return Requirement.parse(spec)
except ValueError as e:
raise DistutilsError(
f"Not a URL, existing file, or requirement spec: {spec!r}"
"Not a URL, existing file, or requirement spec: %r" % (spec,)
) from e
@@ -104,7 +86,7 @@ def parse_bdist_wininst(name):
def egg_info_for_url(url):
parts = urllib.parse.urlparse(url)
_scheme, server, path, _parameters, _query, fragment = parts
scheme, server, path, parameters, query, fragment = parts
base = urllib.parse.unquote(path.split('/')[-1])
if server == 'sourceforge.net' and base == 'download': # XXX Yuck
base = urllib.parse.unquote(path.split('/')[-2])
@@ -116,13 +98,15 @@ def egg_info_for_url(url):
def distros_for_url(url, metadata=None):
"""Yield egg or source distribution objects that might be found at a URL"""
base, fragment = egg_info_for_url(url)
yield from distros_for_location(url, base, metadata)
for dist in distros_for_location(url, base, metadata):
yield dist
if fragment:
match = EGG_FRAGMENT.match(fragment)
if match:
yield from interpret_distro_name(
for dist in interpret_distro_name(
url, match.group(1), metadata, precedence=CHECKOUT_DIST
)
):
yield dist
def distros_for_location(location, basename, metadata=None):
@@ -136,15 +120,13 @@ def distros_for_location(location, basename, metadata=None):
wheel = Wheel(basename)
if not wheel.is_compatible():
return []
return [
Distribution(
location=location,
project_name=wheel.project_name,
version=wheel.version,
# Increase priority over eggs.
precedence=EGG_DIST + 1,
)
]
return [Distribution(
location=location,
project_name=wheel.project_name,
version=wheel.version,
# Increase priority over eggs.
precedence=EGG_DIST + 1,
)]
if basename.endswith('.exe'):
win_base, py_ver, platform = parse_bdist_wininst(basename)
if win_base is not None:
@@ -155,7 +137,7 @@ def distros_for_location(location, basename, metadata=None):
#
for ext in EXTENSIONS:
if basename.endswith(ext):
basename = basename[: -len(ext)]
basename = basename[:-len(ext)]
return interpret_distro_name(location, basename, metadata)
return [] # no extension matched
@@ -168,37 +150,38 @@ def distros_for_filename(filename, metadata=None):
def interpret_distro_name(
location, basename, metadata, py_version=None, precedence=SOURCE_DIST, platform=None
location, basename, metadata, py_version=None, precedence=SOURCE_DIST,
platform=None
):
"""Generate the interpretation of a source distro name
"""Generate alternative interpretations of a source distro name
Note: if `location` is a filesystem filename, you should call
``pkg_resources.normalize_path()`` on it before passing it to this
routine!
"""
# Generate alternative interpretations of a source distro name
# Because some packages are ambiguous as to name/versions split
# e.g. "adns-python-1.1.0", "egenix-mx-commercial", etc.
# So, we generate each possible interpretation (e.g. "adns, python-1.1.0"
# "adns-python, 1.1.0", and "adns-python-1.1.0, no version"). In practice,
# the spurious interpretations should be ignored, because in the event
# there's also an "adns" package, the spurious "python-1.1.0" version will
# compare lower than any numeric version number, and is therefore unlikely
# to match a request for it. It's still a potential problem, though, and
# in the long run PyPI and the distutils should go for "safe" names and
# versions in distribution archive names (sdist and bdist).
parts = basename.split('-')
if not py_version and any(re.match(r'py\d\.\d$', p) for p in parts[2:]):
# it is a bdist_dumb, not an sdist -- bail out
return
# find the pivot (p) that splits the name from the version.
# infer the version as the first item that has a digit.
for p in range(len(parts)):
if parts[p][:1].isdigit():
break
else:
p = len(parts)
yield Distribution(
location,
metadata,
'-'.join(parts[:p]),
'-'.join(parts[p:]),
py_version=py_version,
precedence=precedence,
platform=platform,
)
for p in range(1, len(parts) + 1):
yield Distribution(
location, metadata, '-'.join(parts[:p]), '-'.join(parts[p:]),
py_version=py_version, precedence=precedence,
platform=platform
)
def unique_values(func):
@@ -214,10 +197,8 @@ def unique_values(func):
return wrapper
REL = re.compile(r"""<([^>]*\srel\s{0,10}=\s{0,10}['"]?([^'" >]+)[^>]*)>""", re.I)
"""
Regex for an HTML tag with 'rel="val"' attributes.
"""
REL = re.compile(r"""<([^>]*\srel\s*=\s*['"]?([^'">]+)[^>]*)>""", re.I)
# this line is here to fix emacs' cruddy broken syntax highlighting
@unique_values
@@ -270,7 +251,7 @@ class HashChecker(ContentChecker):
r'(?P<expected>[a-f0-9]+)'
)
def __init__(self, hash_name, expected) -> None:
def __init__(self, hash_name, expected):
self.hash_name = hash_name
self.hash = hashlib.new(hash_name)
self.expected = expected
@@ -301,21 +282,16 @@ class PackageIndex(Environment):
"""A distribution index that scans web pages for download URLs"""
def __init__(
self,
index_url: str = "https://pypi.org/simple/",
hosts=('*',),
ca_bundle=None,
verify_ssl: bool = True,
*args,
**kw,
) -> None:
self, index_url="https://pypi.org/simple/", hosts=('*',),
ca_bundle=None, verify_ssl=True, *args, **kw
):
super().__init__(*args, **kw)
self.index_url = index_url + "/"[: not index_url.endswith('/')]
self.scanned_urls: dict = {}
self.fetched_urls: dict = {}
self.package_pages: dict = {}
self.index_url = index_url + "/" [:not index_url.endswith('/')]
self.scanned_urls = {}
self.fetched_urls = {}
self.package_pages = {}
self.allows = re.compile('|'.join(map(translate, hosts))).match
self.to_scan: list = []
self.to_scan = []
self.opener = urllib.request.urlopen
def add(self, dist):
@@ -323,11 +299,11 @@ class PackageIndex(Environment):
try:
parse_version(dist.version)
except Exception:
return None
return
return super().add(dist)
# FIXME: 'PackageIndex.process_url' is too complex (14)
def process_url(self, url, retrieve: bool = False) -> None: # noqa: C901
def process_url(self, url, retrieve=False): # noqa: C901
"""Evaluate a URL as a possible download, and maybe retrieve it"""
if url in self.scanned_urls and not retrieve:
return
@@ -357,7 +333,7 @@ class PackageIndex(Environment):
if f is None:
return
if isinstance(f, urllib.error.HTTPError) and f.code == 401:
self.info(f"Authentication error: {f.msg}")
self.info("Authentication error: %s" % f.msg)
self.fetched_urls[f.url] = True
if 'html' not in f.headers.get('content-type', '').lower():
f.close() # not html, we can't process it
@@ -380,7 +356,7 @@ class PackageIndex(Environment):
if url.startswith(self.index_url) and getattr(f, 'code', None) != 404:
page = self.process_index(url, page)
def process_filename(self, fn, nested: bool = False) -> None:
def process_filename(self, fn, nested=False):
# process filenames or directories
if not os.path.exists(fn):
self.warn("Not found: %s", fn)
@@ -396,23 +372,20 @@ class PackageIndex(Environment):
self.debug("Found: %s", fn)
list(map(self.add, dists))
def url_ok(self, url, fatal: bool = False) -> bool:
def url_ok(self, url, fatal=False):
s = URL_SCHEME(url)
is_file = s and s.group(1).lower() == 'file'
if is_file or self.allows(urllib.parse.urlparse(url)[1]):
return True
msg = (
"\nNote: Bypassing %s (disallowed host; see "
"https://setuptools.pypa.io/en/latest/deprecated/"
"easy_install.html#restricting-downloads-with-allow-hosts for details).\n"
)
"http://bit.ly/2hrImnY for details).\n")
if fatal:
raise DistutilsError(msg % url)
else:
self.warn(msg, url)
return False
def scan_egg_links(self, search_path) -> None:
def scan_egg_links(self, search_path):
dirs = filter(os.path.isdir, search_path)
egg_links = (
(path, entry)
@@ -422,16 +395,16 @@ class PackageIndex(Environment):
)
list(itertools.starmap(self.scan_egg_link, egg_links))
def scan_egg_link(self, path, entry) -> None:
content = _read_utf8_with_fallback(os.path.join(path, entry))
# filter non-empty lines
lines = list(filter(None, map(str.strip, content.splitlines())))
def scan_egg_link(self, path, entry):
with open(os.path.join(path, entry)) as raw_lines:
# filter non-empty lines
lines = list(filter(None, map(str.strip, raw_lines)))
if len(lines) != 2:
# format is not recognized; punt
return
egg_path, _setup_path = lines
egg_path, setup_path = lines
for dist in find_distributions(os.path.join(path, egg_path)):
dist.location = os.path.join(path, *lines)
@@ -444,7 +417,9 @@ class PackageIndex(Environment):
if not link.startswith(self.index_url):
return NO_MATCH_SENTINEL
parts = list(map(urllib.parse.unquote, link[len(self.index_url) :].split('/')))
parts = list(map(
urllib.parse.unquote, link[len(self.index_url):].split('/')
))
if len(parts) != 2 or '#' in parts[1]:
return NO_MATCH_SENTINEL
@@ -474,30 +449,31 @@ class PackageIndex(Environment):
base, frag = egg_info_for_url(new_url)
if base.endswith('.py') and not frag:
if ver:
new_url += f'#egg={pkg}-{ver}'
new_url += '#egg=%s-%s' % (pkg, ver)
else:
self.need_version_info(url)
self.scan_url(new_url)
return PYPI_MD5.sub(
lambda m: '<a href="{}#md5={}">{}</a>'.format(*m.group(1, 3, 2)), page
lambda m: '<a href="%s#md5=%s">%s</a>' % m.group(1, 3, 2), page
)
def need_version_info(self, url) -> None:
def need_version_info(self, url):
self.scan_all(
"Page at %s links to .py file(s) without version info; an index "
"scan is required.",
url,
"scan is required.", url
)
def scan_all(self, msg=None, *args) -> None:
def scan_all(self, msg=None, *args):
if self.index_url not in self.fetched_urls:
if msg:
self.warn(msg, *args)
self.info("Scanning index of all packages (this may take a while)")
self.info(
"Scanning index of all packages (this may take a while)"
)
self.scan_url(self.index_url)
def find_packages(self, requirement) -> None:
def find_packages(self, requirement):
self.scan_url(self.index_url + requirement.unsafe_name + '/')
if not self.package_pages.get(requirement.key):
@@ -519,22 +495,25 @@ class PackageIndex(Environment):
if dist in requirement:
return dist
self.debug("%s does not match %s", requirement, dist)
return super().obtain(requirement, installer)
return super(PackageIndex, self).obtain(requirement, installer)
def check_hash(self, checker, filename, tfp) -> None:
def check_hash(self, checker, filename, tfp):
"""
checker is a ContentChecker
"""
checker.report(self.debug, f"Validating %s checksum for {filename}")
checker.report(
self.debug,
"Validating %%s checksum for %s" % filename)
if not checker.is_valid():
tfp.close()
os.unlink(filename)
raise DistutilsError(
f"{checker.hash.name} validation failed for {os.path.basename(filename)}; "
"%s validation failed for %s; "
"possible download problem?"
% (checker.hash.name, os.path.basename(filename))
)
def add_find_links(self, urls) -> None:
def add_find_links(self, urls):
"""Add `urls` to the list that will be prescanned for searches"""
for url in urls:
if (
@@ -555,11 +534,13 @@ class PackageIndex(Environment):
list(map(self.scan_url, self.to_scan))
self.to_scan = None # from now on, go ahead and process immediately
def not_found_in_index(self, requirement) -> None:
def not_found_in_index(self, requirement):
if self[requirement.key]: # we've seen at least one distro
meth, msg = self.info, "Couldn't retrieve index page for %r"
else: # no distros seen for this name, might be misspelled
meth, msg = self.warn, "Couldn't find index page for %r (maybe misspelled?)"
meth, msg = (
self.warn,
"Couldn't find index page for %r (maybe misspelled?)")
meth(msg, requirement.unsafe_name)
self.scan_all()
@@ -585,7 +566,7 @@ class PackageIndex(Environment):
scheme = URL_SCHEME(spec)
if scheme:
# It's a url, download it to tmpdir
found = self._download_url(spec, tmpdir)
found = self._download_url(scheme.group(1), spec, tmpdir)
base, fragment = egg_info_for_url(spec)
if base.endswith('.py'):
found = self.gen_setup(found, fragment, tmpdir)
@@ -598,14 +579,8 @@ class PackageIndex(Environment):
return getattr(self.fetch_distribution(spec, tmpdir), 'location', None)
def fetch_distribution( # noqa: C901 # is too complex (14) # FIXME
self,
requirement,
tmpdir,
force_scan: bool = False,
source: bool = False,
develop_ok: bool = False,
local_index=None,
) -> Distribution | None:
self, requirement, tmpdir, force_scan=False, source=False,
develop_ok=False, local_index=None):
"""Obtain a distribution suitable for fulfilling `requirement`
`requirement` must be a ``pkg_resources.Requirement`` instance.
@@ -624,33 +599,34 @@ class PackageIndex(Environment):
"""
# process a Requirement
self.info("Searching for %s", requirement)
skipped = set()
skipped = {}
dist = None
def find(req, env: Environment | None = None):
def find(req, env=None):
if env is None:
env = self
# Find a matching distribution; may be called more than once
for dist in env[req.key]:
if dist.precedence == DEVELOP_DIST and not develop_ok:
if dist not in skipped:
self.warn(
"Skipping development or system egg: %s",
dist,
"Skipping development or system egg: %s", dist,
)
skipped.add(dist)
skipped[dist] = 1
continue
test = dist in req and (dist.precedence <= SOURCE_DIST or not source)
test = (
dist in req
and (dist.precedence <= SOURCE_DIST or not source)
)
if test:
loc = self.download(dist.location, tmpdir)
dist.download_location = loc
if os.path.exists(dist.download_location):
return dist
return None
if force_scan:
self.prescan()
self.find_packages(requirement)
@@ -674,14 +650,11 @@ class PackageIndex(Environment):
(source and "a source distribution of " or ""),
requirement,
)
return None
else:
self.info("Best match: %s", dist)
return dist.clone(location=dist.download_location)
def fetch(
self, requirement, tmpdir, force_scan: bool = False, source: bool = False
) -> str | None:
def fetch(self, requirement, tmpdir, force_scan=False, source=False):
"""Obtain a file suitable for fulfilling `requirement`
DEPRECATED; use the ``fetch_distribution()`` method now instead. For
@@ -696,15 +669,10 @@ class PackageIndex(Environment):
def gen_setup(self, filename, fragment, tmpdir):
match = EGG_FRAGMENT.match(fragment)
dists = (
match
and [
d
for d in interpret_distro_name(filename, match.group(1), None)
if d.version
]
or []
)
dists = match and [
d for d in
interpret_distro_name(filename, match.group(1), None) if d.version
] or []
if len(dists) == 1: # unambiguous ``#egg`` fragment
basename = os.path.basename(filename)
@@ -716,18 +684,22 @@ class PackageIndex(Environment):
shutil.copy2(filename, dst)
filename = dst
with open(os.path.join(tmpdir, 'setup.py'), 'w', encoding="utf-8") as file:
with open(os.path.join(tmpdir, 'setup.py'), 'w') as file:
file.write(
"from setuptools import setup\n"
f"setup(name={dists[0].project_name!r}, version={dists[0].version!r}, py_modules=[{os.path.splitext(basename)[0]!r}])\n"
"setup(name=%r, version=%r, py_modules=[%r])\n"
% (
dists[0].project_name, dists[0].version,
os.path.splitext(basename)[0]
)
)
return filename
elif match:
raise DistutilsError(
f"Can't unambiguously interpret project/version identifier {fragment!r}; "
"Can't unambiguously interpret project/version identifier %r; "
"any dashes in the name or version should be escaped using "
f"underscores. {dists!r}"
"underscores. %r" % (fragment, dists)
)
else:
raise DistutilsError(
@@ -745,7 +717,9 @@ class PackageIndex(Environment):
checker = HashChecker.from_url(url)
fp = self.open_url(url)
if isinstance(fp, urllib.error.HTTPError):
raise DistutilsError(f"Can't download {url}: {fp.code} {fp.msg}")
raise DistutilsError(
"Can't download %s: %s %s" % (url, fp.code, fp.msg)
)
headers = fp.info()
blocknum = 0
bs = self.dl_blocksize
@@ -771,7 +745,7 @@ class PackageIndex(Environment):
if fp:
fp.close()
def reporthook(self, url, filename, blocknum, blksize, size) -> None:
def reporthook(self, url, filename, blocknum, blksize, size):
pass # no-op
# FIXME:
@@ -785,192 +759,164 @@ class PackageIndex(Environment):
if warning:
self.warn(warning, msg)
else:
raise DistutilsError(f'{url} {msg}') from v
raise DistutilsError('%s %s' % (url, msg)) from v
except urllib.error.HTTPError as v:
return v
except urllib.error.URLError as v:
if warning:
self.warn(warning, v.reason)
else:
raise DistutilsError(f"Download error for {url}: {v.reason}") from v
raise DistutilsError("Download error for %s: %s"
% (url, v.reason)) from v
except http.client.BadStatusLine as v:
if warning:
self.warn(warning, v.line)
else:
raise DistutilsError(
f'{url} returned a bad status line. The server might be '
f'down, {v.line}'
'%s returned a bad status line. The server might be '
'down, %s' %
(url, v.line)
) from v
except (http.client.HTTPException, OSError) as v:
except (http.client.HTTPException, socket.error) as v:
if warning:
self.warn(warning, v)
else:
raise DistutilsError(f"Download error for {url}: {v}") from v
raise DistutilsError("Download error for %s: %s"
% (url, v)) from v
@staticmethod
def _sanitize(name):
r"""
Replace unsafe path directives with underscores.
def _download_url(self, scheme, url, tmpdir):
# Determine download filename
#
name, fragment = egg_info_for_url(url)
if name:
while '..' in name:
name = name.replace('..', '.').replace('\\', '_')
else:
name = "__downloaded__" # default if URL has no path contents
>>> san = PackageIndex._sanitize
>>> san('/home/user/.ssh/authorized_keys')
'_home_user_.ssh_authorized_keys'
>>> san('..\\foo\\bing')
'__foo_bing'
>>> san('D:bar')
'D_bar'
>>> san('C:\\bar')
'C__bar'
>>> san('foo..bar')
'foo..bar'
>>> san('D:../foo')
'D___foo'
"""
pattern = '|'.join((
# drive letters
r':',
# path separators
r'[/\\]',
# parent dirs
r'(?:(?<=([/\\]|:))\.\.(?=[/\\]|$))|(?:^\.\.(?=[/\\]|$))',
))
return re.sub(pattern, r'_', name)
if name.endswith('.egg.zip'):
name = name[:-4] # strip the extra .zip before download
@classmethod
def _resolve_download_filename(cls, url, tmpdir):
"""
>>> import pathlib
>>> du = PackageIndex._resolve_download_filename
>>> root = getfixture('tmp_path')
>>> url = 'https://files.pythonhosted.org/packages/a9/5a/0db.../setuptools-78.1.0.tar.gz'
>>> str(pathlib.Path(du(url, root)).relative_to(root))
'setuptools-78.1.0.tar.gz'
"""
name, _fragment = egg_info_for_url(url)
name = cls._sanitize(
name
or
# default if URL has no path contents
'__downloaded__'
)
filename = os.path.join(tmpdir, name)
# strip any extra .zip before download
name = re.sub(r'\.egg\.zip$', '.egg', name)
# Download the file
#
if scheme == 'svn' or scheme.startswith('svn+'):
return self._download_svn(url, filename)
elif scheme == 'git' or scheme.startswith('git+'):
return self._download_git(url, filename)
elif scheme.startswith('hg+'):
return self._download_hg(url, filename)
elif scheme == 'file':
return urllib.request.url2pathname(urllib.parse.urlparse(url)[2])
else:
self.url_ok(url, True) # raises error if not allowed
return self._attempt_download(url, filename)
return os.path.join(tmpdir, name)
def _download_url(self, url, tmpdir):
"""
Determine the download filename.
"""
filename = self._resolve_download_filename(url, tmpdir)
return self._download_vcs(url, filename) or self._download_other(url, filename)
@staticmethod
def _resolve_vcs(url):
"""
>>> rvcs = PackageIndex._resolve_vcs
>>> rvcs('git+http://foo/bar')
'git'
>>> rvcs('hg+https://foo/bar')
'hg'
>>> rvcs('git:myhost')
'git'
>>> rvcs('hg:myhost')
>>> rvcs('http://foo/bar')
"""
scheme = urllib.parse.urlsplit(url).scheme
pre, sep, _post = scheme.partition('+')
# svn and git have their own protocol; hg does not
allowed = set(['svn', 'git'] + ['hg'] * bool(sep))
return next(iter({pre} & allowed), None)
def _download_vcs(self, url, spec_filename):
vcs = self._resolve_vcs(url)
if not vcs:
return None
if vcs == 'svn':
raise DistutilsError(
f"Invalid config, SVN download is not supported: {url}"
)
filename, _, _ = spec_filename.partition('#')
url, rev = self._vcs_split_rev_from_url(url)
self.info(f"Doing {vcs} clone from {url} to {filename}")
subprocess.check_call([vcs, 'clone', '--quiet', url, filename])
co_commands = dict(
git=[vcs, '-C', filename, 'checkout', '--quiet', rev],
hg=[vcs, '--cwd', filename, 'up', '-C', '-r', rev, '-q'],
)
if rev is not None:
self.info(f"Checking out {rev}")
subprocess.check_call(co_commands[vcs])
return filename
def _download_other(self, url, filename):
scheme = urllib.parse.urlsplit(url).scheme
if scheme == 'file': # pragma: no cover
return urllib.request.url2pathname(urllib.parse.urlparse(url).path)
# raise error if not allowed
self.url_ok(url, True)
return self._attempt_download(url, filename)
def scan_url(self, url) -> None:
def scan_url(self, url):
self.process_url(url, True)
def _attempt_download(self, url, filename):
headers = self._download_to(url, filename)
if 'html' in headers.get('content-type', '').lower():
return self._invalid_download_html(url, headers, filename)
return self._download_html(url, headers, filename)
else:
return filename
def _invalid_download_html(self, url, headers, filename):
def _download_html(self, url, headers, filename):
file = open(filename)
for line in file:
if line.strip():
# Check for a subversion index page
if re.search(r'<title>([^- ]+ - )?Revision \d+:', line):
# it's a subversion index page:
file.close()
os.unlink(filename)
return self._download_svn(url, filename)
break # not an index page
file.close()
os.unlink(filename)
raise DistutilsError(f"Unexpected HTML page found at {url}")
raise DistutilsError("Unexpected HTML page found at " + url)
def _download_svn(self, url, filename):
warnings.warn("SVN download support is deprecated", UserWarning)
url = url.split('#', 1)[0] # remove any fragment for svn's sake
creds = ''
if url.lower().startswith('svn:') and '@' in url:
scheme, netloc, path, p, q, f = urllib.parse.urlparse(url)
if not netloc and path.startswith('//') and '/' in path[2:]:
netloc, path = path[2:].split('/', 1)
auth, host = _splituser(netloc)
if auth:
if ':' in auth:
user, pw = auth.split(':', 1)
creds = " --username=%s --password=%s" % (user, pw)
else:
creds = " --username=" + auth
netloc = host
parts = scheme, netloc, url, p, q, f
url = urllib.parse.urlunparse(parts)
self.info("Doing subversion checkout from %s to %s", url, filename)
os.system("svn checkout%s -q %s %s" % (creds, url, filename))
return filename
@staticmethod
def _vcs_split_rev_from_url(url):
"""
Given a possible VCS URL, return a clean URL and resolved revision if any.
def _vcs_split_rev_from_url(url, pop_prefix=False):
scheme, netloc, path, query, frag = urllib.parse.urlsplit(url)
>>> vsrfu = PackageIndex._vcs_split_rev_from_url
>>> vsrfu('git+https://github.com/pypa/setuptools@v69.0.0#egg-info=setuptools')
('https://github.com/pypa/setuptools', 'v69.0.0')
>>> vsrfu('git+https://github.com/pypa/setuptools#egg-info=setuptools')
('https://github.com/pypa/setuptools', None)
>>> vsrfu('http://foo/bar')
('http://foo/bar', None)
"""
parts = urllib.parse.urlsplit(url)
clean_scheme = parts.scheme.split('+', 1)[-1]
scheme = scheme.split('+', 1)[-1]
# Some fragment identification fails
no_fragment_path, _, _ = parts.path.partition('#')
path = path.split('#', 1)[0]
pre, sep, post = no_fragment_path.rpartition('@')
clean_path, rev = (pre, post) if sep else (post, None)
rev = None
if '@' in path:
path, rev = path.rsplit('@', 1)
resolved = parts._replace(
scheme=clean_scheme,
path=clean_path,
# discard the fragment
fragment='',
).geturl()
# Also, discard fragment
url = urllib.parse.urlunsplit((scheme, netloc, path, query, ''))
return resolved, rev
return url, rev
def debug(self, msg, *args) -> None:
def _download_git(self, url, filename):
filename = filename.split('#', 1)[0]
url, rev = self._vcs_split_rev_from_url(url, pop_prefix=True)
self.info("Doing git clone from %s to %s", url, filename)
os.system("git clone --quiet %s %s" % (url, filename))
if rev is not None:
self.info("Checking out %s", rev)
os.system("git -C %s checkout --quiet %s" % (
filename,
rev,
))
return filename
def _download_hg(self, url, filename):
filename = filename.split('#', 1)[0]
url, rev = self._vcs_split_rev_from_url(url, pop_prefix=True)
self.info("Doing hg clone from %s to %s", url, filename)
os.system("hg clone --quiet %s %s" % (url, filename))
if rev is not None:
self.info("Updating to %s", rev)
os.system("hg --cwd %s up -C -r %s -q" % (
filename,
rev,
))
return filename
def debug(self, msg, *args):
log.debug(msg, *args)
def info(self, msg, *args) -> None:
def info(self, msg, *args):
log.info(msg, *args)
def warn(self, msg, *args) -> None:
def warn(self, msg, *args):
log.warn(msg, *args)
@@ -1032,20 +978,21 @@ def _encode_auth(auth):
return encoded.replace('\n', '')
class Credential(NamedTuple):
class Credential:
"""
A username/password pair.
Displayed separated by `:`.
>>> str(Credential('username', 'password'))
'username:password'
A username/password pair. Use like a namedtuple.
"""
username: str
password: str
def __init__(self, username, password):
self.username = username
self.password = password
def __str__(self) -> str:
return f'{self.username}:{self.password}'
def __iter__(self):
yield self.username
yield self.password
def __str__(self):
return '%(username)s:%(password)s' % vars(self)
class PyPIConfig(configparser.RawConfigParser):
@@ -1058,13 +1005,12 @@ class PyPIConfig(configparser.RawConfigParser):
rc = os.path.join(os.path.expanduser('~'), '.pypirc')
if os.path.exists(rc):
_cfg_read_utf8_with_fallback(self, rc)
self.read(rc)
@property
def creds_by_repository(self):
sections_with_repositories = [
section
for section in self.sections()
section for section in self.sections()
if self.get(section, 'repository').strip()
]
@@ -1085,7 +1031,6 @@ class PyPIConfig(configparser.RawConfigParser):
for repository, cred in self.creds_by_repository.items():
if url.startswith(repository):
return cred
return None
def open_with_auth(url, opener=urllib.request.urlopen):
@@ -1102,7 +1047,7 @@ def open_with_auth(url, opener=urllib.request.urlopen):
if scheme in ('http', 'https'):
auth, address = _splituser(netloc)
else:
auth, address = (None, None)
auth = None
if not auth:
cred = PyPIConfig().find_credential(url)
@@ -1135,7 +1080,6 @@ def open_with_auth(url, opener=urllib.request.urlopen):
# copy of urllib.parse._splituser from Python 3.8
# See https://github.com/python/cpython/issues/80072.
def _splituser(host):
"""splituser('user[:passwd]@host[:port]')
--> 'user[:passwd]', 'host[:port]'."""
@@ -1153,7 +1097,7 @@ def fix_sf_url(url):
def local_open(url):
"""Read a local path, with special support for directories"""
_scheme, _server, path, _param, _query, _frag = urllib.parse.urlparse(url)
scheme, server, path, param, query, frag = urllib.parse.urlparse(url)
filename = urllib.request.url2pathname(path)
if os.path.isfile(filename):
return urllib.request.urlopen(url)
@@ -1162,13 +1106,16 @@ def local_open(url):
for f in os.listdir(filename):
filepath = os.path.join(filename, f)
if f == 'index.html':
body = _read_utf8_with_fallback(filepath)
with open(filepath, 'r') as fp:
body = fp.read()
break
elif os.path.isdir(filepath):
f += '/'
files.append(f'<a href="{f}">{f}</a>')
files.append('<a href="{name}">{name}</a>'.format(name=f))
else:
tmpl = "<html><head><title>{url}</title></head><body>{files}</body></html>"
tmpl = (
"<html><head><title>{url}</title>"
"</head><body>{files}</body></html>")
body = tmpl.format(url=url, files='\n'.join(files))
status, message = 200, "OK"
else: