nem sei pq tantos arquivos

This commit is contained in:
2025-02-11 11:07:58 -03:00
parent 66fb4eb17b
commit 2da09a8a25
1841 changed files with 115867 additions and 77478 deletions

View File

@@ -1,23 +1,33 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2012-2022 Vinay Sajip.
# Copyright (C) 2012-2023 Vinay Sajip.
# Licensed to the Python Software Foundation under a contributor agreement.
# See LICENSE.txt and CONTRIBUTORS.txt.
#
import logging
__version__ = '0.3.6'
__version__ = '0.3.9'
class DistlibException(Exception):
pass
try:
from logging import NullHandler
except ImportError: # pragma: no cover
except ImportError: # pragma: no cover
class NullHandler(logging.Handler):
def handle(self, record): pass
def emit(self, record): pass
def createLock(self): self.lock = None
def handle(self, record):
pass
def emit(self, record):
pass
def createLock(self):
self.lock = None
logger = logging.getLogger(__name__)
logger.addHandler(NullHandler())

View File

@@ -8,6 +8,7 @@ from __future__ import absolute_import
import os
import re
import shutil
import sys
try:
@@ -33,9 +34,8 @@ if sys.version_info[0] < 3: # pragma: no cover
import urllib2
from urllib2 import (Request, urlopen, URLError, HTTPError,
HTTPBasicAuthHandler, HTTPPasswordMgr,
HTTPHandler, HTTPRedirectHandler,
build_opener)
HTTPBasicAuthHandler, HTTPPasswordMgr, HTTPHandler,
HTTPRedirectHandler, build_opener)
if ssl:
from urllib2 import HTTPSHandler
import httplib
@@ -50,15 +50,15 @@ if sys.version_info[0] < 3: # pragma: no cover
# Leaving this around for now, in case it needs resurrecting in some way
# _userprog = None
# def splituser(host):
# """splituser('user[:passwd]@host[:port]') --> 'user[:passwd]', 'host[:port]'."""
# global _userprog
# if _userprog is None:
# import re
# _userprog = re.compile('^(.*)@(.*)$')
# """splituser('user[:passwd]@host[:port]') --> 'user[:passwd]', 'host[:port]'."""
# global _userprog
# if _userprog is None:
# import re
# _userprog = re.compile('^(.*)@(.*)$')
# match = _userprog.match(host)
# if match: return match.group(1, 2)
# return None, host
# match = _userprog.match(host)
# if match: return match.group(1, 2)
# return None, host
else: # pragma: no cover
from io import StringIO
@@ -67,14 +67,12 @@ else: # pragma: no cover
from io import TextIOWrapper as file_type
import builtins
import configparser
import shutil
from urllib.parse import (urlparse, urlunparse, urljoin, quote,
unquote, urlsplit, urlunsplit, splittype)
from urllib.parse import (urlparse, urlunparse, urljoin, quote, unquote,
urlsplit, urlunsplit, splittype)
from urllib.request import (urlopen, urlretrieve, Request, url2pathname,
pathname2url,
HTTPBasicAuthHandler, HTTPPasswordMgr,
HTTPHandler, HTTPRedirectHandler,
build_opener)
pathname2url, HTTPBasicAuthHandler,
HTTPPasswordMgr, HTTPHandler,
HTTPRedirectHandler, build_opener)
if ssl:
from urllib.request import HTTPSHandler
from urllib.error import HTTPError, URLError, ContentTooShortError
@@ -88,14 +86,13 @@ else: # pragma: no cover
from itertools import filterfalse
filter = filter
try:
from ssl import match_hostname, CertificateError
except ImportError: # pragma: no cover
except ImportError: # pragma: no cover
class CertificateError(ValueError):
pass
def _dnsname_match(dn, hostname, max_wildcards=1):
"""Matching according to RFC 6125, section 6.4.3
@@ -145,7 +142,6 @@ except ImportError: # pragma: no cover
pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
return pat.match(hostname)
def match_hostname(cert, hostname):
"""Verify that *cert* (in decoded format as returned by
SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125
@@ -178,24 +174,26 @@ except ImportError: # pragma: no cover
dnsnames.append(value)
if len(dnsnames) > 1:
raise CertificateError("hostname %r "
"doesn't match either of %s"
% (hostname, ', '.join(map(repr, dnsnames))))
"doesn't match either of %s" %
(hostname, ', '.join(map(repr, dnsnames))))
elif len(dnsnames) == 1:
raise CertificateError("hostname %r "
"doesn't match %r"
% (hostname, dnsnames[0]))
"doesn't match %r" %
(hostname, dnsnames[0]))
else:
raise CertificateError("no appropriate commonName or "
"subjectAltName fields were found")
"subjectAltName fields were found")
try:
from types import SimpleNamespace as Container
except ImportError: # pragma: no cover
class Container(object):
"""
A generic container for when multiple values need to be returned
"""
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
@@ -214,12 +212,12 @@ except ImportError: # pragma: no cover
path.
"""
# Check that a given file can be accessed with the correct mode.
# Additionally check that `file` is not a directory, as on Windows
# directories pass the os.access check.
def _access_check(fn, mode):
return (os.path.exists(fn) and os.access(fn, mode)
and not os.path.isdir(fn))
return (os.path.exists(fn) and os.access(fn, mode) and not os.path.isdir(fn))
# If we're given a path with a directory part, look it up directly rather
# than referring to PATH directories. This includes checking relative to the
@@ -237,7 +235,7 @@ except ImportError: # pragma: no cover
if sys.platform == "win32":
# The current directory takes precedence on Windows.
if not os.curdir in path:
if os.curdir not in path:
path.insert(0, os.curdir)
# PATHEXT is necessary to check on Windows.
@@ -258,7 +256,7 @@ except ImportError: # pragma: no cover
seen = set()
for dir in path:
normdir = os.path.normcase(dir)
if not normdir in seen:
if normdir not in seen:
seen.add(normdir)
for thefile in files:
name = os.path.join(dir, thefile)
@@ -277,6 +275,7 @@ else: # pragma: no cover
from zipfile import ZipExtFile as BaseZipExtFile
class ZipExtFile(BaseZipExtFile):
def __init__(self, base):
self.__dict__.update(base.__dict__)
@@ -288,6 +287,7 @@ else: # pragma: no cover
# return None, so if an exception occurred, it will propagate
class ZipFile(BaseZipFile):
def __enter__(self):
return self
@@ -299,9 +299,11 @@ else: # pragma: no cover
base = BaseZipFile.open(self, *args, **kwargs)
return ZipExtFile(base)
try:
from platform import python_implementation
except ImportError: # pragma: no cover
except ImportError: # pragma: no cover
def python_implementation():
"""Return a string identifying the Python implementation."""
if 'PyPy' in sys.version:
@@ -312,12 +314,12 @@ except ImportError: # pragma: no cover
return 'IronPython'
return 'CPython'
import shutil
import sysconfig
try:
callable = callable
except NameError: # pragma: no cover
except NameError: # pragma: no cover
from collections.abc import Callable
def callable(obj):
@@ -358,11 +360,11 @@ except AttributeError: # pragma: no cover
raise TypeError("expect bytes or str, not %s" %
type(filename).__name__)
try:
from tokenize import detect_encoding
except ImportError: # pragma: no cover
except ImportError: # pragma: no cover
from codecs import BOM_UTF8, lookup
import re
cookie_re = re.compile(r"coding[:=]\s*([-\w.]+)")
@@ -401,6 +403,7 @@ except ImportError: # pragma: no cover
bom_found = False
encoding = None
default = 'utf-8'
def read_or_stop():
try:
return readline()
@@ -430,8 +433,8 @@ except ImportError: # pragma: no cover
if filename is None:
msg = "unknown encoding: " + encoding
else:
msg = "unknown encoding for {!r}: {}".format(filename,
encoding)
msg = "unknown encoding for {!r}: {}".format(
filename, encoding)
raise SyntaxError(msg)
if bom_found:
@@ -440,7 +443,8 @@ except ImportError: # pragma: no cover
if filename is None:
msg = 'encoding problem: utf-8'
else:
msg = 'encoding problem for {!r}: utf-8'.format(filename)
msg = 'encoding problem for {!r}: utf-8'.format(
filename)
raise SyntaxError(msg)
encoding += '-sig'
return encoding
@@ -467,6 +471,7 @@ except ImportError: # pragma: no cover
return default, [first, second]
# For converting & <-> &amp; etc.
try:
from html import escape
@@ -479,12 +484,13 @@ else:
try:
from collections import ChainMap
except ImportError: # pragma: no cover
except ImportError: # pragma: no cover
from collections import MutableMapping
try:
from reprlib import recursive_repr as _recursive_repr
except ImportError:
def _recursive_repr(fillvalue='...'):
'''
Decorator to make a repr function return fillvalue for a recursive
@@ -509,13 +515,15 @@ except ImportError: # pragma: no cover
wrapper.__module__ = getattr(user_function, '__module__')
wrapper.__doc__ = getattr(user_function, '__doc__')
wrapper.__name__ = getattr(user_function, '__name__')
wrapper.__annotations__ = getattr(user_function, '__annotations__', {})
wrapper.__annotations__ = getattr(user_function,
'__annotations__', {})
return wrapper
return decorating_function
class ChainMap(MutableMapping):
''' A ChainMap groups multiple dicts (or other mappings) together
'''
A ChainMap groups multiple dicts (or other mappings) together
to create a single, updateable view.
The underlying mappings are stored in a list. That list is public and can
@@ -524,7 +532,6 @@ except ImportError: # pragma: no cover
Lookups search the underlying mappings successively until a key is found.
In contrast, writes, updates, and deletions only operate on the first
mapping.
'''
def __init__(self, *maps):
@@ -532,7 +539,7 @@ except ImportError: # pragma: no cover
If no mappings are provided, a single empty dictionary is used.
'''
self.maps = list(maps) or [{}] # always at least one map
self.maps = list(maps) or [{}] # always at least one map
def __missing__(self, key):
raise KeyError(key)
@@ -540,16 +547,19 @@ except ImportError: # pragma: no cover
def __getitem__(self, key):
for mapping in self.maps:
try:
return mapping[key] # can't use 'key in mapping' with defaultdict
return mapping[
key] # can't use 'key in mapping' with defaultdict
except KeyError:
pass
return self.__missing__(key) # support subclasses that define __missing__
return self.__missing__(
key) # support subclasses that define __missing__
def get(self, key, default=None):
return self[key] if key in self else default
def __len__(self):
return len(set().union(*self.maps)) # reuses stored hash values if possible
return len(set().union(
*self.maps)) # reuses stored hash values if possible
def __iter__(self):
return iter(set().union(*self.maps))
@@ -576,12 +586,12 @@ except ImportError: # pragma: no cover
__copy__ = copy
def new_child(self): # like Django's Context.push()
def new_child(self): # like Django's Context.push()
'New ChainMap with a new dict followed by all previous maps.'
return self.__class__({}, *self.maps)
@property
def parents(self): # like Django's Context.pop()
def parents(self): # like Django's Context.pop()
'New ChainMap from maps[1:].'
return self.__class__(*self.maps[1:])
@@ -592,7 +602,8 @@ except ImportError: # pragma: no cover
try:
del self.maps[0][key]
except KeyError:
raise KeyError('Key not found in the first mapping: {!r}'.format(key))
raise KeyError(
'Key not found in the first mapping: {!r}'.format(key))
def popitem(self):
'Remove and return an item pair from maps[0]. Raise KeyError is maps[0] is empty.'
@@ -606,15 +617,18 @@ except ImportError: # pragma: no cover
try:
return self.maps[0].pop(key, *args)
except KeyError:
raise KeyError('Key not found in the first mapping: {!r}'.format(key))
raise KeyError(
'Key not found in the first mapping: {!r}'.format(key))
def clear(self):
'Clear maps[0], leaving maps[1:] intact.'
self.maps[0].clear()
try:
from importlib.util import cache_from_source # Python >= 3.4
except ImportError: # pragma: no cover
def cache_from_source(path, debug_override=None):
assert path.endswith('.py')
if debug_override is None:
@@ -625,12 +639,13 @@ except ImportError: # pragma: no cover
suffix = 'o'
return path + suffix
try:
from collections import OrderedDict
except ImportError: # pragma: no cover
## {{{ http://code.activestate.com/recipes/576693/ (r9)
# Backport of OrderedDict() class that runs on Python 2.4, 2.5, 2.6, 2.7 and pypy.
# Passes Python2.7's test suite and incorporates all the latest updates.
except ImportError: # pragma: no cover
# {{{ http://code.activestate.com/recipes/576693/ (r9)
# Backport of OrderedDict() class that runs on Python 2.4, 2.5, 2.6, 2.7 and pypy.
# Passes Python2.7's test suite and incorporates all the latest updates.
try:
from thread import get_ident as _get_ident
except ImportError:
@@ -641,9 +656,9 @@ except ImportError: # pragma: no cover
except ImportError:
pass
class OrderedDict(dict):
'Dictionary that remembers insertion order'
# An inherited dict maps keys to values.
# The inherited dict provides __getitem__, __len__, __contains__, and get.
# The remaining methods are order-aware.
@@ -661,11 +676,12 @@ except ImportError: # pragma: no cover
'''
if len(args) > 1:
raise TypeError('expected at most 1 arguments, got %d' % len(args))
raise TypeError('expected at most 1 arguments, got %d' %
len(args))
try:
self.__root
except AttributeError:
self.__root = root = [] # sentinel node
self.__root = root = [] # sentinel node
root[:] = [root, root, None]
self.__map = {}
self.__update(*args, **kwds)
@@ -779,7 +795,7 @@ except ImportError: # pragma: no cover
'''
if len(args) > 2:
raise TypeError('update() takes at most 2 positional '
'arguments (%d given)' % (len(args),))
'arguments (%d given)' % (len(args), ))
elif not args:
raise TypeError('update() takes at least 1 argument (0 given)')
self = args[0]
@@ -825,14 +841,15 @@ except ImportError: # pragma: no cover
def __repr__(self, _repr_running=None):
'od.__repr__() <==> repr(od)'
if not _repr_running: _repr_running = {}
if not _repr_running:
_repr_running = {}
call_key = id(self), _get_ident()
if call_key in _repr_running:
return '...'
_repr_running[call_key] = 1
try:
if not self:
return '%s()' % (self.__class__.__name__,)
return '%s()' % (self.__class__.__name__, )
return '%s(%r)' % (self.__class__.__name__, self.items())
finally:
del _repr_running[call_key]
@@ -844,8 +861,8 @@ except ImportError: # pragma: no cover
for k in vars(OrderedDict()):
inst_dict.pop(k, None)
if inst_dict:
return (self.__class__, (items,), inst_dict)
return self.__class__, (items,)
return (self.__class__, (items, ), inst_dict)
return self.__class__, (items, )
def copy(self):
'od.copy() -> a shallow copy of od'
@@ -868,7 +885,8 @@ except ImportError: # pragma: no cover
'''
if isinstance(other, OrderedDict):
return len(self)==len(other) and self.items() == other.items()
return len(self) == len(
other) and self.items() == other.items()
return dict.__eq__(self, other)
def __ne__(self, other):
@@ -888,19 +906,18 @@ except ImportError: # pragma: no cover
"od.viewitems() -> a set-like object providing a view on od's items"
return ItemsView(self)
try:
from logging.config import BaseConfigurator, valid_ident
except ImportError: # pragma: no cover
except ImportError: # pragma: no cover
IDENTIFIER = re.compile('^[a-z_][a-z0-9_]*$', re.I)
def valid_ident(s):
m = IDENTIFIER.match(s)
if not m:
raise ValueError('Not a valid Python identifier: %r' % s)
return True
# The ConvertingXXX classes are wrappers around standard Python containers,
# and they serve to convert any suitable values in the container. The
# conversion converts base dicts, lists and tuples to their wrapped
@@ -916,7 +933,7 @@ except ImportError: # pragma: no cover
def __getitem__(self, key):
value = dict.__getitem__(self, key)
result = self.configurator.convert(value)
#If the converted value is different, save for next time
# If the converted value is different, save for next time
if value is not result:
self[key] = result
if type(result) in (ConvertingDict, ConvertingList,
@@ -928,7 +945,7 @@ except ImportError: # pragma: no cover
def get(self, key, default=None):
value = dict.get(self, key, default)
result = self.configurator.convert(value)
#If the converted value is different, save for next time
# If the converted value is different, save for next time
if value is not result:
self[key] = result
if type(result) in (ConvertingDict, ConvertingList,
@@ -949,10 +966,11 @@ except ImportError: # pragma: no cover
class ConvertingList(list):
"""A converting list wrapper."""
def __getitem__(self, key):
value = list.__getitem__(self, key)
result = self.configurator.convert(value)
#If the converted value is different, save for next time
# If the converted value is different, save for next time
if value is not result:
self[key] = result
if type(result) in (ConvertingDict, ConvertingList,
@@ -972,6 +990,7 @@ except ImportError: # pragma: no cover
class ConvertingTuple(tuple):
"""A converting tuple wrapper."""
def __getitem__(self, key):
value = tuple.__getitem__(self, key)
result = self.configurator.convert(value)
@@ -995,8 +1014,8 @@ except ImportError: # pragma: no cover
DIGIT_PATTERN = re.compile(r'^\d+$')
value_converters = {
'ext' : 'ext_convert',
'cfg' : 'cfg_convert',
'ext': 'ext_convert',
'cfg': 'cfg_convert',
}
# We might want to use a different one, e.g. importlib
@@ -1042,7 +1061,6 @@ except ImportError: # pragma: no cover
else:
rest = rest[m.end():]
d = self.config[m.groups()[0]]
#print d, rest
while rest:
m = self.DOT_PATTERN.match(rest)
if m:
@@ -1055,7 +1073,9 @@ except ImportError: # pragma: no cover
d = d[idx]
else:
try:
n = int(idx) # try as number first (most likely)
n = int(
idx
) # try as number first (most likely)
d = d[n]
except TypeError:
d = d[idx]
@@ -1064,7 +1084,7 @@ except ImportError: # pragma: no cover
else:
raise ValueError('Unable to convert '
'%r at %r' % (value, rest))
#rest should be empty
# rest should be empty
return d
def convert(self, value):
@@ -1073,14 +1093,15 @@ except ImportError: # pragma: no cover
replaced by their converting alternatives. Strings are checked to
see if they have a conversion format and are converted if they do.
"""
if not isinstance(value, ConvertingDict) and isinstance(value, dict):
if not isinstance(value, ConvertingDict) and isinstance(
value, dict):
value = ConvertingDict(value)
value.configurator = self
elif not isinstance(value, ConvertingList) and isinstance(value, list):
elif not isinstance(value, ConvertingList) and isinstance(
value, list):
value = ConvertingList(value)
value.configurator = self
elif not isinstance(value, ConvertingTuple) and\
isinstance(value, tuple):
elif not isinstance(value, ConvertingTuple) and isinstance(value, tuple):
value = ConvertingTuple(value)
value.configurator = self
elif isinstance(value, string_types):

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2012-2017 The Python Software Foundation.
# Copyright (C) 2012-2023 The Python Software Foundation.
# See LICENSE.txt and CONTRIBUTORS.txt.
#
"""PEP 376 implementation."""
@@ -20,24 +20,20 @@ import zipimport
from . import DistlibException, resources
from .compat import StringIO
from .version import get_scheme, UnsupportedVersionError
from .metadata import (Metadata, METADATA_FILENAME, WHEEL_METADATA_FILENAME,
LEGACY_METADATA_FILENAME)
from .util import (parse_requirement, cached_property, parse_name_and_version,
read_exports, write_exports, CSVReader, CSVWriter)
__all__ = ['Distribution', 'BaseInstalledDistribution',
'InstalledDistribution', 'EggInfoDistribution',
'DistributionPath']
from .metadata import (Metadata, METADATA_FILENAME, WHEEL_METADATA_FILENAME, LEGACY_METADATA_FILENAME)
from .util import (parse_requirement, cached_property, parse_name_and_version, read_exports, write_exports, CSVReader,
CSVWriter)
__all__ = [
'Distribution', 'BaseInstalledDistribution', 'InstalledDistribution', 'EggInfoDistribution', 'DistributionPath'
]
logger = logging.getLogger(__name__)
EXPORTS_FILENAME = 'pydist-exports.json'
COMMANDS_FILENAME = 'pydist-commands.json'
DIST_FILES = ('INSTALLER', METADATA_FILENAME, 'RECORD', 'REQUESTED',
'RESOURCES', EXPORTS_FILENAME, 'SHARED')
DIST_FILES = ('INSTALLER', METADATA_FILENAME, 'RECORD', 'REQUESTED', 'RESOURCES', EXPORTS_FILENAME, 'SHARED')
DISTINFO_EXT = '.dist-info'
@@ -46,6 +42,7 @@ class _Cache(object):
"""
A simple cache mapping names and .dist-info paths to distributions
"""
def __init__(self):
"""
Initialise an instance. There is normally one for each DistributionPath.
@@ -76,6 +73,7 @@ class DistributionPath(object):
"""
Represents a set of distributions installed on a path (typically sys.path).
"""
def __init__(self, path=None, include_egg=False):
"""
Create an instance from a path, optionally including legacy (distutils/
@@ -111,7 +109,6 @@ class DistributionPath(object):
self._cache.clear()
self._cache_egg.clear()
def _yield_distributions(self):
"""
Yield .dist-info and/or .egg(-info) distributions.
@@ -134,9 +131,7 @@ class DistributionPath(object):
continue
try:
if self._include_dist and entry.endswith(DISTINFO_EXT):
possible_filenames = [METADATA_FILENAME,
WHEEL_METADATA_FILENAME,
LEGACY_METADATA_FILENAME]
possible_filenames = [METADATA_FILENAME, WHEEL_METADATA_FILENAME, LEGACY_METADATA_FILENAME]
for metadata_filename in possible_filenames:
metadata_path = posixpath.join(entry, metadata_filename)
pydist = finder.find(metadata_path)
@@ -149,10 +144,8 @@ class DistributionPath(object):
metadata = Metadata(fileobj=stream, scheme='legacy')
logger.debug('Found %s', r.path)
seen.add(r.path)
yield new_dist_class(r.path, metadata=metadata,
env=self)
elif self._include_egg and entry.endswith(('.egg-info',
'.egg')):
yield new_dist_class(r.path, metadata=metadata, env=self)
elif self._include_egg and entry.endswith(('.egg-info', '.egg')):
logger.debug('Found %s', r.path)
seen.add(r.path)
yield old_dist_class(r.path, self)
@@ -270,8 +263,7 @@ class DistributionPath(object):
try:
matcher = self._scheme.matcher('%s (%s)' % (name, version))
except ValueError:
raise DistlibException('invalid name or version: %r, %r' %
(name, version))
raise DistlibException('invalid name or version: %r, %r' % (name, version))
for dist in self.get_distributions():
# We hit a problem on Travis where enum34 was installed and doesn't
@@ -346,12 +338,12 @@ class Distribution(object):
"""
self.metadata = metadata
self.name = metadata.name
self.key = self.name.lower() # for case-insensitive comparisons
self.key = self.name.lower() # for case-insensitive comparisons
self.version = metadata.version
self.locator = None
self.digest = None
self.extras = None # additional features requested
self.context = None # environment marker overrides
self.extras = None # additional features requested
self.context = None # environment marker overrides
self.download_urls = set()
self.digests = {}
@@ -362,7 +354,7 @@ class Distribution(object):
"""
return self.metadata.source_url
download_url = source_url # Backward compatibility
download_url = source_url # Backward compatibility
@property
def name_and_version(self):
@@ -386,10 +378,8 @@ class Distribution(object):
def _get_requirements(self, req_attr):
md = self.metadata
reqts = getattr(md, req_attr)
logger.debug('%s: got requirements %r from metadata: %r', self.name, req_attr,
reqts)
return set(md.get_requirements(reqts, extras=self.extras,
env=self.context))
logger.debug('%s: got requirements %r from metadata: %r', self.name, req_attr, reqts)
return set(md.get_requirements(reqts, extras=self.extras, env=self.context))
@property
def run_requires(self):
@@ -426,12 +416,11 @@ class Distribution(object):
matcher = scheme.matcher(r.requirement)
except UnsupportedVersionError:
# XXX compat-mode if cannot read the version
logger.warning('could not read version %r - using name only',
req)
logger.warning('could not read version %r - using name only', req)
name = req.split()[0]
matcher = scheme.matcher(name)
name = matcher.key # case-insensitive
name = matcher.key # case-insensitive
result = False
for p in self.provides:
@@ -466,9 +455,7 @@ class Distribution(object):
if type(other) is not type(self):
result = False
else:
result = (self.name == other.name and
self.version == other.version and
self.source_url == other.source_url)
result = (self.name == other.name and self.version == other.version and self.source_url == other.source_url)
return result
def __hash__(self):
@@ -559,8 +546,7 @@ class InstalledDistribution(BaseInstalledDistribution):
if r is None:
r = finder.find(LEGACY_METADATA_FILENAME)
if r is None:
raise ValueError('no %s found in %s' % (METADATA_FILENAME,
path))
raise ValueError('no %s found in %s' % (METADATA_FILENAME, path))
with contextlib.closing(r.as_stream()) as stream:
metadata = Metadata(fileobj=stream, scheme='legacy')
@@ -571,15 +557,14 @@ class InstalledDistribution(BaseInstalledDistribution):
r = finder.find('REQUESTED')
self.requested = r is not None
p = os.path.join(path, 'top_level.txt')
p = os.path.join(path, 'top_level.txt')
if os.path.exists(p):
with open(p, 'rb') as f:
data = f.read().decode('utf-8')
self.modules = data.splitlines()
def __repr__(self):
return '<InstalledDistribution %r %s at %r>' % (
self.name, self.version, self.path)
return '<InstalledDistribution %r %s at %r>' % (self.name, self.version, self.path)
def __str__(self):
return "%s %s" % (self.name, self.version)
@@ -596,14 +581,14 @@ class InstalledDistribution(BaseInstalledDistribution):
with contextlib.closing(r.as_stream()) as stream:
with CSVReader(stream=stream) as record_reader:
# Base location is parent dir of .dist-info dir
#base_location = os.path.dirname(self.path)
#base_location = os.path.abspath(base_location)
# base_location = os.path.dirname(self.path)
# base_location = os.path.abspath(base_location)
for row in record_reader:
missing = [None for i in range(len(row), 3)]
path, checksum, size = row + missing
#if not os.path.isabs(path):
# path = path.replace('/', os.sep)
# path = os.path.join(base_location, path)
# if not os.path.isabs(path):
# path = path.replace('/', os.sep)
# path = os.path.join(base_location, path)
results.append((path, checksum, size))
return results
@@ -701,8 +686,7 @@ class InstalledDistribution(BaseInstalledDistribution):
size = '%d' % os.path.getsize(path)
with open(path, 'rb') as fp:
hash_value = self.get_hash(fp.read())
if path.startswith(base) or (base_under_prefix and
path.startswith(prefix)):
if path.startswith(base) or (base_under_prefix and path.startswith(prefix)):
path = os.path.relpath(path, base)
writer.writerow((path, hash_value, size))
@@ -791,7 +775,7 @@ class InstalledDistribution(BaseInstalledDistribution):
for key in ('prefix', 'lib', 'headers', 'scripts', 'data'):
path = paths[key]
if os.path.isdir(paths[key]):
lines.append('%s=%s' % (key, path))
lines.append('%s=%s' % (key, path))
for ns in paths.get('namespace', ()):
lines.append('namespace=%s' % ns)
@@ -826,9 +810,8 @@ class InstalledDistribution(BaseInstalledDistribution):
# it's an absolute path?
distinfo_dirname, path = path.split(os.sep)[-2:]
if distinfo_dirname != self.path.split(os.sep)[-1]:
raise DistlibException(
'dist-info file %r does not belong to the %r %s '
'distribution' % (path, self.name, self.version))
raise DistlibException('dist-info file %r does not belong to the %r %s '
'distribution' % (path, self.name, self.version))
# The file must be relative
if path not in DIST_FILES:
@@ -854,8 +837,7 @@ class InstalledDistribution(BaseInstalledDistribution):
yield path
def __eq__(self, other):
return (isinstance(other, InstalledDistribution) and
self.path == other.path)
return (isinstance(other, InstalledDistribution) and self.path == other.path)
# See http://docs.python.org/reference/datamodel#object.__hash__
__hash__ = object.__hash__
@@ -867,13 +849,14 @@ class EggInfoDistribution(BaseInstalledDistribution):
if the given path happens to be a directory, the metadata is read from the
file ``PKG-INFO`` under that directory."""
requested = True # as we have no way of knowing, assume it was
requested = True # as we have no way of knowing, assume it was
shared_locations = {}
def __init__(self, path, env=None):
def set_name_and_version(s, n, v):
s.name = n
s.key = n.lower() # for case-insensitive comparisons
s.key = n.lower() # for case-insensitive comparisons
s.version = v
self.path = path
@@ -903,15 +886,17 @@ class EggInfoDistribution(BaseInstalledDistribution):
lines = data.splitlines()
for line in lines:
line = line.strip()
if line.startswith('['):
logger.warning('Unexpected line: quitting requirement scan: %r',
line)
# sectioned files have bare newlines (separating sections)
if not line: # pragma: no cover
continue
if line.startswith('['): # pragma: no cover
logger.warning('Unexpected line: quitting requirement scan: %r', line)
break
r = parse_requirement(line)
if not r:
if not r: # pragma: no cover
logger.warning('Not recognised as a requirement: %r', line)
continue
if r.extras:
if r.extras: # pragma: no cover
logger.warning('extra requirements in requires.txt are '
'not supported')
if not r.constraints:
@@ -947,8 +932,7 @@ class EggInfoDistribution(BaseInstalledDistribution):
else:
# FIXME handle the case where zipfile is not available
zipf = zipimport.zipimporter(path)
fileobj = StringIO(
zipf.get_data('EGG-INFO/PKG-INFO').decode('utf8'))
fileobj = StringIO(zipf.get_data('EGG-INFO/PKG-INFO').decode('utf8'))
metadata = Metadata(fileobj=fileobj, scheme='legacy')
try:
data = zipf.get_data('EGG-INFO/requires.txt')
@@ -982,8 +966,7 @@ class EggInfoDistribution(BaseInstalledDistribution):
return metadata
def __repr__(self):
return '<EggInfoDistribution %r %s at %r>' % (
self.name, self.version, self.path)
return '<EggInfoDistribution %r %s at %r>' % (self.name, self.version, self.path)
def __str__(self):
return "%s %s" % (self.name, self.version)
@@ -1039,7 +1022,7 @@ class EggInfoDistribution(BaseInstalledDistribution):
logger.warning('Non-existent file: %s', p)
if p.endswith(('.pyc', '.pyo')):
continue
#otherwise fall through and fail
# otherwise fall through and fail
if not os.path.isdir(p):
result.append((p, _md5(p), _size(p)))
result.append((record_path, None, None))
@@ -1075,12 +1058,12 @@ class EggInfoDistribution(BaseInstalledDistribution):
yield line
def __eq__(self, other):
return (isinstance(other, EggInfoDistribution) and
self.path == other.path)
return (isinstance(other, EggInfoDistribution) and self.path == other.path)
# See http://docs.python.org/reference/datamodel#object.__hash__
__hash__ = object.__hash__
new_dist_class = InstalledDistribution
old_dist_class = EggInfoDistribution
@@ -1114,7 +1097,7 @@ class DependencyGraph(object):
"""
self.adjacency_list[distribution] = []
self.reverse_list[distribution] = []
#self.missing[distribution] = []
# self.missing[distribution] = []
def add_edge(self, x, y, label=None):
"""Add an edge from distribution *x* to distribution *y* with the given
@@ -1174,9 +1157,8 @@ class DependencyGraph(object):
if len(adjs) == 0 and not skip_disconnected:
disconnected.append(dist)
for other, label in adjs:
if not label is None:
f.write('"%s" -> "%s" [label="%s"]\n' %
(dist.name, other.name, label))
if label is not None:
f.write('"%s" -> "%s" [label="%s"]\n' % (dist.name, other.name, label))
else:
f.write('"%s" -> "%s"\n' % (dist.name, other.name))
if not skip_disconnected and len(disconnected) > 0:
@@ -1216,8 +1198,7 @@ class DependencyGraph(object):
# Remove from the adjacency list of others
for k, v in alist.items():
alist[k] = [(d, r) for d, r in v if d not in to_remove]
logger.debug('Moving to result: %s',
['%s (%s)' % (d.name, d.version) for d in to_remove])
logger.debug('Moving to result: %s', ['%s (%s)' % (d.name, d.version) for d in to_remove])
result.extend(to_remove)
return result, list(alist.keys())
@@ -1252,19 +1233,17 @@ def make_graph(dists, scheme='default'):
# now make the edges
for dist in dists:
requires = (dist.run_requires | dist.meta_requires |
dist.build_requires | dist.dev_requires)
requires = (dist.run_requires | dist.meta_requires | dist.build_requires | dist.dev_requires)
for req in requires:
try:
matcher = scheme.matcher(req)
except UnsupportedVersionError:
# XXX compat-mode if cannot read the version
logger.warning('could not read version %r - using name only',
req)
logger.warning('could not read version %r - using name only', req)
name = req.split()[0]
matcher = scheme.matcher(name)
name = matcher.key # case-insensitive
name = matcher.key # case-insensitive
matched = False
if name in provided:
@@ -1324,7 +1303,7 @@ def get_required_dists(dists, dist):
req = set() # required distributions
todo = graph.adjacency_list[dist] # list of nodes we should inspect
seen = set(t[0] for t in todo) # already added to todo
seen = set(t[0] for t in todo) # already added to todo
while todo:
d = todo.pop()[0]

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013 Vinay Sajip.
# Copyright (C) 2013-2023 Vinay Sajip.
# Licensed to the Python Software Foundation under a contributor agreement.
# See LICENSE.txt and CONTRIBUTORS.txt.
#
@@ -25,6 +25,7 @@ logger = logging.getLogger(__name__)
DEFAULT_INDEX = 'https://pypi.org/pypi'
DEFAULT_REALM = 'pypi'
class PackageIndex(object):
"""
This class represents a package index compatible with PyPI, the Python
@@ -119,7 +120,7 @@ class PackageIndex(object):
d = metadata.todict()
d[':action'] = 'verify'
request = self.encode_request(d.items(), [])
response = self.send_request(request)
self.send_request(request)
d[':action'] = 'submit'
request = self.encode_request(d.items(), [])
return self.send_request(request)
@@ -358,8 +359,7 @@ class PackageIndex(object):
keystore)
rc, stdout, stderr = self.run_command(cmd)
if rc not in (0, 1):
raise DistlibException('verify command failed with error '
'code %s' % rc)
raise DistlibException('verify command failed with error code %s' % rc)
return rc == 0
def download_file(self, url, destfile, digest=None, reporthook=None):

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2012-2015 Vinay Sajip.
# Copyright (C) 2012-2023 Vinay Sajip.
# Licensed to the Python Software Foundation under a contributor agreement.
# See LICENSE.txt and CONTRIBUTORS.txt.
#
@@ -19,15 +19,12 @@ except ImportError: # pragma: no cover
import zlib
from . import DistlibException
from .compat import (urljoin, urlparse, urlunparse, url2pathname, pathname2url,
queue, quote, unescape, build_opener,
HTTPRedirectHandler as BaseRedirectHandler, text_type,
Request, HTTPError, URLError)
from .compat import (urljoin, urlparse, urlunparse, url2pathname, pathname2url, queue, quote, unescape, build_opener,
HTTPRedirectHandler as BaseRedirectHandler, text_type, Request, HTTPError, URLError)
from .database import Distribution, DistributionPath, make_dist
from .metadata import Metadata, MetadataInvalidError
from .util import (cached_property, ensure_slash, split_filename, get_project_data,
parse_requirement, parse_name_and_version, ServerProxy,
normalize_name)
from .util import (cached_property, ensure_slash, split_filename, get_project_data, parse_requirement,
parse_name_and_version, ServerProxy, normalize_name)
from .version import get_scheme, UnsupportedVersionError
from .wheel import Wheel, is_compatible
@@ -38,6 +35,7 @@ CHARSET = re.compile(r';\s*charset\s*=\s*(.*)\s*$', re.I)
HTML_CONTENT_TYPE = re.compile('text/html|application/x(ht)?ml')
DEFAULT_INDEX = 'https://pypi.org/pypi'
def get_all_distribution_names(url=None):
"""
Return all distribution names known by an index.
@@ -52,10 +50,12 @@ def get_all_distribution_names(url=None):
finally:
client('close')()
class RedirectHandler(BaseRedirectHandler):
"""
A class to work around a bug in some Python 3.2.x releases.
"""
# There's a bug in the base version for some 3.2.x
# (e.g. 3.2.2 on Ubuntu Oneiric). If a Location header
# returns e.g. /abc, it bails because it says the scheme ''
@@ -78,18 +78,18 @@ class RedirectHandler(BaseRedirectHandler):
headers.replace_header(key, newurl)
else:
headers[key] = newurl
return BaseRedirectHandler.http_error_302(self, req, fp, code, msg,
headers)
return BaseRedirectHandler.http_error_302(self, req, fp, code, msg, headers)
http_error_301 = http_error_303 = http_error_307 = http_error_302
class Locator(object):
"""
A base class for locators - things that locate distributions.
"""
source_extensions = ('.tar.gz', '.tar.bz2', '.tar', '.zip', '.tgz', '.tbz')
binary_extensions = ('.egg', '.exe', '.whl')
excluded_extensions = ('.pdf',)
excluded_extensions = ('.pdf', )
# A list of tags indicating which wheels you want to match. The default
# value of None matches against the tags compatible with the running
@@ -97,7 +97,7 @@ class Locator(object):
# instance to a list of tuples (pyver, abi, arch) which you want to match.
wheel_tags = None
downloadable_extensions = source_extensions + ('.whl',)
downloadable_extensions = source_extensions + ('.whl', )
def __init__(self, scheme='default'):
"""
@@ -197,8 +197,7 @@ class Locator(object):
is_downloadable = basename.endswith(self.downloadable_extensions)
if is_wheel:
compatible = is_compatible(Wheel(basename), self.wheel_tags)
return (t.scheme == 'https', 'pypi.org' in t.netloc,
is_downloadable, is_wheel, compatible, basename)
return (t.scheme == 'https', 'pypi.org' in t.netloc, is_downloadable, is_wheel, compatible, basename)
def prefer_url(self, url1, url2):
"""
@@ -236,14 +235,14 @@ class Locator(object):
If it is, a dictionary is returned with keys "name", "version",
"filename" and "url"; otherwise, None is returned.
"""
def same_project(name1, name2):
return normalize_name(name1) == normalize_name(name2)
result = None
scheme, netloc, path, params, query, frag = urlparse(url)
if frag.lower().startswith('egg='): # pragma: no cover
logger.debug('%s: version hint in fragment: %r',
project_name, frag)
logger.debug('%s: version hint in fragment: %r', project_name, frag)
m = HASHER_HASH.match(frag)
if m:
algo, digest = m.groups()
@@ -267,12 +266,10 @@ class Locator(object):
'name': wheel.name,
'version': wheel.version,
'filename': wheel.filename,
'url': urlunparse((scheme, netloc, origpath,
params, query, '')),
'python-version': ', '.join(
['.'.join(list(v[2:])) for v in wheel.pyver]),
'url': urlunparse((scheme, netloc, origpath, params, query, '')),
'python-version': ', '.join(['.'.join(list(v[2:])) for v in wheel.pyver]),
}
except Exception as e: # pragma: no cover
except Exception: # pragma: no cover
logger.warning('invalid path for wheel: %s', path)
elif not path.endswith(self.downloadable_extensions): # pragma: no cover
logger.debug('Not downloadable: %s', path)
@@ -291,9 +288,7 @@ class Locator(object):
'name': name,
'version': version,
'filename': filename,
'url': urlunparse((scheme, netloc, origpath,
params, query, '')),
#'packagetype': 'sdist',
'url': urlunparse((scheme, netloc, origpath, params, query, '')),
}
if pyver: # pragma: no cover
result['python-version'] = pyver
@@ -369,7 +364,7 @@ class Locator(object):
self.matcher = matcher = scheme.matcher(r.requirement)
logger.debug('matcher: %s (%s)', matcher, type(matcher).__name__)
versions = self.get_project(r.name)
if len(versions) > 2: # urls and digests keys are present
if len(versions) > 2: # urls and digests keys are present
# sometimes, versions are invalid
slist = []
vcls = matcher.version_class
@@ -382,12 +377,9 @@ class Locator(object):
else:
if prereleases or not vcls(k).is_prerelease:
slist.append(k)
# else:
# logger.debug('skipping pre-release '
# 'version %s of %s', k, matcher.name)
except Exception: # pragma: no cover
logger.warning('error matching %s with %r', matcher, k)
pass # slist.append(k)
pass # slist.append(k)
if len(slist) > 1:
slist = sorted(slist, key=scheme.key)
if slist:
@@ -413,6 +405,7 @@ class PyPIRPCLocator(Locator):
This locator uses XML-RPC to locate distributions. It therefore
cannot be used with simple mirrors (that only mirror file content).
"""
def __init__(self, url, **kwargs):
"""
Initialise an instance.
@@ -456,11 +449,13 @@ class PyPIRPCLocator(Locator):
result['digests'][url] = digest
return result
class PyPIJSONLocator(Locator):
"""
This locator uses PyPI's JSON interface. It's very limited in functionality
and probably not worth using.
"""
def __init__(self, url, **kwargs):
super(PyPIJSONLocator, self).__init__(**kwargs)
self.base_url = ensure_slash(url)
@@ -476,7 +471,7 @@ class PyPIJSONLocator(Locator):
url = urljoin(self.base_url, '%s/json' % quote(name))
try:
resp = self.opener.open(url)
data = resp.read().decode() # for now
data = resp.read().decode() # for now
d = json.loads(data)
md = Metadata(scheme=self.scheme)
data = d['info']
@@ -487,7 +482,7 @@ class PyPIJSONLocator(Locator):
md.summary = data.get('summary')
dist = Distribution(md)
dist.locator = self
urls = d['urls']
# urls = d['urls']
result[md.version] = dist
for info in d['urls']:
url = info['url']
@@ -498,7 +493,7 @@ class PyPIJSONLocator(Locator):
# Now get other releases
for version, infos in d['releases'].items():
if version == md.version:
continue # already done
continue # already done
omd = Metadata(scheme=self.scheme)
omd.name = md.name
omd.version = version
@@ -511,6 +506,8 @@ class PyPIJSONLocator(Locator):
odist.digests[url] = self._get_digest(info)
result['urls'].setdefault(version, set()).add(url)
result['digests'][url] = self._get_digest(info)
# for info in urls:
# md.source_url = info['url']
# dist.digest = self._get_digest(info)
@@ -534,7 +531,8 @@ class Page(object):
# or immediately followed by a "rel" attribute. The attribute values can be
# declared with double quotes, single quotes or no quotes - which leads to
# the length of the expression.
_href = re.compile("""
_href = re.compile(
"""
(rel\\s*=\\s*(?:"(?P<rel1>[^"]*)"|'(?P<rel2>[^']*)'|(?P<rel3>[^>\\s\n]*))\\s+)?
href\\s*=\\s*(?:"(?P<url1>[^"]*)"|'(?P<url2>[^']*)'|(?P<url3>[^>\\s\n]*))
(\\s+rel\\s*=\\s*(?:"(?P<rel4>[^"]*)"|'(?P<rel5>[^']*)'|(?P<rel6>[^>\\s\n]*)))?
@@ -561,17 +559,16 @@ href\\s*=\\s*(?:"(?P<url1>[^"]*)"|'(?P<url2>[^']*)'|(?P<url3>[^>\\s\n]*))
about their "rel" attribute, for determining which ones to treat as
downloads and which ones to queue for further scraping.
"""
def clean(url):
"Tidy up an URL."
scheme, netloc, path, params, query, frag = urlparse(url)
return urlunparse((scheme, netloc, quote(path),
params, query, frag))
return urlunparse((scheme, netloc, quote(path), params, query, frag))
result = set()
for match in self._href.finditer(self.data):
d = match.groupdict('')
rel = (d['rel1'] or d['rel2'] or d['rel3'] or
d['rel4'] or d['rel5'] or d['rel6'])
rel = (d['rel1'] or d['rel2'] or d['rel3'] or d['rel4'] or d['rel5'] or d['rel6'])
url = d['url1'] or d['url2'] or d['url3']
url = urljoin(self.base_url, url)
url = unescape(url)
@@ -645,7 +642,7 @@ class SimpleScrapingLocator(Locator):
# Note that you need two loops, since you can't say which
# thread will get each sentinel
for t in self._threads:
self._to_fetch.put(None) # sentinel
self._to_fetch.put(None) # sentinel
for t in self._threads:
t.join()
self._threads = []
@@ -693,7 +690,7 @@ class SimpleScrapingLocator(Locator):
info = self.convert_url_to_download_info(url, self.project_name)
logger.debug('process_download: %s -> %s', url, info)
if info:
with self._lock: # needed because self.result is shared
with self._lock: # needed because self.result is shared
self._update_version_data(self.result, info)
return info
@@ -703,8 +700,7 @@ class SimpleScrapingLocator(Locator):
particular "rel" attribute should be queued for scraping.
"""
scheme, netloc, path, _, _, _ = urlparse(link)
if path.endswith(self.source_extensions + self.binary_extensions +
self.excluded_extensions):
if path.endswith(self.source_extensions + self.binary_extensions + self.excluded_extensions):
result = False
elif self.skip_externals and not link.startswith(self.base_url):
result = False
@@ -722,8 +718,7 @@ class SimpleScrapingLocator(Locator):
result = False
else:
result = True
logger.debug('should_queue: %s (%s) from %s -> %s', link, rel,
referrer, result)
logger.debug('should_queue: %s (%s) from %s -> %s', link, rel, referrer, result)
return result
def _fetch(self):
@@ -738,14 +733,13 @@ class SimpleScrapingLocator(Locator):
try:
if url:
page = self.get_page(url)
if page is None: # e.g. after an error
if page is None: # e.g. after an error
continue
for link, rel in page.links:
if link not in self._seen:
try:
self._seen.add(link)
if (not self._process_download(link) and
self._should_queue(link, url, rel)):
if (not self._process_download(link) and self._should_queue(link, url, rel)):
logger.debug('Queueing %s from %s', link, url)
self._to_fetch.put(link)
except MetadataInvalidError: # e.g. invalid versions
@@ -756,7 +750,7 @@ class SimpleScrapingLocator(Locator):
# always do this, to avoid hangs :-)
self._to_fetch.task_done()
if not url:
#logger.debug('Sentinel seen, quitting.')
# logger.debug('Sentinel seen, quitting.')
break
def get_page(self, url):
@@ -793,7 +787,7 @@ class SimpleScrapingLocator(Locator):
data = resp.read()
encoding = headers.get('Content-Encoding')
if encoding:
decoder = self.decoders[encoding] # fail if not found
decoder = self.decoders[encoding] # fail if not found
data = decoder(data)
encoding = 'utf-8'
m = CHARSET.search(content_type)
@@ -802,7 +796,7 @@ class SimpleScrapingLocator(Locator):
try:
data = data.decode(encoding)
except UnicodeError: # pragma: no cover
data = data.decode('latin-1') # fallback
data = data.decode('latin-1') # fallback
result = Page(data, final_url)
self._page_cache[final_url] = result
except HTTPError as e:
@@ -815,7 +809,7 @@ class SimpleScrapingLocator(Locator):
except Exception as e: # pragma: no cover
logger.exception('Fetch failed: %s: %s', url, e)
finally:
self._page_cache[url] = result # even if None (failure)
self._page_cache[url] = result # even if None (failure)
return result
_distname_re = re.compile('<a href=[^>]*>([^<]+)<')
@@ -832,6 +826,7 @@ class SimpleScrapingLocator(Locator):
result.add(match.group(1))
return result
class DirectoryLocator(Locator):
"""
This class locates distributions in a directory tree.
@@ -868,9 +863,7 @@ class DirectoryLocator(Locator):
for fn in files:
if self.should_include(fn, root):
fn = os.path.join(root, fn)
url = urlunparse(('file', '',
pathname2url(os.path.abspath(fn)),
'', '', ''))
url = urlunparse(('file', '', pathname2url(os.path.abspath(fn)), '', '', ''))
info = self.convert_url_to_download_info(url, name)
if info:
self._update_version_data(result, info)
@@ -887,9 +880,7 @@ class DirectoryLocator(Locator):
for fn in files:
if self.should_include(fn, root):
fn = os.path.join(root, fn)
url = urlunparse(('file', '',
pathname2url(os.path.abspath(fn)),
'', '', ''))
url = urlunparse(('file', '', pathname2url(os.path.abspath(fn)), '', '', ''))
info = self.convert_url_to_download_info(url, None)
if info:
result.add(info['name'])
@@ -897,6 +888,7 @@ class DirectoryLocator(Locator):
break
return result
class JSONLocator(Locator):
"""
This locator uses special extended metadata (not available on PyPI) and is
@@ -904,6 +896,7 @@ class JSONLocator(Locator):
require archive downloads before dependencies can be determined! As you
might imagine, that can be slow.
"""
def get_distribution_names(self):
"""
Return all the distribution names known to this locator.
@@ -920,9 +913,9 @@ class JSONLocator(Locator):
# We don't store summary in project metadata as it makes
# the data bigger for no benefit during dependency
# resolution
dist = make_dist(data['name'], info['version'],
summary=data.get('summary',
'Placeholder for summary'),
dist = make_dist(data['name'],
info['version'],
summary=data.get('summary', 'Placeholder for summary'),
scheme=self.scheme)
md = dist.metadata
md.source_url = info['url']
@@ -935,11 +928,13 @@ class JSONLocator(Locator):
result['urls'].setdefault(dist.version, set()).add(info['url'])
return result
class DistPathLocator(Locator):
"""
This locator finds installed distributions in a path. It can be useful for
adding to an :class:`AggregatingLocator`.
"""
def __init__(self, distpath, **kwargs):
"""
Initialise an instance.
@@ -957,8 +952,12 @@ class DistPathLocator(Locator):
else:
result = {
dist.version: dist,
'urls': {dist.version: set([dist.source_url])},
'digests': {dist.version: set([None])}
'urls': {
dist.version: set([dist.source_url])
},
'digests': {
dist.version: set([None])
}
}
return result
@@ -967,6 +966,7 @@ class AggregatingLocator(Locator):
"""
This class allows you to chain and/or merge a list of locators.
"""
def __init__(self, *locators, **kwargs):
"""
Initialise an instance.
@@ -1055,10 +1055,9 @@ class AggregatingLocator(Locator):
# We use a legacy scheme simply because most of the dists on PyPI use legacy
# versions which don't conform to PEP 440.
default_locator = AggregatingLocator(
# JSONLocator(), # don't use as PEP 426 is withdrawn
SimpleScrapingLocator('https://pypi.org/simple/',
timeout=3.0),
scheme='legacy')
# JSONLocator(), # don't use as PEP 426 is withdrawn
SimpleScrapingLocator('https://pypi.org/simple/', timeout=3.0),
scheme='legacy')
locate = default_locator.locate
@@ -1134,7 +1133,7 @@ class DependencyFinder(object):
:return: A set of distribution which can fulfill the requirement.
"""
matcher = self.get_matcher(reqt)
name = matcher.key # case-insensitive
name = matcher.key # case-insensitive
result = set()
provided = self.provided
if name in provided:
@@ -1176,8 +1175,7 @@ class DependencyFinder(object):
unmatched.add(s)
if unmatched:
# can't replace other with provider
problems.add(('cantreplace', provider, other,
frozenset(unmatched)))
problems.add(('cantreplace', provider, other, frozenset(unmatched)))
result = False
else:
# can replace other with provider
@@ -1230,8 +1228,7 @@ class DependencyFinder(object):
dist = odist = requirement
logger.debug('passed %s as requirement', odist)
else:
dist = odist = self.locator.locate(requirement,
prereleases=prereleases)
dist = odist = self.locator.locate(requirement, prereleases=prereleases)
if dist is None:
raise DistlibException('Unable to locate %r' % requirement)
logger.debug('located %s', odist)
@@ -1241,11 +1238,11 @@ class DependencyFinder(object):
install_dists = set([odist])
while todo:
dist = todo.pop()
name = dist.key # case-insensitive
name = dist.key # case-insensitive
if name not in self.dists_by_name:
self.add_distribution(dist)
else:
#import pdb; pdb.set_trace()
# import pdb; pdb.set_trace()
other = self.dists_by_name[name]
if other != dist:
self.try_to_replace(dist, other, problems)
@@ -1278,8 +1275,7 @@ class DependencyFinder(object):
providers.add(provider)
if r in ireqts and dist in install_dists:
install_dists.add(provider)
logger.debug('Adding %s to install_dists',
provider.name_and_version)
logger.debug('Adding %s to install_dists', provider.name_and_version)
for p in providers:
name = p.key
if name not in self.dists_by_name:
@@ -1294,7 +1290,6 @@ class DependencyFinder(object):
for dist in dists:
dist.build_time_dependency = dist not in install_dists
if dist.build_time_dependency:
logger.debug('%s is a build-time dependency only.',
dist.name_and_version)
logger.debug('%s is a build-time dependency only.', dist.name_and_version)
logger.debug('find done for %s', odist)
return dists, problems

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2012-2013 Python Software Foundation.
# Copyright (C) 2012-2023 Python Software Foundation.
# See LICENSE.txt and CONTRIBUTORS.txt.
#
"""
@@ -34,9 +34,11 @@ _COMMENTED_LINE = re.compile('#.*?(?=\n)|\n(?=$)', re.M | re.S)
#
_PYTHON_VERSION = sys.version_info[:2]
class Manifest(object):
"""A list of files built by on exploring the filesystem and filtered by
applying various patterns to what we find there.
"""
A list of files built by exploring the filesystem and filtered by applying various
patterns to what we find there.
"""
def __init__(self, base=None):
@@ -154,10 +156,7 @@ class Manifest(object):
elif action == 'exclude':
for pattern in patterns:
found = self._exclude_pattern(pattern, anchor=True)
#if not found:
# logger.warning('no previously-included files '
# 'found matching %r', pattern)
self._exclude_pattern(pattern, anchor=True)
elif action == 'global-include':
for pattern in patterns:
@@ -167,11 +166,7 @@ class Manifest(object):
elif action == 'global-exclude':
for pattern in patterns:
found = self._exclude_pattern(pattern, anchor=False)
#if not found:
# logger.warning('no previously-included files '
# 'matching %r found anywhere in '
# 'distribution', pattern)
self._exclude_pattern(pattern, anchor=False)
elif action == 'recursive-include':
for pattern in patterns:
@@ -181,11 +176,7 @@ class Manifest(object):
elif action == 'recursive-exclude':
for pattern in patterns:
found = self._exclude_pattern(pattern, prefix=thedir)
#if not found:
# logger.warning('no previously-included files '
# 'matching %r found under directory %r',
# pattern, thedir)
self._exclude_pattern(pattern, prefix=thedir)
elif action == 'graft':
if not self._include_pattern(None, prefix=dirpattern):

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2012-2017 Vinay Sajip.
# Copyright (C) 2012-2023 Vinay Sajip.
# Licensed to the Python Software Foundation under a contributor agreement.
# See LICENSE.txt and CONTRIBUTORS.txt.
#
@@ -19,26 +19,31 @@ import platform
from .compat import string_types
from .util import in_venv, parse_marker
from .version import NormalizedVersion as NV
from .version import LegacyVersion as LV
__all__ = ['interpret']
_VERSION_PATTERN = re.compile(r'((\d+(\.\d+)*\w*)|\'(\d+(\.\d+)*\w*)\'|\"(\d+(\.\d+)*\w*)\")')
_VERSION_MARKERS = {'python_version', 'python_full_version'}
def _is_version_marker(s):
return isinstance(s, string_types) and s in _VERSION_MARKERS
def _is_literal(o):
if not isinstance(o, string_types) or not o:
return False
return o[0] in '\'"'
def _get_versions(s):
result = []
for m in _VERSION_PATTERN.finditer(s):
result.append(NV(m.groups()[0]))
return set(result)
return {LV(m.groups()[0]) for m in _VERSION_PATTERN.finditer(s)}
class Evaluator(object):
"""
This class is used to evaluate marker expessions.
This class is used to evaluate marker expressions.
"""
operations = {
@@ -46,10 +51,10 @@ class Evaluator(object):
'===': lambda x, y: x == y,
'~=': lambda x, y: x == y or x > y,
'!=': lambda x, y: x != y,
'<': lambda x, y: x < y,
'<=': lambda x, y: x == y or x < y,
'>': lambda x, y: x > y,
'>=': lambda x, y: x == y or x > y,
'<': lambda x, y: x < y,
'<=': lambda x, y: x == y or x < y,
'>': lambda x, y: x > y,
'>=': lambda x, y: x == y or x > y,
'and': lambda x, y: x and y,
'or': lambda x, y: x or y,
'in': lambda x, y: x in y,
@@ -80,19 +85,22 @@ class Evaluator(object):
lhs = self.evaluate(elhs, context)
rhs = self.evaluate(erhs, context)
if ((elhs == 'python_version' or erhs == 'python_version') and
op in ('<', '<=', '>', '>=', '===', '==', '!=', '~=')):
lhs = NV(lhs)
rhs = NV(rhs)
elif elhs == 'python_version' and op in ('in', 'not in'):
lhs = NV(lhs)
if ((_is_version_marker(elhs) or _is_version_marker(erhs)) and
op in ('<', '<=', '>', '>=', '===', '==', '!=', '~=')):
lhs = LV(lhs)
rhs = LV(rhs)
elif _is_version_marker(elhs) and op in ('in', 'not in'):
lhs = LV(lhs)
rhs = _get_versions(rhs)
result = self.operations[op](lhs, rhs)
return result
_DIGITS = re.compile(r'\d+\.\d+')
def default_context():
def format_full_version(info):
version = '%s.%s.%s' % (info.major, info.minor, info.micro)
kind = info.releaselevel
@@ -126,11 +134,13 @@ def default_context():
}
return result
DEFAULT_CONTEXT = default_context()
del default_context
evaluator = Evaluator()
def interpret(marker, execution_context=None):
"""
Interpret a marker and return a result depending on environment.

View File

@@ -15,7 +15,6 @@ import json
import logging
import re
from . import DistlibException, __version__
from .compat import StringIO, string_types, text_type
from .markers import interpret
@@ -40,6 +39,7 @@ class MetadataUnrecognizedVersionError(DistlibException):
class MetadataInvalidError(DistlibException):
"""A metadata value is invalid"""
# public API of this module
__all__ = ['Metadata', 'PKG_INFO_ENCODING', 'PKG_INFO_PREFERRED_VERSION']
@@ -52,53 +52,38 @@ PKG_INFO_PREFERRED_VERSION = '1.1'
_LINE_PREFIX_1_2 = re.compile('\n \\|')
_LINE_PREFIX_PRE_1_2 = re.compile('\n ')
_241_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
'Summary', 'Description',
'Keywords', 'Home-page', 'Author', 'Author-email',
'License')
_241_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform', 'Summary', 'Description', 'Keywords', 'Home-page',
'Author', 'Author-email', 'License')
_314_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
'Supported-Platform', 'Summary', 'Description',
'Keywords', 'Home-page', 'Author', 'Author-email',
'License', 'Classifier', 'Download-URL', 'Obsoletes',
_314_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform', 'Supported-Platform', 'Summary', 'Description',
'Keywords', 'Home-page', 'Author', 'Author-email', 'License', 'Classifier', 'Download-URL', 'Obsoletes',
'Provides', 'Requires')
_314_MARKERS = ('Obsoletes', 'Provides', 'Requires', 'Classifier',
'Download-URL')
_314_MARKERS = ('Obsoletes', 'Provides', 'Requires', 'Classifier', 'Download-URL')
_345_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
'Supported-Platform', 'Summary', 'Description',
'Keywords', 'Home-page', 'Author', 'Author-email',
'Maintainer', 'Maintainer-email', 'License',
'Classifier', 'Download-URL', 'Obsoletes-Dist',
'Project-URL', 'Provides-Dist', 'Requires-Dist',
_345_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform', 'Supported-Platform', 'Summary', 'Description',
'Keywords', 'Home-page', 'Author', 'Author-email', 'Maintainer', 'Maintainer-email', 'License',
'Classifier', 'Download-URL', 'Obsoletes-Dist', 'Project-URL', 'Provides-Dist', 'Requires-Dist',
'Requires-Python', 'Requires-External')
_345_MARKERS = ('Provides-Dist', 'Requires-Dist', 'Requires-Python',
'Obsoletes-Dist', 'Requires-External', 'Maintainer',
'Maintainer-email', 'Project-URL')
_345_MARKERS = ('Provides-Dist', 'Requires-Dist', 'Requires-Python', 'Obsoletes-Dist', 'Requires-External',
'Maintainer', 'Maintainer-email', 'Project-URL')
_426_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform',
'Supported-Platform', 'Summary', 'Description',
'Keywords', 'Home-page', 'Author', 'Author-email',
'Maintainer', 'Maintainer-email', 'License',
'Classifier', 'Download-URL', 'Obsoletes-Dist',
'Project-URL', 'Provides-Dist', 'Requires-Dist',
'Requires-Python', 'Requires-External', 'Private-Version',
'Obsoleted-By', 'Setup-Requires-Dist', 'Extension',
'Provides-Extra')
_426_FIELDS = ('Metadata-Version', 'Name', 'Version', 'Platform', 'Supported-Platform', 'Summary', 'Description',
'Keywords', 'Home-page', 'Author', 'Author-email', 'Maintainer', 'Maintainer-email', 'License',
'Classifier', 'Download-URL', 'Obsoletes-Dist', 'Project-URL', 'Provides-Dist', 'Requires-Dist',
'Requires-Python', 'Requires-External', 'Private-Version', 'Obsoleted-By', 'Setup-Requires-Dist',
'Extension', 'Provides-Extra')
_426_MARKERS = ('Private-Version', 'Provides-Extra', 'Obsoleted-By',
'Setup-Requires-Dist', 'Extension')
_426_MARKERS = ('Private-Version', 'Provides-Extra', 'Obsoleted-By', 'Setup-Requires-Dist', 'Extension')
# See issue #106: Sometimes 'Requires' and 'Provides' occur wrongly in
# the metadata. Include them in the tuple literal below to allow them
# (for now).
# Ditto for Obsoletes - see issue #140.
_566_FIELDS = _426_FIELDS + ('Description-Content-Type',
'Requires', 'Provides', 'Obsoletes')
_566_FIELDS = _426_FIELDS + ('Description-Content-Type', 'Requires', 'Provides', 'Obsoletes')
_566_MARKERS = ('Description-Content-Type',)
_566_MARKERS = ('Description-Content-Type', )
_643_MARKERS = ('Dynamic', 'License-File')
@@ -135,18 +120,11 @@ def _version2fieldlist(version):
def _best_version(fields):
"""Detect the best version depending on the fields used."""
def _has_marker(keys, markers):
for marker in markers:
if marker in keys:
return True
return False
keys = []
for key, value in fields.items():
if value in ([], 'UNKNOWN', None):
continue
keys.append(key)
return any(marker in keys for marker in markers)
keys = [key for key, value in fields.items() if value not in ([], 'UNKNOWN', None)]
possible_versions = ['1.0', '1.1', '1.2', '1.3', '2.1', '2.2'] # 2.0 removed
# first let's try to see if a field is not part of one of the version
@@ -171,12 +149,12 @@ def _best_version(fields):
possible_versions.remove('2.2')
logger.debug('Removed 2.2 due to %s', key)
# if key not in _426_FIELDS and '2.0' in possible_versions:
# possible_versions.remove('2.0')
# logger.debug('Removed 2.0 due to %s', key)
# possible_versions.remove('2.0')
# logger.debug('Removed 2.0 due to %s', key)
# possible_version contains qualified versions
if len(possible_versions) == 1:
return possible_versions[0] # found !
return possible_versions[0] # found !
elif len(possible_versions) == 0:
logger.debug('Out of options - unknown metadata set: %s', fields)
raise MetadataConflictError('Unknown metadata set')
@@ -207,28 +185,25 @@ def _best_version(fields):
if is_2_1:
return '2.1'
# if is_2_2:
# return '2.2'
# return '2.2'
return '2.2'
# This follows the rules about transforming keys as described in
# https://www.python.org/dev/peps/pep-0566/#id17
_ATTR2FIELD = {
name.lower().replace("-", "_"): name for name in _ALL_FIELDS
}
_ATTR2FIELD = {name.lower().replace("-", "_"): name for name in _ALL_FIELDS}
_FIELD2ATTR = {field: attr for attr, field in _ATTR2FIELD.items()}
_PREDICATE_FIELDS = ('Requires-Dist', 'Obsoletes-Dist', 'Provides-Dist')
_VERSIONS_FIELDS = ('Requires-Python',)
_VERSION_FIELDS = ('Version',)
_LISTFIELDS = ('Platform', 'Classifier', 'Obsoletes',
'Requires', 'Provides', 'Obsoletes-Dist',
'Provides-Dist', 'Requires-Dist', 'Requires-External',
'Project-URL', 'Supported-Platform', 'Setup-Requires-Dist',
_VERSIONS_FIELDS = ('Requires-Python', )
_VERSION_FIELDS = ('Version', )
_LISTFIELDS = ('Platform', 'Classifier', 'Obsoletes', 'Requires', 'Provides', 'Obsoletes-Dist', 'Provides-Dist',
'Requires-Dist', 'Requires-External', 'Project-URL', 'Supported-Platform', 'Setup-Requires-Dist',
'Provides-Extra', 'Extension', 'License-File')
_LISTTUPLEFIELDS = ('Project-URL',)
_LISTTUPLEFIELDS = ('Project-URL', )
_ELEMENTSFIELD = ('Keywords',)
_ELEMENTSFIELD = ('Keywords', )
_UNICODEFIELDS = ('Author', 'Maintainer', 'Summary', 'Description')
@@ -260,10 +235,10 @@ class LegacyMetadata(object):
- *mapping* is a dict-like object
- *scheme* is a version scheme name
"""
# TODO document the mapping API and UNKNOWN default key
def __init__(self, path=None, fileobj=None, mapping=None,
scheme='default'):
def __init__(self, path=None, fileobj=None, mapping=None, scheme='default'):
if [path, fileobj, mapping].count(None) < 2:
raise TypeError('path, fileobj and mapping are exclusive')
self._fields = {}
@@ -298,8 +273,7 @@ class LegacyMetadata(object):
raise KeyError(name)
def __contains__(self, name):
return (name in self._fields or
self._convert_name(name) in self._fields)
return (name in self._fields or self._convert_name(name) in self._fields)
def _convert_name(self, name):
if name in _ALL_FIELDS:
@@ -327,12 +301,12 @@ class LegacyMetadata(object):
# Public API
#
# dependencies = property(_get_dependencies, _set_dependencies)
def get_fullname(self, filesafe=False):
"""Return the distribution name with version.
"""
Return the distribution name with version.
If filesafe is true, return a filename-escaped form."""
If filesafe is true, return a filename-escaped form.
"""
return _get_name_and_version(self['Name'], self['Version'], filesafe)
def is_field(self, name):
@@ -423,6 +397,7 @@ class LegacyMetadata(object):
Keys that don't match a metadata field or that have an empty value are
dropped.
"""
def _set(key, value):
if key in _ATTR2FIELD and value:
self.set(self._convert_name(key), value)
@@ -445,14 +420,12 @@ class LegacyMetadata(object):
"""Control then set a metadata field."""
name = self._convert_name(name)
if ((name in _ELEMENTSFIELD or name == 'Platform') and
not isinstance(value, (list, tuple))):
if ((name in _ELEMENTSFIELD or name == 'Platform') and not isinstance(value, (list, tuple))):
if isinstance(value, string_types):
value = [v.strip() for v in value.split(',')]
else:
value = []
elif (name in _LISTFIELDS and
not isinstance(value, (list, tuple))):
elif (name in _LISTFIELDS and not isinstance(value, (list, tuple))):
if isinstance(value, string_types):
value = [value]
else:
@@ -466,18 +439,14 @@ class LegacyMetadata(object):
for v in value:
# check that the values are valid
if not scheme.is_valid_matcher(v.split(';')[0]):
logger.warning(
"'%s': '%s' is not valid (field '%s')",
project_name, v, name)
logger.warning("'%s': '%s' is not valid (field '%s')", project_name, v, name)
# FIXME this rejects UNKNOWN, is that right?
elif name in _VERSIONS_FIELDS and value is not None:
if not scheme.is_valid_constraint_list(value):
logger.warning("'%s': '%s' is not a valid version (field '%s')",
project_name, value, name)
logger.warning("'%s': '%s' is not a valid version (field '%s')", project_name, value, name)
elif name in _VERSION_FIELDS and value is not None:
if not scheme.is_valid_version(value):
logger.warning("'%s': '%s' is not a valid version (field '%s')",
project_name, value, name)
logger.warning("'%s': '%s' is not a valid version (field '%s')", project_name, value, name)
if name in _UNICODEFIELDS:
if name == 'Description':
@@ -547,10 +516,8 @@ class LegacyMetadata(object):
return True
for fields, controller in ((_PREDICATE_FIELDS, are_valid_constraints),
(_VERSIONS_FIELDS,
scheme.is_valid_constraint_list),
(_VERSION_FIELDS,
scheme.is_valid_version)):
(_VERSIONS_FIELDS, scheme.is_valid_constraint_list), (_VERSION_FIELDS,
scheme.is_valid_version)):
for field in fields:
value = self.get(field, None)
if value is not None and not controller(value):
@@ -606,8 +573,7 @@ class LegacyMetadata(object):
return [(key, self[key]) for key in self.keys()]
def __repr__(self):
return '<%s %s %s>' % (self.__class__.__name__, self.name,
self.version)
return '<%s %s %s>' % (self.__class__.__name__, self.name, self.version)
METADATA_FILENAME = 'pydist.json'
@@ -639,7 +605,7 @@ class Metadata(object):
MANDATORY_KEYS = {
'name': (),
'version': (),
'summary': ('legacy',),
'summary': ('legacy', ),
}
INDEX_KEYS = ('name version license summary description author '
@@ -652,22 +618,21 @@ class Metadata(object):
SYNTAX_VALIDATORS = {
'metadata_version': (METADATA_VERSION_MATCHER, ()),
'name': (NAME_MATCHER, ('legacy',)),
'version': (VERSION_MATCHER, ('legacy',)),
'summary': (SUMMARY_MATCHER, ('legacy',)),
'dynamic': (FIELDNAME_MATCHER, ('legacy',)),
'name': (NAME_MATCHER, ('legacy', )),
'version': (VERSION_MATCHER, ('legacy', )),
'summary': (SUMMARY_MATCHER, ('legacy', )),
'dynamic': (FIELDNAME_MATCHER, ('legacy', )),
}
__slots__ = ('_legacy', '_data', 'scheme')
def __init__(self, path=None, fileobj=None, mapping=None,
scheme='default'):
def __init__(self, path=None, fileobj=None, mapping=None, scheme='default'):
if [path, fileobj, mapping].count(None) < 2:
raise TypeError('path, fileobj and mapping are exclusive')
self._legacy = None
self._data = None
self.scheme = scheme
#import pdb; pdb.set_trace()
# import pdb; pdb.set_trace()
if mapping is not None:
try:
self._validate_mapping(mapping, scheme)
@@ -701,8 +666,7 @@ class Metadata(object):
# The ValueError comes from the json.load - if that
# succeeds and we get a validation error, we want
# that to propagate
self._legacy = LegacyMetadata(fileobj=StringIO(data),
scheme=scheme)
self._legacy = LegacyMetadata(fileobj=StringIO(data), scheme=scheme)
self.validate()
common_keys = set(('name', 'version', 'license', 'keywords', 'summary'))
@@ -740,8 +704,7 @@ class Metadata(object):
result = self._legacy.get(lk)
else:
value = None if maker is None else maker()
if key not in ('commands', 'exports', 'modules', 'namespaces',
'classifiers'):
if key not in ('commands', 'exports', 'modules', 'namespaces', 'classifiers'):
result = self._data.get(key, value)
else:
# special cases for PEP 459
@@ -778,8 +741,7 @@ class Metadata(object):
m = pattern.match(value)
if not m:
raise MetadataInvalidError("'%s' is an invalid value for "
"the '%s' property" % (value,
key))
"the '%s' property" % (value, key))
def __setattr__(self, key, value):
self._validate_value(key, value)
@@ -791,8 +753,7 @@ class Metadata(object):
if lk is None:
raise NotImplementedError
self._legacy[lk] = value
elif key not in ('commands', 'exports', 'modules', 'namespaces',
'classifiers'):
elif key not in ('commands', 'exports', 'modules', 'namespaces', 'classifiers'):
self._data[key] = value
else:
# special cases for PEP 459
@@ -880,8 +841,7 @@ class Metadata(object):
# A recursive call, but it should terminate since 'test'
# has been removed from the extras
reqts = self._data.get('%s_requires' % key, [])
result.extend(self.get_requirements(reqts, extras=extras,
env=env))
result.extend(self.get_requirements(reqts, extras=extras, env=env))
return result
@property
@@ -922,8 +882,7 @@ class Metadata(object):
if self._legacy:
missing, warnings = self._legacy.check(True)
if missing or warnings:
logger.warning('Metadata: missing: %s, warnings: %s',
missing, warnings)
logger.warning('Metadata: missing: %s, warnings: %s', missing, warnings)
else:
self._validate_mapping(self._data, self.scheme)
@@ -940,9 +899,8 @@ class Metadata(object):
'metadata_version': self.METADATA_VERSION,
'generator': self.GENERATOR,
}
lmd = self._legacy.todict(True) # skip missing ones
for k in ('name', 'version', 'license', 'summary', 'description',
'classifier'):
lmd = self._legacy.todict(True) # skip missing ones
for k in ('name', 'version', 'license', 'summary', 'description', 'classifier'):
if k in lmd:
if k == 'classifier':
nk = 'classifiers'
@@ -953,14 +911,13 @@ class Metadata(object):
if kw == ['']:
kw = []
result['keywords'] = kw
keys = (('requires_dist', 'run_requires'),
('setup_requires_dist', 'build_requires'))
keys = (('requires_dist', 'run_requires'), ('setup_requires_dist', 'build_requires'))
for ok, nk in keys:
if ok in lmd and lmd[ok]:
result[nk] = [{'requires': lmd[ok]}]
result['provides'] = self.provides
author = {}
maintainer = {}
# author = {}
# maintainer = {}
return result
LEGACY_MAPPING = {
@@ -977,6 +934,7 @@ class Metadata(object):
}
def _to_legacy(self):
def process_entries(entries):
reqts = set()
for e in entries:
@@ -1045,12 +1003,10 @@ class Metadata(object):
else:
d = self._data
if fileobj:
json.dump(d, fileobj, ensure_ascii=True, indent=2,
sort_keys=True)
json.dump(d, fileobj, ensure_ascii=True, indent=2, sort_keys=True)
else:
with codecs.open(path, 'w', 'utf-8') as f:
json.dump(d, f, ensure_ascii=True, indent=2,
sort_keys=True)
json.dump(d, f, ensure_ascii=True, indent=2, sort_keys=True)
def add_requirements(self, requirements):
if self._legacy:
@@ -1063,7 +1019,7 @@ class Metadata(object):
always = entry
break
if always is None:
always = { 'requires': requirements }
always = {'requires': requirements}
run_requires.insert(0, always)
else:
rset = set(always['requires']) | set(requirements)
@@ -1072,5 +1028,4 @@ class Metadata(object):
def __repr__(self):
name = self.name or '(no name)'
version = self.version or 'no version'
return '<%s %s %s (%s)>' % (self.__class__.__name__,
self.metadata_version, name, version)
return '<%s %s %s (%s)>' % (self.__class__.__name__, self.metadata_version, name, version)

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013-2015 Vinay Sajip.
# Copyright (C) 2013-2023 Vinay Sajip.
# Licensed to the Python Software Foundation under a contributor agreement.
# See LICENSE.txt and CONTRIBUTORS.txt.
#
@@ -15,8 +15,7 @@ from zipfile import ZipInfo
from .compat import sysconfig, detect_encoding, ZipFile
from .resources import finder
from .util import (FileOperator, get_export_entry, convert_path,
get_executable, get_platform, in_venv)
from .util import (FileOperator, get_export_entry, convert_path, get_executable, get_platform, in_venv)
logger = logging.getLogger(__name__)
@@ -49,6 +48,25 @@ if __name__ == '__main__':
sys.exit(%(func)s())
'''
# Pre-fetch the contents of all executable wrapper stubs.
# This is to address https://github.com/pypa/pip/issues/12666.
# When updating pip, we rename the old pip in place before installing the
# new version. If we try to fetch a wrapper *after* that rename, the finder
# machinery will be confused as the package is no longer available at the
# location where it was imported from. So we load everything into memory in
# advance.
if os.name == 'nt' or (os.name == 'java' and os._name == 'nt'):
# Issue 31: don't hardcode an absolute package name, but
# determine it relative to the current package
DISTLIB_PACKAGE = __name__.rsplit('.', 1)[0]
WRAPPERS = {
r.name: r.bytes
for r in finder(DISTLIB_PACKAGE).iterator("")
if r.name.endswith(".exe")
}
def enquote_executable(executable):
if ' ' in executable:
@@ -65,9 +83,11 @@ def enquote_executable(executable):
executable = '"%s"' % executable
return executable
# Keep the old name around (for now), as there is at least one project using it!
_enquote_executable = enquote_executable
class ScriptMaker(object):
"""
A class to copy or create scripts from source scripts or callable
@@ -77,21 +97,18 @@ class ScriptMaker(object):
executable = None # for shebangs
def __init__(self, source_dir, target_dir, add_launchers=True,
dry_run=False, fileop=None):
def __init__(self, source_dir, target_dir, add_launchers=True, dry_run=False, fileop=None):
self.source_dir = source_dir
self.target_dir = target_dir
self.add_launchers = add_launchers
self.force = False
self.clobber = False
# It only makes sense to set mode bits on POSIX.
self.set_mode = (os.name == 'posix') or (os.name == 'java' and
os._name == 'posix')
self.set_mode = (os.name == 'posix') or (os.name == 'java' and os._name == 'posix')
self.variants = set(('', 'X.Y'))
self._fileop = fileop or FileOperator(dry_run)
self._is_nt = os.name == 'nt' or (
os.name == 'java' and os._name == 'nt')
self._is_nt = os.name == 'nt' or (os.name == 'java' and os._name == 'nt')
self.version_info = sys.version_info
def _get_alternate_executable(self, executable, options):
@@ -102,6 +119,7 @@ class ScriptMaker(object):
return executable
if sys.platform.startswith('java'): # pragma: no cover
def _is_shell(self, executable):
"""
Determine if the specified executable is a script
@@ -139,6 +157,12 @@ class ScriptMaker(object):
"""
if os.name != 'posix':
simple_shebang = True
elif getattr(sys, "cross_compiling", False):
# In a cross-compiling environment, the shebang will likely be a
# script; this *must* be invoked with the "safe" version of the
# shebang, or else using os.exec() to run the entry script will
# fail, raising "OSError 8 [Errno 8] Exec format error".
simple_shebang = False
else:
# Add 3 for '#!' prefix and newline suffix.
shebang_length = len(executable) + len(post_interp) + 3
@@ -146,37 +170,35 @@ class ScriptMaker(object):
max_shebang_length = 512
else:
max_shebang_length = 127
simple_shebang = ((b' ' not in executable) and
(shebang_length <= max_shebang_length))
simple_shebang = ((b' ' not in executable) and (shebang_length <= max_shebang_length))
if simple_shebang:
result = b'#!' + executable + post_interp + b'\n'
else:
result = b'#!/bin/sh\n'
result += b"'''exec' " + executable + post_interp + b' "$0" "$@"\n'
result += b"' '''"
result += b"' '''\n"
return result
def _get_shebang(self, encoding, post_interp=b'', options=None):
enquote = True
if self.executable:
executable = self.executable
enquote = False # assume this will be taken care of
enquote = False # assume this will be taken care of
elif not sysconfig.is_python_build():
executable = get_executable()
elif in_venv(): # pragma: no cover
executable = os.path.join(sysconfig.get_path('scripts'),
'python%s' % sysconfig.get_config_var('EXE'))
executable = os.path.join(sysconfig.get_path('scripts'), 'python%s' % sysconfig.get_config_var('EXE'))
else: # pragma: no cover
executable = os.path.join(
sysconfig.get_config_var('BINDIR'),
'python%s%s' % (sysconfig.get_config_var('VERSION'),
sysconfig.get_config_var('EXE')))
if not os.path.isfile(executable):
if os.name == 'nt':
# for Python builds from source on Windows, no Python executables with
# a version suffix are created, so we use python.exe
executable = os.path.join(sysconfig.get_config_var('BINDIR'),
'python%s' % (sysconfig.get_config_var('EXE')))
'python%s' % (sysconfig.get_config_var('EXE')))
else:
executable = os.path.join(
sysconfig.get_config_var('BINDIR'),
'python%s%s' % (sysconfig.get_config_var('VERSION'), sysconfig.get_config_var('EXE')))
if options:
executable = self._get_alternate_executable(executable, options)
@@ -200,8 +222,8 @@ class ScriptMaker(object):
# check that the shebang is decodable using utf-8.
executable = executable.encode('utf-8')
# in case of IronPython, play safe and enable frames support
if (sys.platform == 'cli' and '-X:Frames' not in post_interp
and '-X:FullFrames' not in post_interp): # pragma: no cover
if (sys.platform == 'cli' and '-X:Frames' not in post_interp and
'-X:FullFrames' not in post_interp): # pragma: no cover
post_interp += b' -X:Frames'
shebang = self._build_shebang(executable, post_interp)
# Python parser starts to read a script using UTF-8 until
@@ -212,8 +234,7 @@ class ScriptMaker(object):
try:
shebang.decode('utf-8')
except UnicodeDecodeError: # pragma: no cover
raise ValueError(
'The shebang (%r) is not decodable from utf-8' % shebang)
raise ValueError('The shebang (%r) is not decodable from utf-8' % shebang)
# If the script is encoded to a custom encoding (use a
# #coding:xxx cookie), the shebang has to be decodable from
# the script encoding too.
@@ -221,15 +242,13 @@ class ScriptMaker(object):
try:
shebang.decode(encoding)
except UnicodeDecodeError: # pragma: no cover
raise ValueError(
'The shebang (%r) is not decodable '
'from the script encoding (%r)' % (shebang, encoding))
raise ValueError('The shebang (%r) is not decodable '
'from the script encoding (%r)' % (shebang, encoding))
return shebang
def _get_script_text(self, entry):
return self.script_template % dict(module=entry.prefix,
import_name=entry.suffix.split('.')[0],
func=entry.suffix)
return self.script_template % dict(
module=entry.prefix, import_name=entry.suffix.split('.')[0], func=entry.suffix)
manifest = _DEFAULT_MANIFEST
@@ -239,9 +258,6 @@ class ScriptMaker(object):
def _write_script(self, names, shebang, script_bytes, filenames, ext):
use_launcher = self.add_launchers and self._is_nt
linesep = os.linesep.encode('utf-8')
if not shebang.endswith(linesep):
shebang += linesep
if not use_launcher:
script_bytes = shebang + script_bytes
else: # pragma: no cover
@@ -275,7 +291,7 @@ class ScriptMaker(object):
'use .deleteme logic')
dfname = '%s.deleteme' % outname
if os.path.exists(dfname):
os.remove(dfname) # Not allowed to fail here
os.remove(dfname) # Not allowed to fail here
os.rename(outname, dfname) # nor here
self._fileop.write_binary_file(outname, script_bytes)
logger.debug('Able to replace executable using '
@@ -283,7 +299,7 @@ class ScriptMaker(object):
try:
os.remove(dfname)
except Exception:
pass # still in use - ignore error
pass # still in use - ignore error
else:
if self._is_nt and not outname.endswith('.' + ext): # pragma: no cover
outname = '%s.%s' % (outname, ext)
@@ -304,8 +320,7 @@ class ScriptMaker(object):
if 'X' in self.variants:
result.add('%s%s' % (name, self.version_info[0]))
if 'X.Y' in self.variants:
result.add('%s%s%s.%s' % (name, self.variant_separator,
self.version_info[0], self.version_info[1]))
result.add('%s%s%s.%s' % (name, self.variant_separator, self.version_info[0], self.version_info[1]))
return result
def _make_script(self, entry, filenames, options=None):
@@ -360,8 +375,7 @@ class ScriptMaker(object):
self._fileop.set_executable_mode([outname])
filenames.append(outname)
else:
logger.info('copying and adjusting %s -> %s', script,
self.target_dir)
logger.info('copying and adjusting %s -> %s', script, self.target_dir)
if not self._fileop.dry_run:
encoding, lines = detect_encoding(f.readline)
f.seek(0)
@@ -388,21 +402,17 @@ class ScriptMaker(object):
# Launchers are from https://bitbucket.org/vinay.sajip/simple_launcher/
def _get_launcher(self, kind):
if struct.calcsize('P') == 8: # 64-bit
if struct.calcsize('P') == 8: # 64-bit
bits = '64'
else:
bits = '32'
platform_suffix = '-arm' if get_platform() == 'win-arm64' else ''
name = '%s%s%s.exe' % (kind, bits, platform_suffix)
# Issue 31: don't hardcode an absolute package name, but
# determine it relative to the current package
distlib_package = __name__.rsplit('.', 1)[0]
resource = finder(distlib_package).find(name)
if not resource:
msg = ('Unable to find resource %s in package %s' % (name,
distlib_package))
if name not in WRAPPERS:
msg = ('Unable to find resource %s in package %s' %
(name, DISTLIB_PACKAGE))
raise ValueError(msg)
return resource.bytes
return WRAPPERS[name]
# Public API follows

View File

@@ -1,5 +1,5 @@
#
# Copyright (C) 2012-2021 The Python Software Foundation.
# Copyright (C) 2012-2023 The Python Software Foundation.
# See LICENSE.txt and CONTRIBUTORS.txt.
#
import codecs
@@ -31,11 +31,9 @@ except ImportError: # pragma: no cover
import time
from . import DistlibException
from .compat import (string_types, text_type, shutil, raw_input, StringIO,
cache_from_source, urlopen, urljoin, httplib, xmlrpclib,
splittype, HTTPHandler, BaseConfigurator, valid_ident,
Container, configparser, URLError, ZipFile, fsdecode,
unquote, urlparse)
from .compat import (string_types, text_type, shutil, raw_input, StringIO, cache_from_source, urlopen, urljoin, httplib,
xmlrpclib, HTTPHandler, BaseConfigurator, valid_ident, Container, configparser, URLError, ZipFile,
fsdecode, unquote, urlparse)
logger = logging.getLogger(__name__)
@@ -62,6 +60,7 @@ def parse_marker(marker_string):
interpreted as a literal string, and a string not contained in quotes is a
variable (such as os_name).
"""
def marker_var(remaining):
# either identifier, or literal string
m = IDENTIFIER.match(remaining)
@@ -95,7 +94,7 @@ def parse_marker(marker_string):
raise SyntaxError('unterminated string: %s' % s)
parts.append(q)
result = ''.join(parts)
remaining = remaining[1:].lstrip() # skip past closing quote
remaining = remaining[1:].lstrip() # skip past closing quote
return result, remaining
def marker_expr(remaining):
@@ -263,8 +262,7 @@ def parse_requirement(req):
rs = distname
else:
rs = '%s %s' % (distname, ', '.join(['%s %s' % con for con in versions]))
return Container(name=distname, extras=extras, constraints=versions,
marker=mark_expr, url=uri, requirement=rs)
return Container(name=distname, extras=extras, constraints=versions, marker=mark_expr, url=uri, requirement=rs)
def get_resources_dests(resources_root, rules):
@@ -304,15 +302,15 @@ def in_venv():
def get_executable():
# The __PYVENV_LAUNCHER__ dance is apparently no longer needed, as
# changes to the stub launcher mean that sys.executable always points
# to the stub on OS X
# if sys.platform == 'darwin' and ('__PYVENV_LAUNCHER__'
# in os.environ):
# result = os.environ['__PYVENV_LAUNCHER__']
# else:
# result = sys.executable
# return result
# The __PYVENV_LAUNCHER__ dance is apparently no longer needed, as
# changes to the stub launcher mean that sys.executable always points
# to the stub on OS X
# if sys.platform == 'darwin' and ('__PYVENV_LAUNCHER__'
# in os.environ):
# result = os.environ['__PYVENV_LAUNCHER__']
# else:
# result = sys.executable
# return result
# Avoid normcasing: see issue #143
# result = os.path.normcase(sys.executable)
result = sys.executable
@@ -346,6 +344,7 @@ def extract_by_key(d, keys):
result[key] = d[key]
return result
def read_exports(stream):
if sys.version_info[0] >= 3:
# needs to be a text stream
@@ -388,7 +387,7 @@ def read_exports(stream):
s = '%s = %s' % (name, value)
entry = get_export_entry(s)
assert entry is not None
#entry.dist = self
# entry.dist = self
entries[name] = entry
return result
@@ -420,6 +419,7 @@ def tempdir():
finally:
shutil.rmtree(td)
@contextlib.contextmanager
def chdir(d):
cwd = os.getcwd()
@@ -441,19 +441,21 @@ def socket_timeout(seconds=15):
class cached_property(object):
def __init__(self, func):
self.func = func
#for attr in ('__name__', '__module__', '__doc__'):
# setattr(self, attr, getattr(func, attr, None))
# for attr in ('__name__', '__module__', '__doc__'):
# setattr(self, attr, getattr(func, attr, None))
def __get__(self, obj, cls=None):
if obj is None:
return self
value = self.func(obj)
object.__setattr__(obj, self.func.__name__, value)
#obj.__dict__[self.func.__name__] = value = self.func(obj)
# obj.__dict__[self.func.__name__] = value = self.func(obj)
return value
def convert_path(pathname):
"""Return 'pathname' as a name that will work on the native filesystem.
@@ -482,6 +484,7 @@ def convert_path(pathname):
class FileOperator(object):
def __init__(self, dry_run=False):
self.dry_run = dry_run
self.ensured = set()
@@ -509,8 +512,7 @@ class FileOperator(object):
second will have the same "age".
"""
if not os.path.exists(source):
raise DistlibException("file '%r' does not exist" %
os.path.abspath(source))
raise DistlibException("file '%r' does not exist" % os.path.abspath(source))
if not os.path.exists(target):
return True
@@ -598,8 +600,10 @@ class FileOperator(object):
diagpath = path[len(prefix):]
compile_kwargs = {}
if hashed_invalidation and hasattr(py_compile, 'PycInvalidationMode'):
compile_kwargs['invalidation_mode'] = py_compile.PycInvalidationMode.CHECKED_HASH
py_compile.compile(path, dpath, diagpath, True, **compile_kwargs) # raise error
if not isinstance(hashed_invalidation, py_compile.PycInvalidationMode):
hashed_invalidation = py_compile.PycInvalidationMode.CHECKED_HASH
compile_kwargs['invalidation_mode'] = hashed_invalidation
py_compile.compile(path, dpath, diagpath, True, **compile_kwargs) # raise error
self.record_as_written(dpath)
return dpath
@@ -661,9 +665,10 @@ class FileOperator(object):
assert flist == ['__pycache__']
sd = os.path.join(d, flist[0])
os.rmdir(sd)
os.rmdir(d) # should fail if non-empty
os.rmdir(d) # should fail if non-empty
self._init_record()
def resolve(module_name, dotted_path):
if module_name in sys.modules:
mod = sys.modules[module_name]
@@ -680,6 +685,7 @@ def resolve(module_name, dotted_path):
class ExportEntry(object):
def __init__(self, name, prefix, suffix, flags):
self.name = name
self.prefix = prefix
@@ -691,27 +697,26 @@ class ExportEntry(object):
return resolve(self.prefix, self.suffix)
def __repr__(self): # pragma: no cover
return '<ExportEntry %s = %s:%s %s>' % (self.name, self.prefix,
self.suffix, self.flags)
return '<ExportEntry %s = %s:%s %s>' % (self.name, self.prefix, self.suffix, self.flags)
def __eq__(self, other):
if not isinstance(other, ExportEntry):
result = False
else:
result = (self.name == other.name and
self.prefix == other.prefix and
self.suffix == other.suffix and
result = (self.name == other.name and self.prefix == other.prefix and self.suffix == other.suffix and
self.flags == other.flags)
return result
__hash__ = object.__hash__
ENTRY_RE = re.compile(r'''(?P<name>(\w|[-.+])+)
ENTRY_RE = re.compile(
r'''(?P<name>([^\[]\S*))
\s*=\s*(?P<callable>(\w+)([:\.]\w+)*)
\s*(\[\s*(?P<flags>[\w-]+(=\w+)?(,\s*\w+(=\w+)?)*)\s*\])?
''', re.VERBOSE)
def get_export_entry(specification):
m = ENTRY_RE.search(specification)
if not m:
@@ -784,7 +789,7 @@ def get_cache_base(suffix=None):
return os.path.join(result, suffix)
def path_to_cache_dir(path):
def path_to_cache_dir(path, use_abspath=True):
"""
Convert an absolute path to a directory name for use in a cache.
@@ -794,7 +799,7 @@ def path_to_cache_dir(path):
#. Any occurrence of ``os.sep`` is replaced with ``'--'``.
#. ``'.cache'`` is appended.
"""
d, p = os.path.splitdrive(os.path.abspath(path))
d, p = os.path.splitdrive(os.path.abspath(path) if use_abspath else path)
if d:
d = d.replace(':', '---')
p = p.replace(os.sep, '--')
@@ -827,6 +832,7 @@ def get_process_umask():
os.umask(result)
return result
def is_string_sequence(seq):
result = True
i = None
@@ -837,6 +843,7 @@ def is_string_sequence(seq):
assert i is not None
return result
PROJECT_NAME_AND_VERSION = re.compile('([a-z0-9_]+([.-][a-z_][a-z0-9_]*)*)-'
'([a-z0-9_.+-]+)', re.I)
PYTHON_VERSION = re.compile(r'-py(\d\.?\d?)')
@@ -866,10 +873,12 @@ def split_filename(filename, project_name=None):
result = m.group(1), m.group(3), pyver
return result
# Allow spaces in name because of legacy dists like "Twisted Core"
NAME_VERSION_RE = re.compile(r'(?P<name>[\w .-]+)\s*'
r'\(\s*(?P<ver>[^\s)]+)\)$')
def parse_name_and_version(p):
"""
A utility method used to get name and version from a string.
@@ -885,6 +894,7 @@ def parse_name_and_version(p):
d = m.groupdict()
return d['name'].strip().lower(), d['ver']
def get_extras(requested, available):
result = set()
requested = set(requested or [])
@@ -906,10 +916,13 @@ def get_extras(requested, available):
logger.warning('undeclared extra: %s' % r)
result.add(r)
return result
#
# Extended metadata functionality
#
def _get_external_data(url):
result = {}
try:
@@ -923,21 +936,24 @@ def _get_external_data(url):
logger.debug('Unexpected response for JSON request: %s', ct)
else:
reader = codecs.getreader('utf-8')(resp)
#data = reader.read().decode('utf-8')
#result = json.loads(data)
# data = reader.read().decode('utf-8')
# result = json.loads(data)
result = json.load(reader)
except Exception as e:
logger.exception('Failed to get external data for %s: %s', url, e)
return result
_external_data_base_url = 'https://www.red-dove.com/pypi/projects/'
def get_project_data(name):
url = '%s/%s/project.json' % (name[0].upper(), name)
url = urljoin(_external_data_base_url, url)
result = _get_external_data(url)
return result
def get_package_data(name, version):
url = '%s/%s/package-%s.json' % (name[0].upper(), name, version)
url = urljoin(_external_data_base_url, url)
@@ -965,11 +981,11 @@ class Cache(object):
logger.warning('Directory \'%s\' is not private', base)
self.base = os.path.abspath(os.path.normpath(base))
def prefix_to_dir(self, prefix):
def prefix_to_dir(self, prefix, use_abspath=True):
"""
Converts a resource prefix to a directory name in the cache.
"""
return path_to_cache_dir(prefix)
return path_to_cache_dir(prefix, use_abspath=use_abspath)
def clear(self):
"""
@@ -992,6 +1008,7 @@ class EventMixin(object):
"""
A very simple publish/subscribe system.
"""
def __init__(self):
self._subscribers = {}
@@ -1053,18 +1070,19 @@ class EventMixin(object):
logger.exception('Exception during event publication')
value = None
result.append(value)
logger.debug('publish %s: args = %s, kwargs = %s, result = %s',
event, args, kwargs, result)
logger.debug('publish %s: args = %s, kwargs = %s, result = %s', event, args, kwargs, result)
return result
#
# Simple sequencing
#
class Sequencer(object):
def __init__(self):
self._preds = {}
self._succs = {}
self._nodes = set() # nodes with no preds/succs
self._nodes = set() # nodes with no preds/succs
def add_node(self, node):
self._nodes.add(node)
@@ -1104,8 +1122,7 @@ class Sequencer(object):
raise ValueError('%r not a successor of %r' % (succ, pred))
def is_step(self, step):
return (step in self._preds or step in self._succs or
step in self._nodes)
return (step in self._preds or step in self._succs or step in self._nodes)
def get_steps(self, final):
if not self.is_step(final):
@@ -1134,7 +1151,7 @@ class Sequencer(object):
@property
def strong_connections(self):
#http://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
# http://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
index_counter = [0]
stack = []
lowlinks = {}
@@ -1159,11 +1176,11 @@ class Sequencer(object):
if successor not in lowlinks:
# Successor has not yet been visited
strongconnect(successor)
lowlinks[node] = min(lowlinks[node],lowlinks[successor])
lowlinks[node] = min(lowlinks[node], lowlinks[successor])
elif successor in stack:
# the successor is in the stack and hence in the current
# strongly connected component (SCC)
lowlinks[node] = min(lowlinks[node],index[successor])
lowlinks[node] = min(lowlinks[node], index[successor])
# If `node` is a root node, pop the stack and generate an SCC
if lowlinks[node] == index[node]:
@@ -1172,7 +1189,8 @@ class Sequencer(object):
while True:
successor = stack.pop()
connected_component.append(successor)
if successor == node: break
if successor == node:
break
component = tuple(connected_component)
# storing the result
result.append(component)
@@ -1195,12 +1213,13 @@ class Sequencer(object):
result.append('}')
return '\n'.join(result)
#
# Unarchiving functionality for zip, tar, tgz, tbz, whl
#
ARCHIVE_EXTENSIONS = ('.tar.gz', '.tar.bz2', '.tar', '.zip',
'.tgz', '.tbz', '.whl')
ARCHIVE_EXTENSIONS = ('.tar.gz', '.tar.bz2', '.tar', '.zip', '.tgz', '.tbz', '.whl')
def unarchive(archive_filename, dest_dir, format=None, check=True):
@@ -1249,6 +1268,20 @@ def unarchive(archive_filename, dest_dir, format=None, check=True):
for tarinfo in archive.getmembers():
if not isinstance(tarinfo.name, text_type):
tarinfo.name = tarinfo.name.decode('utf-8')
# Limit extraction of dangerous items, if this Python
# allows it easily. If not, just trust the input.
# See: https://docs.python.org/3/library/tarfile.html#extraction-filters
def extraction_filter(member, path):
"""Run tarfile.tar_filter, but raise the expected ValueError"""
# This is only called if the current Python has tarfile filters
try:
return tarfile.tar_filter(member, path)
except tarfile.FilterError as exc:
raise ValueError(str(exc))
archive.extraction_filter = extraction_filter
archive.extractall(dest_dir)
finally:
@@ -1269,11 +1302,12 @@ def zip_dir(directory):
zf.write(full, dest)
return result
#
# Simple progress bar
#
UNITS = ('', 'K', 'M', 'G','T','P')
UNITS = ('', 'K', 'M', 'G', 'T', 'P')
class Progress(object):
@@ -1328,8 +1362,8 @@ class Progress(object):
def format_duration(self, duration):
if (duration <= 0) and self.max is None or self.cur == self.min:
result = '??:??:??'
#elif duration < 1:
# result = '--:--:--'
# elif duration < 1:
# result = '--:--:--'
else:
result = time.strftime('%H:%M:%S', time.gmtime(duration))
return result
@@ -1339,7 +1373,7 @@ class Progress(object):
if self.done:
prefix = 'Done'
t = self.elapsed
#import pdb; pdb.set_trace()
# import pdb; pdb.set_trace()
else:
prefix = 'ETA '
if self.max is None:
@@ -1347,7 +1381,7 @@ class Progress(object):
elif self.elapsed == 0 or (self.cur == self.min):
t = 0
else:
#import pdb; pdb.set_trace()
# import pdb; pdb.set_trace()
t = float(self.max - self.min)
t /= self.cur - self.min
t = (t - 1) * self.elapsed
@@ -1365,6 +1399,7 @@ class Progress(object):
result /= 1000.0
return '%d %sB/s' % (result, unit)
#
# Glob functionality
#
@@ -1412,18 +1447,17 @@ def _iglob(path_glob):
for fn in _iglob(os.path.join(path, radical)):
yield fn
if ssl:
from .compat import (HTTPSHandler as BaseHTTPSHandler, match_hostname,
CertificateError)
from .compat import (HTTPSHandler as BaseHTTPSHandler, match_hostname, CertificateError)
#
# HTTPSConnection which verifies certificates/matches domains
#
#
# HTTPSConnection which verifies certificates/matches domains
#
class HTTPSConnection(httplib.HTTPSConnection):
ca_certs = None # set this to the path to the certs file (.pem)
check_domain = True # only used if ca_certs is not None
ca_certs = None # set this to the path to the certs file (.pem)
check_domain = True # only used if ca_certs is not None
# noinspection PyPropertyAccess
def connect(self):
@@ -1435,7 +1469,7 @@ if ssl:
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
if hasattr(ssl, 'OP_NO_SSLv2'):
context.options |= ssl.OP_NO_SSLv2
if self.cert_file:
if getattr(self, 'cert_file', None):
context.load_cert_chain(self.cert_file, self.key_file)
kwargs = {}
if self.ca_certs:
@@ -1455,6 +1489,7 @@ if ssl:
raise
class HTTPSHandler(BaseHTTPSHandler):
def __init__(self, ca_certs, check_domain=True):
BaseHTTPSHandler.__init__(self)
self.ca_certs = ca_certs
@@ -1496,14 +1531,17 @@ if ssl:
# handler for HTTP itself.
#
class HTTPSOnlyHandler(HTTPSHandler, HTTPHandler):
def http_open(self, req):
raise URLError('Unexpected HTTP request on what should be a secure '
'connection: %s' % req)
#
# XML-RPC with timeouts
#
class Transport(xmlrpclib.Transport):
def __init__(self, timeout, use_datetime=0):
self.timeout = timeout
xmlrpclib.Transport.__init__(self, use_datetime)
@@ -1515,8 +1553,11 @@ class Transport(xmlrpclib.Transport):
self._connection = host, httplib.HTTPConnection(h)
return self._connection[1]
if ssl:
class SafeTransport(xmlrpclib.SafeTransport):
def __init__(self, timeout, use_datetime=0):
self.timeout = timeout
xmlrpclib.SafeTransport.__init__(self, use_datetime)
@@ -1528,12 +1569,12 @@ if ssl:
kwargs['timeout'] = self.timeout
if not self._connection or host != self._connection[0]:
self._extra_headers = eh
self._connection = host, httplib.HTTPSConnection(h, None,
**kwargs)
self._connection = host, httplib.HTTPSConnection(h, None, **kwargs)
return self._connection[1]
class ServerProxy(xmlrpclib.ServerProxy):
def __init__(self, uri, **kwargs):
self.timeout = timeout = kwargs.pop('timeout', None)
# The above classes only come into play if a timeout
@@ -1550,11 +1591,13 @@ class ServerProxy(xmlrpclib.ServerProxy):
self.transport = t
xmlrpclib.ServerProxy.__init__(self, uri, **kwargs)
#
# CSV functionality. This is provided because on 2.x, the csv module can't
# handle Unicode. However, we need to deal with Unicode in e.g. RECORD files.
#
def _csv_open(fn, mode, **kwargs):
if sys.version_info[0] < 3:
mode += 'b'
@@ -1568,9 +1611,9 @@ def _csv_open(fn, mode, **kwargs):
class CSVBase(object):
defaults = {
'delimiter': str(','), # The strs are used because we need native
'quotechar': str('"'), # str in the csv API (2.x won't take
'lineterminator': str('\n') # Unicode)
'delimiter': str(','), # The strs are used because we need native
'quotechar': str('"'), # str in the csv API (2.x won't take
'lineterminator': str('\n') # Unicode)
}
def __enter__(self):
@@ -1581,6 +1624,7 @@ class CSVBase(object):
class CSVReader(CSVBase):
def __init__(self, **kwargs):
if 'stream' in kwargs:
stream = kwargs['stream']
@@ -1605,7 +1649,9 @@ class CSVReader(CSVBase):
__next__ = next
class CSVWriter(CSVBase):
def __init__(self, fn, **kwargs):
self.stream = _csv_open(fn, 'w')
self.writer = csv.writer(self.stream, **self.defaults)
@@ -1620,10 +1666,12 @@ class CSVWriter(CSVBase):
row = r
self.writer.writerow(row)
#
# Configurator functionality
#
class Configurator(BaseConfigurator):
value_converters = dict(BaseConfigurator.value_converters)
@@ -1634,6 +1682,7 @@ class Configurator(BaseConfigurator):
self.base = base or os.getcwd()
def configure_custom(self, config):
def convert(o):
if isinstance(o, (list, tuple)):
result = type(o)([convert(i) for i in o])
@@ -1683,6 +1732,7 @@ class SubprocessMixin(object):
"""
Mixin for running subprocesses and capturing their output
"""
def __init__(self, verbose=False, progress=None):
self.verbose = verbose
self.progress = progress
@@ -1709,8 +1759,7 @@ class SubprocessMixin(object):
stream.close()
def run_command(self, cmd, **kwargs):
p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, **kwargs)
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs)
t1 = threading.Thread(target=self.reader, args=(p.stdout, 'stdout'))
t1.start()
t2 = threading.Thread(target=self.reader, args=(p.stderr, 'stderr'))
@@ -1730,15 +1779,17 @@ def normalize_name(name):
# https://www.python.org/dev/peps/pep-0503/#normalized-names
return re.sub('[-_.]+', '-', name).lower()
# def _get_pypirc_command():
# """
# Get the distutils command for interacting with PyPI configurations.
# :return: the command.
# """
# from distutils.core import Distribution
# from distutils.config import PyPIRCCommand
# d = Distribution()
# return PyPIRCCommand(d)
# """
# Get the distutils command for interacting with PyPI configurations.
# :return: the command.
# """
# from distutils.core import Distribution
# from distutils.config import PyPIRCCommand
# d = Distribution()
# return PyPIRCCommand(d)
class PyPIRCFile(object):
@@ -1763,9 +1814,7 @@ class PyPIRCFile(object):
if 'distutils' in sections:
# let's get the list of servers
index_servers = config.get('distutils', 'index-servers')
_servers = [server.strip() for server in
index_servers.split('\n')
if server.strip() != '']
_servers = [server.strip() for server in index_servers.split('\n') if server.strip() != '']
if _servers == []:
# nothing set, let's try to get the default pypi
if 'pypi' in sections:
@@ -1776,8 +1825,7 @@ class PyPIRCFile(object):
result['username'] = config.get(server, 'username')
# optional params
for key, default in (('repository', self.DEFAULT_REPOSITORY),
('realm', self.DEFAULT_REALM),
for key, default in (('repository', self.DEFAULT_REPOSITORY), ('realm', self.DEFAULT_REALM),
('password', None)):
if config.has_option(server, key):
result[key] = config.get(server, key)
@@ -1787,11 +1835,9 @@ class PyPIRCFile(object):
# work around people having "repository" for the "pypi"
# section of their config set to the HTTP (rather than
# HTTPS) URL
if (server == 'pypi' and
repository in (self.DEFAULT_REPOSITORY, 'pypi')):
if (server == 'pypi' and repository in (self.DEFAULT_REPOSITORY, 'pypi')):
result['repository'] = self.DEFAULT_REPOSITORY
elif (result['server'] != repository and
result['repository'] != repository):
elif (result['server'] != repository and result['repository'] != repository):
result = {}
elif 'server-login' in sections:
# old format
@@ -1821,20 +1867,24 @@ class PyPIRCFile(object):
with open(fn, 'w') as f:
config.write(f)
def _load_pypirc(index):
"""
Read the PyPI access configuration as supported by distutils.
"""
return PyPIRCFile(url=index.url).read()
def _store_pypirc(index):
PyPIRCFile().update(index.username, index.password)
#
# get_platform()/get_host_platform() copied from Python 3.10.a0 source, with some minor
# tweaks
#
def get_host_platform():
"""Return a string that identifies the current platform. This is used mainly to
distinguish platform-specific build directories and platform-specific built
@@ -1886,16 +1936,16 @@ def get_host_platform():
# At least on Linux/Intel, 'machine' is the processor --
# i386, etc.
# XXX what about Alpha, SPARC, etc?
return "%s-%s" % (osname, machine)
return "%s-%s" % (osname, machine)
elif osname[:5] == 'sunos':
if release[0] >= '5': # SunOS 5 == Solaris 2
if release[0] >= '5': # SunOS 5 == Solaris 2
osname = 'solaris'
release = '%d.%s' % (int(release[0]) - 3, release[2:])
# We can't use 'platform.architecture()[0]' because a
# bootstrap problem. We use a dict to get an error
# if some suspicious happens.
bitness = {2147483647:'32bit', 9223372036854775807:'64bit'}
bitness = {2147483647: '32bit', 9223372036854775807: '64bit'}
machine += '.%s' % bitness[sys.maxsize]
# fall through to standard osname-release-machine representation
elif osname[:3] == 'aix':
@@ -1903,23 +1953,25 @@ def get_host_platform():
return aix_platform()
elif osname[:6] == 'cygwin':
osname = 'cygwin'
rel_re = re.compile (r'[\d.]+', re.ASCII)
rel_re = re.compile(r'[\d.]+', re.ASCII)
m = rel_re.match(release)
if m:
release = m.group()
elif osname[:6] == 'darwin':
import _osx_support, distutils.sysconfig
osname, release, machine = _osx_support.get_platform_osx(
distutils.sysconfig.get_config_vars(),
osname, release, machine)
import _osx_support
try:
from distutils import sysconfig
except ImportError:
import sysconfig
osname, release, machine = _osx_support.get_platform_osx(sysconfig.get_config_vars(), osname, release, machine)
return '%s-%s-%s' % (osname, release, machine)
_TARGET_TO_PLAT = {
'x86' : 'win32',
'x64' : 'win-amd64',
'arm' : 'win-arm32',
'x86': 'win32',
'x64': 'win-amd64',
'arm': 'win-arm32',
}

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2012-2017 The Python Software Foundation.
# Copyright (C) 2012-2023 The Python Software Foundation.
# See LICENSE.txt and CONTRIBUTORS.txt.
#
"""
@@ -176,9 +176,9 @@ class Matcher(object):
return self._string
PEP440_VERSION_RE = re.compile(r'^v?(\d+!)?(\d+(\.\d+)*)((a|b|c|rc)(\d+))?'
r'(\.(post)(\d+))?(\.(dev)(\d+))?'
r'(\+([a-zA-Z\d]+(\.[a-zA-Z\d]+)?))?$')
PEP440_VERSION_RE = re.compile(r'^v?(\d+!)?(\d+(\.\d+)*)((a|alpha|b|beta|c|rc|pre|preview)(\d+)?)?'
r'(\.(post|r|rev)(\d+)?)?([._-]?(dev)(\d+)?)?'
r'(\+([a-zA-Z\d]+(\.[a-zA-Z\d]+)?))?$', re.I)
def _pep_440_key(s):
@@ -202,15 +202,24 @@ def _pep_440_key(s):
if pre == (None, None):
pre = ()
else:
pre = pre[0], int(pre[1])
if pre[1] is None:
pre = pre[0], 0
else:
pre = pre[0], int(pre[1])
if post == (None, None):
post = ()
else:
post = post[0], int(post[1])
if post[1] is None:
post = post[0], 0
else:
post = post[0], int(post[1])
if dev == (None, None):
dev = ()
else:
dev = dev[0], int(dev[1])
if dev[1] is None:
dev = dev[0], 0
else:
dev = dev[0], int(dev[1])
if local is None:
local = ()
else:
@@ -238,7 +247,6 @@ def _pep_440_key(s):
if not dev:
dev = ('final',)
#print('%s -> %s' % (s, m.groups()))
return epoch, nums, pre, post, dev, local
@@ -378,6 +386,7 @@ class NormalizedMatcher(Matcher):
pfx = '.'.join([str(i) for i in release_clause])
return _match_prefix(version, pfx)
_REPLACEMENTS = (
(re.compile('[.+-]$'), ''), # remove trailing puncts
(re.compile(r'^[.](\d)'), r'0.\1'), # .N -> 0.N at start
@@ -388,7 +397,7 @@ _REPLACEMENTS = (
(re.compile('[.]{2,}'), '.'), # multiple runs of '.'
(re.compile(r'\b(alfa|apha)\b'), 'alpha'), # misspelt alpha
(re.compile(r'\b(pre-alpha|prealpha)\b'),
'pre.alpha'), # standardise
'pre.alpha'), # standardise
(re.compile(r'\(beta\)$'), 'beta'), # remove parentheses
)
@@ -416,7 +425,7 @@ def _suggest_semantic_version(s):
# Now look for numeric prefix, and separate it out from
# the rest.
#import pdb; pdb.set_trace()
# import pdb; pdb.set_trace()
m = _NUMERIC_PREFIX.match(result)
if not m:
prefix = '0.0.0'
@@ -434,7 +443,7 @@ def _suggest_semantic_version(s):
prefix = '.'.join([str(i) for i in prefix])
suffix = suffix.strip()
if suffix:
#import pdb; pdb.set_trace()
# import pdb; pdb.set_trace()
# massage the suffix.
for pat, repl in _SUFFIX_REPLACEMENTS:
suffix = pat.sub(repl, suffix)
@@ -504,7 +513,7 @@ def _suggest_normalized_version(s):
rs = rs[1:]
# Clean leading '0's on numbers.
#TODO: unintended side-effect on, e.g., "2003.05.09"
# TODO: unintended side-effect on, e.g., "2003.05.09"
# PyPI stats: 77 (~2%) better
rs = re.sub(r"\b0+(\d+)(?!\d)", r"\1", rs)
@@ -563,6 +572,7 @@ def _suggest_normalized_version(s):
# Legacy version processing (distribute-compatible)
#
_VERSION_PART = re.compile(r'([a-z]+|\d+|[\.-])', re.I)
_VERSION_REPLACE = {
'pre': 'c',
@@ -609,8 +619,7 @@ class LegacyVersion(Version):
def is_prerelease(self):
result = False
for x in self._parts:
if (isinstance(x, string_types) and x.startswith('*') and
x < '*final'):
if (isinstance(x, string_types) and x.startswith('*') and x < '*final'):
result = True
break
return result
@@ -641,6 +650,7 @@ class LegacyMatcher(Matcher):
# Semantic versioning
#
_SEMVER_RE = re.compile(r'^(\d+)\.(\d+)\.(\d+)'
r'(-[a-z0-9]+(\.[a-z0-9-]+)*)?'
r'(\+[a-z0-9]+(\.[a-z0-9-]+)*)?$', re.I)
@@ -722,6 +732,7 @@ class VersionScheme(object):
result = self.suggester(s)
return result
_SCHEMES = {
'normalized': VersionScheme(_normalized_key, NormalizedMatcher,
_suggest_normalized_version),

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013-2020 Vinay Sajip.
# Copyright (C) 2013-2023 Vinay Sajip.
# Licensed to the Python Software Foundation under a contributor agreement.
# See LICENSE.txt and CONTRIBUTORS.txt.
#
@@ -24,16 +24,14 @@ import zipfile
from . import __version__, DistlibException
from .compat import sysconfig, ZipFile, fsdecode, text_type, filter
from .database import InstalledDistribution
from .metadata import (Metadata, METADATA_FILENAME, WHEEL_METADATA_FILENAME,
LEGACY_METADATA_FILENAME)
from .util import (FileOperator, convert_path, CSVReader, CSVWriter, Cache,
cached_property, get_cache_base, read_exports, tempdir,
get_platform)
from .metadata import Metadata, WHEEL_METADATA_FILENAME, LEGACY_METADATA_FILENAME
from .util import (FileOperator, convert_path, CSVReader, CSVWriter, Cache, cached_property, get_cache_base,
read_exports, tempdir, get_platform)
from .version import NormalizedVersion, UnsupportedVersionError
logger = logging.getLogger(__name__)
cache = None # created when needed
cache = None # created when needed
if hasattr(sys, 'pypy_version_info'): # pragma: no cover
IMP_PREFIX = 'pp'
@@ -45,7 +43,7 @@ else:
IMP_PREFIX = 'cp'
VER_SUFFIX = sysconfig.get_config_var('py_version_nodot')
if not VER_SUFFIX: # pragma: no cover
if not VER_SUFFIX: # pragma: no cover
VER_SUFFIX = '%s%s' % sys.version_info[:2]
PYVER = 'py' + VER_SUFFIX
IMPVER = IMP_PREFIX + VER_SUFFIX
@@ -56,6 +54,7 @@ ABI = sysconfig.get_config_var('SOABI')
if ABI and ABI.startswith('cpython-'):
ABI = ABI.replace('cpython-', 'cp').split('-')[0]
else:
def _derive_abi():
parts = ['cp', VER_SUFFIX]
if sysconfig.get_config_var('Py_DEBUG'):
@@ -73,10 +72,12 @@ else:
if us == 4 or (us is None and sys.maxunicode == 0x10FFFF):
parts.append('u')
return ''.join(parts)
ABI = _derive_abi()
del _derive_abi
FILENAME_RE = re.compile(r'''
FILENAME_RE = re.compile(
r'''
(?P<nm>[^-]+)
-(?P<vn>\d+[^-]*)
(-(?P<bn>\d+[^-]*))?
@@ -109,12 +110,14 @@ else:
import importlib.machinery
import importlib.util
def _get_suffixes():
if imp:
return [s[0] for s in imp.get_suffixes()]
else:
return importlib.machinery.EXTENSION_SUFFIXES
def _load_dynamic(name, path):
# https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
if imp:
@@ -126,7 +129,9 @@ def _load_dynamic(name, path):
spec.loader.exec_module(module)
return module
class Mounter(object):
def __init__(self):
self.impure_wheels = {}
self.libs = {}
@@ -161,6 +166,7 @@ class Mounter(object):
result.__package__ = parts[0]
return result
_hook = Mounter()
@@ -227,8 +233,7 @@ class Wheel(object):
arch = '.'.join(self.arch)
# replace - with _ as a local version separator
version = self.version.replace('-', '_')
return '%s-%s%s-%s-%s-%s.whl' % (self.name, version, buildver,
pyver, abi, arch)
return '%s-%s%s-%s-%s-%s.whl' % (self.name, version, buildver, pyver, abi, arch)
@property
def exists(self):
@@ -249,14 +254,14 @@ class Wheel(object):
info_dir = '%s.dist-info' % name_ver
wrapper = codecs.getreader('utf-8')
with ZipFile(pathname, 'r') as zf:
wheel_metadata = self.get_wheel_metadata(zf)
wv = wheel_metadata['Wheel-Version'].split('.', 1)
file_version = tuple([int(i) for i in wv])
self.get_wheel_metadata(zf)
# wv = wheel_metadata['Wheel-Version'].split('.', 1)
# file_version = tuple([int(i) for i in wv])
# if file_version < (1, 1):
# fns = [WHEEL_METADATA_FILENAME, METADATA_FILENAME,
# LEGACY_METADATA_FILENAME]
# fns = [WHEEL_METADATA_FILENAME, METADATA_FILENAME,
# LEGACY_METADATA_FILENAME]
# else:
# fns = [WHEEL_METADATA_FILENAME, METADATA_FILENAME]
# fns = [WHEEL_METADATA_FILENAME, METADATA_FILENAME]
fns = [WHEEL_METADATA_FILENAME, LEGACY_METADATA_FILENAME]
result = None
for fn in fns:
@@ -332,7 +337,7 @@ class Wheel(object):
return hash_kind, result
def write_record(self, records, record_path, archive_record_path):
records = list(records) # make a copy, as mutated
records = list(records) # make a copy, as mutated
records.append((archive_record_path, '', ''))
with CSVWriter(record_path) as writer:
for row in records:
@@ -341,7 +346,7 @@ class Wheel(object):
def write_records(self, info, libdir, archive_paths):
records = []
distinfo, info_dir = info
hasher = getattr(hashlib, self.hash_kind)
# hasher = getattr(hashlib, self.hash_kind)
for ap, p in archive_paths:
with open(p, 'rb') as f:
data = f.read()
@@ -466,6 +471,7 @@ class Wheel(object):
if '.dist-info' in ap:
n += 10000
return (n, ap)
archive_paths = sorted(archive_paths, key=sorter)
# Now, at last, RECORD.
@@ -503,7 +509,7 @@ class Wheel(object):
installed, and the headers, scripts, data and dist-info metadata are
not written. If kwarg ``bytecode_hashed_invalidation`` is True, written
bytecode will try to use file-hash based invalidation (PEP-552) on
supported interpreter versions (CPython 2.7+).
supported interpreter versions (CPython 3.7+).
The return value is a :class:`InstalledDistribution` instance unless
``options.lib_only`` is True, in which case the return value is ``None``.
@@ -553,11 +559,11 @@ class Wheel(object):
# make a new instance rather than a copy of maker's,
# as we mutate it
fileop = FileOperator(dry_run=dry_run)
fileop.record = True # so we can rollback if needed
fileop.record = True # so we can rollback if needed
bc = not sys.dont_write_bytecode # Double negatives. Lovely!
bc = not sys.dont_write_bytecode # Double negatives. Lovely!
outfiles = [] # for RECORD writing
outfiles = [] # for RECORD writing
# for script copying/shebang processing
workdir = tempfile.mkdtemp()
@@ -591,8 +597,7 @@ class Wheel(object):
if lib_only and u_arcname.startswith((info_pfx, data_pfx)):
logger.debug('lib_only: skipping %s', u_arcname)
continue
is_script = (u_arcname.startswith(script_pfx)
and not u_arcname.endswith('.exe'))
is_script = (u_arcname.startswith(script_pfx) and not u_arcname.endswith('.exe'))
if u_arcname.startswith(data_pfx):
_, where, rp = u_arcname.split('/', 2)
@@ -624,14 +629,12 @@ class Wheel(object):
'%s' % outfile)
if bc and outfile.endswith('.py'):
try:
pyc = fileop.byte_compile(outfile,
hashed_invalidation=bc_hashed_invalidation)
pyc = fileop.byte_compile(outfile, hashed_invalidation=bc_hashed_invalidation)
outfiles.append(pyc)
except Exception:
# Don't give up if byte-compilation fails,
# but log it and perhaps warn the user
logger.warning('Byte-compilation failed',
exc_info=True)
logger.warning('Byte-compilation failed', exc_info=True)
else:
fn = os.path.basename(convert_path(arcname))
workname = os.path.join(workdir, fn)
@@ -700,7 +703,7 @@ class Wheel(object):
fileop.set_executable_mode(filenames)
if gui_scripts:
options = {'gui': True }
options = {'gui': True}
for k, v in gui_scripts.items():
script = '%s = %s' % (k, v)
filenames = maker.make(script, options)
@@ -710,7 +713,7 @@ class Wheel(object):
dist = InstalledDistribution(p)
# Write SHARED
paths = dict(paths) # don't change passed in dict
paths = dict(paths) # don't change passed in dict
del paths['purelib']
del paths['platlib']
paths['lib'] = libdir
@@ -719,8 +722,7 @@ class Wheel(object):
outfiles.append(p)
# Write RECORD
dist.write_installed_files(outfiles, paths['prefix'],
dry_run)
dist.write_installed_files(outfiles, paths['prefix'], dry_run)
return dist
except Exception: # pragma: no cover
logger.exception('installation failed.')
@@ -733,8 +735,7 @@ class Wheel(object):
global cache
if cache is None:
# Use native string to avoid issues on 2.x: see Python #20140.
base = os.path.join(get_cache_base(), str('dylib-cache'),
'%s.%s' % sys.version_info[:2])
base = os.path.join(get_cache_base(), str('dylib-cache'), '%s.%s' % sys.version_info[:2])
cache = Cache(base)
return cache
@@ -751,7 +752,7 @@ class Wheel(object):
wf = wrapper(bf)
extensions = json.load(wf)
cache = self._get_dylib_cache()
prefix = cache.prefix_to_dir(pathname)
prefix = cache.prefix_to_dir(self.filename, use_abspath=False)
cache_base = os.path.join(cache.base, prefix)
if not os.path.isdir(cache_base):
os.makedirs(cache_base)
@@ -782,7 +783,7 @@ class Wheel(object):
"""
Determine if a wheel is asserted as mountable by its metadata.
"""
return True # for now - metadata details TBD
return True # for now - metadata details TBD
def mount(self, append=False):
pathname = os.path.abspath(os.path.join(self.dirname, self.filename))
@@ -820,10 +821,10 @@ class Wheel(object):
def verify(self):
pathname = os.path.join(self.dirname, self.filename)
name_ver = '%s-%s' % (self.name, self.version)
data_dir = '%s.data' % name_ver
# data_dir = '%s.data' % name_ver
info_dir = '%s.dist-info' % name_ver
metadata_name = posixpath.join(info_dir, LEGACY_METADATA_FILENAME)
# metadata_name = posixpath.join(info_dir, LEGACY_METADATA_FILENAME)
wheel_metadata_name = posixpath.join(info_dir, 'WHEEL')
record_name = posixpath.join(info_dir, 'RECORD')
@@ -832,9 +833,9 @@ class Wheel(object):
with ZipFile(pathname, 'r') as zf:
with zf.open(wheel_metadata_name) as bwf:
wf = wrapper(bwf)
message = message_from_file(wf)
wv = message['Wheel-Version'].split('.', 1)
file_version = tuple([int(i) for i in wv])
message_from_file(wf)
# wv = message['Wheel-Version'].split('.', 1)
# file_version = tuple([int(i) for i in wv])
# TODO version verification
records = {}
@@ -903,15 +904,14 @@ class Wheel(object):
def update_version(version, path):
updated = None
try:
v = NormalizedVersion(version)
NormalizedVersion(version)
i = version.find('-')
if i < 0:
updated = '%s+1' % version
else:
parts = [int(s) for s in version[i + 1:].split('.')]
parts[-1] += 1
updated = '%s+%s' % (version[:i],
'.'.join(str(i) for i in parts))
updated = '%s+%s' % (version[:i], '.'.join(str(i) for i in parts))
except UnsupportedVersionError:
logger.debug('Cannot update non-compliant (PEP-440) '
'version %r', version)
@@ -920,8 +920,7 @@ class Wheel(object):
md.version = updated
legacy = path.endswith(LEGACY_METADATA_FILENAME)
md.write(path=path, legacy=legacy)
logger.debug('Version updated from %r to %r', version,
updated)
logger.debug('Version updated from %r to %r', version, updated)
pathname = os.path.join(self.dirname, self.filename)
name_ver = '%s-%s' % (self.name, self.version)
@@ -957,9 +956,7 @@ class Wheel(object):
update_version(current_version, path)
# Decide where the new wheel goes.
if dest_dir is None:
fd, newpath = tempfile.mkstemp(suffix='.whl',
prefix='wheel-update-',
dir=workdir)
fd, newpath = tempfile.mkstemp(suffix='.whl', prefix='wheel-update-', dir=workdir)
os.close(fd)
else:
if not os.path.isdir(dest_dir):
@@ -974,6 +971,7 @@ class Wheel(object):
shutil.copyfile(newpath, pathname)
return modified
def _get_glibc_version():
import platform
ver = platform.libc_ver()
@@ -984,15 +982,25 @@ def _get_glibc_version():
result = tuple(result)
return result
def compatible_tags():
"""
Return (pyver, abi, arch) tuples compatible with this Python.
"""
versions = [VER_SUFFIX]
major = VER_SUFFIX[0]
for minor in range(sys.version_info[1] - 1, - 1, -1):
versions.append(''.join([major, str(minor)]))
class _Version:
def __init__(self, major, minor):
self.major = major
self.major_minor = (major, minor)
self.string = ''.join((str(major), str(minor)))
def __str__(self):
return self.string
versions = [
_Version(sys.version_info.major, minor_version)
for minor_version in range(sys.version_info.minor, -1, -1)
]
abis = []
for suffix in _get_suffixes():
if suffix.startswith('.abi'):
@@ -1023,40 +1031,50 @@ def compatible_tags():
while minor >= 0:
for match in matches:
s = '%s_%s_%s_%s' % (name, major, minor, match)
if s != ARCH: # already there
if s != ARCH: # already there
arches.append(s)
minor -= 1
# Most specific - our Python version, ABI and arch
for abi in abis:
for arch in arches:
result.append((''.join((IMP_PREFIX, versions[0])), abi, arch))
# manylinux
if abi != 'none' and sys.platform.startswith('linux'):
arch = arch.replace('linux_', '')
parts = _get_glibc_version()
if len(parts) == 2:
if parts >= (2, 5):
result.append((''.join((IMP_PREFIX, versions[0])), abi,
'manylinux1_%s' % arch))
if parts >= (2, 12):
result.append((''.join((IMP_PREFIX, versions[0])), abi,
'manylinux2010_%s' % arch))
if parts >= (2, 17):
result.append((''.join((IMP_PREFIX, versions[0])), abi,
'manylinux2014_%s' % arch))
result.append((''.join((IMP_PREFIX, versions[0])), abi,
'manylinux_%s_%s_%s' % (parts[0], parts[1],
arch)))
for i, version_object in enumerate(versions):
version = str(version_object)
add_abis = []
if i == 0:
add_abis = abis
if IMP_PREFIX == 'cp' and version_object.major_minor >= (3, 2):
limited_api_abi = 'abi' + str(version_object.major)
if limited_api_abi not in add_abis:
add_abis.append(limited_api_abi)
for abi in add_abis:
for arch in arches:
result.append((''.join((IMP_PREFIX, version)), abi, arch))
# manylinux
if abi != 'none' and sys.platform.startswith('linux'):
arch = arch.replace('linux_', '')
parts = _get_glibc_version()
if len(parts) == 2:
if parts >= (2, 5):
result.append((''.join((IMP_PREFIX, version)), abi, 'manylinux1_%s' % arch))
if parts >= (2, 12):
result.append((''.join((IMP_PREFIX, version)), abi, 'manylinux2010_%s' % arch))
if parts >= (2, 17):
result.append((''.join((IMP_PREFIX, version)), abi, 'manylinux2014_%s' % arch))
result.append((''.join(
(IMP_PREFIX, version)), abi, 'manylinux_%s_%s_%s' % (parts[0], parts[1], arch)))
# where no ABI / arch dependency, but IMP_PREFIX dependency
for i, version in enumerate(versions):
for i, version_object in enumerate(versions):
version = str(version_object)
result.append((''.join((IMP_PREFIX, version)), 'none', 'any'))
if i == 0:
result.append((''.join((IMP_PREFIX, version[0])), 'none', 'any'))
# no IMP_PREFIX, ABI or arch dependency
for i, version in enumerate(versions):
for i, version_object in enumerate(versions):
version = str(version_object)
result.append((''.join(('py', version)), 'none', 'any'))
if i == 0:
result.append((''.join(('py', version[0])), 'none', 'any'))
@@ -1071,7 +1089,7 @@ del compatible_tags
def is_compatible(wheel, tags=None):
if not isinstance(wheel, Wheel):
wheel = Wheel(wheel) # assume it's a filename
wheel = Wheel(wheel) # assume it's a filename
result = False
if tags is None:
tags = COMPATIBLE_TAGS