diff options
author | Jason R. Coombs <jaraco@jaraco.com> | 2020-02-11 22:15:31 -0500 |
---|---|---|
committer | Jason R. Coombs <jaraco@jaraco.com> | 2020-02-11 22:15:31 -0500 |
commit | ce8e3ee174ed9dee84aa08a461fb174ac1e53535 (patch) | |
tree | da78c024b4491210e61b73e9e2f755d46c8bc1ff /setuptools | |
parent | 53b8523fdfa19173d2e6b11d6b4d175f54b9dfea (diff) | |
parent | a5dec2f14e3414e4ee5dd146bff9c289d573de9a (diff) | |
download | external_python_setuptools-ce8e3ee174ed9dee84aa08a461fb174ac1e53535.tar.gz external_python_setuptools-ce8e3ee174ed9dee84aa08a461fb174ac1e53535.tar.bz2 external_python_setuptools-ce8e3ee174ed9dee84aa08a461fb174ac1e53535.zip |
Merge branch 'master' into fix/1557
Diffstat (limited to 'setuptools')
73 files changed, 3426 insertions, 1940 deletions
diff --git a/setuptools/__init__.py b/setuptools/__init__.py index a71b2bbd..07d6b6fa 100644 --- a/setuptools/__init__.py +++ b/setuptools/__init__.py @@ -1,7 +1,6 @@ """Extensions to the 'distutils' for large or complex distributions""" import os -import sys import functools import distutils.core import distutils.filelist @@ -31,7 +30,7 @@ __all__ = [ ] if PY3: - __all__.append('find_namespace_packages') + __all__.append('find_namespace_packages') __version__ = setuptools.version.__version__ @@ -123,7 +122,7 @@ class PEP420PackageFinder(PackageFinder): find_packages = PackageFinder.find if PY3: - find_namespace_packages = PEP420PackageFinder.find + find_namespace_packages = PEP420PackageFinder.find def _install_setup_requires(attrs): @@ -144,6 +143,7 @@ def setup(**attrs): _install_setup_requires(attrs) return distutils.core.setup(**attrs) + setup.__doc__ = distutils.core.setup.__doc__ @@ -191,8 +191,8 @@ class Command(_Command): ok = False if not ok: raise DistutilsOptionError( - "'%s' must be a list of strings (got %r)" - % (option, val)) + "'%s' must be a list of strings (got %r)" + % (option, val)) def reinitialize_command(self, command, reinit_subcommands=0, **kw): cmd = _Command.reinitialize_command(self, command, reinit_subcommands) diff --git a/setuptools/_imp.py b/setuptools/_imp.py new file mode 100644 index 00000000..451e45a8 --- /dev/null +++ b/setuptools/_imp.py @@ -0,0 +1,82 @@ +""" +Re-implementation of find_module and get_frozen_object +from the deprecated imp module. +""" + +import os +import importlib.util +import importlib.machinery + +from .py34compat import module_from_spec + + +PY_SOURCE = 1 +PY_COMPILED = 2 +C_EXTENSION = 3 +C_BUILTIN = 6 +PY_FROZEN = 7 + + +def find_spec(module, paths): + finder = ( + importlib.machinery.PathFinder().find_spec + if isinstance(paths, list) else + importlib.util.find_spec + ) + return finder(module, paths) + + +def find_module(module, paths=None): + """Just like 'imp.find_module()', but with package support""" + spec = find_spec(module, paths) + if spec is None: + raise ImportError("Can't find %s" % module) + if not spec.has_location and hasattr(spec, 'submodule_search_locations'): + spec = importlib.util.spec_from_loader('__init__.py', spec.loader) + + kind = -1 + file = None + static = isinstance(spec.loader, type) + if spec.origin == 'frozen' or static and issubclass( + spec.loader, importlib.machinery.FrozenImporter): + kind = PY_FROZEN + path = None # imp compabilty + suffix = mode = '' # imp compability + elif spec.origin == 'built-in' or static and issubclass( + spec.loader, importlib.machinery.BuiltinImporter): + kind = C_BUILTIN + path = None # imp compabilty + suffix = mode = '' # imp compability + elif spec.has_location: + path = spec.origin + suffix = os.path.splitext(path)[1] + mode = 'r' if suffix in importlib.machinery.SOURCE_SUFFIXES else 'rb' + + if suffix in importlib.machinery.SOURCE_SUFFIXES: + kind = PY_SOURCE + elif suffix in importlib.machinery.BYTECODE_SUFFIXES: + kind = PY_COMPILED + elif suffix in importlib.machinery.EXTENSION_SUFFIXES: + kind = C_EXTENSION + + if kind in {PY_SOURCE, PY_COMPILED}: + file = open(path, mode) + else: + path = None + suffix = mode = '' + + return file, path, (suffix, mode, kind) + + +def get_frozen_object(module, paths=None): + spec = find_spec(module, paths) + if not spec: + raise ImportError("Can't find %s" % module) + return spec.loader.get_code(module) + + +def get_module(module, paths, info): + spec = find_spec(module, paths) + if not spec: + raise ImportError("Can't find %s" % module) + return module_from_spec(spec) diff --git a/setuptools/_vendor/ordered_set.py b/setuptools/_vendor/ordered_set.py new file mode 100644 index 00000000..14876000 --- /dev/null +++ b/setuptools/_vendor/ordered_set.py @@ -0,0 +1,488 @@ +""" +An OrderedSet is a custom MutableSet that remembers its order, so that every +entry has an index that can be looked up. + +Based on a recipe originally posted to ActiveState Recipes by Raymond Hettiger, +and released under the MIT license. +""" +import itertools as it +from collections import deque + +try: + # Python 3 + from collections.abc import MutableSet, Sequence +except ImportError: + # Python 2.7 + from collections import MutableSet, Sequence + +SLICE_ALL = slice(None) +__version__ = "3.1" + + +def is_iterable(obj): + """ + Are we being asked to look up a list of things, instead of a single thing? + We check for the `__iter__` attribute so that this can cover types that + don't have to be known by this module, such as NumPy arrays. + + Strings, however, should be considered as atomic values to look up, not + iterables. The same goes for tuples, since they are immutable and therefore + valid entries. + + We don't need to check for the Python 2 `unicode` type, because it doesn't + have an `__iter__` attribute anyway. + """ + return ( + hasattr(obj, "__iter__") + and not isinstance(obj, str) + and not isinstance(obj, tuple) + ) + + +class OrderedSet(MutableSet, Sequence): + """ + An OrderedSet is a custom MutableSet that remembers its order, so that + every entry has an index that can be looked up. + + Example: + >>> OrderedSet([1, 1, 2, 3, 2]) + OrderedSet([1, 2, 3]) + """ + + def __init__(self, iterable=None): + self.items = [] + self.map = {} + if iterable is not None: + self |= iterable + + def __len__(self): + """ + Returns the number of unique elements in the ordered set + + Example: + >>> len(OrderedSet([])) + 0 + >>> len(OrderedSet([1, 2])) + 2 + """ + return len(self.items) + + def __getitem__(self, index): + """ + Get the item at a given index. + + If `index` is a slice, you will get back that slice of items, as a + new OrderedSet. + + If `index` is a list or a similar iterable, you'll get a list of + items corresponding to those indices. This is similar to NumPy's + "fancy indexing". The result is not an OrderedSet because you may ask + for duplicate indices, and the number of elements returned should be + the number of elements asked for. + + Example: + >>> oset = OrderedSet([1, 2, 3]) + >>> oset[1] + 2 + """ + if isinstance(index, slice) and index == SLICE_ALL: + return self.copy() + elif is_iterable(index): + return [self.items[i] for i in index] + elif hasattr(index, "__index__") or isinstance(index, slice): + result = self.items[index] + if isinstance(result, list): + return self.__class__(result) + else: + return result + else: + raise TypeError("Don't know how to index an OrderedSet by %r" % index) + + def copy(self): + """ + Return a shallow copy of this object. + + Example: + >>> this = OrderedSet([1, 2, 3]) + >>> other = this.copy() + >>> this == other + True + >>> this is other + False + """ + return self.__class__(self) + + def __getstate__(self): + if len(self) == 0: + # The state can't be an empty list. + # We need to return a truthy value, or else __setstate__ won't be run. + # + # This could have been done more gracefully by always putting the state + # in a tuple, but this way is backwards- and forwards- compatible with + # previous versions of OrderedSet. + return (None,) + else: + return list(self) + + def __setstate__(self, state): + if state == (None,): + self.__init__([]) + else: + self.__init__(state) + + def __contains__(self, key): + """ + Test if the item is in this ordered set + + Example: + >>> 1 in OrderedSet([1, 3, 2]) + True + >>> 5 in OrderedSet([1, 3, 2]) + False + """ + return key in self.map + + def add(self, key): + """ + Add `key` as an item to this OrderedSet, then return its index. + + If `key` is already in the OrderedSet, return the index it already + had. + + Example: + >>> oset = OrderedSet() + >>> oset.append(3) + 0 + >>> print(oset) + OrderedSet([3]) + """ + if key not in self.map: + self.map[key] = len(self.items) + self.items.append(key) + return self.map[key] + + append = add + + def update(self, sequence): + """ + Update the set with the given iterable sequence, then return the index + of the last element inserted. + + Example: + >>> oset = OrderedSet([1, 2, 3]) + >>> oset.update([3, 1, 5, 1, 4]) + 4 + >>> print(oset) + OrderedSet([1, 2, 3, 5, 4]) + """ + item_index = None + try: + for item in sequence: + item_index = self.add(item) + except TypeError: + raise ValueError( + "Argument needs to be an iterable, got %s" % type(sequence) + ) + return item_index + + def index(self, key): + """ + Get the index of a given entry, raising an IndexError if it's not + present. + + `key` can be an iterable of entries that is not a string, in which case + this returns a list of indices. + + Example: + >>> oset = OrderedSet([1, 2, 3]) + >>> oset.index(2) + 1 + """ + if is_iterable(key): + return [self.index(subkey) for subkey in key] + return self.map[key] + + # Provide some compatibility with pd.Index + get_loc = index + get_indexer = index + + def pop(self): + """ + Remove and return the last element from the set. + + Raises KeyError if the set is empty. + + Example: + >>> oset = OrderedSet([1, 2, 3]) + >>> oset.pop() + 3 + """ + if not self.items: + raise KeyError("Set is empty") + + elem = self.items[-1] + del self.items[-1] + del self.map[elem] + return elem + + def discard(self, key): + """ + Remove an element. Do not raise an exception if absent. + + The MutableSet mixin uses this to implement the .remove() method, which + *does* raise an error when asked to remove a non-existent item. + + Example: + >>> oset = OrderedSet([1, 2, 3]) + >>> oset.discard(2) + >>> print(oset) + OrderedSet([1, 3]) + >>> oset.discard(2) + >>> print(oset) + OrderedSet([1, 3]) + """ + if key in self: + i = self.map[key] + del self.items[i] + del self.map[key] + for k, v in self.map.items(): + if v >= i: + self.map[k] = v - 1 + + def clear(self): + """ + Remove all items from this OrderedSet. + """ + del self.items[:] + self.map.clear() + + def __iter__(self): + """ + Example: + >>> list(iter(OrderedSet([1, 2, 3]))) + [1, 2, 3] + """ + return iter(self.items) + + def __reversed__(self): + """ + Example: + >>> list(reversed(OrderedSet([1, 2, 3]))) + [3, 2, 1] + """ + return reversed(self.items) + + def __repr__(self): + if not self: + return "%s()" % (self.__class__.__name__,) + return "%s(%r)" % (self.__class__.__name__, list(self)) + + def __eq__(self, other): + """ + Returns true if the containers have the same items. If `other` is a + Sequence, then order is checked, otherwise it is ignored. + + Example: + >>> oset = OrderedSet([1, 3, 2]) + >>> oset == [1, 3, 2] + True + >>> oset == [1, 2, 3] + False + >>> oset == [2, 3] + False + >>> oset == OrderedSet([3, 2, 1]) + False + """ + # In Python 2 deque is not a Sequence, so treat it as one for + # consistent behavior with Python 3. + if isinstance(other, (Sequence, deque)): + # Check that this OrderedSet contains the same elements, in the + # same order, as the other object. + return list(self) == list(other) + try: + other_as_set = set(other) + except TypeError: + # If `other` can't be converted into a set, it's not equal. + return False + else: + return set(self) == other_as_set + + def union(self, *sets): + """ + Combines all unique items. + Each items order is defined by its first appearance. + + Example: + >>> oset = OrderedSet.union(OrderedSet([3, 1, 4, 1, 5]), [1, 3], [2, 0]) + >>> print(oset) + OrderedSet([3, 1, 4, 5, 2, 0]) + >>> oset.union([8, 9]) + OrderedSet([3, 1, 4, 5, 2, 0, 8, 9]) + >>> oset | {10} + OrderedSet([3, 1, 4, 5, 2, 0, 10]) + """ + cls = self.__class__ if isinstance(self, OrderedSet) else OrderedSet + containers = map(list, it.chain([self], sets)) + items = it.chain.from_iterable(containers) + return cls(items) + + def __and__(self, other): + # the parent implementation of this is backwards + return self.intersection(other) + + def intersection(self, *sets): + """ + Returns elements in common between all sets. Order is defined only + by the first set. + + Example: + >>> oset = OrderedSet.intersection(OrderedSet([0, 1, 2, 3]), [1, 2, 3]) + >>> print(oset) + OrderedSet([1, 2, 3]) + >>> oset.intersection([2, 4, 5], [1, 2, 3, 4]) + OrderedSet([2]) + >>> oset.intersection() + OrderedSet([1, 2, 3]) + """ + cls = self.__class__ if isinstance(self, OrderedSet) else OrderedSet + if sets: + common = set.intersection(*map(set, sets)) + items = (item for item in self if item in common) + else: + items = self + return cls(items) + + def difference(self, *sets): + """ + Returns all elements that are in this set but not the others. + + Example: + >>> OrderedSet([1, 2, 3]).difference(OrderedSet([2])) + OrderedSet([1, 3]) + >>> OrderedSet([1, 2, 3]).difference(OrderedSet([2]), OrderedSet([3])) + OrderedSet([1]) + >>> OrderedSet([1, 2, 3]) - OrderedSet([2]) + OrderedSet([1, 3]) + >>> OrderedSet([1, 2, 3]).difference() + OrderedSet([1, 2, 3]) + """ + cls = self.__class__ + if sets: + other = set.union(*map(set, sets)) + items = (item for item in self if item not in other) + else: + items = self + return cls(items) + + def issubset(self, other): + """ + Report whether another set contains this set. + + Example: + >>> OrderedSet([1, 2, 3]).issubset({1, 2}) + False + >>> OrderedSet([1, 2, 3]).issubset({1, 2, 3, 4}) + True + >>> OrderedSet([1, 2, 3]).issubset({1, 4, 3, 5}) + False + """ + if len(self) > len(other): # Fast check for obvious cases + return False + return all(item in other for item in self) + + def issuperset(self, other): + """ + Report whether this set contains another set. + + Example: + >>> OrderedSet([1, 2]).issuperset([1, 2, 3]) + False + >>> OrderedSet([1, 2, 3, 4]).issuperset({1, 2, 3}) + True + >>> OrderedSet([1, 4, 3, 5]).issuperset({1, 2, 3}) + False + """ + if len(self) < len(other): # Fast check for obvious cases + return False + return all(item in self for item in other) + + def symmetric_difference(self, other): + """ + Return the symmetric difference of two OrderedSets as a new set. + That is, the new set will contain all elements that are in exactly + one of the sets. + + Their order will be preserved, with elements from `self` preceding + elements from `other`. + + Example: + >>> this = OrderedSet([1, 4, 3, 5, 7]) + >>> other = OrderedSet([9, 7, 1, 3, 2]) + >>> this.symmetric_difference(other) + OrderedSet([4, 5, 9, 2]) + """ + cls = self.__class__ if isinstance(self, OrderedSet) else OrderedSet + diff1 = cls(self).difference(other) + diff2 = cls(other).difference(self) + return diff1.union(diff2) + + def _update_items(self, items): + """ + Replace the 'items' list of this OrderedSet with a new one, updating + self.map accordingly. + """ + self.items = items + self.map = {item: idx for (idx, item) in enumerate(items)} + + def difference_update(self, *sets): + """ + Update this OrderedSet to remove items from one or more other sets. + + Example: + >>> this = OrderedSet([1, 2, 3]) + >>> this.difference_update(OrderedSet([2, 4])) + >>> print(this) + OrderedSet([1, 3]) + + >>> this = OrderedSet([1, 2, 3, 4, 5]) + >>> this.difference_update(OrderedSet([2, 4]), OrderedSet([1, 4, 6])) + >>> print(this) + OrderedSet([3, 5]) + """ + items_to_remove = set() + for other in sets: + items_to_remove |= set(other) + self._update_items([item for item in self.items if item not in items_to_remove]) + + def intersection_update(self, other): + """ + Update this OrderedSet to keep only items in another set, preserving + their order in this set. + + Example: + >>> this = OrderedSet([1, 4, 3, 5, 7]) + >>> other = OrderedSet([9, 7, 1, 3, 2]) + >>> this.intersection_update(other) + >>> print(this) + OrderedSet([1, 3, 7]) + """ + other = set(other) + self._update_items([item for item in self.items if item in other]) + + def symmetric_difference_update(self, other): + """ + Update this OrderedSet to remove items from another set, then + add items from the other set that were not present in this set. + + Example: + >>> this = OrderedSet([1, 4, 3, 5, 7]) + >>> other = OrderedSet([9, 7, 1, 3, 2]) + >>> this.symmetric_difference_update(other) + >>> print(this) + OrderedSet([4, 5, 9, 2]) + """ + items_to_add = [item for item in other if item not in self] + items_to_remove = set(other) + self._update_items( + [item for item in self.items if item not in items_to_remove] + items_to_add + ) diff --git a/setuptools/_vendor/packaging/__about__.py b/setuptools/_vendor/packaging/__about__.py index 95d330ef..dc95138d 100644 --- a/setuptools/_vendor/packaging/__about__.py +++ b/setuptools/_vendor/packaging/__about__.py @@ -4,18 +4,24 @@ from __future__ import absolute_import, division, print_function __all__ = [ - "__title__", "__summary__", "__uri__", "__version__", "__author__", - "__email__", "__license__", "__copyright__", + "__title__", + "__summary__", + "__uri__", + "__version__", + "__author__", + "__email__", + "__license__", + "__copyright__", ] __title__ = "packaging" __summary__ = "Core utilities for Python packages" __uri__ = "https://github.com/pypa/packaging" -__version__ = "16.8" +__version__ = "19.2" __author__ = "Donald Stufft and individual contributors" __email__ = "donald@stufft.io" __license__ = "BSD or Apache License, Version 2.0" -__copyright__ = "Copyright 2014-2016 %s" % __author__ +__copyright__ = "Copyright 2014-2019 %s" % __author__ diff --git a/setuptools/_vendor/packaging/__init__.py b/setuptools/_vendor/packaging/__init__.py index 5ee62202..a0cf67df 100644 --- a/setuptools/_vendor/packaging/__init__.py +++ b/setuptools/_vendor/packaging/__init__.py @@ -4,11 +4,23 @@ from __future__ import absolute_import, division, print_function from .__about__ import ( - __author__, __copyright__, __email__, __license__, __summary__, __title__, - __uri__, __version__ + __author__, + __copyright__, + __email__, + __license__, + __summary__, + __title__, + __uri__, + __version__, ) __all__ = [ - "__title__", "__summary__", "__uri__", "__version__", "__author__", - "__email__", "__license__", "__copyright__", + "__title__", + "__summary__", + "__uri__", + "__version__", + "__author__", + "__email__", + "__license__", + "__copyright__", ] diff --git a/setuptools/_vendor/packaging/_compat.py b/setuptools/_vendor/packaging/_compat.py index 210bb80b..25da473c 100644 --- a/setuptools/_vendor/packaging/_compat.py +++ b/setuptools/_vendor/packaging/_compat.py @@ -12,9 +12,9 @@ PY3 = sys.version_info[0] == 3 # flake8: noqa if PY3: - string_types = str, + string_types = (str,) else: - string_types = basestring, + string_types = (basestring,) def with_metaclass(meta, *bases): @@ -27,4 +27,5 @@ def with_metaclass(meta, *bases): class metaclass(meta): def __new__(cls, name, this_bases, d): return meta(name, bases, d) - return type.__new__(metaclass, 'temporary_class', (), {}) + + return type.__new__(metaclass, "temporary_class", (), {}) diff --git a/setuptools/_vendor/packaging/_structures.py b/setuptools/_vendor/packaging/_structures.py index ccc27861..68dcca63 100644 --- a/setuptools/_vendor/packaging/_structures.py +++ b/setuptools/_vendor/packaging/_structures.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function class Infinity(object): - def __repr__(self): return "Infinity" @@ -33,11 +32,11 @@ class Infinity(object): def __neg__(self): return NegativeInfinity + Infinity = Infinity() class NegativeInfinity(object): - def __repr__(self): return "-Infinity" @@ -65,4 +64,5 @@ class NegativeInfinity(object): def __neg__(self): return Infinity + NegativeInfinity = NegativeInfinity() diff --git a/setuptools/_vendor/packaging/markers.py b/setuptools/_vendor/packaging/markers.py index 031332a3..4bdfdb24 100644 --- a/setuptools/_vendor/packaging/markers.py +++ b/setuptools/_vendor/packaging/markers.py @@ -17,8 +17,11 @@ from .specifiers import Specifier, InvalidSpecifier __all__ = [ - "InvalidMarker", "UndefinedComparison", "UndefinedEnvironmentName", - "Marker", "default_environment", + "InvalidMarker", + "UndefinedComparison", + "UndefinedEnvironmentName", + "Marker", + "default_environment", ] @@ -42,7 +45,6 @@ class UndefinedEnvironmentName(ValueError): class Node(object): - def __init__(self, value): self.value = value @@ -57,62 +59,52 @@ class Node(object): class Variable(Node): - def serialize(self): return str(self) class Value(Node): - def serialize(self): return '"{0}"'.format(self) class Op(Node): - def serialize(self): return str(self) VARIABLE = ( - L("implementation_version") | - L("platform_python_implementation") | - L("implementation_name") | - L("python_full_version") | - L("platform_release") | - L("platform_version") | - L("platform_machine") | - L("platform_system") | - L("python_version") | - L("sys_platform") | - L("os_name") | - L("os.name") | # PEP-345 - L("sys.platform") | # PEP-345 - L("platform.version") | # PEP-345 - L("platform.machine") | # PEP-345 - L("platform.python_implementation") | # PEP-345 - L("python_implementation") | # undocumented setuptools legacy - L("extra") + L("implementation_version") + | L("platform_python_implementation") + | L("implementation_name") + | L("python_full_version") + | L("platform_release") + | L("platform_version") + | L("platform_machine") + | L("platform_system") + | L("python_version") + | L("sys_platform") + | L("os_name") + | L("os.name") + | L("sys.platform") # PEP-345 + | L("platform.version") # PEP-345 + | L("platform.machine") # PEP-345 + | L("platform.python_implementation") # PEP-345 + | L("python_implementation") # PEP-345 + | L("extra") # undocumented setuptools legacy ) ALIASES = { - 'os.name': 'os_name', - 'sys.platform': 'sys_platform', - 'platform.version': 'platform_version', - 'platform.machine': 'platform_machine', - 'platform.python_implementation': 'platform_python_implementation', - 'python_implementation': 'platform_python_implementation' + "os.name": "os_name", + "sys.platform": "sys_platform", + "platform.version": "platform_version", + "platform.machine": "platform_machine", + "platform.python_implementation": "platform_python_implementation", + "python_implementation": "platform_python_implementation", } VARIABLE.setParseAction(lambda s, l, t: Variable(ALIASES.get(t[0], t[0]))) VERSION_CMP = ( - L("===") | - L("==") | - L(">=") | - L("<=") | - L("!=") | - L("~=") | - L(">") | - L("<") + L("===") | L("==") | L(">=") | L("<=") | L("!=") | L("~=") | L(">") | L("<") ) MARKER_OP = VERSION_CMP | L("not in") | L("in") @@ -152,8 +144,11 @@ def _format_marker(marker, first=True): # where the single item is itself it's own list. In that case we want skip # the rest of this function so that we don't get extraneous () on the # outside. - if (isinstance(marker, list) and len(marker) == 1 and - isinstance(marker[0], (list, tuple))): + if ( + isinstance(marker, list) + and len(marker) == 1 + and isinstance(marker[0], (list, tuple)) + ): return _format_marker(marker[0]) if isinstance(marker, list): @@ -239,20 +234,20 @@ def _evaluate_markers(markers, environment): def format_full_version(info): - version = '{0.major}.{0.minor}.{0.micro}'.format(info) + version = "{0.major}.{0.minor}.{0.micro}".format(info) kind = info.releaselevel - if kind != 'final': + if kind != "final": version += kind[0] + str(info.serial) return version def default_environment(): - if hasattr(sys, 'implementation'): + if hasattr(sys, "implementation"): iver = format_full_version(sys.implementation.version) implementation_name = sys.implementation.name else: - iver = '0' - implementation_name = '' + iver = "0" + implementation_name = "" return { "implementation_name": implementation_name, @@ -264,19 +259,19 @@ def default_environment(): "platform_version": platform.version(), "python_full_version": platform.python_version(), "platform_python_implementation": platform.python_implementation(), - "python_version": platform.python_version()[:3], + "python_version": ".".join(platform.python_version_tuple()[:2]), "sys_platform": sys.platform, } class Marker(object): - def __init__(self, marker): try: self._markers = _coerce_parse_result(MARKER.parseString(marker)) except ParseException as e: err_str = "Invalid marker: {0!r}, parse error at {1!r}".format( - marker, marker[e.loc:e.loc + 8]) + marker, marker[e.loc : e.loc + 8] + ) raise InvalidMarker(err_str) def __str__(self): diff --git a/setuptools/_vendor/packaging/requirements.py b/setuptools/_vendor/packaging/requirements.py index 5b493416..8a0c2cb9 100644 --- a/setuptools/_vendor/packaging/requirements.py +++ b/setuptools/_vendor/packaging/requirements.py @@ -38,8 +38,8 @@ IDENTIFIER = Combine(ALPHANUM + ZeroOrMore(IDENTIFIER_END)) NAME = IDENTIFIER("name") EXTRA = IDENTIFIER -URI = Regex(r'[^ ]+')("url") -URL = (AT + URI) +URI = Regex(r"[^ ]+")("url") +URL = AT + URI EXTRAS_LIST = EXTRA + ZeroOrMore(COMMA + EXTRA) EXTRAS = (LBRACKET + Optional(EXTRAS_LIST) + RBRACKET)("extras") @@ -48,28 +48,31 @@ VERSION_PEP440 = Regex(Specifier._regex_str, re.VERBOSE | re.IGNORECASE) VERSION_LEGACY = Regex(LegacySpecifier._regex_str, re.VERBOSE | re.IGNORECASE) VERSION_ONE = VERSION_PEP440 ^ VERSION_LEGACY -VERSION_MANY = Combine(VERSION_ONE + ZeroOrMore(COMMA + VERSION_ONE), - joinString=",", adjacent=False)("_raw_spec") +VERSION_MANY = Combine( + VERSION_ONE + ZeroOrMore(COMMA + VERSION_ONE), joinString=",", adjacent=False +)("_raw_spec") _VERSION_SPEC = Optional(((LPAREN + VERSION_MANY + RPAREN) | VERSION_MANY)) -_VERSION_SPEC.setParseAction(lambda s, l, t: t._raw_spec or '') +_VERSION_SPEC.setParseAction(lambda s, l, t: t._raw_spec or "") VERSION_SPEC = originalTextFor(_VERSION_SPEC)("specifier") VERSION_SPEC.setParseAction(lambda s, l, t: t[1]) MARKER_EXPR = originalTextFor(MARKER_EXPR())("marker") MARKER_EXPR.setParseAction( - lambda s, l, t: Marker(s[t._original_start:t._original_end]) + lambda s, l, t: Marker(s[t._original_start : t._original_end]) ) -MARKER_SEPERATOR = SEMICOLON -MARKER = MARKER_SEPERATOR + MARKER_EXPR +MARKER_SEPARATOR = SEMICOLON +MARKER = MARKER_SEPARATOR + MARKER_EXPR VERSION_AND_MARKER = VERSION_SPEC + Optional(MARKER) URL_AND_MARKER = URL + Optional(MARKER) -NAMED_REQUIREMENT = \ - NAME + Optional(EXTRAS) + (URL_AND_MARKER | VERSION_AND_MARKER) +NAMED_REQUIREMENT = NAME + Optional(EXTRAS) + (URL_AND_MARKER | VERSION_AND_MARKER) REQUIREMENT = stringStart + NAMED_REQUIREMENT + stringEnd +# setuptools.extern.pyparsing isn't thread safe during initialization, so we do it eagerly, see +# issue #104 +REQUIREMENT.parseString("x[]") class Requirement(object): @@ -90,15 +93,21 @@ class Requirement(object): req = REQUIREMENT.parseString(requirement_string) except ParseException as e: raise InvalidRequirement( - "Invalid requirement, parse error at \"{0!r}\"".format( - requirement_string[e.loc:e.loc + 8])) + 'Parse error at "{0!r}": {1}'.format( + requirement_string[e.loc : e.loc + 8], e.msg + ) + ) self.name = req.name if req.url: parsed_url = urlparse.urlparse(req.url) - if not (parsed_url.scheme and parsed_url.netloc) or ( - not parsed_url.scheme and not parsed_url.netloc): - raise InvalidRequirement("Invalid URL given") + if parsed_url.scheme == "file": + if urlparse.urlunparse(parsed_url) != req.url: + raise InvalidRequirement("Invalid URL given") + elif not (parsed_url.scheme and parsed_url.netloc) or ( + not parsed_url.scheme and not parsed_url.netloc + ): + raise InvalidRequirement("Invalid URL: {0}".format(req.url)) self.url = req.url else: self.url = None @@ -117,6 +126,8 @@ class Requirement(object): if self.url: parts.append("@ {0}".format(self.url)) + if self.marker: + parts.append(" ") if self.marker: parts.append("; {0}".format(self.marker)) diff --git a/setuptools/_vendor/packaging/specifiers.py b/setuptools/_vendor/packaging/specifiers.py index 7f5a76cf..743576a0 100644 --- a/setuptools/_vendor/packaging/specifiers.py +++ b/setuptools/_vendor/packaging/specifiers.py @@ -19,7 +19,6 @@ class InvalidSpecifier(ValueError): class BaseSpecifier(with_metaclass(abc.ABCMeta, object)): - @abc.abstractmethod def __str__(self): """ @@ -84,10 +83,7 @@ class _IndividualSpecifier(BaseSpecifier): if not match: raise InvalidSpecifier("Invalid specifier: '{0}'".format(spec)) - self._spec = ( - match.group("operator").strip(), - match.group("version").strip(), - ) + self._spec = (match.group("operator").strip(), match.group("version").strip()) # Store whether or not this Specifier should accept prereleases self._prereleases = prereleases @@ -99,11 +95,7 @@ class _IndividualSpecifier(BaseSpecifier): else "" ) - return "<{0}({1!r}{2})>".format( - self.__class__.__name__, - str(self), - pre, - ) + return "<{0}({1!r}{2})>".format(self.__class__.__name__, str(self), pre) def __str__(self): return "{0}{1}".format(*self._spec) @@ -194,11 +186,12 @@ class _IndividualSpecifier(BaseSpecifier): # If our version is a prerelease, and we were not set to allow # prereleases, then we'll store it for later incase nothing # else matches this specifier. - if (parsed_version.is_prerelease and not - (prereleases or self.prereleases)): + if parsed_version.is_prerelease and not ( + prereleases or self.prereleases + ): found_prereleases.append(version) # Either this is not a prerelease, or we should have been - # accepting prereleases from the begining. + # accepting prereleases from the beginning. else: yielded = True yield version @@ -213,8 +206,7 @@ class _IndividualSpecifier(BaseSpecifier): class LegacySpecifier(_IndividualSpecifier): - _regex_str = ( - r""" + _regex_str = r""" (?P<operator>(==|!=|<=|>=|<|>)) \s* (?P<version> @@ -225,10 +217,8 @@ class LegacySpecifier(_IndividualSpecifier): # them, and a comma since it's a version separator. ) """ - ) - _regex = re.compile( - r"^\s*" + _regex_str + r"\s*$", re.VERBOSE | re.IGNORECASE) + _regex = re.compile(r"^\s*" + _regex_str + r"\s*$", re.VERBOSE | re.IGNORECASE) _operators = { "==": "equal", @@ -269,13 +259,13 @@ def _require_version_compare(fn): if not isinstance(prospective, Version): return False return fn(self, prospective, spec) + return wrapped class Specifier(_IndividualSpecifier): - _regex_str = ( - r""" + _regex_str = r""" (?P<operator>(~=|==|!=|<=|>=|<|>|===)) (?P<version> (?: @@ -367,10 +357,8 @@ class Specifier(_IndividualSpecifier): ) ) """ - ) - _regex = re.compile( - r"^\s*" + _regex_str + r"\s*$", re.VERBOSE | re.IGNORECASE) + _regex = re.compile(r"^\s*" + _regex_str + r"\s*$", re.VERBOSE | re.IGNORECASE) _operators = { "~=": "compatible", @@ -397,8 +385,7 @@ class Specifier(_IndividualSpecifier): prefix = ".".join( list( itertools.takewhile( - lambda x: (not x.startswith("post") and not - x.startswith("dev")), + lambda x: (not x.startswith("post") and not x.startswith("dev")), _version_split(spec), ) )[:-1] @@ -407,8 +394,9 @@ class Specifier(_IndividualSpecifier): # Add the prefix notation to the end of our string prefix += ".*" - return (self._get_operator(">=")(prospective, spec) and - self._get_operator("==")(prospective, prefix)) + return self._get_operator(">=")(prospective, spec) and self._get_operator("==")( + prospective, prefix + ) @_require_version_compare def _compare_equal(self, prospective, spec): @@ -428,7 +416,7 @@ class Specifier(_IndividualSpecifier): # Shorten the prospective version to be the same length as the spec # so that we can determine if the specifier is a prefix of the # prospective version or not. - prospective = prospective[:len(spec)] + prospective = prospective[: len(spec)] # Pad out our two sides with zeros so that they both equal the same # length. @@ -503,7 +491,7 @@ class Specifier(_IndividualSpecifier): return False # Ensure that we do not allow a local version of the version mentioned - # in the specifier, which is techincally greater than, to match. + # in the specifier, which is technically greater than, to match. if prospective.local is not None: if Version(prospective.base_version) == Version(spec.base_version): return False @@ -567,27 +555,17 @@ def _pad_version(left, right): right_split.append(list(itertools.takewhile(lambda x: x.isdigit(), right))) # Get the rest of our versions - left_split.append(left[len(left_split[0]):]) - right_split.append(right[len(right_split[0]):]) + left_split.append(left[len(left_split[0]) :]) + right_split.append(right[len(right_split[0]) :]) # Insert our padding - left_split.insert( - 1, - ["0"] * max(0, len(right_split[0]) - len(left_split[0])), - ) - right_split.insert( - 1, - ["0"] * max(0, len(left_split[0]) - len(right_split[0])), - ) + left_split.insert(1, ["0"] * max(0, len(right_split[0]) - len(left_split[0]))) + right_split.insert(1, ["0"] * max(0, len(left_split[0]) - len(right_split[0]))) - return ( - list(itertools.chain(*left_split)), - list(itertools.chain(*right_split)), - ) + return (list(itertools.chain(*left_split)), list(itertools.chain(*right_split))) class SpecifierSet(BaseSpecifier): - def __init__(self, specifiers="", prereleases=None): # Split on , to break each indidivual specifier into it's own item, and # strip each item to remove leading/trailing whitespace. @@ -721,10 +699,7 @@ class SpecifierSet(BaseSpecifier): # given version is contained within all of them. # Note: This use of all() here means that an empty set of specifiers # will always return True, this is an explicit design decision. - return all( - s.contains(item, prereleases=prereleases) - for s in self._specs - ) + return all(s.contains(item, prereleases=prereleases) for s in self._specs) def filter(self, iterable, prereleases=None): # Determine if we're forcing a prerelease or not, if we're not forcing diff --git a/setuptools/_vendor/packaging/tags.py b/setuptools/_vendor/packaging/tags.py new file mode 100644 index 00000000..ec9942f0 --- /dev/null +++ b/setuptools/_vendor/packaging/tags.py @@ -0,0 +1,404 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import absolute_import + +import distutils.util + +try: + from importlib.machinery import EXTENSION_SUFFIXES +except ImportError: # pragma: no cover + import imp + + EXTENSION_SUFFIXES = [x[0] for x in imp.get_suffixes()] + del imp +import platform +import re +import sys +import sysconfig +import warnings + + +INTERPRETER_SHORT_NAMES = { + "python": "py", # Generic. + "cpython": "cp", + "pypy": "pp", + "ironpython": "ip", + "jython": "jy", +} + + +_32_BIT_INTERPRETER = sys.maxsize <= 2 ** 32 + + +class Tag(object): + + __slots__ = ["_interpreter", "_abi", "_platform"] + + def __init__(self, interpreter, abi, platform): + self._interpreter = interpreter.lower() + self._abi = abi.lower() + self._platform = platform.lower() + + @property + def interpreter(self): + return self._interpreter + + @property + def abi(self): + return self._abi + + @property + def platform(self): + return self._platform + + def __eq__(self, other): + return ( + (self.platform == other.platform) + and (self.abi == other.abi) + and (self.interpreter == other.interpreter) + ) + + def __hash__(self): + return hash((self._interpreter, self._abi, self._platform)) + + def __str__(self): + return "{}-{}-{}".format(self._interpreter, self._abi, self._platform) + + def __repr__(self): + return "<{self} @ {self_id}>".format(self=self, self_id=id(self)) + + +def parse_tag(tag): + tags = set() + interpreters, abis, platforms = tag.split("-") + for interpreter in interpreters.split("."): + for abi in abis.split("."): + for platform_ in platforms.split("."): + tags.add(Tag(interpreter, abi, platform_)) + return frozenset(tags) + + +def _normalize_string(string): + return string.replace(".", "_").replace("-", "_") + + +def _cpython_interpreter(py_version): + # TODO: Is using py_version_nodot for interpreter version critical? + return "cp{major}{minor}".format(major=py_version[0], minor=py_version[1]) + + +def _cpython_abis(py_version): + abis = [] + version = "{}{}".format(*py_version[:2]) + debug = pymalloc = ucs4 = "" + with_debug = sysconfig.get_config_var("Py_DEBUG") + has_refcount = hasattr(sys, "gettotalrefcount") + # Windows doesn't set Py_DEBUG, so checking for support of debug-compiled + # extension modules is the best option. + # https://github.com/pypa/pip/issues/3383#issuecomment-173267692 + has_ext = "_d.pyd" in EXTENSION_SUFFIXES + if with_debug or (with_debug is None and (has_refcount or has_ext)): + debug = "d" + if py_version < (3, 8): + with_pymalloc = sysconfig.get_config_var("WITH_PYMALLOC") + if with_pymalloc or with_pymalloc is None: + pymalloc = "m" + if py_version < (3, 3): + unicode_size = sysconfig.get_config_var("Py_UNICODE_SIZE") + if unicode_size == 4 or ( + unicode_size is None and sys.maxunicode == 0x10FFFF + ): + ucs4 = "u" + elif debug: + # Debug builds can also load "normal" extension modules. + # We can also assume no UCS-4 or pymalloc requirement. + abis.append("cp{version}".format(version=version)) + abis.insert( + 0, + "cp{version}{debug}{pymalloc}{ucs4}".format( + version=version, debug=debug, pymalloc=pymalloc, ucs4=ucs4 + ), + ) + return abis + + +def _cpython_tags(py_version, interpreter, abis, platforms): + for abi in abis: + for platform_ in platforms: + yield Tag(interpreter, abi, platform_) + for tag in (Tag(interpreter, "abi3", platform_) for platform_ in platforms): + yield tag + for tag in (Tag(interpreter, "none", platform_) for platform_ in platforms): + yield tag + # PEP 384 was first implemented in Python 3.2. + for minor_version in range(py_version[1] - 1, 1, -1): + for platform_ in platforms: + interpreter = "cp{major}{minor}".format( + major=py_version[0], minor=minor_version + ) + yield Tag(interpreter, "abi3", platform_) + + +def _pypy_interpreter(): + return "pp{py_major}{pypy_major}{pypy_minor}".format( + py_major=sys.version_info[0], + pypy_major=sys.pypy_version_info.major, + pypy_minor=sys.pypy_version_info.minor, + ) + + +def _generic_abi(): + abi = sysconfig.get_config_var("SOABI") + if abi: + return _normalize_string(abi) + else: + return "none" + + +def _pypy_tags(py_version, interpreter, abi, platforms): + for tag in (Tag(interpreter, abi, platform) for platform in platforms): + yield tag + for tag in (Tag(interpreter, "none", platform) for platform in platforms): + yield tag + + +def _generic_tags(interpreter, py_version, abi, platforms): + for tag in (Tag(interpreter, abi, platform) for platform in platforms): + yield tag + if abi != "none": + tags = (Tag(interpreter, "none", platform_) for platform_ in platforms) + for tag in tags: + yield tag + + +def _py_interpreter_range(py_version): + """ + Yield Python versions in descending order. + + After the latest version, the major-only version will be yielded, and then + all following versions up to 'end'. + """ + yield "py{major}{minor}".format(major=py_version[0], minor=py_version[1]) + yield "py{major}".format(major=py_version[0]) + for minor in range(py_version[1] - 1, -1, -1): + yield "py{major}{minor}".format(major=py_version[0], minor=minor) + + +def _independent_tags(interpreter, py_version, platforms): + """ + Return the sequence of tags that are consistent across implementations. + + The tags consist of: + - py*-none-<platform> + - <interpreter>-none-any + - py*-none-any + """ + for version in _py_interpreter_range(py_version): + for platform_ in platforms: + yield Tag(version, "none", platform_) + yield Tag(interpreter, "none", "any") + for version in _py_interpreter_range(py_version): + yield Tag(version, "none", "any") + + +def _mac_arch(arch, is_32bit=_32_BIT_INTERPRETER): + if not is_32bit: + return arch + + if arch.startswith("ppc"): + return "ppc" + + return "i386" + + +def _mac_binary_formats(version, cpu_arch): + formats = [cpu_arch] + if cpu_arch == "x86_64": + if version < (10, 4): + return [] + formats.extend(["intel", "fat64", "fat32"]) + + elif cpu_arch == "i386": + if version < (10, 4): + return [] + formats.extend(["intel", "fat32", "fat"]) + + elif cpu_arch == "ppc64": + # TODO: Need to care about 32-bit PPC for ppc64 through 10.2? + if version > (10, 5) or version < (10, 4): + return [] + formats.append("fat64") + + elif cpu_arch == "ppc": + if version > (10, 6): + return [] + formats.extend(["fat32", "fat"]) + + formats.append("universal") + return formats + + +def _mac_platforms(version=None, arch=None): + version_str, _, cpu_arch = platform.mac_ver() + if version is None: + version = tuple(map(int, version_str.split(".")[:2])) + if arch is None: + arch = _mac_arch(cpu_arch) + platforms = [] + for minor_version in range(version[1], -1, -1): + compat_version = version[0], minor_version + binary_formats = _mac_binary_formats(compat_version, arch) + for binary_format in binary_formats: + platforms.append( + "macosx_{major}_{minor}_{binary_format}".format( + major=compat_version[0], + minor=compat_version[1], + binary_format=binary_format, + ) + ) + return platforms + + +# From PEP 513. +def _is_manylinux_compatible(name, glibc_version): + # Check for presence of _manylinux module. + try: + import _manylinux + + return bool(getattr(_manylinux, name + "_compatible")) + except (ImportError, AttributeError): + # Fall through to heuristic check below. + pass + + return _have_compatible_glibc(*glibc_version) + + +def _glibc_version_string(): + # Returns glibc version string, or None if not using glibc. + import ctypes + + # ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen + # manpage says, "If filename is NULL, then the returned handle is for the + # main program". This way we can let the linker do the work to figure out + # which libc our process is actually using. + process_namespace = ctypes.CDLL(None) + try: + gnu_get_libc_version = process_namespace.gnu_get_libc_version + except AttributeError: + # Symbol doesn't exist -> therefore, we are not linked to + # glibc. + return None + + # Call gnu_get_libc_version, which returns a string like "2.5" + gnu_get_libc_version.restype = ctypes.c_char_p + version_str = gnu_get_libc_version() + # py2 / py3 compatibility: + if not isinstance(version_str, str): + version_str = version_str.decode("ascii") + + return version_str + + +# Separated out from have_compatible_glibc for easier unit testing. +def _check_glibc_version(version_str, required_major, minimum_minor): + # Parse string and check against requested version. + # + # We use a regexp instead of str.split because we want to discard any + # random junk that might come after the minor version -- this might happen + # in patched/forked versions of glibc (e.g. Linaro's version of glibc + # uses version strings like "2.20-2014.11"). See gh-3588. + m = re.match(r"(?P<major>[0-9]+)\.(?P<minor>[0-9]+)", version_str) + if not m: + warnings.warn( + "Expected glibc version with 2 components major.minor," + " got: %s" % version_str, + RuntimeWarning, + ) + return False + return ( + int(m.group("major")) == required_major + and int(m.group("minor")) >= minimum_minor + ) + + +def _have_compatible_glibc(required_major, minimum_minor): + version_str = _glibc_version_string() + if version_str is None: + return False + return _check_glibc_version(version_str, required_major, minimum_minor) + + +def _linux_platforms(is_32bit=_32_BIT_INTERPRETER): + linux = _normalize_string(distutils.util.get_platform()) + if linux == "linux_x86_64" and is_32bit: + linux = "linux_i686" + manylinux_support = ( + ("manylinux2014", (2, 17)), # CentOS 7 w/ glibc 2.17 (PEP 599) + ("manylinux2010", (2, 12)), # CentOS 6 w/ glibc 2.12 (PEP 571) + ("manylinux1", (2, 5)), # CentOS 5 w/ glibc 2.5 (PEP 513) + ) + manylinux_support_iter = iter(manylinux_support) + for name, glibc_version in manylinux_support_iter: + if _is_manylinux_compatible(name, glibc_version): + platforms = [linux.replace("linux", name)] + break + else: + platforms = [] + # Support for a later manylinux implies support for an earlier version. + platforms += [linux.replace("linux", name) for name, _ in manylinux_support_iter] + platforms.append(linux) + return platforms + + +def _generic_platforms(): + platform = _normalize_string(distutils.util.get_platform()) + return [platform] + + +def _interpreter_name(): + name = platform.python_implementation().lower() + return INTERPRETER_SHORT_NAMES.get(name) or name + + +def _generic_interpreter(name, py_version): + version = sysconfig.get_config_var("py_version_nodot") + if not version: + version = "".join(map(str, py_version[:2])) + return "{name}{version}".format(name=name, version=version) + + +def sys_tags(): + """ + Returns the sequence of tag triples for the running interpreter. + + The order of the sequence corresponds to priority order for the + interpreter, from most to least important. + """ + py_version = sys.version_info[:2] + interpreter_name = _interpreter_name() + if platform.system() == "Darwin": + platforms = _mac_platforms() + elif platform.system() == "Linux": + platforms = _linux_platforms() + else: + platforms = _generic_platforms() + + if interpreter_name == "cp": + interpreter = _cpython_interpreter(py_version) + abis = _cpython_abis(py_version) + for tag in _cpython_tags(py_version, interpreter, abis, platforms): + yield tag + elif interpreter_name == "pp": + interpreter = _pypy_interpreter() + abi = _generic_abi() + for tag in _pypy_tags(py_version, interpreter, abi, platforms): + yield tag + else: + interpreter = _generic_interpreter(interpreter_name, py_version) + abi = _generic_abi() + for tag in _generic_tags(interpreter, py_version, abi, platforms): + yield tag + for tag in _independent_tags(interpreter, py_version, platforms): + yield tag diff --git a/setuptools/_vendor/packaging/utils.py b/setuptools/_vendor/packaging/utils.py index 942387ce..88418786 100644 --- a/setuptools/_vendor/packaging/utils.py +++ b/setuptools/_vendor/packaging/utils.py @@ -5,6 +5,8 @@ from __future__ import absolute_import, division, print_function import re +from .version import InvalidVersion, Version + _canonicalize_regex = re.compile(r"[-_.]+") @@ -12,3 +14,44 @@ _canonicalize_regex = re.compile(r"[-_.]+") def canonicalize_name(name): # This is taken from PEP 503. return _canonicalize_regex.sub("-", name).lower() + + +def canonicalize_version(version): + """ + This is very similar to Version.__str__, but has one subtle differences + with the way it handles the release segment. + """ + + try: + version = Version(version) + except InvalidVersion: + # Legacy versions cannot be normalized + return version + + parts = [] + + # Epoch + if version.epoch != 0: + parts.append("{0}!".format(version.epoch)) + + # Release segment + # NB: This strips trailing '.0's to normalize + parts.append(re.sub(r"(\.0)+$", "", ".".join(str(x) for x in version.release))) + + # Pre-release + if version.pre is not None: + parts.append("".join(str(x) for x in version.pre)) + + # Post-release + if version.post is not None: + parts.append(".post{0}".format(version.post)) + + # Development release + if version.dev is not None: + parts.append(".dev{0}".format(version.dev)) + + # Local version segment + if version.local is not None: + parts.append("+{0}".format(version.local)) + + return "".join(parts) diff --git a/setuptools/_vendor/packaging/version.py b/setuptools/_vendor/packaging/version.py index 83b5ee8c..95157a1f 100644 --- a/setuptools/_vendor/packaging/version.py +++ b/setuptools/_vendor/packaging/version.py @@ -10,14 +10,11 @@ import re from ._structures import Infinity -__all__ = [ - "parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN" -] +__all__ = ["parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN"] _Version = collections.namedtuple( - "_Version", - ["epoch", "release", "dev", "pre", "post", "local"], + "_Version", ["epoch", "release", "dev", "pre", "post", "local"] ) @@ -40,7 +37,6 @@ class InvalidVersion(ValueError): class _BaseVersion(object): - def __hash__(self): return hash(self._key) @@ -70,7 +66,6 @@ class _BaseVersion(object): class LegacyVersion(_BaseVersion): - def __init__(self, version): self._version = str(version) self._key = _legacy_cmpkey(self._version) @@ -90,6 +85,26 @@ class LegacyVersion(_BaseVersion): return self._version @property + def epoch(self): + return -1 + + @property + def release(self): + return None + + @property + def pre(self): + return None + + @property + def post(self): + return None + + @property + def dev(self): + return None + + @property def local(self): return None @@ -101,13 +116,19 @@ class LegacyVersion(_BaseVersion): def is_postrelease(self): return False + @property + def is_devrelease(self): + return False -_legacy_version_component_re = re.compile( - r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE, -) + +_legacy_version_component_re = re.compile(r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE) _legacy_version_replacement_map = { - "pre": "c", "preview": "c", "-": "final-", "rc": "c", "dev": "@", + "pre": "c", + "preview": "c", + "-": "final-", + "rc": "c", + "dev": "@", } @@ -154,6 +175,7 @@ def _legacy_cmpkey(version): return epoch, parts + # Deliberately not anchored to the start and end of the string, to make it # easier for 3rd party code to reuse VERSION_PATTERN = r""" @@ -190,10 +212,7 @@ VERSION_PATTERN = r""" class Version(_BaseVersion): - _regex = re.compile( - r"^\s*" + VERSION_PATTERN + r"\s*$", - re.VERBOSE | re.IGNORECASE, - ) + _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE) def __init__(self, version): # Validate the version and parse it into pieces @@ -205,18 +224,11 @@ class Version(_BaseVersion): self._version = _Version( epoch=int(match.group("epoch")) if match.group("epoch") else 0, release=tuple(int(i) for i in match.group("release").split(".")), - pre=_parse_letter_version( - match.group("pre_l"), - match.group("pre_n"), - ), + pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")), post=_parse_letter_version( - match.group("post_l"), - match.group("post_n1") or match.group("post_n2"), - ), - dev=_parse_letter_version( - match.group("dev_l"), - match.group("dev_n"), + match.group("post_l"), match.group("post_n1") or match.group("post_n2") ), + dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")), local=_parse_local_version(match.group("local")), ) @@ -237,33 +249,58 @@ class Version(_BaseVersion): parts = [] # Epoch - if self._version.epoch != 0: - parts.append("{0}!".format(self._version.epoch)) + if self.epoch != 0: + parts.append("{0}!".format(self.epoch)) # Release segment - parts.append(".".join(str(x) for x in self._version.release)) + parts.append(".".join(str(x) for x in self.release)) # Pre-release - if self._version.pre is not None: - parts.append("".join(str(x) for x in self._version.pre)) + if self.pre is not None: + parts.append("".join(str(x) for x in self.pre)) # Post-release - if self._version.post is not None: - parts.append(".post{0}".format(self._version.post[1])) + if self.post is not None: + parts.append(".post{0}".format(self.post)) # Development release - if self._version.dev is not None: - parts.append(".dev{0}".format(self._version.dev[1])) + if self.dev is not None: + parts.append(".dev{0}".format(self.dev)) # Local version segment - if self._version.local is not None: - parts.append( - "+{0}".format(".".join(str(x) for x in self._version.local)) - ) + if self.local is not None: + parts.append("+{0}".format(self.local)) return "".join(parts) @property + def epoch(self): + return self._version.epoch + + @property + def release(self): + return self._version.release + + @property + def pre(self): + return self._version.pre + + @property + def post(self): + return self._version.post[1] if self._version.post else None + + @property + def dev(self): + return self._version.dev[1] if self._version.dev else None + + @property + def local(self): + if self._version.local: + return ".".join(str(x) for x in self._version.local) + else: + return None + + @property def public(self): return str(self).split("+", 1)[0] @@ -272,27 +309,25 @@ class Version(_BaseVersion): parts = [] # Epoch - if self._version.epoch != 0: - parts.append("{0}!".format(self._version.epoch)) + if self.epoch != 0: + parts.append("{0}!".format(self.epoch)) # Release segment - parts.append(".".join(str(x) for x in self._version.release)) + parts.append(".".join(str(x) for x in self.release)) return "".join(parts) @property - def local(self): - version_string = str(self) - if "+" in version_string: - return version_string.split("+", 1)[1] - - @property def is_prerelease(self): - return bool(self._version.dev or self._version.pre) + return self.dev is not None or self.pre is not None @property def is_postrelease(self): - return bool(self._version.post) + return self.post is not None + + @property + def is_devrelease(self): + return self.dev is not None def _parse_letter_version(letter, number): @@ -326,7 +361,7 @@ def _parse_letter_version(letter, number): return letter, int(number) -_local_version_seperators = re.compile(r"[\._-]") +_local_version_separators = re.compile(r"[\._-]") def _parse_local_version(local): @@ -336,7 +371,7 @@ def _parse_local_version(local): if local is not None: return tuple( part.lower() if not part.isdigit() else int(part) - for part in _local_version_seperators.split(local) + for part in _local_version_separators.split(local) ) @@ -347,12 +382,7 @@ def _cmpkey(epoch, release, pre, post, dev, local): # re-reverse it back into the correct order and make it a tuple and use # that for our sorting key. release = tuple( - reversed(list( - itertools.dropwhile( - lambda x: x == 0, - reversed(release), - ) - )) + reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release)))) ) # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0. @@ -385,9 +415,6 @@ def _cmpkey(epoch, release, pre, post, dev, local): # - Numeric segments sort numerically # - Shorter versions sort before longer versions when the prefixes # match exactly - local = tuple( - (i, "") if isinstance(i, int) else (-Infinity, i) - for i in local - ) + local = tuple((i, "") if isinstance(i, int) else (-Infinity, i) for i in local) return epoch, release, pre, post, dev, local diff --git a/setuptools/_vendor/vendored.txt b/setuptools/_vendor/vendored.txt index 7d77863d..65183d9a 100644 --- a/setuptools/_vendor/vendored.txt +++ b/setuptools/_vendor/vendored.txt @@ -1,3 +1,4 @@ -packaging==16.8 +packaging==19.2 pyparsing==2.2.1 six==1.10.0 +ordered-set==3.1.1 diff --git a/setuptools/archive_util.py b/setuptools/archive_util.py index 81436044..64528ca7 100644 --- a/setuptools/archive_util.py +++ b/setuptools/archive_util.py @@ -25,7 +25,8 @@ def default_filter(src, dst): return dst -def unpack_archive(filename, extract_dir, progress_filter=default_filter, +def unpack_archive( + filename, extract_dir, progress_filter=default_filter, drivers=None): """Unpack `filename` to `extract_dir`, or raise ``UnrecognizedFormat`` @@ -148,7 +149,8 @@ def unpack_tarfile(filename, extract_dir, progress_filter=default_filter): # resolve any links and to extract the link targets as normal # files - while member is not None and (member.islnk() or member.issym()): + while member is not None and ( + member.islnk() or member.issym()): linkpath = member.linkname if member.issym(): base = posixpath.dirname(member.name) diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index 10c4b528..a1c951cf 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -48,6 +48,7 @@ __all__ = ['get_requires_for_build_sdist', '__legacy__', 'SetupRequirementsError'] + class SetupRequirementsError(BaseException): def __init__(self, specifiers): self.specifiers = specifiers @@ -143,7 +144,8 @@ class _BuildMetaBackend(object): def get_requires_for_build_wheel(self, config_settings=None): config_settings = self._fix_config(config_settings) - return self._get_build_requires(config_settings, requirements=['wheel']) + return self._get_build_requires( + config_settings, requirements=['wheel']) def get_requires_for_build_sdist(self, config_settings=None): config_settings = self._fix_config(config_settings) @@ -160,8 +162,10 @@ class _BuildMetaBackend(object): dist_infos = [f for f in os.listdir(dist_info_directory) if f.endswith('.dist-info')] - if (len(dist_infos) == 0 and - len(_get_immediate_subdirectories(dist_info_directory)) == 1): + if ( + len(dist_infos) == 0 and + len(_get_immediate_subdirectories(dist_info_directory)) == 1 + ): dist_info_directory = os.path.join( dist_info_directory, os.listdir(dist_info_directory)[0]) @@ -193,7 +197,8 @@ class _BuildMetaBackend(object): config_settings["--global-option"]) self.run_setup() - result_basename = _file_with_extension(tmp_dist_dir, result_extension) + result_basename = _file_with_extension( + tmp_dist_dir, result_extension) result_path = os.path.join(result_directory, result_basename) if os.path.exists(result_path): # os.rename will fail overwriting on non-Unix. @@ -202,7 +207,6 @@ class _BuildMetaBackend(object): return result_basename - def build_wheel(self, wheel_directory, config_settings=None, metadata_directory=None): return self._build_with_temp_dir(['bdist_wheel'], '.whl', @@ -217,9 +221,12 @@ class _BuildMetaBackend(object): class _BuildMetaLegacyBackend(_BuildMetaBackend): """Compatibility backend for setuptools - This is a version of setuptools.build_meta that endeavors to maintain backwards - compatibility with pre-PEP 517 modes of invocation. It exists as a temporary - bridge between the old packaging mechanism and the new packaging mechanism, + This is a version of setuptools.build_meta that endeavors + to maintain backwards + compatibility with pre-PEP 517 modes of invocation. It + exists as a temporary + bridge between the old packaging mechanism and the new + packaging mechanism, and will eventually be removed. """ def run_setup(self, setup_script='setup.py'): @@ -232,6 +239,12 @@ class _BuildMetaLegacyBackend(_BuildMetaBackend): if script_dir not in sys.path: sys.path.insert(0, script_dir) + # Some setup.py scripts (e.g. in pygame and numpy) use sys.argv[0] to + # get the directory of the source code. They expect it to refer to the + # setup.py script. + sys_argv_0 = sys.argv[0] + sys.argv[0] = setup_script + try: super(_BuildMetaLegacyBackend, self).run_setup(setup_script=setup_script) @@ -242,6 +255,8 @@ class _BuildMetaLegacyBackend(_BuildMetaBackend): # the original path so that the path manipulation does not persist # within the hook after run_setup is called. sys.path[:] = sys_path + sys.argv[0] = sys_argv_0 + # The primary backend _BACKEND = _BuildMetaBackend() diff --git a/setuptools/command/__init__.py b/setuptools/command/__init__.py index fe619e2e..743f5588 100644 --- a/setuptools/command/__init__.py +++ b/setuptools/command/__init__.py @@ -2,8 +2,7 @@ __all__ = [ 'alias', 'bdist_egg', 'bdist_rpm', 'build_ext', 'build_py', 'develop', 'easy_install', 'egg_info', 'install', 'install_lib', 'rotate', 'saveopts', 'sdist', 'setopt', 'test', 'install_egg_info', 'install_scripts', - 'register', 'bdist_wininst', 'upload_docs', 'upload', 'build_clib', - 'dist_info', + 'bdist_wininst', 'upload_docs', 'build_clib', 'dist_info', ] from distutils.command.bdist import bdist diff --git a/setuptools/command/bdist_egg.py b/setuptools/command/bdist_egg.py index ae44eaa2..1b28d4c9 100644 --- a/setuptools/command/bdist_egg.py +++ b/setuptools/command/bdist_egg.py @@ -291,7 +291,7 @@ class bdist_egg(Command): "or refer to a module" % (ep,) ) - pyver = sys.version[:3] + pyver = '{}.{}'.format(*sys.version_info) pkg = ep.module_name full = '.'.join(ep.attrs) base = ep.attrs[0] diff --git a/setuptools/command/build_clib.py b/setuptools/command/build_clib.py index 09caff6f..67ce2444 100644 --- a/setuptools/command/build_clib.py +++ b/setuptools/command/build_clib.py @@ -25,9 +25,9 @@ class build_clib(orig.build_clib): sources = build_info.get('sources') if sources is None or not isinstance(sources, (list, tuple)): raise DistutilsSetupError( - "in 'libraries' option (library '%s'), " - "'sources' must be present and must be " - "a list of source filenames" % lib_name) + "in 'libraries' option (library '%s'), " + "'sources' must be present and must be " + "a list of source filenames" % lib_name) sources = list(sources) log.info("building '%s' library", lib_name) @@ -38,9 +38,9 @@ class build_clib(orig.build_clib): obj_deps = build_info.get('obj_deps', dict()) if not isinstance(obj_deps, dict): raise DistutilsSetupError( - "in 'libraries' option (library '%s'), " - "'obj_deps' must be a dictionary of " - "type 'source: list'" % lib_name) + "in 'libraries' option (library '%s'), " + "'obj_deps' must be a dictionary of " + "type 'source: list'" % lib_name) dependencies = [] # Get the global dependencies that are specified by the '' key. @@ -48,9 +48,9 @@ class build_clib(orig.build_clib): global_deps = obj_deps.get('', list()) if not isinstance(global_deps, (list, tuple)): raise DistutilsSetupError( - "in 'libraries' option (library '%s'), " - "'obj_deps' must be a dictionary of " - "type 'source: list'" % lib_name) + "in 'libraries' option (library '%s'), " + "'obj_deps' must be a dictionary of " + "type 'source: list'" % lib_name) # Build the list to be used by newer_pairwise_group # each source will be auto-added to its dependencies. @@ -60,39 +60,42 @@ class build_clib(orig.build_clib): extra_deps = obj_deps.get(source, list()) if not isinstance(extra_deps, (list, tuple)): raise DistutilsSetupError( - "in 'libraries' option (library '%s'), " - "'obj_deps' must be a dictionary of " - "type 'source: list'" % lib_name) + "in 'libraries' option (library '%s'), " + "'obj_deps' must be a dictionary of " + "type 'source: list'" % lib_name) src_deps.extend(extra_deps) dependencies.append(src_deps) expected_objects = self.compiler.object_filenames( - sources, - output_dir=self.build_temp - ) + sources, + output_dir=self.build_temp, + ) - if newer_pairwise_group(dependencies, expected_objects) != ([], []): + if ( + newer_pairwise_group(dependencies, expected_objects) + != ([], []) + ): # First, compile the source code to object files in the library # directory. (This should probably change to putting object # files in a temporary build directory.) macros = build_info.get('macros') include_dirs = build_info.get('include_dirs') cflags = build_info.get('cflags') - objects = self.compiler.compile( - sources, - output_dir=self.build_temp, - macros=macros, - include_dirs=include_dirs, - extra_postargs=cflags, - debug=self.debug - ) + self.compiler.compile( + sources, + output_dir=self.build_temp, + macros=macros, + include_dirs=include_dirs, + extra_postargs=cflags, + debug=self.debug + ) # Now "link" the object files together into a static library. # (On Unix at least, this isn't really linking -- it just # builds an archive. Whatever.) self.compiler.create_static_lib( - expected_objects, - lib_name, - output_dir=self.build_clib, - debug=self.debug - ) + expected_objects, + lib_name, + output_dir=self.build_clib, + debug=self.debug + ) diff --git a/setuptools/command/build_ext.py b/setuptools/command/build_ext.py index 60a8a32f..03b6f346 100644 --- a/setuptools/command/build_ext.py +++ b/setuptools/command/build_ext.py @@ -1,7 +1,6 @@ import os import sys import itertools -import imp from distutils.command.build_ext import build_ext as _du_build_ext from distutils.file_util import copy_file from distutils.ccompiler import new_compiler @@ -12,6 +11,14 @@ from distutils import log from setuptools.extension import Library from setuptools.extern import six +if six.PY2: + import imp + + EXTENSION_SUFFIXES = [ + s for s, _, tp in imp.get_suffixes() if tp == imp.C_EXTENSION] +else: + from importlib.machinery import EXTENSION_SUFFIXES + try: # Attempt to use Cython for building extensions, if available from Cython.Distutils.build_ext import build_ext as _build_ext @@ -23,7 +30,7 @@ except ImportError: # make sure _config_vars is initialized get_config_var("LDSHARED") -from distutils.sysconfig import _config_vars as _CONFIG_VARS +from distutils.sysconfig import _config_vars as _CONFIG_VARS # noqa def _customize_compiler_for_shlib(compiler): @@ -59,12 +66,14 @@ elif os.name != 'nt': except ImportError: pass -if_dl = lambda s: s if have_rtld else '' + +def if_dl(s): + return s if have_rtld else '' def get_abi3_suffix(): """Return the file extension for an abi3-compliant Extension()""" - for suffix, _, _ in (s for s in imp.get_suffixes() if s[2] == imp.C_EXTENSION): + for suffix in EXTENSION_SUFFIXES: if '.abi3' in suffix: # Unix return suffix elif suffix == '.pyd': # Windows @@ -107,7 +116,7 @@ class build_ext(_build_ext): if fullname in self.ext_map: ext = self.ext_map[fullname] use_abi3 = ( - six.PY3 + not six.PY2 and getattr(ext, 'py_limited_api') and get_abi3_suffix() ) diff --git a/setuptools/command/develop.py b/setuptools/command/develop.py index 009e4f93..b5619246 100644 --- a/setuptools/command/develop.py +++ b/setuptools/command/develop.py @@ -108,7 +108,7 @@ class develop(namespaces.DevelopInstaller, easy_install): return path_to_setup def install_for_development(self): - if six.PY3 and getattr(self.distribution, 'use_2to3', False): + if not six.PY2 and getattr(self.distribution, 'use_2to3', False): # If we run 2to3 we can not do this inplace: # Ensure metadata is up-to-date diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index 06c98271..abca1ae1 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -121,7 +121,8 @@ else: return False -_one_liner = lambda text: textwrap.dedent(text).strip().replace('\n', '; ') +def _one_liner(text): + return textwrap.dedent(text).strip().replace('\n', '; ') class easy_install(Command): @@ -156,19 +157,16 @@ class easy_install(Command): "allow building eggs from local checkouts"), ('version', None, "print version information and exit"), ('no-find-links', None, - "Don't load find-links defined in packages being installed") + "Don't load find-links defined in packages being installed"), + ('user', None, "install in user site-package '%s'" % site.USER_SITE) ] boolean_options = [ 'zip-ok', 'multi-version', 'exclude-scripts', 'upgrade', 'always-copy', 'editable', - 'no-deps', 'local-snapshots-ok', 'version' + 'no-deps', 'local-snapshots-ok', 'version', + 'user' ] - if site.ENABLE_USER_SITE: - help_msg = "install in user site-package '%s'" % site.USER_SITE - user_options.append(('user', None, help_msg)) - boolean_options.append('user') - negative_opt = {'always-unzip': 'zip-ok'} create_index = PackageIndex @@ -241,7 +239,7 @@ class easy_install(Command): """ Render the Setuptools version and installation details, then exit. """ - ver = sys.version[:3] + ver = '{}.{}'.format(*sys.version_info) dist = get_distribution('setuptools') tmpl = 'setuptools {dist.version} from {dist.location} (Python {ver})' print(tmpl.format(**locals())) @@ -272,6 +270,9 @@ class easy_install(Command): self.config_vars['userbase'] = self.install_userbase self.config_vars['usersite'] = self.install_usersite + elif self.user: + log.warn("WARNING: The user site-packages directory is disabled.") + self._fix_install_dir_for_user_site() self.expand_basedirs() @@ -410,7 +411,13 @@ class easy_install(Command): ] self._expand_attrs(dirs) - def run(self): + def run(self, show_deprecation=True): + if show_deprecation: + self.announce( + "WARNING: The easy_install command is deprecated " + "and will be removed in a future version.", + log.WARN, + ) if self.verbose != self.distribution.verbose: log.set_verbosity(self.verbose) try: @@ -472,8 +479,9 @@ class easy_install(Command): self.cant_write_to_target() if not is_site_dir and not self.multi_version: - # Can't install non-multi to non-site dir - raise DistutilsError(self.no_default_version_msg()) + # Can't install non-multi to non-site dir with easy_install + pythonpath = os.environ.get('PYTHONPATH', '') + log.warn(self.__no_default_msg, self.install_dir, pythonpath) if is_site_dir: if self.pth_file is None: @@ -501,13 +509,13 @@ class easy_install(Command): the distutils default setting) was: %s - """).lstrip() + """).lstrip() # noqa __not_exists_id = textwrap.dedent(""" This directory does not currently exist. Please create it and try again, or choose a different installation directory (using the -d or --install-dir option). - """).lstrip() + """).lstrip() # noqa __access_msg = textwrap.dedent(""" Perhaps your account does not have write access to this directory? If the @@ -523,7 +531,7 @@ class easy_install(Command): https://setuptools.readthedocs.io/en/latest/easy_install.html Please make the appropriate changes for your system and try again. - """).lstrip() + """).lstrip() # noqa def cant_write_to_target(self): msg = self.__cant_write_msg % (sys.exc_info()[1], self.install_dir,) @@ -1087,13 +1095,13 @@ class easy_install(Command): pkg_resources.require("%(name)s") # latest installed version pkg_resources.require("%(name)s==%(version)s") # this exact version pkg_resources.require("%(name)s>=%(version)s") # this version or higher - """).lstrip() + """).lstrip() # noqa __id_warning = textwrap.dedent(""" Note also that the installation directory must be on sys.path at runtime for this to work. (e.g. by being the application's script directory, by being on PYTHONPATH, or by being added to sys.path by your code.) - """) + """) # noqa def installation_report(self, req, dist, what="Installed"): """Helpful installation message for display to package users""" @@ -1118,7 +1126,7 @@ class easy_install(Command): %(python)s setup.py develop See the setuptools documentation for the "develop" command for more info. - """).lstrip() + """).lstrip() # noqa def report_editable(self, spec, setup_script): dirname = os.path.dirname(setup_script) @@ -1180,8 +1188,7 @@ class easy_install(Command): # to the setup.cfg file. ei_opts = self.distribution.get_option_dict('easy_install').copy() fetch_directives = ( - 'find_links', 'site_dirs', 'index_url', 'optimize', - 'site_dirs', 'allow_hosts', + 'find_links', 'site_dirs', 'index_url', 'optimize', 'allow_hosts', ) fetch_options = {} for key, val in ei_opts.items(): @@ -1302,11 +1309,8 @@ class easy_install(Command): https://setuptools.readthedocs.io/en/latest/easy_install.html#custom-installation-locations - Please make the appropriate changes for your system and try again.""").lstrip() - - def no_default_version_msg(self): - template = self.__no_default_msg - return template % (self.install_dir, os.environ.get('PYTHONPATH', '')) + Please make the appropriate changes for your system and try again. + """).strip() def install_site_py(self): """Make sure there's a site.py in the target dir, if needed""" @@ -1412,7 +1416,7 @@ def get_site_dirs(): os.path.join( prefix, "lib", - "python" + sys.version[:3], + "python{}.{}".format(*sys.version_info), "site-packages", ), os.path.join(prefix, "lib", "site-python"), @@ -1433,7 +1437,7 @@ def get_site_dirs(): home, 'Library', 'Python', - sys.version[:3], + '{}.{}'.format(*sys.version_info), 'site-packages', ) sitedirs.append(home_sp) @@ -1562,7 +1566,7 @@ def get_exe_prefixes(exe_filename): continue if parts[0].upper() in ('PURELIB', 'PLATLIB'): contents = z.read(name) - if six.PY3: + if not six.PY2: contents = contents.decode() for pth in yield_lines(contents): pth = pth.strip().replace('\\', '/') @@ -2088,7 +2092,8 @@ class ScriptWriter: @classmethod def get_script_header(cls, script_text, executable=None, wininst=False): # for backward compatibility - warnings.warn("Use get_header", EasyInstallDeprecationWarning, stacklevel=2) + warnings.warn( + "Use get_header", EasyInstallDeprecationWarning, stacklevel=2) if wininst: executable = "python.exe" return cls.get_header(script_text, executable) @@ -2337,6 +2342,8 @@ def _patch_usage(): finally: distutils.core.gen_usage = saved + class EasyInstallDeprecationWarning(SetuptoolsDeprecationWarning): - """Class for warning about deprecations in EasyInstall in SetupTools. Not ignored by default, unlike DeprecationWarning.""" - + """ + Warning for EasyInstall deprecations, bypassing suppression. + """ diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index 5d8f451e..7fa89541 100644 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -33,6 +33,7 @@ from setuptools.glob import glob from setuptools.extern import packaging from setuptools import SetuptoolsDeprecationWarning + def translate_pattern(glob): """ Translate a file path glob like '*.txt' in to a regular expression. @@ -113,7 +114,7 @@ def translate_pattern(glob): pat += sep pat += r'\Z' - return re.compile(pat, flags=re.MULTILINE|re.DOTALL) + return re.compile(pat, flags=re.MULTILINE | re.DOTALL) class InfoCommon: @@ -266,7 +267,7 @@ class egg_info(InfoCommon, Command): to the file. """ log.info("writing %s to %s", what, filename) - if six.PY3: + if not six.PY2: data = data.encode("utf-8") if not self.dry_run: f = open(filename, 'wb') @@ -637,7 +638,9 @@ def warn_depends_obsolete(cmd, basename, filename): def _write_requirements(stream, reqs): lines = yield_lines(reqs or ()) - append_cr = lambda line: line + '\n' + + def append_cr(line): + return line + '\n' lines = map(append_cr, lines) stream.writelines(lines) @@ -703,7 +706,8 @@ def get_pkg_info_revision(): Get a -r### off of PKG-INFO Version in case this is an sdist of a subversion revision. """ - warnings.warn("get_pkg_info_revision is deprecated.", EggInfoDeprecationWarning) + warnings.warn( + "get_pkg_info_revision is deprecated.", EggInfoDeprecationWarning) if os.path.exists('PKG-INFO'): with io.open('PKG-INFO') as f: for line in f: @@ -714,4 +718,4 @@ def get_pkg_info_revision(): class EggInfoDeprecationWarning(SetuptoolsDeprecationWarning): - """Class for warning about deprecations in eggInfo in setupTools. Not ignored by default, unlike DeprecationWarning.""" + """Deprecated behavior warning for EggInfo, bypassing suppression.""" diff --git a/setuptools/command/install.py b/setuptools/command/install.py index 31a5ddb5..72b9a3e4 100644 --- a/setuptools/command/install.py +++ b/setuptools/command/install.py @@ -114,7 +114,7 @@ class install(orig.install): args.insert(0, setuptools.bootstrap_install_from) cmd.args = args - cmd.run() + cmd.run(show_deprecation=False) setuptools.bootstrap_install_from = None diff --git a/setuptools/command/install_lib.py b/setuptools/command/install_lib.py index 2b31c3e3..2e9d8757 100644 --- a/setuptools/command/install_lib.py +++ b/setuptools/command/install_lib.py @@ -1,5 +1,5 @@ import os -import imp +import sys from itertools import product, starmap import distutils.command.install_lib as orig @@ -74,10 +74,11 @@ class install_lib(orig.install_lib): yield '__init__.pyc' yield '__init__.pyo' - if not hasattr(imp, 'get_tag'): + if not hasattr(sys, 'implementation'): return - base = os.path.join('__pycache__', '__init__.' + imp.get_tag()) + base = os.path.join( + '__pycache__', '__init__.' + sys.implementation.cache_tag) yield base + '.pyc' yield base + '.pyo' yield base + '.opt-1.pyc' diff --git a/setuptools/command/install_scripts.py b/setuptools/command/install_scripts.py index 16234273..8c9a15e2 100644 --- a/setuptools/command/install_scripts.py +++ b/setuptools/command/install_scripts.py @@ -32,8 +32,11 @@ class install_scripts(orig.install_scripts): ) bs_cmd = self.get_finalized_command('build_scripts') exec_param = getattr(bs_cmd, 'executable', None) - bw_cmd = self.get_finalized_command("bdist_wininst") - is_wininst = getattr(bw_cmd, '_is_running', False) + try: + bw_cmd = self.get_finalized_command("bdist_wininst") + is_wininst = getattr(bw_cmd, '_is_running', False) + except ImportError: + is_wininst = False writer = ei.ScriptWriter if is_wininst: exec_param = "python.exe" diff --git a/setuptools/command/py36compat.py b/setuptools/command/py36compat.py index 61063e75..28860558 100644 --- a/setuptools/command/py36compat.py +++ b/setuptools/command/py36compat.py @@ -132,5 +132,5 @@ class sdist_add_defaults: if hasattr(sdist.sdist, '_add_defaults_standards'): # disable the functionality already available upstream - class sdist_add_defaults: + class sdist_add_defaults: # noqa pass diff --git a/setuptools/command/register.py b/setuptools/command/register.py index 98bc0156..b8266b9a 100644 --- a/setuptools/command/register.py +++ b/setuptools/command/register.py @@ -1,18 +1,18 @@ from distutils import log import distutils.command.register as orig +from setuptools.errors import RemovedCommandError + class register(orig.register): - __doc__ = orig.register.__doc__ + """Formerly used to register packages on PyPI.""" def run(self): - try: - # Make sure that we are using valid current name/version info - self.run_command('egg_info') - orig.register.run(self) - finally: - self.announce( - "WARNING: Registering is deprecated, use twine to " - "upload instead (https://pypi.org/p/twine/)", - log.WARN - ) + msg = ( + "The register command has been removed, use twine to upload " + + "instead (https://pypi.org/p/twine)" + ) + + self.announce("ERROR: " + msg, log.ERROR) + + raise RemovedCommandError(msg) diff --git a/setuptools/command/sdist.py b/setuptools/command/sdist.py index dc253981..8c3438ea 100644 --- a/setuptools/command/sdist.py +++ b/setuptools/command/sdist.py @@ -5,7 +5,7 @@ import sys import io import contextlib -from setuptools.extern import six +from setuptools.extern import six, ordered_set from .py36compat import sdist_add_defaults @@ -121,19 +121,40 @@ class sdist(sdist_add_defaults, orig.sdist): if has_leaky_handle: read_template = __read_template_hack + def _add_defaults_optional(self): + if six.PY2: + sdist_add_defaults._add_defaults_optional(self) + else: + super()._add_defaults_optional() + if os.path.isfile('pyproject.toml'): + self.filelist.append('pyproject.toml') + def _add_defaults_python(self): """getting python files""" if self.distribution.has_pure_modules(): build_py = self.get_finalized_command('build_py') self.filelist.extend(build_py.get_source_files()) - # This functionality is incompatible with include_package_data, and - # will in fact create an infinite recursion if include_package_data - # is True. Use of include_package_data will imply that - # distutils-style automatic handling of package_data is disabled - if not self.distribution.include_package_data: - for _, src_dir, _, filenames in build_py.data_files: - self.filelist.extend([os.path.join(src_dir, filename) - for filename in filenames]) + self._add_data_files(self._safe_data_files(build_py)) + + def _safe_data_files(self, build_py): + """ + Extracting data_files from build_py is known to cause + infinite recursion errors when `include_package_data` + is enabled, so suppress it in that case. + """ + if self.distribution.include_package_data: + return () + return build_py.data_files + + def _add_data_files(self, data_files): + """ + Add data files as found in build_py.data_files. + """ + self.filelist.extend( + os.path.join(src_dir, name) + for _, src_dir, _, filenames in data_files + for name in filenames + ) def _add_defaults_data_files(self): try: @@ -186,7 +207,7 @@ class sdist(sdist_add_defaults, orig.sdist): manifest = open(self.manifest, 'rb') for line in manifest: # The manifest must contain UTF-8. See #303. - if six.PY3: + if not six.PY2: try: line = line.decode('UTF-8') except UnicodeDecodeError: @@ -200,10 +221,12 @@ class sdist(sdist_add_defaults, orig.sdist): manifest.close() def check_license(self): - """Checks if license_file' is configured and adds it to - 'self.filelist' if the value contains a valid path. + """Checks if license_file' or 'license_files' is configured and adds any + valid paths to 'self.filelist'. """ + files = ordered_set.OrderedSet() + opts = self.distribution.get_option_dict('metadata') # ignore the source of the value @@ -211,11 +234,19 @@ class sdist(sdist_add_defaults, orig.sdist): if license_file is None: log.debug("'license_file' option was not specified") - return + else: + files.add(license_file) - if not os.path.exists(license_file): - log.warn("warning: Failed to find the configured license file '%s'", - license_file) - return + try: + files.update(self.distribution.metadata.license_files) + except TypeError: + log.warn("warning: 'license_files' option is malformed") + + for f in files: + if not os.path.exists(f): + log.warn( + "warning: Failed to find the configured license file '%s'", + f) + files.remove(f) - self.filelist.append(license_file) + self.filelist.extend(files) diff --git a/setuptools/command/test.py b/setuptools/command/test.py index 973e4eb2..2d83967d 100644 --- a/setuptools/command/test.py +++ b/setuptools/command/test.py @@ -74,7 +74,7 @@ class NonDataProperty: class test(Command): """Command to run unit tests after in-place build""" - description = "run unit tests after in-place build" + description = "run unit tests after in-place build (deprecated)" user_options = [ ('test-module=', 'm', "Run 'test_suite' in specified module"), @@ -129,7 +129,8 @@ class test(Command): @contextlib.contextmanager def project_on_sys_path(self, include_dists=[]): - with_2to3 = six.PY3 and getattr(self.distribution, 'use_2to3', False) + with_2to3 = not six.PY2 and getattr( + self.distribution, 'use_2to3', False) if with_2to3: # If we run 2to3 we can not do this inplace: @@ -214,6 +215,14 @@ class test(Command): return itertools.chain(ir_d, tr_d, er_d) def run(self): + self.announce( + "WARNING: Testing via this command is deprecated and will be " + "removed in a future version. Users looking for a generic test " + "entry point independent of test runner are encouraged to use " + "tox.", + log.WARN, + ) + installed_dists = self.install_dists(self.distribution) cmd = ' '.join(self._argv) @@ -232,7 +241,7 @@ class test(Command): # Purge modules under test from sys.modules. The test loader will # re-import them from the build location. Required when 2to3 is used # with namespace packages. - if six.PY3 and getattr(self.distribution, 'use_2to3', False): + if not six.PY2 and getattr(self.distribution, 'use_2to3', False): module = self.test_suite.split('.')[0] if module in _namespace_packages: del_modules = [] diff --git a/setuptools/command/upload.py b/setuptools/command/upload.py index 6db8888b..ec7f81e2 100644 --- a/setuptools/command/upload.py +++ b/setuptools/command/upload.py @@ -1,196 +1,17 @@ -import io -import os -import hashlib -import getpass - -from base64 import standard_b64encode - from distutils import log from distutils.command import upload as orig -from distutils.spawn import spawn - -from distutils.errors import DistutilsError -from setuptools.extern.six.moves.urllib.request import urlopen, Request -from setuptools.extern.six.moves.urllib.error import HTTPError -from setuptools.extern.six.moves.urllib.parse import urlparse +from setuptools.errors import RemovedCommandError class upload(orig.upload): - """ - Override default upload behavior to obtain password - in a variety of different ways. - """ - def run(self): - try: - orig.upload.run(self) - finally: - self.announce( - "WARNING: Uploading via this command is deprecated, use twine " - "to upload instead (https://pypi.org/p/twine/)", - log.WARN - ) + """Formerly used to upload packages to PyPI.""" - def finalize_options(self): - orig.upload.finalize_options(self) - self.username = ( - self.username or - getpass.getuser() - ) - # Attempt to obtain password. Short circuit evaluation at the first - # sign of success. - self.password = ( - self.password or - self._load_password_from_keyring() or - self._prompt_for_password() + def run(self): + msg = ( + "The upload command has been removed, use twine to upload " + + "instead (https://pypi.org/p/twine)" ) - def upload_file(self, command, pyversion, filename): - # Makes sure the repository URL is compliant - schema, netloc, url, params, query, fragments = \ - urlparse(self.repository) - if params or query or fragments: - raise AssertionError("Incompatible url %s" % self.repository) - - if schema not in ('http', 'https'): - raise AssertionError("unsupported schema " + schema) - - # Sign if requested - if self.sign: - gpg_args = ["gpg", "--detach-sign", "-a", filename] - if self.identity: - gpg_args[2:2] = ["--local-user", self.identity] - spawn(gpg_args, - dry_run=self.dry_run) - - # Fill in the data - send all the meta-data in case we need to - # register a new release - with open(filename, 'rb') as f: - content = f.read() - - meta = self.distribution.metadata - - data = { - # action - ':action': 'file_upload', - 'protocol_version': '1', - - # identify release - 'name': meta.get_name(), - 'version': meta.get_version(), - - # file content - 'content': (os.path.basename(filename), content), - 'filetype': command, - 'pyversion': pyversion, - 'md5_digest': hashlib.md5(content).hexdigest(), - - # additional meta-data - 'metadata_version': str(meta.get_metadata_version()), - 'summary': meta.get_description(), - 'home_page': meta.get_url(), - 'author': meta.get_contact(), - 'author_email': meta.get_contact_email(), - 'license': meta.get_licence(), - 'description': meta.get_long_description(), - 'keywords': meta.get_keywords(), - 'platform': meta.get_platforms(), - 'classifiers': meta.get_classifiers(), - 'download_url': meta.get_download_url(), - # PEP 314 - 'provides': meta.get_provides(), - 'requires': meta.get_requires(), - 'obsoletes': meta.get_obsoletes(), - } - - data['comment'] = '' - - if self.sign: - data['gpg_signature'] = (os.path.basename(filename) + ".asc", - open(filename+".asc", "rb").read()) - - # set up the authentication - user_pass = (self.username + ":" + self.password).encode('ascii') - # The exact encoding of the authentication string is debated. - # Anyway PyPI only accepts ascii for both username or password. - auth = "Basic " + standard_b64encode(user_pass).decode('ascii') - - # Build up the MIME payload for the POST data - boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254' - sep_boundary = b'\r\n--' + boundary.encode('ascii') - end_boundary = sep_boundary + b'--\r\n' - body = io.BytesIO() - for key, value in data.items(): - title = '\r\nContent-Disposition: form-data; name="%s"' % key - # handle multiple entries for the same name - if not isinstance(value, list): - value = [value] - for value in value: - if type(value) is tuple: - title += '; filename="%s"' % value[0] - value = value[1] - else: - value = str(value).encode('utf-8') - body.write(sep_boundary) - body.write(title.encode('utf-8')) - body.write(b"\r\n\r\n") - body.write(value) - body.write(end_boundary) - body = body.getvalue() - - msg = "Submitting %s to %s" % (filename, self.repository) - self.announce(msg, log.INFO) - - # build the Request - headers = { - 'Content-type': 'multipart/form-data; boundary=%s' % boundary, - 'Content-length': str(len(body)), - 'Authorization': auth, - } - - request = Request(self.repository, data=body, - headers=headers) - # send the data - try: - result = urlopen(request) - status = result.getcode() - reason = result.msg - except HTTPError as e: - status = e.code - reason = e.msg - except OSError as e: - self.announce(str(e), log.ERROR) - raise - - if status == 200: - self.announce('Server response (%s): %s' % (status, reason), - log.INFO) - if self.show_response: - text = getattr(self, '_read_pypi_response', - lambda x: None)(result) - if text is not None: - msg = '\n'.join(('-' * 75, text, '-' * 75)) - self.announce(msg, log.INFO) - else: - msg = 'Upload failed (%s): %s' % (status, reason) - self.announce(msg, log.ERROR) - raise DistutilsError(msg) - - def _load_password_from_keyring(self): - """ - Attempt to load password from keyring. Suppress Exceptions. - """ - try: - keyring = __import__('keyring') - return keyring.get_password(self.repository, self.username) - except Exception: - pass - - def _prompt_for_password(self): - """ - Prompt for a password on the tty. Suppress Exceptions. - """ - try: - return getpass.getpass() - except (Exception, KeyboardInterrupt): - pass + self.announce("ERROR: " + msg, log.ERROR) + raise RemovedCommandError(msg) diff --git a/setuptools/command/upload_docs.py b/setuptools/command/upload_docs.py index 07aa564a..0351da77 100644 --- a/setuptools/command/upload_docs.py +++ b/setuptools/command/upload_docs.py @@ -24,7 +24,7 @@ from .upload import upload def _encode(s): - errors = 'surrogateescape' if six.PY3 else 'strict' + errors = 'strict' if six.PY2 else 'surrogateescape' return s.encode('utf-8', errors) @@ -127,8 +127,8 @@ class upload_docs(upload): """ Build up the MIME payload for the POST data """ - boundary = b'--------------GHSKFJDLGDS7543FJKLFHRE75642756743254' - sep_boundary = b'\n--' + boundary + boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254' + sep_boundary = b'\n--' + boundary.encode('ascii') end_boundary = sep_boundary + b'--' end_items = end_boundary, b"\n", builder = functools.partial( @@ -138,7 +138,7 @@ class upload_docs(upload): part_groups = map(builder, data.items()) parts = itertools.chain.from_iterable(part_groups) body_items = itertools.chain(parts, end_items) - content_type = 'multipart/form-data; boundary=%s' % boundary.decode('ascii') + content_type = 'multipart/form-data; boundary=%s' % boundary return b''.join(body_items), content_type def upload_file(self, filename): @@ -153,7 +153,7 @@ class upload_docs(upload): # set up the authentication credentials = _encode(self.username + ':' + self.password) credentials = standard_b64encode(credentials) - if six.PY3: + if not six.PY2: credentials = credentials.decode('ascii') auth = "Basic " + credentials diff --git a/setuptools/config.py b/setuptools/config.py index b6626043..9b9a0c45 100644 --- a/setuptools/config.py +++ b/setuptools/config.py @@ -12,6 +12,7 @@ from importlib import import_module from distutils.errors import DistutilsOptionError, DistutilsFileError from setuptools.extern.packaging.version import LegacyVersion, parse +from setuptools.extern.packaging.specifiers import SpecifierSet from setuptools.extern.six import string_types, PY3 @@ -482,6 +483,7 @@ class ConfigMetadataHandler(ConfigHandler): 'obsoletes': parse_list, 'classifiers': self._get_parser_compound(parse_file, parse_list), 'license': exclude_files_parser('license'), + 'license_files': parse_list, 'description': parse_file, 'long_description': parse_file, 'version': self._parse_version, @@ -554,6 +556,7 @@ class ConfigOptionsHandler(ConfigHandler): 'packages': self._parse_packages, 'entry_points': self._parse_file, 'py_modules': parse_list, + 'python_requires': SpecifierSet, } def _parse_packages(self, value): diff --git a/setuptools/dep_util.py b/setuptools/dep_util.py index 2931c13e..521eb716 100644 --- a/setuptools/dep_util.py +++ b/setuptools/dep_util.py @@ -1,5 +1,6 @@ from distutils.dep_util import newer_group + # yes, this is was almost entirely copy-pasted from # 'newer_pairwise()', this is just another convenience # function. @@ -10,7 +11,8 @@ def newer_pairwise_group(sources_groups, targets): of 'newer_group()'. """ if len(sources_groups) != len(targets): - raise ValueError("'sources_group' and 'targets' must be the same length") + raise ValueError( + "'sources_group' and 'targets' must be the same length") # build a pair of lists (sources_groups, targets) where source is newer n_sources = [] diff --git a/setuptools/depends.py b/setuptools/depends.py index 45e7052d..a37675cb 100644 --- a/setuptools/depends.py +++ b/setuptools/depends.py @@ -1,11 +1,13 @@ import sys -import imp import marshal +import contextlib from distutils.version import StrictVersion -from imp import PKG_DIRECTORY, PY_COMPILED, PY_SOURCE, PY_FROZEN from .py33compat import Bytecode +from .py27compat import find_module, PY_COMPILED, PY_FROZEN, PY_SOURCE +from . import py27compat + __all__ = [ 'Require', 'find_module', 'get_module_constant', 'extract_constant' @@ -15,7 +17,8 @@ __all__ = [ class Require: """A prerequisite to building or installing a distribution""" - def __init__(self, name, requested_version, module, homepage='', + def __init__( + self, name, requested_version, module, homepage='', attribute=None, format=None): if format is None and requested_version is not None: @@ -79,23 +82,15 @@ class Require: return self.version_ok(version) -def find_module(module, paths=None): - """Just like 'imp.find_module()', but with package support""" - - parts = module.split('.') - - while parts: - part = parts.pop(0) - f, path, (suffix, mode, kind) = info = imp.find_module(part, paths) - - if kind == PKG_DIRECTORY: - parts = parts or ['__init__'] - paths = [path] - - elif parts: - raise ImportError("Can't find %r in %s" % (parts, module)) +def maybe_close(f): + @contextlib.contextmanager + def empty(): + yield + return + if not f: + return empty() - return info + return contextlib.closing(f) def get_module_constant(module, symbol, default=-1, paths=None): @@ -106,28 +101,23 @@ def get_module_constant(module, symbol, default=-1, paths=None): constant. Otherwise, return 'default'.""" try: - f, path, (suffix, mode, kind) = find_module(module, paths) + f, path, (suffix, mode, kind) = info = find_module(module, paths) except ImportError: # Module doesn't exist return None - try: + with maybe_close(f): if kind == PY_COMPILED: f.read(8) # skip magic & date code = marshal.load(f) elif kind == PY_FROZEN: - code = imp.get_frozen_object(module) + code = py27compat.get_frozen_object(module, paths) elif kind == PY_SOURCE: code = compile(f.read(), path, 'exec') else: # Not something we can parse; we'll have to import it. :( - if module not in sys.modules: - imp.load_module(module, f, path, (suffix, mode, kind)) - return getattr(sys.modules[module], symbol, None) - - finally: - if f: - f.close() + imported = py27compat.get_module(module, paths, info) + return getattr(imported, symbol, None) return extract_constant(code, symbol, default) diff --git a/setuptools/dist.py b/setuptools/dist.py index 9a165de0..ad54839b 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -27,6 +27,7 @@ from distutils.version import StrictVersion from setuptools.extern import six from setuptools.extern import packaging +from setuptools.extern import ordered_set from setuptools.extern.six.moves import map, filter, filterfalse from . import SetuptoolsDeprecationWarning @@ -54,7 +55,8 @@ def get_metadata_version(self): mv = StrictVersion('2.1') elif (self.maintainer is not None or self.maintainer_email is not None or - getattr(self, 'python_requires', None) is not None): + getattr(self, 'python_requires', None) is not None or + self.project_urls): mv = StrictVersion('1.2') elif (self.provides or self.requires or self.obsoletes or self.classifiers or self.download_url): @@ -160,7 +162,7 @@ def write_pkg_file(self, file): if self.download_url: write_field('Download-URL', self.download_url) for project_url in self.project_urls.items(): - write_field('Project-URL', '%s, %s' % project_url) + write_field('Project-URL', '%s, %s' % project_url) long_desc = rfc822_escape(self.get_long_description()) write_field('Description', long_desc) @@ -212,8 +214,12 @@ def check_importable(dist, attr, value): def assert_string_list(dist, attr, value): - """Verify that value is a string list or None""" + """Verify that value is a string list""" try: + # verify that value is a list or tuple to exclude unordered + # or single-use iterables + assert isinstance(value, (list, tuple)) + # verify that elements of value are strings assert ''.join(value) != value except (TypeError, ValueError, AttributeError, AssertionError): raise DistutilsSetupError( @@ -306,20 +312,17 @@ def check_test_suite(dist, attr, value): def check_package_data(dist, attr, value): """Verify that value is a dictionary of package names to glob lists""" - if isinstance(value, dict): - for k, v in value.items(): - if not isinstance(k, str): - break - try: - iter(v) - except TypeError: - break - else: - return - raise DistutilsSetupError( - attr + " must be a dictionary mapping package names to lists of " - "wildcard patterns" - ) + if not isinstance(value, dict): + raise DistutilsSetupError( + "{!r} must be a dictionary mapping package names to lists of " + "string wildcard patterns".format(attr)) + for k, v in value.items(): + if not isinstance(k, six.string_types): + raise DistutilsSetupError( + "keys of {!r} dict must be strings (got {!r})" + .format(attr, k) + ) + assert_string_list(dist, 'values of {!r} dict'.format(attr), v) def check_packages(dist, attr, value): @@ -405,7 +408,8 @@ class Distribution(_Distribution): _DISTUTILS_UNSUPPORTED_METADATA = { 'long_description_content_type': None, 'project_urls': dict, - 'provides_extras': set, + 'provides_extras': ordered_set.OrderedSet, + 'license_files': ordered_set.OrderedSet, } _patched_dist = None @@ -567,7 +571,7 @@ class Distribution(_Distribution): from setuptools.extern.six.moves.configparser import ConfigParser # Ignore install directory options if we have a venv - if six.PY3 and sys.prefix != sys.base_prefix: + if not six.PY2 and sys.prefix != sys.base_prefix: ignore_options = [ 'install-base', 'install-platbase', 'install-lib', 'install-platlib', 'install-purelib', 'install-headers', @@ -589,7 +593,7 @@ class Distribution(_Distribution): with io.open(filename, encoding='utf-8') as reader: if DEBUG: self.announce(" reading {filename}".format(**locals())) - (parser.read_file if six.PY3 else parser.readfp)(reader) + (parser.readfp if six.PY2 else parser.read_file)(reader) for section in parser.sections(): options = parser.options(section) opt_dict = self.get_option_dict(section) @@ -632,7 +636,7 @@ class Distribution(_Distribution): Ref #1653 """ - if six.PY3: + if not six.PY2: return val try: return val.encode() @@ -721,15 +725,28 @@ class Distribution(_Distribution): return resolved_dists def finalize_options(self): - _Distribution.finalize_options(self) - if self.features: - self._set_global_opts_from_features() + """ + Allow plugins to apply arbitrary operations to the + distribution. Each hook may optionally define a 'order' + to influence the order of execution. Smaller numbers + go first and the default is 0. + """ + hook_key = 'setuptools.finalize_distribution_options' + def by_order(hook): + return getattr(hook, 'order', 0) + eps = pkg_resources.iter_entry_points(hook_key) + for ep in sorted(eps, key=by_order): + ep.load()(self) + + def _finalize_setup_keywords(self): for ep in pkg_resources.iter_entry_points('distutils.setup_keywords'): value = getattr(self, ep.name, None) if value is not None: ep.require(installer=self.fetch_build_egg) ep.load()(self, ep.name, value) + + def _finalize_2to3_doctests(self): if getattr(self, 'convert_2to3_doctests', None): # XXX may convert to set here when we can rely on set being builtin self.convert_2to3_doctests = [ @@ -756,36 +773,15 @@ class Distribution(_Distribution): def fetch_build_egg(self, req): """Fetch an egg needed for building""" - from setuptools.command.easy_install import easy_install - dist = self.__class__({'script_args': ['easy_install']}) - opts = dist.get_option_dict('easy_install') - opts.clear() - opts.update( - (k, v) - for k, v in self.get_option_dict('easy_install').items() - if k in ( - # don't use any other settings - 'find_links', 'site_dirs', 'index_url', - 'optimize', 'site_dirs', 'allow_hosts', - )) - if self.dependency_links: - links = self.dependency_links[:] - if 'find_links' in opts: - links = opts['find_links'][1] + links - opts['find_links'] = ('setup', links) - install_dir = self.get_egg_cache_dir() - cmd = easy_install( - dist, args=["x"], install_dir=install_dir, - exclude_scripts=True, - always_copy=False, build_directory=None, editable=False, - upgrade=False, multi_version=True, no_report=True, user=False - ) - cmd.ensure_finalized() - return cmd.easy_install(req) + from setuptools.installer import fetch_build_egg + return fetch_build_egg(self, req) - def _set_global_opts_from_features(self): + def _finalize_feature_opts(self): """Add --with-X/--without-X options based on optional features""" + if not self.features: + return + go = [] no = self.negative_opt.copy() diff --git a/setuptools/errors.py b/setuptools/errors.py new file mode 100644 index 00000000..2701747f --- /dev/null +++ b/setuptools/errors.py @@ -0,0 +1,16 @@ +"""setuptools.errors + +Provides exceptions used by setuptools modules. +""" + +from distutils.errors import DistutilsError + + +class RemovedCommandError(DistutilsError, RuntimeError): + """Error used for commands that have been removed in setuptools. + + Since ``setuptools`` is built on ``distutils``, simply removing a command + from ``setuptools`` will make the behavior fall back to ``distutils``; this + error is raised if a command exists in ``distutils`` but has been actively + removed in ``setuptools``. + """ diff --git a/setuptools/extern/__init__.py b/setuptools/extern/__init__.py index cb2fa329..e8c616f9 100644 --- a/setuptools/extern/__init__.py +++ b/setuptools/extern/__init__.py @@ -69,5 +69,5 @@ class VendorImporter: sys.meta_path.append(self) -names = 'six', 'packaging', 'pyparsing', +names = 'six', 'packaging', 'pyparsing', 'ordered_set', VendorImporter(__name__, names, 'setuptools._vendor').install() diff --git a/setuptools/glibc.py b/setuptools/glibc.py deleted file mode 100644 index a134591c..00000000 --- a/setuptools/glibc.py +++ /dev/null @@ -1,86 +0,0 @@ -# This file originally from pip: -# https://github.com/pypa/pip/blob/8f4f15a5a95d7d5b511ceaee9ed261176c181970/src/pip/_internal/utils/glibc.py -from __future__ import absolute_import - -import ctypes -import re -import warnings - - -def glibc_version_string(): - "Returns glibc version string, or None if not using glibc." - - # ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen - # manpage says, "If filename is NULL, then the returned handle is for the - # main program". This way we can let the linker do the work to figure out - # which libc our process is actually using. - process_namespace = ctypes.CDLL(None) - try: - gnu_get_libc_version = process_namespace.gnu_get_libc_version - except AttributeError: - # Symbol doesn't exist -> therefore, we are not linked to - # glibc. - return None - - # Call gnu_get_libc_version, which returns a string like "2.5" - gnu_get_libc_version.restype = ctypes.c_char_p - version_str = gnu_get_libc_version() - # py2 / py3 compatibility: - if not isinstance(version_str, str): - version_str = version_str.decode("ascii") - - return version_str - - -# Separated out from have_compatible_glibc for easier unit testing -def check_glibc_version(version_str, required_major, minimum_minor): - # Parse string and check against requested version. - # - # We use a regexp instead of str.split because we want to discard any - # random junk that might come after the minor version -- this might happen - # in patched/forked versions of glibc (e.g. Linaro's version of glibc - # uses version strings like "2.20-2014.11"). See gh-3588. - m = re.match(r"(?P<major>[0-9]+)\.(?P<minor>[0-9]+)", version_str) - if not m: - warnings.warn("Expected glibc version with 2 components major.minor," - " got: %s" % version_str, RuntimeWarning) - return False - return (int(m.group("major")) == required_major and - int(m.group("minor")) >= minimum_minor) - - -def have_compatible_glibc(required_major, minimum_minor): - version_str = glibc_version_string() - if version_str is None: - return False - return check_glibc_version(version_str, required_major, minimum_minor) - - -# platform.libc_ver regularly returns completely nonsensical glibc -# versions. E.g. on my computer, platform says: -# -# ~$ python2.7 -c 'import platform; print(platform.libc_ver())' -# ('glibc', '2.7') -# ~$ python3.5 -c 'import platform; print(platform.libc_ver())' -# ('glibc', '2.9') -# -# But the truth is: -# -# ~$ ldd --version -# ldd (Debian GLIBC 2.22-11) 2.22 -# -# This is unfortunate, because it means that the linehaul data on libc -# versions that was generated by pip 8.1.2 and earlier is useless and -# misleading. Solution: instead of using platform, use our code that actually -# works. -def libc_ver(): - """Try to determine the glibc version - - Returns a tuple of strings (lib, version) which default to empty strings - in case the lookup fails. - """ - glibc_version = glibc_version_string() - if glibc_version is None: - return ("", "") - else: - return ("glibc", glibc_version) diff --git a/setuptools/installer.py b/setuptools/installer.py new file mode 100644 index 00000000..1f183bd9 --- /dev/null +++ b/setuptools/installer.py @@ -0,0 +1,150 @@ +import glob +import os +import subprocess +import sys +from distutils import log +from distutils.errors import DistutilsError + +import pkg_resources +from setuptools.command.easy_install import easy_install +from setuptools.extern import six +from setuptools.wheel import Wheel + +from .py31compat import TemporaryDirectory + + +def _fixup_find_links(find_links): + """Ensure find-links option end-up being a list of strings.""" + if isinstance(find_links, six.string_types): + return find_links.split() + assert isinstance(find_links, (tuple, list)) + return find_links + + +def _legacy_fetch_build_egg(dist, req): + """Fetch an egg needed for building. + + Legacy path using EasyInstall. + """ + tmp_dist = dist.__class__({'script_args': ['easy_install']}) + opts = tmp_dist.get_option_dict('easy_install') + opts.clear() + opts.update( + (k, v) + for k, v in dist.get_option_dict('easy_install').items() + if k in ( + # don't use any other settings + 'find_links', 'site_dirs', 'index_url', + 'optimize', 'site_dirs', 'allow_hosts', + )) + if dist.dependency_links: + links = dist.dependency_links[:] + if 'find_links' in opts: + links = _fixup_find_links(opts['find_links'][1]) + links + opts['find_links'] = ('setup', links) + install_dir = dist.get_egg_cache_dir() + cmd = easy_install( + tmp_dist, args=["x"], install_dir=install_dir, + exclude_scripts=True, + always_copy=False, build_directory=None, editable=False, + upgrade=False, multi_version=True, no_report=True, user=False + ) + cmd.ensure_finalized() + return cmd.easy_install(req) + + +def fetch_build_egg(dist, req): + """Fetch an egg needed for building. + + Use pip/wheel to fetch/build a wheel.""" + # Check pip is available. + try: + pkg_resources.get_distribution('pip') + except pkg_resources.DistributionNotFound: + dist.announce( + 'WARNING: The pip package is not available, falling back ' + 'to EasyInstall for handling setup_requires/test_requires; ' + 'this is deprecated and will be removed in a future version.', + log.WARN + ) + return _legacy_fetch_build_egg(dist, req) + # Warn if wheel is not. + try: + pkg_resources.get_distribution('wheel') + except pkg_resources.DistributionNotFound: + dist.announce('WARNING: The wheel package is not available.', log.WARN) + # Ignore environment markers; if supplied, it is required. + req = strip_marker(req) + # Take easy_install options into account, but do not override relevant + # pip environment variables (like PIP_INDEX_URL or PIP_QUIET); they'll + # take precedence. + opts = dist.get_option_dict('easy_install') + if 'allow_hosts' in opts: + raise DistutilsError('the `allow-hosts` option is not supported ' + 'when using pip to install requirements.') + if 'PIP_QUIET' in os.environ or 'PIP_VERBOSE' in os.environ: + quiet = False + else: + quiet = True + if 'PIP_INDEX_URL' in os.environ: + index_url = None + elif 'index_url' in opts: + index_url = opts['index_url'][1] + else: + index_url = None + if 'find_links' in opts: + find_links = _fixup_find_links(opts['find_links'][1])[:] + else: + find_links = [] + if dist.dependency_links: + find_links.extend(dist.dependency_links) + eggs_dir = os.path.realpath(dist.get_egg_cache_dir()) + environment = pkg_resources.Environment() + for egg_dist in pkg_resources.find_distributions(eggs_dir): + if egg_dist in req and environment.can_add(egg_dist): + return egg_dist + with TemporaryDirectory() as tmpdir: + cmd = [ + sys.executable, '-m', 'pip', + '--disable-pip-version-check', + 'wheel', '--no-deps', + '-w', tmpdir, + ] + if quiet: + cmd.append('--quiet') + if index_url is not None: + cmd.extend(('--index-url', index_url)) + if find_links is not None: + for link in find_links: + cmd.extend(('--find-links', link)) + # If requirement is a PEP 508 direct URL, directly pass + # the URL to pip, as `req @ url` does not work on the + # command line. + if req.url: + cmd.append(req.url) + else: + cmd.append(str(req)) + try: + subprocess.check_call(cmd) + except subprocess.CalledProcessError as e: + raise DistutilsError(str(e)) + wheel = Wheel(glob.glob(os.path.join(tmpdir, '*.whl'))[0]) + dist_location = os.path.join(eggs_dir, wheel.egg_name()) + wheel.install_as_egg(dist_location) + dist_metadata = pkg_resources.PathMetadata( + dist_location, os.path.join(dist_location, 'EGG-INFO')) + dist = pkg_resources.Distribution.from_filename( + dist_location, metadata=dist_metadata) + return dist + + +def strip_marker(req): + """ + Return a new requirement without the environment marker to avoid + calling pip with something like `babel; extra == "i18n"`, which + would always be ignored. + """ + # create a copy to avoid mutating the input + req = pkg_resources.Requirement.parse(str(req)) + req.marker = None + return req diff --git a/setuptools/msvc.py b/setuptools/msvc.py index b9c472f1..c2cbd1e5 100644 --- a/setuptools/msvc.py +++ b/setuptools/msvc.py @@ -11,13 +11,18 @@ Microsoft Visual C++ 9.0: Microsoft Visual C++ 10.0: Microsoft Windows SDK 7.1 (x86, x64, ia64) -Microsoft Visual C++ 14.0: +Microsoft Visual C++ 14.X: Microsoft Visual C++ Build Tools 2015 (x86, x64, arm) - Microsoft Visual Studio 2017 (x86, x64, arm, arm64) Microsoft Visual Studio Build Tools 2017 (x86, x64, arm, arm64) + Microsoft Visual Studio Build Tools 2019 (x86, x64, arm, arm64) + +This may also support compilers shipped with compatible Visual Studio versions. """ -import os +import json +from io import open +from os import listdir, pathsep +from os.path import join, isfile, isdir, dirname import sys import platform import itertools @@ -30,12 +35,9 @@ from .monkey import get_unpatched if platform.system() == 'Windows': from setuptools.extern.six.moves import winreg - safe_env = os.environ + from os import environ else: - """ - Mock winreg and environ so the module can be imported - on this platform. - """ + # Mock winreg and environ so the module can be imported on this platform. class winreg: HKEY_USERS = None @@ -43,7 +45,7 @@ else: HKEY_LOCAL_MACHINE = None HKEY_CLASSES_ROOT = None - safe_env = dict() + environ = dict() _msvc9_suppress_errors = ( # msvc9compiler isn't available on some platforms @@ -63,15 +65,13 @@ except _msvc9_suppress_errors: def msvc9_find_vcvarsall(version): """ Patched "distutils.msvc9compiler.find_vcvarsall" to use the standalone - compiler build for Python (VCForPython). Fall back to original behavior - when the standalone compiler is not available. + compiler build for Python + (VCForPython / Microsoft Visual C++ Compiler for Python 2.7). - Redirect the path of "vcvarsall.bat". + Fall back to original behavior when the standalone compiler is not + available. - Known supported compilers - ------------------------- - Microsoft Visual C++ 9.0: - Microsoft Visual C++ Compiler for Python 2.7 (x86, amd64) + Redirect the path of "vcvarsall.bat". Parameters ---------- @@ -80,24 +80,25 @@ def msvc9_find_vcvarsall(version): Return ------ - vcvarsall.bat path: str + str + vcvarsall.bat path """ - VC_BASE = r'Software\%sMicrosoft\DevDiv\VCForPython\%0.1f' - key = VC_BASE % ('', version) + vc_base = r'Software\%sMicrosoft\DevDiv\VCForPython\%0.1f' + key = vc_base % ('', version) try: # Per-user installs register the compiler path here productdir = Reg.get_value(key, "installdir") except KeyError: try: # All-user installs on a 64-bit system register here - key = VC_BASE % ('Wow6432Node\\', version) + key = vc_base % ('Wow6432Node\\', version) productdir = Reg.get_value(key, "installdir") except KeyError: productdir = None if productdir: - vcvarsall = os.path.os.path.join(productdir, "vcvarsall.bat") - if os.path.isfile(vcvarsall): + vcvarsall = join(productdir, "vcvarsall.bat") + if isfile(vcvarsall): return vcvarsall return get_unpatched(msvc9_find_vcvarsall)(version) @@ -106,20 +107,10 @@ def msvc9_find_vcvarsall(version): def msvc9_query_vcvarsall(ver, arch='x86', *args, **kwargs): """ Patched "distutils.msvc9compiler.query_vcvarsall" for support extra - compilers. + Microsoft Visual C++ 9.0 and 10.0 compilers. Set environment without use of "vcvarsall.bat". - Known supported compilers - ------------------------- - Microsoft Visual C++ 9.0: - Microsoft Visual C++ Compiler for Python 2.7 (x86, amd64) - Microsoft Windows SDK 6.1 (x86, x64, ia64) - Microsoft Windows SDK 7.0 (x86, x64, ia64) - - Microsoft Visual C++ 10.0: - Microsoft Windows SDK 7.1 (x86, x64, ia64) - Parameters ---------- ver: float @@ -129,9 +120,10 @@ def msvc9_query_vcvarsall(ver, arch='x86', *args, **kwargs): Return ------ - environment: dict + dict + environment """ - # Try to get environement from vcvarsall.bat (Classical way) + # Try to get environment from vcvarsall.bat (Classical way) try: orig = get_unpatched(msvc9_query_vcvarsall) return orig(ver, arch, *args, **kwargs) @@ -153,17 +145,10 @@ def msvc9_query_vcvarsall(ver, arch='x86', *args, **kwargs): def msvc14_get_vc_env(plat_spec): """ Patched "distutils._msvccompiler._get_vc_env" for support extra - compilers. + Microsoft Visual C++ 14.X compilers. Set environment without use of "vcvarsall.bat". - Known supported compilers - ------------------------- - Microsoft Visual C++ 14.0: - Microsoft Visual C++ Build Tools 2015 (x86, x64, arm) - Microsoft Visual Studio 2017 (x86, x64, arm, arm64) - Microsoft Visual Studio Build Tools 2017 (x86, x64, arm, arm64) - Parameters ---------- plat_spec: str @@ -171,7 +156,8 @@ def msvc14_get_vc_env(plat_spec): Return ------ - environment: dict + dict + environment """ # Try to get environment from vcvarsall.bat (Classical way) try: @@ -217,9 +203,9 @@ def _augment_exception(exc, version, arch=''): if version == 9.0: if arch.lower().find('ia64') > -1: # For VC++ 9.0, if IA64 support is needed, redirect user - # to Windows SDK 7.0 - message += ' Get it with "Microsoft Windows SDK 7.0": ' - message += msdownload % 3138 + # to Windows SDK 7.0. + # Note: No download link available from Microsoft. + message += ' Get it with "Microsoft Windows SDK 7.0"' else: # For VC++ 9.0 redirect user to Vc++ for Python 2.7 : # This redirection link is maintained by Microsoft. @@ -230,8 +216,8 @@ def _augment_exception(exc, version, arch=''): message += ' Get it with "Microsoft Windows SDK 7.1": ' message += msdownload % 8279 elif version >= 14.0: - # For VC++ 14.0 Redirect user to Visual C++ Build Tools - message += (' Get it with "Microsoft Visual C++ Build Tools": ' + # For VC++ 14.X Redirect user to latest Visual C++ Build Tools + message += (' Get it with "Build Tools for Visual Studio": ' r'https://visualstudio.microsoft.com/downloads/') exc.args = (message, ) @@ -239,26 +225,50 @@ def _augment_exception(exc, version, arch=''): class PlatformInfo: """ - Current and Target Architectures informations. + Current and Target Architectures information. Parameters ---------- arch: str Target architecture. """ - current_cpu = safe_env.get('processor_architecture', '').lower() + current_cpu = environ.get('processor_architecture', '').lower() def __init__(self, arch): self.arch = arch.lower().replace('x64', 'amd64') @property def target_cpu(self): + """ + Return Target CPU architecture. + + Return + ------ + str + Target CPU + """ return self.arch[self.arch.find('_') + 1:] def target_is_x86(self): + """ + Return True if target CPU is x86 32 bits.. + + Return + ------ + bool + CPU is x86 32 bits + """ return self.target_cpu == 'x86' def current_is_x86(self): + """ + Return True if current CPU is x86 32 bits.. + + Return + ------ + bool + CPU is x86 32 bits + """ return self.current_cpu == 'x86' def current_dir(self, hidex86=False, x64=False): @@ -274,8 +284,8 @@ class PlatformInfo: Return ------ - subfolder: str - '\target', or '' (see hidex86 parameter) + str + subfolder: '\target', or '' (see hidex86 parameter) """ return ( '' if (self.current_cpu == 'x86' and hidex86) else @@ -296,8 +306,8 @@ class PlatformInfo: Return ------ - subfolder: str - '\current', or '' (see hidex86 parameter) + str + subfolder: '\current', or '' (see hidex86 parameter) """ return ( '' if (self.target_cpu == 'x86' and hidex86) else @@ -312,13 +322,13 @@ class PlatformInfo: Parameters ---------- forcex86: bool - Use 'x86' as current architecture even if current acritecture is + Use 'x86' as current architecture even if current architecture is not x86. Return ------ - subfolder: str - '' if target architecture is current architecture, + str + subfolder: '' if target architecture is current architecture, '\current_target' if not. """ current = 'x86' if forcex86 else self.current_cpu @@ -330,7 +340,7 @@ class PlatformInfo: class RegistryInfo: """ - Microsoft Visual Studio related registry informations. + Microsoft Visual Studio related registry information. Parameters ---------- @@ -349,6 +359,11 @@ class RegistryInfo: def visualstudio(self): """ Microsoft Visual Studio root registry key. + + Return + ------ + str + Registry key """ return 'VisualStudio' @@ -356,27 +371,47 @@ class RegistryInfo: def sxs(self): """ Microsoft Visual Studio SxS registry key. + + Return + ------ + str + Registry key """ - return os.path.join(self.visualstudio, 'SxS') + return join(self.visualstudio, 'SxS') @property def vc(self): """ Microsoft Visual C++ VC7 registry key. + + Return + ------ + str + Registry key """ - return os.path.join(self.sxs, 'VC7') + return join(self.sxs, 'VC7') @property def vs(self): """ Microsoft Visual Studio VS7 registry key. + + Return + ------ + str + Registry key """ - return os.path.join(self.sxs, 'VS7') + return join(self.sxs, 'VS7') @property def vc_for_python(self): """ Microsoft Visual C++ for Python registry key. + + Return + ------ + str + Registry key """ return r'DevDiv\VCForPython' @@ -384,6 +419,11 @@ class RegistryInfo: def microsoft_sdk(self): """ Microsoft SDK registry key. + + Return + ------ + str + Registry key """ return 'Microsoft SDKs' @@ -391,20 +431,35 @@ class RegistryInfo: def windows_sdk(self): """ Microsoft Windows/Platform SDK registry key. + + Return + ------ + str + Registry key """ - return os.path.join(self.microsoft_sdk, 'Windows') + return join(self.microsoft_sdk, 'Windows') @property def netfx_sdk(self): """ Microsoft .NET Framework SDK registry key. + + Return + ------ + str + Registry key """ - return os.path.join(self.microsoft_sdk, 'NETFXSDK') + return join(self.microsoft_sdk, 'NETFXSDK') @property def windows_kits_roots(self): """ Microsoft Windows Kits Roots registry key. + + Return + ------ + str + Registry key """ return r'Windows Kits\Installed Roots' @@ -421,10 +476,11 @@ class RegistryInfo: Return ------ - str: value + str + Registry key """ node64 = '' if self.pi.current_is_x86() or x86 else 'Wow6432Node' - return os.path.join('Software', node64, 'Microsoft', key) + return join('Software', node64, 'Microsoft', key) def lookup(self, key, name): """ @@ -439,18 +495,19 @@ class RegistryInfo: Return ------ - str: value + str + value """ - KEY_READ = winreg.KEY_READ + key_read = winreg.KEY_READ openkey = winreg.OpenKey ms = self.microsoft for hkey in self.HKEYS: try: - bkey = openkey(hkey, ms(key), 0, KEY_READ) + bkey = openkey(hkey, ms(key), 0, key_read) except (OSError, IOError): if not self.pi.current_is_x86(): try: - bkey = openkey(hkey, ms(key, True), 0, KEY_READ) + bkey = openkey(hkey, ms(key, True), 0, key_read) except (OSError, IOError): continue else: @@ -463,7 +520,7 @@ class RegistryInfo: class SystemInfo: """ - Microsoft Windows and Visual Studio related system inormations. + Microsoft Windows and Visual Studio related system information. Parameters ---------- @@ -474,30 +531,52 @@ class SystemInfo: """ # Variables and properties in this class use originals CamelCase variables - # names from Microsoft source files for more easy comparaison. - WinDir = safe_env.get('WinDir', '') - ProgramFiles = safe_env.get('ProgramFiles', '') - ProgramFilesx86 = safe_env.get('ProgramFiles(x86)', ProgramFiles) + # names from Microsoft source files for more easy comparison. + WinDir = environ.get('WinDir', '') + ProgramFiles = environ.get('ProgramFiles', '') + ProgramFilesx86 = environ.get('ProgramFiles(x86)', ProgramFiles) def __init__(self, registry_info, vc_ver=None): self.ri = registry_info self.pi = self.ri.pi - self.vc_ver = vc_ver or self._find_latest_available_vc_ver() - def _find_latest_available_vc_ver(self): - try: - return self.find_available_vc_vers()[-1] - except IndexError: - err = 'No Microsoft Visual C++ version found' - raise distutils.errors.DistutilsPlatformError(err) + self.known_vs_paths = self.find_programdata_vs_vers() + + # Except for VS15+, VC version is aligned with VS version + self.vs_ver = self.vc_ver = ( + vc_ver or self._find_latest_available_vs_ver()) + + def _find_latest_available_vs_ver(self): + """ + Find the latest VC version + + Return + ------ + float + version + """ + reg_vc_vers = self.find_reg_vs_vers() + + if not (reg_vc_vers or self.known_vs_paths): + raise distutils.errors.DistutilsPlatformError( + 'No Microsoft Visual C++ version found') + + vc_vers = set(reg_vc_vers) + vc_vers.update(self.known_vs_paths) + return sorted(vc_vers)[-1] - def find_available_vc_vers(self): + def find_reg_vs_vers(self): """ - Find all available Microsoft Visual C++ versions. + Find Microsoft Visual Studio versions available in registry. + + Return + ------ + list of float + Versions """ ms = self.ri.microsoft vckeys = (self.ri.vc, self.ri.vc_for_python, self.ri.vs) - vc_vers = [] + vs_vers = [] for hkey in self.ri.HKEYS: for key in vckeys: try: @@ -508,49 +587,108 @@ class SystemInfo: for i in range(values): try: ver = float(winreg.EnumValue(bkey, i)[0]) - if ver not in vc_vers: - vc_vers.append(ver) + if ver not in vs_vers: + vs_vers.append(ver) except ValueError: pass for i in range(subkeys): try: ver = float(winreg.EnumKey(bkey, i)) - if ver not in vc_vers: - vc_vers.append(ver) + if ver not in vs_vers: + vs_vers.append(ver) except ValueError: pass - return sorted(vc_vers) + return sorted(vs_vers) + + def find_programdata_vs_vers(self): + r""" + Find Visual studio 2017+ versions from information in + "C:\ProgramData\Microsoft\VisualStudio\Packages\_Instances". + + Return + ------ + dict + float version as key, path as value. + """ + vs_versions = {} + instances_dir = \ + r'C:\ProgramData\Microsoft\VisualStudio\Packages\_Instances' + + try: + hashed_names = listdir(instances_dir) + + except (OSError, IOError): + # Directory not exists with all Visual Studio versions + return vs_versions + + for name in hashed_names: + try: + # Get VS installation path from "state.json" file + state_path = join(instances_dir, name, 'state.json') + with open(state_path, 'rt', encoding='utf-8') as state_file: + state = json.load(state_file) + vs_path = state['installationPath'] + + # Raises OSError if this VS installation does not contain VC + listdir(join(vs_path, r'VC\Tools\MSVC')) + + # Store version and path + vs_versions[self._as_float_version( + state['installationVersion'])] = vs_path + + except (OSError, IOError, KeyError): + # Skip if "state.json" file is missing or bad format + continue + + return vs_versions + + @staticmethod + def _as_float_version(version): + """ + Return a string version as a simplified float version (major.minor) + + Parameters + ---------- + version: str + Version. + + Return + ------ + float + version + """ + return float('.'.join(version.split('.')[:2])) @property def VSInstallDir(self): """ Microsoft Visual Studio directory. + + Return + ------ + str + path """ # Default path - name = 'Microsoft Visual Studio %0.1f' % self.vc_ver - default = os.path.join(self.ProgramFilesx86, name) + default = join(self.ProgramFilesx86, + 'Microsoft Visual Studio %0.1f' % self.vs_ver) # Try to get path from registry, if fail use default path - return self.ri.lookup(self.ri.vs, '%0.1f' % self.vc_ver) or default + return self.ri.lookup(self.ri.vs, '%0.1f' % self.vs_ver) or default @property def VCInstallDir(self): """ Microsoft Visual C++ directory. - """ - self.VSInstallDir - - guess_vc = self._guess_vc() or self._guess_vc_legacy() - - # Try to get "VC++ for Python" path from registry as default path - reg_path = os.path.join(self.ri.vc_for_python, '%0.1f' % self.vc_ver) - python_vc = self.ri.lookup(reg_path, 'installdir') - default_vc = os.path.join(python_vc, 'VC') if python_vc else guess_vc - # Try to get path from registry, if fail use default path - path = self.ri.lookup(self.ri.vc, '%0.1f' % self.vc_ver) or default_vc + Return + ------ + str + path + """ + path = self._guess_vc() or self._guess_vc_legacy() - if not os.path.isdir(path): + if not isdir(path): msg = 'Microsoft Visual C++ directory not found' raise distutils.errors.DistutilsPlatformError(msg) @@ -558,186 +696,256 @@ class SystemInfo: def _guess_vc(self): """ - Locate Visual C for 2017 + Locate Visual C++ for VS2017+. + + Return + ------ + str + path """ - if self.vc_ver <= 14.0: - return + if self.vs_ver <= 14.0: + return '' + + try: + # First search in known VS paths + vs_dir = self.known_vs_paths[self.vs_ver] + except KeyError: + # Else, search with path from registry + vs_dir = self.VSInstallDir + + guess_vc = join(vs_dir, r'VC\Tools\MSVC') - default = r'VC\Tools\MSVC' - guess_vc = os.path.join(self.VSInstallDir, default) # Subdir with VC exact version as name try: - vc_exact_ver = os.listdir(guess_vc)[-1] - return os.path.join(guess_vc, vc_exact_ver) + # Update the VC version with real one instead of VS version + vc_ver = listdir(guess_vc)[-1] + self.vc_ver = self._as_float_version(vc_ver) + return join(guess_vc, vc_ver) except (OSError, IOError, IndexError): - pass + return '' def _guess_vc_legacy(self): """ - Locate Visual C for versions prior to 2017 + Locate Visual C++ for versions prior to 2017. + + Return + ------ + str + path """ - default = r'Microsoft Visual Studio %0.1f\VC' % self.vc_ver - return os.path.join(self.ProgramFilesx86, default) + default = join(self.ProgramFilesx86, + r'Microsoft Visual Studio %0.1f\VC' % self.vs_ver) + + # Try to get "VC++ for Python" path from registry as default path + reg_path = join(self.ri.vc_for_python, '%0.1f' % self.vs_ver) + python_vc = self.ri.lookup(reg_path, 'installdir') + default_vc = join(python_vc, 'VC') if python_vc else default + + # Try to get path from registry, if fail use default path + return self.ri.lookup(self.ri.vc, '%0.1f' % self.vs_ver) or default_vc @property def WindowsSdkVersion(self): """ Microsoft Windows SDK versions for specified MSVC++ version. - """ - if self.vc_ver <= 9.0: - return ('7.0', '6.1', '6.0a') - elif self.vc_ver == 10.0: - return ('7.1', '7.0a') - elif self.vc_ver == 11.0: - return ('8.0', '8.0a') - elif self.vc_ver == 12.0: - return ('8.1', '8.1a') - elif self.vc_ver >= 14.0: - return ('10.0', '8.1') + + Return + ------ + tuple of str + versions + """ + if self.vs_ver <= 9.0: + return '7.0', '6.1', '6.0a' + elif self.vs_ver == 10.0: + return '7.1', '7.0a' + elif self.vs_ver == 11.0: + return '8.0', '8.0a' + elif self.vs_ver == 12.0: + return '8.1', '8.1a' + elif self.vs_ver >= 14.0: + return '10.0', '8.1' @property def WindowsSdkLastVersion(self): """ - Microsoft Windows SDK last version + Microsoft Windows SDK last version. + + Return + ------ + str + version """ - return self._use_last_dir_name(os.path.join( - self.WindowsSdkDir, 'lib')) + return self._use_last_dir_name(join(self.WindowsSdkDir, 'lib')) @property def WindowsSdkDir(self): """ Microsoft Windows SDK directory. + + Return + ------ + str + path """ sdkdir = '' for ver in self.WindowsSdkVersion: # Try to get it from registry - loc = os.path.join(self.ri.windows_sdk, 'v%s' % ver) + loc = join(self.ri.windows_sdk, 'v%s' % ver) sdkdir = self.ri.lookup(loc, 'installationfolder') if sdkdir: break - if not sdkdir or not os.path.isdir(sdkdir): + if not sdkdir or not isdir(sdkdir): # Try to get "VC++ for Python" version from registry - path = os.path.join(self.ri.vc_for_python, '%0.1f' % self.vc_ver) + path = join(self.ri.vc_for_python, '%0.1f' % self.vc_ver) install_base = self.ri.lookup(path, 'installdir') if install_base: - sdkdir = os.path.join(install_base, 'WinSDK') - if not sdkdir or not os.path.isdir(sdkdir): + sdkdir = join(install_base, 'WinSDK') + if not sdkdir or not isdir(sdkdir): # If fail, use default new path for ver in self.WindowsSdkVersion: intver = ver[:ver.rfind('.')] - path = r'Microsoft SDKs\Windows Kits\%s' % (intver) - d = os.path.join(self.ProgramFiles, path) - if os.path.isdir(d): + path = r'Microsoft SDKs\Windows Kits\%s' % intver + d = join(self.ProgramFiles, path) + if isdir(d): sdkdir = d - if not sdkdir or not os.path.isdir(sdkdir): + if not sdkdir or not isdir(sdkdir): # If fail, use default old path for ver in self.WindowsSdkVersion: path = r'Microsoft SDKs\Windows\v%s' % ver - d = os.path.join(self.ProgramFiles, path) - if os.path.isdir(d): + d = join(self.ProgramFiles, path) + if isdir(d): sdkdir = d if not sdkdir: # If fail, use Platform SDK - sdkdir = os.path.join(self.VCInstallDir, 'PlatformSDK') + sdkdir = join(self.VCInstallDir, 'PlatformSDK') return sdkdir @property def WindowsSDKExecutablePath(self): """ Microsoft Windows SDK executable directory. + + Return + ------ + str + path """ # Find WinSDK NetFx Tools registry dir name - if self.vc_ver <= 11.0: + if self.vs_ver <= 11.0: netfxver = 35 arch = '' else: netfxver = 40 - hidex86 = True if self.vc_ver <= 12.0 else False + hidex86 = True if self.vs_ver <= 12.0 else False arch = self.pi.current_dir(x64=True, hidex86=hidex86) fx = 'WinSDK-NetFx%dTools%s' % (netfxver, arch.replace('\\', '-')) - # liste all possibles registry paths + # list all possibles registry paths regpaths = [] - if self.vc_ver >= 14.0: + if self.vs_ver >= 14.0: for ver in self.NetFxSdkVersion: - regpaths += [os.path.join(self.ri.netfx_sdk, ver, fx)] + regpaths += [join(self.ri.netfx_sdk, ver, fx)] for ver in self.WindowsSdkVersion: - regpaths += [os.path.join(self.ri.windows_sdk, 'v%sA' % ver, fx)] + regpaths += [join(self.ri.windows_sdk, 'v%sA' % ver, fx)] # Return installation folder from the more recent path for path in regpaths: execpath = self.ri.lookup(path, 'installationfolder') if execpath: - break - return execpath + return execpath @property def FSharpInstallDir(self): """ Microsoft Visual F# directory. + + Return + ------ + str + path """ - path = r'%0.1f\Setup\F#' % self.vc_ver - path = os.path.join(self.ri.visualstudio, path) + path = join(self.ri.visualstudio, r'%0.1f\Setup\F#' % self.vs_ver) return self.ri.lookup(path, 'productdir') or '' @property def UniversalCRTSdkDir(self): """ Microsoft Universal CRT SDK directory. + + Return + ------ + str + path """ # Set Kit Roots versions for specified MSVC++ version - if self.vc_ver >= 14.0: - vers = ('10', '81') - else: - vers = () + vers = ('10', '81') if self.vs_ver >= 14.0 else () # Find path of the more recent Kit for ver in vers: sdkdir = self.ri.lookup(self.ri.windows_kits_roots, 'kitsroot%s' % ver) if sdkdir: - break - return sdkdir or '' + return sdkdir or '' @property def UniversalCRTSdkLastVersion(self): """ - Microsoft Universal C Runtime SDK last version + Microsoft Universal C Runtime SDK last version. + + Return + ------ + str + version """ - return self._use_last_dir_name(os.path.join( - self.UniversalCRTSdkDir, 'lib')) + return self._use_last_dir_name(join(self.UniversalCRTSdkDir, 'lib')) @property def NetFxSdkVersion(self): """ Microsoft .NET Framework SDK versions. + + Return + ------ + tuple of str + versions """ - # Set FxSdk versions for specified MSVC++ version - if self.vc_ver >= 14.0: - return ('4.6.1', '4.6') - else: - return () + # Set FxSdk versions for specified VS version + return (('4.7.2', '4.7.1', '4.7', + '4.6.2', '4.6.1', '4.6', + '4.5.2', '4.5.1', '4.5') + if self.vs_ver >= 14.0 else ()) @property def NetFxSdkDir(self): """ Microsoft .NET Framework SDK directory. + + Return + ------ + str + path """ + sdkdir = '' for ver in self.NetFxSdkVersion: - loc = os.path.join(self.ri.netfx_sdk, ver) + loc = join(self.ri.netfx_sdk, ver) sdkdir = self.ri.lookup(loc, 'kitsinstallationfolder') if sdkdir: break - return sdkdir or '' + return sdkdir @property def FrameworkDir32(self): """ Microsoft .NET Framework 32bit directory. + + Return + ------ + str + path """ # Default path - guess_fw = os.path.join(self.WinDir, r'Microsoft.NET\Framework') + guess_fw = join(self.WinDir, r'Microsoft.NET\Framework') # Try to get path from registry, if fail use default path return self.ri.lookup(self.ri.vc, 'frameworkdir32') or guess_fw @@ -746,9 +954,14 @@ class SystemInfo: def FrameworkDir64(self): """ Microsoft .NET Framework 64bit directory. + + Return + ------ + str + path """ # Default path - guess_fw = os.path.join(self.WinDir, r'Microsoft.NET\Framework64') + guess_fw = join(self.WinDir, r'Microsoft.NET\Framework64') # Try to get path from registry, if fail use default path return self.ri.lookup(self.ri.vc, 'frameworkdir64') or guess_fw @@ -757,6 +970,11 @@ class SystemInfo: def FrameworkVersion32(self): """ Microsoft .NET Framework 32bit versions. + + Return + ------ + tuple of str + versions """ return self._find_dot_net_versions(32) @@ -764,6 +982,11 @@ class SystemInfo: def FrameworkVersion64(self): """ Microsoft .NET Framework 64bit versions. + + Return + ------ + tuple of str + versions """ return self._find_dot_net_versions(64) @@ -775,6 +998,11 @@ class SystemInfo: ---------- bits: int Platform number of bits: 32 or 64. + + Return + ------ + tuple of str + versions """ # Find actual .NET version in registry reg_ver = self.ri.lookup(self.ri.vc, 'frameworkver%d' % bits) @@ -782,18 +1010,17 @@ class SystemInfo: ver = reg_ver or self._use_last_dir_name(dot_net_dir, 'v') or '' # Set .NET versions for specified MSVC++ version - if self.vc_ver >= 12.0: - frameworkver = (ver, 'v4.0') - elif self.vc_ver >= 10.0: - frameworkver = ('v4.0.30319' if ver.lower()[:2] != 'v4' else ver, - 'v3.5') - elif self.vc_ver == 9.0: - frameworkver = ('v3.5', 'v2.0.50727') - if self.vc_ver == 8.0: - frameworkver = ('v3.0', 'v2.0.50727') - return frameworkver - - def _use_last_dir_name(self, path, prefix=''): + if self.vs_ver >= 12.0: + return ver, 'v4.0' + elif self.vs_ver >= 10.0: + return 'v4.0.30319' if ver.lower()[:2] != 'v4' else ver, 'v3.5' + elif self.vs_ver == 9.0: + return 'v3.5', 'v2.0.50727' + elif self.vs_ver == 8.0: + return 'v3.0', 'v2.0.50727' + + @staticmethod + def _use_last_dir_name(path, prefix=''): """ Return name of the last dir in path or '' if no dir found. @@ -802,12 +1029,17 @@ class SystemInfo: path: str Use dirs in this path prefix: str - Use only dirs startings by this prefix + Use only dirs starting by this prefix + + Return + ------ + str + name """ matching_dirs = ( dir_name - for dir_name in reversed(os.listdir(path)) - if os.path.isdir(os.path.join(path, dir_name)) and + for dir_name in reversed(listdir(path)) + if isdir(join(path, dir_name)) and dir_name.startswith(prefix) ) return next(matching_dirs, None) or '' @@ -818,7 +1050,7 @@ class EnvironmentInfo: Return environment variables for specified Microsoft Visual C++ version and platform : Lib, Include, Path and libpath. - This function is compatible with Microsoft Visual C++ 9.0 to 14.0. + This function is compatible with Microsoft Visual C++ 9.0 to 14.X. Script created by analysing Microsoft environment configuration files like "vcvars[...].bat", "SetEnv.Cmd", "vcbuildtools.bat", ... @@ -835,7 +1067,7 @@ class EnvironmentInfo: """ # Variables and properties in this class use originals CamelCase variables - # names from Microsoft source files for more easy comparaison. + # names from Microsoft source files for more easy comparison. def __init__(self, arch, vc_ver=None, vc_min_ver=0): self.pi = PlatformInfo(arch) @@ -847,204 +1079,257 @@ class EnvironmentInfo: raise distutils.errors.DistutilsPlatformError(err) @property + def vs_ver(self): + """ + Microsoft Visual Studio. + + Return + ------ + float + version + """ + return self.si.vs_ver + + @property def vc_ver(self): """ Microsoft Visual C++ version. + + Return + ------ + float + version """ return self.si.vc_ver @property def VSTools(self): """ - Microsoft Visual Studio Tools + Microsoft Visual Studio Tools. + + Return + ------ + list of str + paths """ paths = [r'Common7\IDE', r'Common7\Tools'] - if self.vc_ver >= 14.0: + if self.vs_ver >= 14.0: arch_subdir = self.pi.current_dir(hidex86=True, x64=True) paths += [r'Common7\IDE\CommonExtensions\Microsoft\TestWindow'] paths += [r'Team Tools\Performance Tools'] paths += [r'Team Tools\Performance Tools%s' % arch_subdir] - return [os.path.join(self.si.VSInstallDir, path) for path in paths] + return [join(self.si.VSInstallDir, path) for path in paths] @property def VCIncludes(self): """ - Microsoft Visual C++ & Microsoft Foundation Class Includes + Microsoft Visual C++ & Microsoft Foundation Class Includes. + + Return + ------ + list of str + paths """ - return [os.path.join(self.si.VCInstallDir, 'Include'), - os.path.join(self.si.VCInstallDir, r'ATLMFC\Include')] + return [join(self.si.VCInstallDir, 'Include'), + join(self.si.VCInstallDir, r'ATLMFC\Include')] @property def VCLibraries(self): """ - Microsoft Visual C++ & Microsoft Foundation Class Libraries + Microsoft Visual C++ & Microsoft Foundation Class Libraries. + + Return + ------ + list of str + paths """ - if self.vc_ver >= 15.0: + if self.vs_ver >= 15.0: arch_subdir = self.pi.target_dir(x64=True) else: arch_subdir = self.pi.target_dir(hidex86=True) paths = ['Lib%s' % arch_subdir, r'ATLMFC\Lib%s' % arch_subdir] - if self.vc_ver >= 14.0: + if self.vs_ver >= 14.0: paths += [r'Lib\store%s' % arch_subdir] - return [os.path.join(self.si.VCInstallDir, path) for path in paths] + return [join(self.si.VCInstallDir, path) for path in paths] @property def VCStoreRefs(self): """ - Microsoft Visual C++ store references Libraries + Microsoft Visual C++ store references Libraries. + + Return + ------ + list of str + paths """ - if self.vc_ver < 14.0: + if self.vs_ver < 14.0: return [] - return [os.path.join(self.si.VCInstallDir, r'Lib\store\references')] + return [join(self.si.VCInstallDir, r'Lib\store\references')] @property def VCTools(self): """ - Microsoft Visual C++ Tools + Microsoft Visual C++ Tools. + + Return + ------ + list of str + paths """ si = self.si - tools = [os.path.join(si.VCInstallDir, 'VCPackages')] + tools = [join(si.VCInstallDir, 'VCPackages')] - forcex86 = True if self.vc_ver <= 10.0 else False + forcex86 = True if self.vs_ver <= 10.0 else False arch_subdir = self.pi.cross_dir(forcex86) if arch_subdir: - tools += [os.path.join(si.VCInstallDir, 'Bin%s' % arch_subdir)] + tools += [join(si.VCInstallDir, 'Bin%s' % arch_subdir)] - if self.vc_ver == 14.0: + if self.vs_ver == 14.0: path = 'Bin%s' % self.pi.current_dir(hidex86=True) - tools += [os.path.join(si.VCInstallDir, path)] + tools += [join(si.VCInstallDir, path)] - elif self.vc_ver >= 15.0: + elif self.vs_ver >= 15.0: host_dir = (r'bin\HostX86%s' if self.pi.current_is_x86() else r'bin\HostX64%s') - tools += [os.path.join( + tools += [join( si.VCInstallDir, host_dir % self.pi.target_dir(x64=True))] if self.pi.current_cpu != self.pi.target_cpu: - tools += [os.path.join( + tools += [join( si.VCInstallDir, host_dir % self.pi.current_dir(x64=True))] else: - tools += [os.path.join(si.VCInstallDir, 'Bin')] + tools += [join(si.VCInstallDir, 'Bin')] return tools @property def OSLibraries(self): """ - Microsoft Windows SDK Libraries + Microsoft Windows SDK Libraries. + + Return + ------ + list of str + paths """ - if self.vc_ver <= 10.0: + if self.vs_ver <= 10.0: arch_subdir = self.pi.target_dir(hidex86=True, x64=True) - return [os.path.join(self.si.WindowsSdkDir, 'Lib%s' % arch_subdir)] + return [join(self.si.WindowsSdkDir, 'Lib%s' % arch_subdir)] else: arch_subdir = self.pi.target_dir(x64=True) - lib = os.path.join(self.si.WindowsSdkDir, 'lib') + lib = join(self.si.WindowsSdkDir, 'lib') libver = self._sdk_subdir - return [os.path.join(lib, '%sum%s' % (libver , arch_subdir))] + return [join(lib, '%sum%s' % (libver, arch_subdir))] @property def OSIncludes(self): """ - Microsoft Windows SDK Include + Microsoft Windows SDK Include. + + Return + ------ + list of str + paths """ - include = os.path.join(self.si.WindowsSdkDir, 'include') + include = join(self.si.WindowsSdkDir, 'include') - if self.vc_ver <= 10.0: - return [include, os.path.join(include, 'gl')] + if self.vs_ver <= 10.0: + return [include, join(include, 'gl')] else: - if self.vc_ver >= 14.0: + if self.vs_ver >= 14.0: sdkver = self._sdk_subdir else: sdkver = '' - return [os.path.join(include, '%sshared' % sdkver), - os.path.join(include, '%sum' % sdkver), - os.path.join(include, '%swinrt' % sdkver)] + return [join(include, '%sshared' % sdkver), + join(include, '%sum' % sdkver), + join(include, '%swinrt' % sdkver)] @property def OSLibpath(self): """ - Microsoft Windows SDK Libraries Paths + Microsoft Windows SDK Libraries Paths. + + Return + ------ + list of str + paths """ - ref = os.path.join(self.si.WindowsSdkDir, 'References') + ref = join(self.si.WindowsSdkDir, 'References') libpath = [] - if self.vc_ver <= 9.0: + if self.vs_ver <= 9.0: libpath += self.OSLibraries - if self.vc_ver >= 11.0: - libpath += [os.path.join(ref, r'CommonConfiguration\Neutral')] + if self.vs_ver >= 11.0: + libpath += [join(ref, r'CommonConfiguration\Neutral')] - if self.vc_ver >= 14.0: + if self.vs_ver >= 14.0: libpath += [ ref, - os.path.join(self.si.WindowsSdkDir, 'UnionMetadata'), - os.path.join( - ref, - 'Windows.Foundation.UniversalApiContract', - '1.0.0.0', - ), - os.path.join( - ref, - 'Windows.Foundation.FoundationContract', - '1.0.0.0', - ), - os.path.join( - ref, - 'Windows.Networking.Connectivity.WwanContract', - '1.0.0.0', - ), - os.path.join( - self.si.WindowsSdkDir, - 'ExtensionSDKs', - 'Microsoft.VCLibs', - '%0.1f' % self.vc_ver, - 'References', - 'CommonConfiguration', - 'neutral', - ), + join(self.si.WindowsSdkDir, 'UnionMetadata'), + join( + ref, 'Windows.Foundation.UniversalApiContract', '1.0.0.0'), + join(ref, 'Windows.Foundation.FoundationContract', '1.0.0.0'), + join( + ref, 'Windows.Networking.Connectivity.WwanContract', + '1.0.0.0'), + join( + self.si.WindowsSdkDir, 'ExtensionSDKs', 'Microsoft.VCLibs', + '%0.1f' % self.vs_ver, 'References', 'CommonConfiguration', + 'neutral'), ] return libpath @property def SdkTools(self): """ - Microsoft Windows SDK Tools + Microsoft Windows SDK Tools. + + Return + ------ + list of str + paths """ return list(self._sdk_tools()) def _sdk_tools(self): """ - Microsoft Windows SDK Tools paths generator + Microsoft Windows SDK Tools paths generator. + + Return + ------ + generator of str + paths """ - if self.vc_ver < 15.0: - bin_dir = 'Bin' if self.vc_ver <= 11.0 else r'Bin\x86' - yield os.path.join(self.si.WindowsSdkDir, bin_dir) + if self.vs_ver < 15.0: + bin_dir = 'Bin' if self.vs_ver <= 11.0 else r'Bin\x86' + yield join(self.si.WindowsSdkDir, bin_dir) if not self.pi.current_is_x86(): arch_subdir = self.pi.current_dir(x64=True) path = 'Bin%s' % arch_subdir - yield os.path.join(self.si.WindowsSdkDir, path) + yield join(self.si.WindowsSdkDir, path) - if self.vc_ver == 10.0 or self.vc_ver == 11.0: + if self.vs_ver in (10.0, 11.0): if self.pi.target_is_x86(): arch_subdir = '' else: arch_subdir = self.pi.current_dir(hidex86=True, x64=True) path = r'Bin\NETFX 4.0 Tools%s' % arch_subdir - yield os.path.join(self.si.WindowsSdkDir, path) + yield join(self.si.WindowsSdkDir, path) - elif self.vc_ver >= 15.0: - path = os.path.join(self.si.WindowsSdkDir, 'Bin') + elif self.vs_ver >= 15.0: + path = join(self.si.WindowsSdkDir, 'Bin') arch_subdir = self.pi.current_dir(x64=True) sdkver = self.si.WindowsSdkLastVersion - yield os.path.join(path, '%s%s' % (sdkver, arch_subdir)) + yield join(path, '%s%s' % (sdkver, arch_subdir)) if self.si.WindowsSDKExecutablePath: yield self.si.WindowsSDKExecutablePath @@ -1052,7 +1337,12 @@ class EnvironmentInfo: @property def _sdk_subdir(self): """ - Microsoft Windows SDK version subdir + Microsoft Windows SDK version subdir. + + Return + ------ + str + subdir """ ucrtver = self.si.WindowsSdkLastVersion return ('%s\\' % ucrtver) if ucrtver else '' @@ -1060,22 +1350,32 @@ class EnvironmentInfo: @property def SdkSetup(self): """ - Microsoft Windows SDK Setup + Microsoft Windows SDK Setup. + + Return + ------ + list of str + paths """ - if self.vc_ver > 9.0: + if self.vs_ver > 9.0: return [] - return [os.path.join(self.si.WindowsSdkDir, 'Setup')] + return [join(self.si.WindowsSdkDir, 'Setup')] @property def FxTools(self): """ - Microsoft .NET Framework Tools + Microsoft .NET Framework Tools. + + Return + ------ + list of str + paths """ pi = self.pi si = self.si - if self.vc_ver <= 10.0: + if self.vs_ver <= 10.0: include32 = True include64 = not pi.target_is_x86() and not pi.current_is_x86() else: @@ -1084,102 +1384,142 @@ class EnvironmentInfo: tools = [] if include32: - tools += [os.path.join(si.FrameworkDir32, ver) + tools += [join(si.FrameworkDir32, ver) for ver in si.FrameworkVersion32] if include64: - tools += [os.path.join(si.FrameworkDir64, ver) + tools += [join(si.FrameworkDir64, ver) for ver in si.FrameworkVersion64] return tools @property def NetFxSDKLibraries(self): """ - Microsoft .Net Framework SDK Libraries + Microsoft .Net Framework SDK Libraries. + + Return + ------ + list of str + paths """ - if self.vc_ver < 14.0 or not self.si.NetFxSdkDir: + if self.vs_ver < 14.0 or not self.si.NetFxSdkDir: return [] arch_subdir = self.pi.target_dir(x64=True) - return [os.path.join(self.si.NetFxSdkDir, r'lib\um%s' % arch_subdir)] + return [join(self.si.NetFxSdkDir, r'lib\um%s' % arch_subdir)] @property def NetFxSDKIncludes(self): """ - Microsoft .Net Framework SDK Includes + Microsoft .Net Framework SDK Includes. + + Return + ------ + list of str + paths """ - if self.vc_ver < 14.0 or not self.si.NetFxSdkDir: + if self.vs_ver < 14.0 or not self.si.NetFxSdkDir: return [] - return [os.path.join(self.si.NetFxSdkDir, r'include\um')] + return [join(self.si.NetFxSdkDir, r'include\um')] @property def VsTDb(self): """ - Microsoft Visual Studio Team System Database + Microsoft Visual Studio Team System Database. + + Return + ------ + list of str + paths """ - return [os.path.join(self.si.VSInstallDir, r'VSTSDB\Deploy')] + return [join(self.si.VSInstallDir, r'VSTSDB\Deploy')] @property def MSBuild(self): """ - Microsoft Build Engine + Microsoft Build Engine. + + Return + ------ + list of str + paths """ - if self.vc_ver < 12.0: + if self.vs_ver < 12.0: return [] - elif self.vc_ver < 15.0: + elif self.vs_ver < 15.0: base_path = self.si.ProgramFilesx86 arch_subdir = self.pi.current_dir(hidex86=True) else: base_path = self.si.VSInstallDir arch_subdir = '' - path = r'MSBuild\%0.1f\bin%s' % (self.vc_ver, arch_subdir) - build = [os.path.join(base_path, path)] + path = r'MSBuild\%0.1f\bin%s' % (self.vs_ver, arch_subdir) + build = [join(base_path, path)] - if self.vc_ver >= 15.0: + if self.vs_ver >= 15.0: # Add Roslyn C# & Visual Basic Compiler - build += [os.path.join(base_path, path, 'Roslyn')] + build += [join(base_path, path, 'Roslyn')] return build @property def HTMLHelpWorkshop(self): """ - Microsoft HTML Help Workshop + Microsoft HTML Help Workshop. + + Return + ------ + list of str + paths """ - if self.vc_ver < 11.0: + if self.vs_ver < 11.0: return [] - return [os.path.join(self.si.ProgramFilesx86, 'HTML Help Workshop')] + return [join(self.si.ProgramFilesx86, 'HTML Help Workshop')] @property def UCRTLibraries(self): """ - Microsoft Universal C Runtime SDK Libraries + Microsoft Universal C Runtime SDK Libraries. + + Return + ------ + list of str + paths """ - if self.vc_ver < 14.0: + if self.vs_ver < 14.0: return [] arch_subdir = self.pi.target_dir(x64=True) - lib = os.path.join(self.si.UniversalCRTSdkDir, 'lib') + lib = join(self.si.UniversalCRTSdkDir, 'lib') ucrtver = self._ucrt_subdir - return [os.path.join(lib, '%sucrt%s' % (ucrtver, arch_subdir))] + return [join(lib, '%sucrt%s' % (ucrtver, arch_subdir))] @property def UCRTIncludes(self): """ - Microsoft Universal C Runtime SDK Include + Microsoft Universal C Runtime SDK Include. + + Return + ------ + list of str + paths """ - if self.vc_ver < 14.0: + if self.vs_ver < 14.0: return [] - include = os.path.join(self.si.UniversalCRTSdkDir, 'include') - return [os.path.join(include, '%sucrt' % self._ucrt_subdir)] + include = join(self.si.UniversalCRTSdkDir, 'include') + return [join(include, '%sucrt' % self._ucrt_subdir)] @property def _ucrt_subdir(self): """ - Microsoft Universal C Runtime SDK version subdir + Microsoft Universal C Runtime SDK version subdir. + + Return + ------ + str + subdir """ ucrtver = self.si.UniversalCRTSdkLastVersion return ('%s\\' % ucrtver) if ucrtver else '' @@ -1187,31 +1527,52 @@ class EnvironmentInfo: @property def FSharp(self): """ - Microsoft Visual F# + Microsoft Visual F#. + + Return + ------ + list of str + paths """ - if self.vc_ver < 11.0 and self.vc_ver > 12.0: + if 11.0 > self.vs_ver > 12.0: return [] - return self.si.FSharpInstallDir + return [self.si.FSharpInstallDir] @property def VCRuntimeRedist(self): """ - Microsoft Visual C++ runtime redistribuable dll - """ - arch_subdir = self.pi.target_dir(x64=True) - if self.vc_ver < 15: - redist_path = self.si.VCInstallDir - vcruntime = 'redist%s\\Microsoft.VC%d0.CRT\\vcruntime%d0.dll' - else: - redist_path = self.si.VCInstallDir.replace('\\Tools', '\\Redist') - vcruntime = 'onecore%s\\Microsoft.VC%d0.CRT\\vcruntime%d0.dll' - - # Visual Studio 2017 is still Visual C++ 14.0 - dll_ver = 14.0 if self.vc_ver == 15 else self.vc_ver + Microsoft Visual C++ runtime redistributable dll. - vcruntime = vcruntime % (arch_subdir, self.vc_ver, dll_ver) - return os.path.join(redist_path, vcruntime) + Return + ------ + str + path + """ + vcruntime = 'vcruntime%d0.dll' % self.vc_ver + arch_subdir = self.pi.target_dir(x64=True).strip('\\') + + # Installation prefixes candidates + prefixes = [] + tools_path = self.si.VCInstallDir + redist_path = dirname(tools_path.replace(r'\Tools', r'\Redist')) + if isdir(redist_path): + # Redist version may not be exactly the same as tools + redist_path = join(redist_path, listdir(redist_path)[-1]) + prefixes += [redist_path, join(redist_path, 'onecore')] + + prefixes += [join(tools_path, 'redist')] # VS14 legacy path + + # CRT directory + crt_dirs = ('Microsoft.VC%d.CRT' % (self.vc_ver * 10), + # Sometime store in directory with VS version instead of VC + 'Microsoft.VC%d.CRT' % (int(self.vs_ver) * 10)) + + # vcruntime path + for prefix, crt_dir in itertools.product(prefixes, crt_dirs): + path = join(prefix, arch_subdir, crt_dir, vcruntime) + if isfile(path): + return path def return_env(self, exists=True): """ @@ -1221,6 +1582,11 @@ class EnvironmentInfo: ---------- exists: bool It True, only return existing paths. + + Return + ------ + dict + environment """ env = dict( include=self._build_paths('include', @@ -1254,7 +1620,7 @@ class EnvironmentInfo: self.FSharp], exists), ) - if self.vc_ver >= 14 and os.path.isfile(self.VCRuntimeRedist): + if self.vs_ver >= 14 and isfile(self.VCRuntimeRedist): env['py_vcruntime_redist'] = self.VCRuntimeRedist return env @@ -1265,20 +1631,35 @@ class EnvironmentInfo: unique, extant, directories from those paths and from the environment variable. Raise an error if no paths are resolved. + + Parameters + ---------- + name: str + Environment variable name + spec_path_lists: list of str + Paths + exists: bool + It True, only return existing paths. + + Return + ------ + str + Pathsep-separated paths """ # flatten spec_path_lists spec_paths = itertools.chain.from_iterable(spec_path_lists) - env_paths = safe_env.get(name, '').split(os.pathsep) + env_paths = environ.get(name, '').split(pathsep) paths = itertools.chain(spec_paths, env_paths) - extant_paths = list(filter(os.path.isdir, paths)) if exists else paths + extant_paths = list(filter(isdir, paths)) if exists else paths if not extant_paths: msg = "%s environment variable is empty" % name.upper() raise distutils.errors.DistutilsPlatformError(msg) unique_paths = self._unique_everseen(extant_paths) - return os.pathsep.join(unique_paths) + return pathsep.join(unique_paths) # from Python docs - def _unique_everseen(self, iterable, key=None): + @staticmethod + def _unique_everseen(iterable, key=None): """ List unique elements, preserving order. Remember all elements ever seen. diff --git a/setuptools/namespaces.py b/setuptools/namespaces.py index dc16106d..5f403c96 100644 --- a/setuptools/namespaces.py +++ b/setuptools/namespaces.py @@ -47,13 +47,17 @@ class Installer: "p = os.path.join(%(root)s, *%(pth)r)", "importlib = has_mfs and __import__('importlib.util')", "has_mfs and __import__('importlib.machinery')", - "m = has_mfs and " + ( + "m = has_mfs and " "sys.modules.setdefault(%(pkg)r, " - "importlib.util.module_from_spec(" - "importlib.machinery.PathFinder.find_spec(%(pkg)r, " - "[os.path.dirname(p)])))", - "m = m or " - "sys.modules.setdefault(%(pkg)r, types.ModuleType(%(pkg)r))", + "importlib.util.module_from_spec(" + "importlib.machinery.PathFinder.find_spec(%(pkg)r, " + "[os.path.dirname(p)])))" + ), + ( + "m = m or " + "sys.modules.setdefault(%(pkg)r, types.ModuleType(%(pkg)r))" + ), "mp = (m or []) and m.__dict__.setdefault('__path__',[])", "(p not in mp) and mp.append(p)", ) diff --git a/setuptools/package_index.py b/setuptools/package_index.py index 6b06f2ca..82eb4516 100644 --- a/setuptools/package_index.py +++ b/setuptools/package_index.py @@ -46,7 +46,8 @@ __all__ = [ _SOCKET_TIMEOUT = 15 _tmpl = "setuptools/{setuptools.__version__} Python-urllib/{py_major}" -user_agent = _tmpl.format(py_major=sys.version[:3], setuptools=setuptools) +user_agent = _tmpl.format( + py_major='{}.{}'.format(*sys.version_info), setuptools=setuptools) def parse_requirement_arg(spec): @@ -1092,7 +1093,8 @@ def open_with_auth(url, opener=urllib.request.urlopen): # copy of urllib.parse._splituser from Python 3.8 def _splituser(host): - """splituser('user[:passwd]@host[:port]') --> 'user[:passwd]', 'host[:port]'.""" + """splituser('user[:passwd]@host[:port]') + --> 'user[:passwd]', 'host[:port]'.""" user, delim, host = host.rpartition('@') return (user if delim else None), host diff --git a/setuptools/pep425tags.py b/setuptools/pep425tags.py deleted file mode 100644 index 48745a29..00000000 --- a/setuptools/pep425tags.py +++ /dev/null @@ -1,319 +0,0 @@ -# This file originally from pip: -# https://github.com/pypa/pip/blob/8f4f15a5a95d7d5b511ceaee9ed261176c181970/src/pip/_internal/pep425tags.py -"""Generate and work with PEP 425 Compatibility Tags.""" -from __future__ import absolute_import - -import distutils.util -from distutils import log -import platform -import re -import sys -import sysconfig -import warnings -from collections import OrderedDict - -from .extern import six - -from . import glibc - -_osx_arch_pat = re.compile(r'(.+)_(\d+)_(\d+)_(.+)') - - -def get_config_var(var): - try: - return sysconfig.get_config_var(var) - except IOError as e: # Issue #1074 - warnings.warn("{}".format(e), RuntimeWarning) - return None - - -def get_abbr_impl(): - """Return abbreviated implementation name.""" - if hasattr(sys, 'pypy_version_info'): - pyimpl = 'pp' - elif sys.platform.startswith('java'): - pyimpl = 'jy' - elif sys.platform == 'cli': - pyimpl = 'ip' - else: - pyimpl = 'cp' - return pyimpl - - -def get_impl_ver(): - """Return implementation version.""" - impl_ver = get_config_var("py_version_nodot") - if not impl_ver or get_abbr_impl() == 'pp': - impl_ver = ''.join(map(str, get_impl_version_info())) - return impl_ver - - -def get_impl_version_info(): - """Return sys.version_info-like tuple for use in decrementing the minor - version.""" - if get_abbr_impl() == 'pp': - # as per https://github.com/pypa/pip/issues/2882 - return (sys.version_info[0], sys.pypy_version_info.major, - sys.pypy_version_info.minor) - else: - return sys.version_info[0], sys.version_info[1] - - -def get_impl_tag(): - """ - Returns the Tag for this specific implementation. - """ - return "{}{}".format(get_abbr_impl(), get_impl_ver()) - - -def get_flag(var, fallback, expected=True, warn=True): - """Use a fallback method for determining SOABI flags if the needed config - var is unset or unavailable.""" - val = get_config_var(var) - if val is None: - if warn: - log.debug("Config variable '%s' is unset, Python ABI tag may " - "be incorrect", var) - return fallback() - return val == expected - - -def get_abi_tag(): - """Return the ABI tag based on SOABI (if available) or emulate SOABI - (CPython 2, PyPy).""" - soabi = get_config_var('SOABI') - impl = get_abbr_impl() - if not soabi and impl in {'cp', 'pp'} and hasattr(sys, 'maxunicode'): - d = '' - m = '' - u = '' - if get_flag('Py_DEBUG', - lambda: hasattr(sys, 'gettotalrefcount'), - warn=(impl == 'cp')): - d = 'd' - if get_flag('WITH_PYMALLOC', - lambda: impl == 'cp', - warn=(impl == 'cp')): - m = 'm' - if get_flag('Py_UNICODE_SIZE', - lambda: sys.maxunicode == 0x10ffff, - expected=4, - warn=(impl == 'cp' and - six.PY2)) \ - and six.PY2: - u = 'u' - abi = '%s%s%s%s%s' % (impl, get_impl_ver(), d, m, u) - elif soabi and soabi.startswith('cpython-'): - abi = 'cp' + soabi.split('-')[1] - elif soabi: - abi = soabi.replace('.', '_').replace('-', '_') - else: - abi = None - return abi - - -def _is_running_32bit(): - return sys.maxsize == 2147483647 - - -def get_platform(): - """Return our platform name 'win32', 'linux_x86_64'""" - if sys.platform == 'darwin': - # distutils.util.get_platform() returns the release based on the value - # of MACOSX_DEPLOYMENT_TARGET on which Python was built, which may - # be significantly older than the user's current machine. - release, _, machine = platform.mac_ver() - split_ver = release.split('.') - - if machine == "x86_64" and _is_running_32bit(): - machine = "i386" - elif machine == "ppc64" and _is_running_32bit(): - machine = "ppc" - - return 'macosx_{}_{}_{}'.format(split_ver[0], split_ver[1], machine) - - # XXX remove distutils dependency - result = distutils.util.get_platform().replace('.', '_').replace('-', '_') - if result == "linux_x86_64" and _is_running_32bit(): - # 32 bit Python program (running on a 64 bit Linux): pip should only - # install and run 32 bit compiled extensions in that case. - result = "linux_i686" - - return result - - -def is_manylinux1_compatible(): - # Only Linux, and only x86-64 / i686 - if get_platform() not in {"linux_x86_64", "linux_i686"}: - return False - - # Check for presence of _manylinux module - try: - import _manylinux - return bool(_manylinux.manylinux1_compatible) - except (ImportError, AttributeError): - # Fall through to heuristic check below - pass - - # Check glibc version. CentOS 5 uses glibc 2.5. - return glibc.have_compatible_glibc(2, 5) - - -def get_darwin_arches(major, minor, machine): - """Return a list of supported arches (including group arches) for - the given major, minor and machine architecture of a macOS machine. - """ - arches = [] - - def _supports_arch(major, minor, arch): - # Looking at the application support for macOS versions in the chart - # provided by https://en.wikipedia.org/wiki/OS_X#Versions it appears - # our timeline looks roughly like: - # - # 10.0 - Introduces ppc support. - # 10.4 - Introduces ppc64, i386, and x86_64 support, however the ppc64 - # and x86_64 support is CLI only, and cannot be used for GUI - # applications. - # 10.5 - Extends ppc64 and x86_64 support to cover GUI applications. - # 10.6 - Drops support for ppc64 - # 10.7 - Drops support for ppc - # - # Given that we do not know if we're installing a CLI or a GUI - # application, we must be conservative and assume it might be a GUI - # application and behave as if ppc64 and x86_64 support did not occur - # until 10.5. - # - # Note: The above information is taken from the "Application support" - # column in the chart not the "Processor support" since I believe - # that we care about what instruction sets an application can use - # not which processors the OS supports. - if arch == 'ppc': - return (major, minor) <= (10, 5) - if arch == 'ppc64': - return (major, minor) == (10, 5) - if arch == 'i386': - return (major, minor) >= (10, 4) - if arch == 'x86_64': - return (major, minor) >= (10, 5) - if arch in groups: - for garch in groups[arch]: - if _supports_arch(major, minor, garch): - return True - return False - - groups = OrderedDict([ - ("fat", ("i386", "ppc")), - ("intel", ("x86_64", "i386")), - ("fat64", ("x86_64", "ppc64")), - ("fat32", ("x86_64", "i386", "ppc")), - ]) - - if _supports_arch(major, minor, machine): - arches.append(machine) - - for garch in groups: - if machine in groups[garch] and _supports_arch(major, minor, garch): - arches.append(garch) - - arches.append('universal') - - return arches - - -def get_supported(versions=None, noarch=False, platform=None, - impl=None, abi=None): - """Return a list of supported tags for each version specified in - `versions`. - - :param versions: a list of string versions, of the form ["33", "32"], - or None. The first version will be assumed to support our ABI. - :param platform: specify the exact platform you want valid - tags for, or None. If None, use the local system platform. - :param impl: specify the exact implementation you want valid - tags for, or None. If None, use the local interpreter impl. - :param abi: specify the exact abi you want valid - tags for, or None. If None, use the local interpreter abi. - """ - supported = [] - - # Versions must be given with respect to the preference - if versions is None: - versions = [] - version_info = get_impl_version_info() - major = version_info[:-1] - # Support all previous minor Python versions. - for minor in range(version_info[-1], -1, -1): - versions.append(''.join(map(str, major + (minor,)))) - - impl = impl or get_abbr_impl() - - abis = [] - - abi = abi or get_abi_tag() - if abi: - abis[0:0] = [abi] - - abi3s = set() - import imp - for suffix in imp.get_suffixes(): - if suffix[0].startswith('.abi'): - abi3s.add(suffix[0].split('.', 2)[1]) - - abis.extend(sorted(list(abi3s))) - - abis.append('none') - - if not noarch: - arch = platform or get_platform() - if arch.startswith('macosx'): - # support macosx-10.6-intel on macosx-10.9-x86_64 - match = _osx_arch_pat.match(arch) - if match: - name, major, minor, actual_arch = match.groups() - tpl = '{}_{}_%i_%s'.format(name, major) - arches = [] - for m in reversed(range(int(minor) + 1)): - for a in get_darwin_arches(int(major), m, actual_arch): - arches.append(tpl % (m, a)) - else: - # arch pattern didn't match (?!) - arches = [arch] - elif platform is None and is_manylinux1_compatible(): - arches = [arch.replace('linux', 'manylinux1'), arch] - else: - arches = [arch] - - # Current version, current API (built specifically for our Python): - for abi in abis: - for arch in arches: - supported.append(('%s%s' % (impl, versions[0]), abi, arch)) - - # abi3 modules compatible with older version of Python - for version in versions[1:]: - # abi3 was introduced in Python 3.2 - if version in {'31', '30'}: - break - for abi in abi3s: # empty set if not Python 3 - for arch in arches: - supported.append(("%s%s" % (impl, version), abi, arch)) - - # Has binaries, does not use the Python API: - for arch in arches: - supported.append(('py%s' % (versions[0][0]), 'none', arch)) - - # No abi / arch, but requires our implementation: - supported.append(('%s%s' % (impl, versions[0]), 'none', 'any')) - # Tagged specifically as being cross-version compatible - # (with just the major version specified) - supported.append(('%s%s' % (impl, versions[0][0]), 'none', 'any')) - - # No abi / arch, generic Python - for i, version in enumerate(versions): - supported.append(('py%s' % (version,), 'none', 'any')) - if i == 0: - supported.append(('py%s' % (version[0]), 'none', 'any')) - - return supported - - -implementation_tag = get_impl_tag() diff --git a/setuptools/py27compat.py b/setuptools/py27compat.py index 2985011b..ba39af52 100644 --- a/setuptools/py27compat.py +++ b/setuptools/py27compat.py @@ -2,6 +2,7 @@ Compatibility Support for Python 2.7 and earlier """ +import sys import platform from setuptools.extern import six @@ -15,7 +16,7 @@ def get_all_headers(message, key): if six.PY2: - def get_all_headers(message, key): + def get_all_headers(message, key): # noqa return message.getheaders(key) @@ -26,3 +27,34 @@ linux_py2_ascii = ( rmtree_safe = str if linux_py2_ascii else lambda x: x """Workaround for http://bugs.python.org/issue24672""" + + +try: + from ._imp import find_module, PY_COMPILED, PY_FROZEN, PY_SOURCE + from ._imp import get_frozen_object, get_module +except ImportError: + import imp + from imp import PY_COMPILED, PY_FROZEN, PY_SOURCE # noqa + + def find_module(module, paths=None): + """Just like 'imp.find_module()', but with package support""" + parts = module.split('.') + while parts: + part = parts.pop(0) + f, path, (suffix, mode, kind) = info = imp.find_module(part, paths) + + if kind == imp.PKG_DIRECTORY: + parts = parts or ['__init__'] + paths = [path] + + elif parts: + raise ImportError("Can't find %r in %s" % (parts, module)) + + return info + + def get_frozen_object(module, paths): + return imp.get_frozen_object(module) + + def get_module(module, paths, info): + imp.load_module(module, *info) + return sys.modules[module] diff --git a/setuptools/py33compat.py b/setuptools/py33compat.py index 87cf5398..cb694436 100644 --- a/setuptools/py33compat.py +++ b/setuptools/py33compat.py @@ -52,4 +52,8 @@ class Bytecode_compat: Bytecode = getattr(dis, 'Bytecode', Bytecode_compat) -unescape = getattr(html, 'unescape', html_parser.HTMLParser().unescape) +unescape = getattr(html, 'unescape', None) +if unescape is None: + # HTMLParser.unescape is deprecated since Python 3.4, and will be removed + # from 3.9. + unescape = html_parser.HTMLParser().unescape diff --git a/setuptools/py34compat.py b/setuptools/py34compat.py new file mode 100644 index 00000000..3ad91722 --- /dev/null +++ b/setuptools/py34compat.py @@ -0,0 +1,13 @@ +import importlib + +try: + import importlib.util +except ImportError: + pass + + +try: + module_from_spec = importlib.util.module_from_spec +except AttributeError: + def module_from_spec(spec): + return spec.loader.load_module(spec.name) diff --git a/setuptools/sandbox.py b/setuptools/sandbox.py index 685f3f72..e46dfc8d 100644 --- a/setuptools/sandbox.py +++ b/setuptools/sandbox.py @@ -13,6 +13,8 @@ from setuptools.extern import six from setuptools.extern.six.moves import builtins, map import pkg_resources.py31compat +from distutils.errors import DistutilsError +from pkg_resources import working_set if sys.platform.startswith('java'): import org.python.modules.posix.PosixModule as _os @@ -23,8 +25,6 @@ try: except NameError: _file = None _open = open -from distutils.errors import DistutilsError -from pkg_resources import working_set __all__ = [ @@ -374,7 +374,7 @@ class AbstractSandbox: if hasattr(os, 'devnull'): - _EXCEPTIONS = [os.devnull,] + _EXCEPTIONS = [os.devnull] else: _EXCEPTIONS = [] @@ -466,7 +466,8 @@ class DirectorySandbox(AbstractSandbox): WRITE_FLAGS = functools.reduce( - operator.or_, [getattr(_os, a, 0) for a in + operator.or_, [ + getattr(_os, a, 0) for a in "O_WRONLY O_RDWR O_APPEND O_CREAT O_TRUNC O_TEMPORARY".split()] ) diff --git a/setuptools/site-patch.py b/setuptools/site-patch.py index 40b00de0..be0d43d3 100644 --- a/setuptools/site-patch.py +++ b/setuptools/site-patch.py @@ -38,22 +38,24 @@ def __boot(): else: raise ImportError("Couldn't find the real 'site' module") - known_paths = dict([(makepath(item)[1], 1) for item in sys.path]) # 2.2 comp + # 2.2 comp + known_paths = dict([( + makepath(item)[1], 1) for item in sys.path]) # noqa oldpos = getattr(sys, '__egginsert', 0) # save old insertion position sys.__egginsert = 0 # and reset the current one for item in PYTHONPATH: - addsitedir(item) + addsitedir(item) # noqa sys.__egginsert += oldpos # restore effective old position - d, nd = makepath(stdpath[0]) + d, nd = makepath(stdpath[0]) # noqa insert_at = None new_path = [] for item in sys.path: - p, np = makepath(item) + p, np = makepath(item) # noqa if np == nd and insert_at is None: # We've hit the first 'system' path entry, so added entries go here diff --git a/setuptools/ssl_support.py b/setuptools/ssl_support.py index 226db694..17c14c46 100644 --- a/setuptools/ssl_support.py +++ b/setuptools/ssl_support.py @@ -35,7 +35,8 @@ try: except AttributeError: HTTPSHandler = HTTPSConnection = object -is_available = ssl is not None and object not in (HTTPSHandler, HTTPSConnection) +is_available = ssl is not None and object not in ( + HTTPSHandler, HTTPSConnection) try: @@ -85,8 +86,10 @@ if not match_hostname: return dn.lower() == hostname.lower() # RFC 6125, section 6.4.3, subitem 1. - # The client SHOULD NOT attempt to match a presented identifier in which - # the wildcard character comprises a label other than the left-most label. + # The client SHOULD NOT attempt to match a + # presented identifier in which the wildcard + # character comprises a label other than the + # left-most label. if leftmost == '*': # When '*' is a fragment by itself, it matches a non-empty dotless # fragment. @@ -137,15 +140,16 @@ if not match_hostname: return dnsnames.append(value) if len(dnsnames) > 1: - raise CertificateError("hostname %r " - "doesn't match either of %s" + raise CertificateError( + "hostname %r doesn't match either of %s" % (hostname, ', '.join(map(repr, dnsnames)))) elif len(dnsnames) == 1: - raise CertificateError("hostname %r " - "doesn't match %r" + raise CertificateError( + "hostname %r doesn't match %r" % (hostname, dnsnames[0])) else: - raise CertificateError("no appropriate commonName or " + raise CertificateError( + "no appropriate commonName or " "subjectAltName fields were found") @@ -158,7 +162,8 @@ class VerifyingHTTPSHandler(HTTPSHandler): def https_open(self, req): return self.do_open( - lambda host, **kw: VerifyingHTTPSConn(host, self.ca_bundle, **kw), req + lambda host, **kw: VerifyingHTTPSConn(host, self.ca_bundle, **kw), + req ) diff --git a/setuptools/tests/__init__.py b/setuptools/tests/__init__.py index 5f4a1c29..9c77b51f 100644 --- a/setuptools/tests/__init__.py +++ b/setuptools/tests/__init__.py @@ -6,7 +6,7 @@ from setuptools.extern.six import PY2, PY3 __all__ = [ - 'fail_on_ascii', 'py2_only', 'py3_only' + 'fail_on_ascii', 'py2_only', 'py3_only' ] diff --git a/setuptools/tests/requirements.txt b/setuptools/tests/requirements.txt new file mode 100644 index 00000000..19bf5aef --- /dev/null +++ b/setuptools/tests/requirements.txt @@ -0,0 +1,12 @@ +mock +pytest-flake8 +flake8-2020; python_version>="3.6" +virtualenv>=13.0.0 +pytest-virtualenv>=1.2.7 +pytest>=3.7 +wheel +coverage>=4.5.1 +pytest-cov>=2.5.1 +paver; python_version>="3.6" +futures; python_version=="2.7" +pip>=19.1 # For proper file:// URLs support. diff --git a/setuptools/tests/server.py b/setuptools/tests/server.py index fc3a5975..8b17b081 100644 --- a/setuptools/tests/server.py +++ b/setuptools/tests/server.py @@ -1,10 +1,13 @@ """Basic http server for tests to simulate PyPI or custom indexes """ +import os import time import threading from setuptools.extern.six.moves import BaseHTTPServer, SimpleHTTPServer +from setuptools.extern.six.moves.urllib_parse import urljoin +from setuptools.extern.six.moves.urllib.request import pathname2url class IndexServer(BaseHTTPServer.HTTPServer): @@ -70,5 +73,19 @@ class MockServer(BaseHTTPServer.HTTPServer, threading.Thread): self.serve_forever() @property + def netloc(self): + return 'localhost:%s' % self.server_port + + @property def url(self): - return 'http://localhost:%(server_port)s/' % vars(self) + return 'http://%s/' % self.netloc + + +def path_to_url(path, authority=None): + """ Convert a path to a file: URL. """ + path = os.path.normpath(os.path.abspath(path)) + base = 'file:' + if authority is not None: + base += '//' + authority + url = urljoin(base, pathname2url(path)) + return url diff --git a/setuptools/tests/test_bdist_egg.py b/setuptools/tests/test_bdist_egg.py index e28f87de..8760ea30 100644 --- a/setuptools/tests/test_bdist_egg.py +++ b/setuptools/tests/test_bdist_egg.py @@ -43,7 +43,7 @@ class Test: # let's see if we got our egg link at the right place [content] = os.listdir('dist') - assert re.match(r'foo-0.0.0-py[23].\d.egg$', content) + assert re.match(r'foo-0.0.0-py[23].\d+.egg$', content) @pytest.mark.xfail( os.environ.get('PYTHONDONTWRITEBYTECODE'), diff --git a/setuptools/tests/test_build_clib.py b/setuptools/tests/test_build_clib.py index 3779e679..48bea2b4 100644 --- a/setuptools/tests/test_build_clib.py +++ b/setuptools/tests/test_build_clib.py @@ -8,8 +8,7 @@ from setuptools.dist import Distribution class TestBuildCLib: @mock.patch( - 'setuptools.command.build_clib.newer_pairwise_group' - ) + 'setuptools.command.build_clib.newer_pairwise_group') def test_build_libraries(self, mock_newer): dist = Distribution() cmd = build_clib(dist) diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index e1efe561..8fcf3055 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -23,6 +23,7 @@ class BuildBackendBase: self.env = env self.backend_name = backend_name + class BuildBackend(BuildBackendBase): """PEP 517 Build Backend""" @@ -262,6 +263,27 @@ class TestBuildMetaBackend: assert os.path.isfile( os.path.join(os.path.abspath("out_sdist"), sdist_name)) + def test_build_sdist_pyproject_toml_exists(self, tmpdir_cwd): + files = { + 'setup.py': DALS(""" + __import__('setuptools').setup( + name='foo', + version='0.0.0', + py_modules=['hello'] + )"""), + 'hello.py': '', + 'pyproject.toml': DALS(""" + [build-system] + requires = ["setuptools", "wheel"] + build-backend = "setuptools.build_meta + """), + } + build_files(files) + build_backend = self.get_build_backend() + targz_path = build_backend.build_sdist("temp") + with tarfile.open(os.path.join("temp", targz_path)) as tar: + assert any('pyproject.toml' in name for name in tar.getnames()) + def test_build_sdist_setup_py_exists(self, tmpdir_cwd): # If build_sdist is called from a script other than setup.py, # ensure setup.py is included @@ -385,6 +407,28 @@ class TestBuildMetaBackend: assert expected == sorted(actual) + _sys_argv_0_passthrough = { + 'setup.py': DALS(""" + import os + import sys + + __import__('setuptools').setup( + name='foo', + version='0.0.0', + ) + + sys_argv = os.path.abspath(sys.argv[0]) + file_path = os.path.abspath('setup.py') + assert sys_argv == file_path + """) + } + + def test_sys_argv_passthrough(self, tmpdir_cwd): + build_files(self._sys_argv_0_passthrough) + build_backend = self.get_build_backend() + with pytest.raises(AssertionError): + build_backend.build_sdist("temp") + class TestBuildMetaLegacyBackend(TestBuildMetaBackend): backend_name = 'setuptools.build_meta:__legacy__' @@ -396,3 +440,9 @@ class TestBuildMetaLegacyBackend(TestBuildMetaBackend): build_backend = self.get_build_backend() build_backend.build_sdist("temp") + + def test_sys_argv_passthrough(self, tmpdir_cwd): + build_files(self._sys_argv_0_passthrough) + + build_backend = self.get_build_backend() + build_backend.build_sdist("temp") diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index bc97664d..2fa0b374 100644 --- a/setuptools/tests/test_config.py +++ b/setuptools/tests/test_config.py @@ -1,4 +1,4 @@ -# -*- coding: UTF-8 -*- +# -*- coding: utf-8 -*- from __future__ import unicode_literals import contextlib @@ -695,7 +695,7 @@ class TestOptions: ) with get_dist(tmpdir) as dist: assert set(dist.packages) == set( - ['fake_package', 'fake_package.sub_two']) + ['fake_package', 'fake_package.sub_two']) @py2_only def test_find_namespace_directive_fails_on_py2(self, tmpdir): @@ -748,7 +748,7 @@ class TestOptions: ) with get_dist(tmpdir) as dist: assert set(dist.packages) == { - 'fake_package', 'fake_package.sub_two' + 'fake_package', 'fake_package.sub_two' } def test_extras_require(self, tmpdir): @@ -819,6 +819,40 @@ class TestOptions: ] assert sorted(dist.data_files) == sorted(expected) + def test_python_requires_simple(self, tmpdir): + fake_env( + tmpdir, + DALS(""" + [options] + python_requires=>=2.7 + """), + ) + with get_dist(tmpdir) as dist: + dist.parse_config_files() + + def test_python_requires_compound(self, tmpdir): + fake_env( + tmpdir, + DALS(""" + [options] + python_requires=>=2.7,!=3.0.* + """), + ) + with get_dist(tmpdir) as dist: + dist.parse_config_files() + + def test_python_requires_invalid(self, tmpdir): + fake_env( + tmpdir, + DALS(""" + [options] + python_requires=invalid + """), + ) + with pytest.raises(Exception): + with get_dist(tmpdir) as dist: + dist.parse_config_files() + saved_dist_init = _Distribution.__init__ @@ -847,7 +881,7 @@ class TestExternalSetters: return None @patch.object(_Distribution, '__init__', autospec=True) - def test_external_setters(self, mock_parent_init, tmpdir): + def test_external_setters(self, mock_parent_init, tmpdir): mock_parent_init.side_effect = self._fake_distribution_init dist = Distribution(attrs={ diff --git a/setuptools/tests/test_develop.py b/setuptools/tests/test_develop.py index 00d4bd9a..792975fd 100644 --- a/setuptools/tests/test_develop.py +++ b/setuptools/tests/test_develop.py @@ -95,7 +95,7 @@ class TestDevelop: with io.open(fn) as init_file: init = init_file.read().strip() - expected = 'print("foo")' if six.PY3 else 'print "foo"' + expected = 'print "foo"' if six.PY2 else 'print("foo")' assert init == expected def test_console_scripts(self, tmpdir): @@ -161,7 +161,7 @@ class TestNamespaces: reason="https://github.com/pypa/setuptools/issues/851", ) @pytest.mark.skipif( - platform.python_implementation() == 'PyPy' and six.PY3, + platform.python_implementation() == 'PyPy' and not six.PY2, reason="https://github.com/pypa/setuptools/issues/1202", ) def test_namespace_package_importable(self, tmpdir): diff --git a/setuptools/tests/test_dist.py b/setuptools/tests/test_dist.py index 390c3dfc..6e8c45fd 100644 --- a/setuptools/tests/test_dist.py +++ b/setuptools/tests/test_dist.py @@ -3,7 +3,14 @@ from __future__ import unicode_literals import io -from setuptools.dist import DistDeprecationWarning, _get_unpatched +import collections +import re +from distutils.errors import DistutilsSetupError +from setuptools.dist import ( + _get_unpatched, + check_package_data, + DistDeprecationWarning, +) from setuptools import Distribution from setuptools.extern.six.moves.urllib.request import pathname2url from setuptools.extern.six.moves.urllib_parse import urljoin @@ -54,7 +61,8 @@ def test_dist_fetch_build_egg(tmpdir): dist.fetch_build_egg(r) for r in reqs ] - assert [dist.key for dist in resolved_dists if dist] == reqs + # noqa below because on Python 2 it causes flakes + assert [dist.key for dist in resolved_dists if dist] == reqs # noqa def test_dist__get_unpatched_deprecated(): @@ -263,3 +271,64 @@ def test_maintainer_author(name, attrs, tmpdir): else: line = '%s: %s' % (fkey, val) assert line in pkg_lines_set + + +def test_provides_extras_deterministic_order(): + extras = collections.OrderedDict() + extras['a'] = ['foo'] + extras['b'] = ['bar'] + attrs = dict(extras_require=extras) + dist = Distribution(attrs) + assert dist.metadata.provides_extras == ['a', 'b'] + attrs['extras_require'] = collections.OrderedDict( + reversed(list(attrs['extras_require'].items()))) + dist = Distribution(attrs) + assert dist.metadata.provides_extras == ['b', 'a'] + + +CHECK_PACKAGE_DATA_TESTS = ( + # Valid. + ({ + '': ['*.txt', '*.rst'], + 'hello': ['*.msg'], + }, None), + # Not a dictionary. + (( + ('', ['*.txt', '*.rst']), + ('hello', ['*.msg']), + ), ( + "'package_data' must be a dictionary mapping package" + " names to lists of string wildcard patterns" + )), + # Invalid key type. + ({ + 400: ['*.txt', '*.rst'], + }, ( + "keys of 'package_data' dict must be strings (got 400)" + )), + # Invalid value type. + ({ + 'hello': str('*.msg'), + }, ( + "\"values of 'package_data' dict\" " + "must be a list of strings (got '*.msg')" + )), + # Invalid value type (generators are single use) + ({ + 'hello': (x for x in "generator"), + }, ( + "\"values of 'package_data' dict\" must be a list of strings " + "(got <generator object" + )), +) + + +@pytest.mark.parametrize( + 'package_data, expected_message', CHECK_PACKAGE_DATA_TESTS) +def test_check_package_data(package_data, expected_message): + if expected_message is None: + assert check_package_data(None, 'package_data', package_data) is None + else: + with pytest.raises( + DistutilsSetupError, match=re.escape(expected_message)): + check_package_data(None, str('package_data'), package_data) diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py index c3fd1c6e..534392b9 100644 --- a/setuptools/tests/test_easy_install.py +++ b/setuptools/tests/test_easy_install.py @@ -15,28 +15,29 @@ import distutils.errors import io import zipfile import mock -from setuptools.command.easy_install import ( - EasyInstallDeprecationWarning, ScriptWriter, WindowsScriptWriter, -) import time + from setuptools.extern import six -from setuptools.extern.six.moves import urllib import pytest from setuptools import sandbox from setuptools.sandbox import run_setup import setuptools.command.easy_install as ei -from setuptools.command.easy_install import PthDistributions +from setuptools.command.easy_install import ( + EasyInstallDeprecationWarning, ScriptWriter, PthDistributions, + WindowsScriptWriter, +) from setuptools.command import easy_install as easy_install_pkg from setuptools.dist import Distribution from pkg_resources import normalize_path, working_set from pkg_resources import Distribution as PRDistribution -import setuptools.tests.server +from setuptools.tests.server import MockServer, path_to_url from setuptools.tests import fail_on_ascii import pkg_resources from . import contexts +from .files import build_files from .textwrap import DALS __metaclass__ = type @@ -440,35 +441,40 @@ def distutils_package(): yield +@pytest.fixture +def mock_index(): + # set up a server which will simulate an alternate package index. + p_index = MockServer() + if p_index.server_port == 0: + # Some platforms (Jython) don't find a port to which to bind, + # so skip test for them. + pytest.skip("could not find a valid port") + p_index.start() + return p_index + + class TestDistutilsPackage: def test_bdist_egg_available_on_distutils_pkg(self, distutils_package): run_setup('setup.py', ['bdist_egg']) class TestSetupRequires: - def test_setup_requires_honors_fetch_params(self): + + def test_setup_requires_honors_fetch_params(self, mock_index, monkeypatch): """ When easy_install installs a source distribution which specifies setup_requires, it should honor the fetch parameters (such as - allow-hosts, index-url, and find-links). + index-url, and find-links). """ - # set up a server which will simulate an alternate package index. - p_index = setuptools.tests.server.MockServer() - p_index.start() - netloc = 1 - p_index_loc = urllib.parse.urlparse(p_index.url)[netloc] - if p_index_loc.endswith(':0'): - # Some platforms (Jython) don't find a port to which to bind, - # so skip this test for them. - return + monkeypatch.setenv(str('PIP_RETRIES'), str('0')) + monkeypatch.setenv(str('PIP_TIMEOUT'), str('0')) with contexts.quiet(): # create an sdist that has a build-time dependency. with TestSetupRequires.create_sdist() as dist_file: with contexts.tempdir() as temp_install_dir: with contexts.environment(PYTHONPATH=temp_install_dir): ei_params = [ - '--index-url', p_index.url, - '--allow-hosts', p_index_loc, + '--index-url', mock_index.url, '--exclude-scripts', '--install-dir', temp_install_dir, dist_file, @@ -478,10 +484,8 @@ class TestSetupRequires: # fail because it doesn't exist. with pytest.raises(SystemExit): easy_install_pkg.main(ei_params) - # there should have been two or three requests to the server - # (three happens on Python 3.3a) - assert 2 <= len(p_index.requests) <= 3 - assert p_index.requests[0].path == '/does-not-exist/' + # there should have been one requests to the server + assert [r.path for r in mock_index.requests] == ['/does-not-exist/'] @staticmethod @contextlib.contextmanager @@ -500,7 +504,9 @@ class TestSetupRequires: version="1.0", setup_requires = ['does-not-exist'], ) - """))]) + """)), + ('setup.cfg', ''), + ]) yield dist_path use_setup_cfg = ( @@ -623,7 +629,7 @@ class TestSetupRequires: test_pkg = create_setup_requires_package( temp_dir, setup_attrs=dict(version='attr: foobar.version'), make_package=make_dependency_sdist, - use_setup_cfg=use_setup_cfg+('version',), + use_setup_cfg=use_setup_cfg + ('version',), ) test_setup_py = os.path.join(test_pkg, 'setup.py') with contexts.quiet() as (stdout, stderr): @@ -632,6 +638,208 @@ class TestSetupRequires: assert len(lines) > 0 assert lines[-1].strip() == '42' + def test_setup_requires_honors_pip_env(self, mock_index, monkeypatch): + monkeypatch.setenv(str('PIP_RETRIES'), str('0')) + monkeypatch.setenv(str('PIP_TIMEOUT'), str('0')) + monkeypatch.setenv(str('PIP_INDEX_URL'), mock_index.url) + with contexts.save_pkg_resources_state(): + with contexts.tempdir() as temp_dir: + test_pkg = create_setup_requires_package( + temp_dir, 'python-xlib', '0.19', + setup_attrs=dict(dependency_links=[])) + test_setup_cfg = os.path.join(test_pkg, 'setup.cfg') + with open(test_setup_cfg, 'w') as fp: + fp.write(DALS( + ''' + [easy_install] + index_url = https://pypi.org/legacy/ + ''')) + test_setup_py = os.path.join(test_pkg, 'setup.py') + with pytest.raises(distutils.errors.DistutilsError): + run_setup(test_setup_py, [str('--version')]) + assert len(mock_index.requests) == 1 + assert mock_index.requests[0].path == '/python-xlib/' + + def test_setup_requires_with_pep508_url(self, mock_index, monkeypatch): + monkeypatch.setenv(str('PIP_RETRIES'), str('0')) + monkeypatch.setenv(str('PIP_TIMEOUT'), str('0')) + monkeypatch.setenv(str('PIP_INDEX_URL'), mock_index.url) + with contexts.save_pkg_resources_state(): + with contexts.tempdir() as temp_dir: + dep_sdist = os.path.join(temp_dir, 'dep.tar.gz') + make_trivial_sdist(dep_sdist, 'dependency', '42') + dep_url = path_to_url(dep_sdist, authority='localhost') + test_pkg = create_setup_requires_package( + temp_dir, + # Ignored (overriden by setup_attrs) + 'python-xlib', '0.19', + setup_attrs=dict( + setup_requires='dependency @ %s' % dep_url)) + test_setup_py = os.path.join(test_pkg, 'setup.py') + run_setup(test_setup_py, [str('--version')]) + assert len(mock_index.requests) == 0 + + def test_setup_requires_with_allow_hosts(self, mock_index): + ''' The `allow-hosts` option in not supported anymore. ''' + with contexts.save_pkg_resources_state(): + with contexts.tempdir() as temp_dir: + test_pkg = os.path.join(temp_dir, 'test_pkg') + test_setup_py = os.path.join(test_pkg, 'setup.py') + test_setup_cfg = os.path.join(test_pkg, 'setup.cfg') + os.mkdir(test_pkg) + with open(test_setup_py, 'w') as fp: + fp.write(DALS( + ''' + from setuptools import setup + setup(setup_requires='python-xlib') + ''')) + with open(test_setup_cfg, 'w') as fp: + fp.write(DALS( + ''' + [easy_install] + allow_hosts = * + ''')) + with pytest.raises(distutils.errors.DistutilsError): + run_setup(test_setup_py, [str('--version')]) + assert len(mock_index.requests) == 0 + + def test_setup_requires_with_python_requires(self, monkeypatch, tmpdir): + ''' Check `python_requires` is honored. ''' + monkeypatch.setenv(str('PIP_RETRIES'), str('0')) + monkeypatch.setenv(str('PIP_TIMEOUT'), str('0')) + monkeypatch.setenv(str('PIP_NO_INDEX'), str('1')) + monkeypatch.setenv(str('PIP_VERBOSE'), str('1')) + dep_1_0_sdist = 'dep-1.0.tar.gz' + dep_1_0_url = path_to_url(str(tmpdir / dep_1_0_sdist)) + dep_1_0_python_requires = '>=2.7' + make_python_requires_sdist( + str(tmpdir / dep_1_0_sdist), 'dep', '1.0', dep_1_0_python_requires) + dep_2_0_sdist = 'dep-2.0.tar.gz' + dep_2_0_url = path_to_url(str(tmpdir / dep_2_0_sdist)) + dep_2_0_python_requires = '!=' + '.'.join( + map(str, sys.version_info[:2])) + '.*' + make_python_requires_sdist( + str(tmpdir / dep_2_0_sdist), 'dep', '2.0', dep_2_0_python_requires) + index = tmpdir / 'index.html' + index.write_text(DALS( + ''' + <!DOCTYPE html> + <html><head><title>Links for dep</title></head> + <body> + <h1>Links for dep</h1> + <a href="{dep_1_0_url}" data-requires-python="{dep_1_0_python_requires}">{dep_1_0_sdist}</a><br/> + <a href="{dep_2_0_url}" data-requires-python="{dep_2_0_python_requires}">{dep_2_0_sdist}</a><br/> + </body> + </html> + ''').format( # noqa + dep_1_0_url=dep_1_0_url, + dep_1_0_sdist=dep_1_0_sdist, + dep_1_0_python_requires=dep_1_0_python_requires, + dep_2_0_url=dep_2_0_url, + dep_2_0_sdist=dep_2_0_sdist, + dep_2_0_python_requires=dep_2_0_python_requires, + ), 'utf-8') + index_url = path_to_url(str(index)) + with contexts.save_pkg_resources_state(): + test_pkg = create_setup_requires_package( + str(tmpdir), + 'python-xlib', '0.19', # Ignored (overriden by setup_attrs). + setup_attrs=dict( + setup_requires='dep', dependency_links=[index_url])) + test_setup_py = os.path.join(test_pkg, 'setup.py') + run_setup(test_setup_py, [str('--version')]) + eggs = list(map(str, pkg_resources.find_distributions( + os.path.join(test_pkg, '.eggs')))) + assert eggs == ['dep 1.0'] + + @pytest.mark.parametrize( + 'use_legacy_installer,with_dependency_links_in_setup_py', + itertools.product((False, True), (False, True))) + def test_setup_requires_with_find_links_in_setup_cfg( + self, monkeypatch, use_legacy_installer, + with_dependency_links_in_setup_py): + monkeypatch.setenv(str('PIP_RETRIES'), str('0')) + monkeypatch.setenv(str('PIP_TIMEOUT'), str('0')) + with contexts.save_pkg_resources_state(): + with contexts.tempdir() as temp_dir: + make_trivial_sdist( + os.path.join(temp_dir, 'python-xlib-42.tar.gz'), + 'python-xlib', + '42') + test_pkg = os.path.join(temp_dir, 'test_pkg') + test_setup_py = os.path.join(test_pkg, 'setup.py') + test_setup_cfg = os.path.join(test_pkg, 'setup.cfg') + os.mkdir(test_pkg) + with open(test_setup_py, 'w') as fp: + if with_dependency_links_in_setup_py: + dependency_links = [os.path.join(temp_dir, 'links')] + else: + dependency_links = [] + fp.write(DALS( + ''' + from setuptools import installer, setup + if {use_legacy_installer}: + installer.fetch_build_egg = installer._legacy_fetch_build_egg + setup(setup_requires='python-xlib==42', + dependency_links={dependency_links!r}) + ''').format(use_legacy_installer=use_legacy_installer, # noqa + dependency_links=dependency_links)) + with open(test_setup_cfg, 'w') as fp: + fp.write(DALS( + ''' + [easy_install] + index_url = {index_url} + find_links = {find_links} + ''').format(index_url=os.path.join(temp_dir, 'index'), + find_links=temp_dir)) + run_setup(test_setup_py, [str('--version')]) + + def test_setup_requires_with_transitive_extra_dependency( + self, monkeypatch): + # Use case: installing a package with a build dependency on + # an already installed `dep[extra]`, which in turn depends + # on `extra_dep` (whose is not already installed). + with contexts.save_pkg_resources_state(): + with contexts.tempdir() as temp_dir: + # Create source distribution for `extra_dep`. + make_trivial_sdist( + os.path.join(temp_dir, 'extra_dep-1.0.tar.gz'), + 'extra_dep', '1.0') + # Create source tree for `dep`. + dep_pkg = os.path.join(temp_dir, 'dep') + os.mkdir(dep_pkg) + build_files({ + 'setup.py': + DALS(""" + import setuptools + setuptools.setup( + name='dep', version='2.0', + extras_require={'extra': ['extra_dep']}, + ) + """), + 'setup.cfg': '', + }, prefix=dep_pkg) + # "Install" dep. + run_setup( + os.path.join(dep_pkg, 'setup.py'), [str('dist_info')]) + working_set.add_entry(dep_pkg) + # Create source tree for test package. + test_pkg = os.path.join(temp_dir, 'test_pkg') + test_setup_py = os.path.join(test_pkg, 'setup.py') + os.mkdir(test_pkg) + with open(test_setup_py, 'w') as fp: + fp.write(DALS( + ''' + from setuptools import installer, setup + setup(setup_requires='dep[extra]') + ''')) + # Check... + monkeypatch.setenv(str('PIP_FIND_LINKS'), str(temp_dir)) + monkeypatch.setenv(str('PIP_NO_INDEX'), str('1')) + monkeypatch.setenv(str('PIP_RETRIES'), str('0')) + monkeypatch.setenv(str('PIP_TIMEOUT'), str('0')) + run_setup(test_setup_py, [str('--version')]) + def make_trivial_sdist(dist_path, distname, version): """ @@ -647,7 +855,9 @@ def make_trivial_sdist(dist_path, distname, version): name=%r, version=%r ) - """ % (distname, version)))]) + """ % (distname, version))), + ('setup.cfg', ''), + ]) def make_nspkg_sdist(dist_path, distname, version): @@ -683,12 +893,32 @@ def make_nspkg_sdist(dist_path, distname, version): make_sdist(dist_path, files) +def make_python_requires_sdist(dist_path, distname, version, python_requires): + make_sdist(dist_path, [ + ( + 'setup.py', + DALS("""\ + import setuptools + setuptools.setup( + name={name!r}, + version={version!r}, + python_requires={python_requires!r}, + ) + """).format( + name=distname, version=version, + python_requires=python_requires)), + ('setup.cfg', ''), + ]) + + def make_sdist(dist_path, files): """ Create a simple sdist tarball at dist_path, containing the files listed in ``files`` as ``(filename, content)`` tuples. """ + # Distributions with only one file don't play well with pip. + assert len(files) > 1 with tarfile.open(dist_path, 'w:gz') as dist: for filename, content in files: file_bytes = io.BytesIO(content.encode('utf-8')) @@ -721,8 +951,8 @@ def create_setup_requires_package(path, distname='foobar', version='0.1', test_pkg = os.path.join(path, 'test_pkg') os.mkdir(test_pkg) + # setup.cfg if use_setup_cfg: - test_setup_cfg = os.path.join(test_pkg, 'setup.cfg') options = [] metadata = [] for name in use_setup_cfg: @@ -734,27 +964,29 @@ def create_setup_requires_package(path, distname='foobar', version='0.1', if isinstance(value, (tuple, list)): value = ';'.join(value) section.append('%s: %s' % (name, value)) - with open(test_setup_cfg, 'w') as f: - f.write(DALS( - """ - [metadata] - {metadata} - [options] - {options} - """ - ).format( - options='\n'.join(options), - metadata='\n'.join(metadata), - )) - - test_setup_py = os.path.join(test_pkg, 'setup.py') + test_setup_cfg_contents = DALS( + """ + [metadata] + {metadata} + [options] + {options} + """ + ).format( + options='\n'.join(options), + metadata='\n'.join(metadata), + ) + else: + test_setup_cfg_contents = '' + with open(os.path.join(test_pkg, 'setup.cfg'), 'w') as f: + f.write(test_setup_cfg_contents) + # setup.py if setup_py_template is None: setup_py_template = DALS("""\ import setuptools setuptools.setup(**%r) """) - with open(test_setup_py, 'w') as f: + with open(os.path.join(test_pkg, 'setup.py'), 'w') as f: f.write(setup_py_template % test_setup_attrs) foobar_path = os.path.join(path, '%s-%s.tar.gz' % (distname, version)) diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index db9c3873..109f9135 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -524,28 +524,28 @@ class TestEggInfo: [metadata] license_file = LICENSE """), - 'LICENSE': DALS("Test license") - }, True), # with license + 'LICENSE': "Test license" + }, True), # with license ({ 'setup.cfg': DALS(""" [metadata] license_file = INVALID_LICENSE """), - 'LICENSE': DALS("Test license") - }, False), # with an invalid license + 'LICENSE': "Test license" + }, False), # with an invalid license ({ 'setup.cfg': DALS(""" """), - 'LICENSE': DALS("Test license") - }, False), # no license_file attribute + 'LICENSE': "Test license" + }, False), # no license_file attribute ({ 'setup.cfg': DALS(""" [metadata] license_file = LICENSE """), - 'MANIFEST.in': DALS("exclude LICENSE"), - 'LICENSE': DALS("Test license") - }, False) # license file is manually excluded + 'MANIFEST.in': "exclude LICENSE", + 'LICENSE': "Test license" + }, False) # license file is manually excluded ]) def test_setup_cfg_license_file( self, tmpdir_cwd, env, files, license_in_sources): @@ -565,7 +565,211 @@ class TestEggInfo: assert 'LICENSE' in sources_text else: assert 'LICENSE' not in sources_text - assert 'INVALID_LICENSE' not in sources_text # for invalid license test + # for invalid license test + assert 'INVALID_LICENSE' not in sources_text + + @pytest.mark.parametrize("files, incl_licenses, excl_licenses", [ + ({ + 'setup.cfg': DALS(""" + [metadata] + license_files = + LICENSE-ABC + LICENSE-XYZ + """), + 'LICENSE-ABC': "ABC license", + 'LICENSE-XYZ': "XYZ license" + }, ['LICENSE-ABC', 'LICENSE-XYZ'], []), # with licenses + ({ + 'setup.cfg': DALS(""" + [metadata] + license_files = LICENSE-ABC, LICENSE-XYZ + """), + 'LICENSE-ABC': "ABC license", + 'LICENSE-XYZ': "XYZ license" + }, ['LICENSE-ABC', 'LICENSE-XYZ'], []), # with commas + ({ + 'setup.cfg': DALS(""" + [metadata] + license_files = + LICENSE-ABC + """), + 'LICENSE-ABC': "ABC license", + 'LICENSE-XYZ': "XYZ license" + }, ['LICENSE-ABC'], ['LICENSE-XYZ']), # with one license + ({ + 'setup.cfg': DALS(""" + [metadata] + license_files = + """), + 'LICENSE-ABC': "ABC license", + 'LICENSE-XYZ': "XYZ license" + }, [], ['LICENSE-ABC', 'LICENSE-XYZ']), # empty + ({ + 'setup.cfg': DALS(""" + [metadata] + license_files = LICENSE-XYZ + """), + 'LICENSE-ABC': "ABC license", + 'LICENSE-XYZ': "XYZ license" + }, ['LICENSE-XYZ'], ['LICENSE-ABC']), # on same line + ({ + 'setup.cfg': DALS(""" + [metadata] + license_files = + LICENSE-ABC + INVALID_LICENSE + """), + 'LICENSE-ABC': "Test license" + }, ['LICENSE-ABC'], ['INVALID_LICENSE']), # with an invalid license + ({ + 'setup.cfg': DALS(""" + """), + 'LICENSE': "Test license" + }, [], ['LICENSE']), # no license_files attribute + ({ + 'setup.cfg': DALS(""" + [metadata] + license_files = LICENSE + """), + 'MANIFEST.in': "exclude LICENSE", + 'LICENSE': "Test license" + }, [], ['LICENSE']), # license file is manually excluded + ({ + 'setup.cfg': DALS(""" + [metadata] + license_files = + LICENSE-ABC + LICENSE-XYZ + """), + 'MANIFEST.in': "exclude LICENSE-XYZ", + 'LICENSE-ABC': "ABC license", + 'LICENSE-XYZ': "XYZ license" + }, ['LICENSE-ABC'], ['LICENSE-XYZ']) # subset is manually excluded + ]) + def test_setup_cfg_license_files( + self, tmpdir_cwd, env, files, incl_licenses, excl_licenses): + self._create_project() + build_files(files) + + environment.run_setup_py( + cmd=['egg_info'], + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]) + ) + egg_info_dir = os.path.join('.', 'foo.egg-info') + + with open(os.path.join(egg_info_dir, 'SOURCES.txt')) as sources_file: + sources_lines = list(line.strip() for line in sources_file) + + for lf in incl_licenses: + assert sources_lines.count(lf) == 1 + + for lf in excl_licenses: + assert sources_lines.count(lf) == 0 + + @pytest.mark.parametrize("files, incl_licenses, excl_licenses", [ + ({ + 'setup.cfg': DALS(""" + [metadata] + license_file = + license_files = + """), + 'LICENSE-ABC': "ABC license", + 'LICENSE-XYZ': "XYZ license" + }, [], ['LICENSE-ABC', 'LICENSE-XYZ']), # both empty + ({ + 'setup.cfg': DALS(""" + [metadata] + license_file = + LICENSE-ABC + LICENSE-XYZ + """), + 'LICENSE-ABC': "ABC license", + 'LICENSE-XYZ': "XYZ license" + # license_file is still singular + }, [], ['LICENSE-ABC', 'LICENSE-XYZ']), + ({ + 'setup.cfg': DALS(""" + [metadata] + license_file = LICENSE-ABC + license_files = + LICENSE-XYZ + LICENSE-PQR + """), + 'LICENSE-ABC': "ABC license", + 'LICENSE-PQR': "PQR license", + 'LICENSE-XYZ': "XYZ license" + }, ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'], []), # combined + ({ + 'setup.cfg': DALS(""" + [metadata] + license_file = LICENSE-ABC + license_files = + LICENSE-ABC + LICENSE-XYZ + LICENSE-PQR + """), + 'LICENSE-ABC': "ABC license", + 'LICENSE-PQR': "PQR license", + 'LICENSE-XYZ': "XYZ license" + # duplicate license + }, ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'], []), + ({ + 'setup.cfg': DALS(""" + [metadata] + license_file = LICENSE-ABC + license_files = + LICENSE-XYZ + """), + 'LICENSE-ABC': "ABC license", + 'LICENSE-PQR': "PQR license", + 'LICENSE-XYZ': "XYZ license" + # combined subset + }, ['LICENSE-ABC', 'LICENSE-XYZ'], ['LICENSE-PQR']), + ({ + 'setup.cfg': DALS(""" + [metadata] + license_file = LICENSE-ABC + license_files = + LICENSE-XYZ + LICENSE-PQR + """), + 'LICENSE-PQR': "Test license" + # with invalid licenses + }, ['LICENSE-PQR'], ['LICENSE-ABC', 'LICENSE-XYZ']), + ({ + 'setup.cfg': DALS(""" + [metadata] + license_file = LICENSE-ABC + license_files = + LICENSE-PQR + LICENSE-XYZ + """), + 'MANIFEST.in': "exclude LICENSE-ABC\nexclude LICENSE-PQR", + 'LICENSE-ABC': "ABC license", + 'LICENSE-PQR': "PQR license", + 'LICENSE-XYZ': "XYZ license" + # manually excluded + }, ['LICENSE-XYZ'], ['LICENSE-ABC', 'LICENSE-PQR']) + ]) + def test_setup_cfg_license_file_license_files( + self, tmpdir_cwd, env, files, incl_licenses, excl_licenses): + self._create_project() + build_files(files) + + environment.run_setup_py( + cmd=['egg_info'], + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]) + ) + egg_info_dir = os.path.join('.', 'foo.egg-info') + + with open(os.path.join(egg_info_dir, 'SOURCES.txt')) as sources_file: + sources_lines = list(line.strip() for line in sources_file) + + for lf in incl_licenses: + assert sources_lines.count(lf) == 1 + + for lf in excl_licenses: + assert sources_lines.count(lf) == 0 def test_long_description_content_type(self, tmpdir_cwd, env): # Test that specifying a `long_description_content_type` keyword arg to @@ -622,6 +826,7 @@ class TestEggInfo: assert expected_line in pkg_info_lines expected_line = 'Project-URL: Link Two, https://example.com/two/' assert expected_line in pkg_info_lines + assert 'Metadata-Version: 1.2' in pkg_info_lines def test_python_requires_egg_info(self, tmpdir_cwd, env): self._setup_script_with_requires( diff --git a/setuptools/tests/test_glibc.py b/setuptools/tests/test_glibc.py deleted file mode 100644 index 795fdc56..00000000 --- a/setuptools/tests/test_glibc.py +++ /dev/null @@ -1,52 +0,0 @@ -import warnings - -import pytest - -from setuptools.glibc import check_glibc_version - -__metaclass__ = type - - -@pytest.fixture(params=[ - "2.20", - # used by "linaro glibc", see gh-3588 - "2.20-2014.11", - # weird possibilities that I just made up - "2.20+dev", - "2.20-custom", - "2.20.1", - ]) -def two_twenty(request): - return request.param - - -@pytest.fixture(params=["asdf", "", "foo.bar"]) -def bad_string(request): - return request.param - - -class TestGlibc: - def test_manylinux1_check_glibc_version(self, two_twenty): - """ - Test that the check_glibc_version function is robust against weird - glibc version strings. - """ - assert check_glibc_version(two_twenty, 2, 15) - assert check_glibc_version(two_twenty, 2, 20) - assert not check_glibc_version(two_twenty, 2, 21) - assert not check_glibc_version(two_twenty, 3, 15) - assert not check_glibc_version(two_twenty, 1, 15) - - def test_bad_versions(self, bad_string): - """ - For unparseable strings, warn and return False - """ - with warnings.catch_warnings(record=True) as ws: - warnings.filterwarnings("always") - assert not check_glibc_version(bad_string, 2, 5) - for w in ws: - if "Expected glibc version with" in str(w.message): - break - else: - # Didn't find the warning we were expecting - assert False diff --git a/setuptools/tests/test_integration.py b/setuptools/tests/test_integration.py index 1e132188..f1a27f8b 100644 --- a/setuptools/tests/test_integration.py +++ b/setuptools/tests/test_integration.py @@ -64,7 +64,7 @@ def install_context(request, tmpdir, monkeypatch): monkeypatch.setattr('site.USER_BASE', user_base.strpath) monkeypatch.setattr('site.USER_SITE', user_site.strpath) monkeypatch.setattr('sys.path', sys.path + [install_dir.strpath]) - monkeypatch.setenv('PYTHONPATH', os.path.pathsep.join(sys.path)) + monkeypatch.setenv(str('PYTHONPATH'), str(os.path.pathsep.join(sys.path))) # Set up the command for performing the installation. dist = Distribution() @@ -143,6 +143,7 @@ def test_build_deps_on_distutils(request, tmpdir_factory, build_dep): 'tests_require', 'python_requires', 'install_requires', + 'long_description_content_type', ] assert not match or match.group(1).strip('"\'') in allowed_unknowns diff --git a/setuptools/tests/test_pep425tags.py b/setuptools/tests/test_pep425tags.py deleted file mode 100644 index 30afdec7..00000000 --- a/setuptools/tests/test_pep425tags.py +++ /dev/null @@ -1,170 +0,0 @@ -import sys - -import pytest -from mock import patch - -from setuptools import pep425tags - -__metaclass__ = type - - -class TestPEP425Tags: - - def mock_get_config_var(self, **kwd): - """ - Patch sysconfig.get_config_var for arbitrary keys. - """ - get_config_var = pep425tags.sysconfig.get_config_var - - def _mock_get_config_var(var): - if var in kwd: - return kwd[var] - return get_config_var(var) - return _mock_get_config_var - - def abi_tag_unicode(self, flags, config_vars): - """ - Used to test ABI tags, verify correct use of the `u` flag - """ - config_vars.update({'SOABI': None}) - base = pep425tags.get_abbr_impl() + pep425tags.get_impl_ver() - - if sys.version_info < (3, 3): - config_vars.update({'Py_UNICODE_SIZE': 2}) - mock_gcf = self.mock_get_config_var(**config_vars) - with patch( - 'setuptools.pep425tags.sysconfig.get_config_var', - mock_gcf): - abi_tag = pep425tags.get_abi_tag() - assert abi_tag == base + flags - - config_vars.update({'Py_UNICODE_SIZE': 4}) - mock_gcf = self.mock_get_config_var(**config_vars) - with patch('setuptools.pep425tags.sysconfig.get_config_var', - mock_gcf): - abi_tag = pep425tags.get_abi_tag() - assert abi_tag == base + flags + 'u' - - else: - # On Python >= 3.3, UCS-4 is essentially permanently enabled, and - # Py_UNICODE_SIZE is None. SOABI on these builds does not include - # the 'u' so manual SOABI detection should not do so either. - config_vars.update({'Py_UNICODE_SIZE': None}) - mock_gcf = self.mock_get_config_var(**config_vars) - with patch('setuptools.pep425tags.sysconfig.get_config_var', - mock_gcf): - abi_tag = pep425tags.get_abi_tag() - assert abi_tag == base + flags - - def test_broken_sysconfig(self): - """ - Test that pep425tags still works when sysconfig is broken. - Can be a problem on Python 2.7 - Issue #1074. - """ - def raises_ioerror(var): - raise IOError("I have the wrong path!") - - with patch('setuptools.pep425tags.sysconfig.get_config_var', - raises_ioerror): - with pytest.warns(RuntimeWarning): - assert len(pep425tags.get_supported()) - - def test_no_hyphen_tag(self): - """ - Test that no tag contains a hyphen. - """ - mock_gcf = self.mock_get_config_var(SOABI='cpython-35m-darwin') - - with patch('setuptools.pep425tags.sysconfig.get_config_var', - mock_gcf): - supported = pep425tags.get_supported() - - for (py, abi, plat) in supported: - assert '-' not in py - assert '-' not in abi - assert '-' not in plat - - def test_manual_abi_noflags(self): - """ - Test that no flags are set on a non-PyDebug, non-Pymalloc ABI tag. - """ - self.abi_tag_unicode('', {'Py_DEBUG': False, 'WITH_PYMALLOC': False}) - - def test_manual_abi_d_flag(self): - """ - Test that the `d` flag is set on a PyDebug, non-Pymalloc ABI tag. - """ - self.abi_tag_unicode('d', {'Py_DEBUG': True, 'WITH_PYMALLOC': False}) - - def test_manual_abi_m_flag(self): - """ - Test that the `m` flag is set on a non-PyDebug, Pymalloc ABI tag. - """ - self.abi_tag_unicode('m', {'Py_DEBUG': False, 'WITH_PYMALLOC': True}) - - def test_manual_abi_dm_flags(self): - """ - Test that the `dm` flags are set on a PyDebug, Pymalloc ABI tag. - """ - self.abi_tag_unicode('dm', {'Py_DEBUG': True, 'WITH_PYMALLOC': True}) - - -class TestManylinux1Tags: - - @patch('setuptools.pep425tags.get_platform', lambda: 'linux_x86_64') - @patch('setuptools.glibc.have_compatible_glibc', - lambda major, minor: True) - def test_manylinux1_compatible_on_linux_x86_64(self): - """ - Test that manylinux1 is enabled on linux_x86_64 - """ - assert pep425tags.is_manylinux1_compatible() - - @patch('setuptools.pep425tags.get_platform', lambda: 'linux_i686') - @patch('setuptools.glibc.have_compatible_glibc', - lambda major, minor: True) - def test_manylinux1_compatible_on_linux_i686(self): - """ - Test that manylinux1 is enabled on linux_i686 - """ - assert pep425tags.is_manylinux1_compatible() - - @patch('setuptools.pep425tags.get_platform', lambda: 'linux_x86_64') - @patch('setuptools.glibc.have_compatible_glibc', - lambda major, minor: False) - def test_manylinux1_2(self): - """ - Test that manylinux1 is disabled with incompatible glibc - """ - assert not pep425tags.is_manylinux1_compatible() - - @patch('setuptools.pep425tags.get_platform', lambda: 'arm6vl') - @patch('setuptools.glibc.have_compatible_glibc', - lambda major, minor: True) - def test_manylinux1_3(self): - """ - Test that manylinux1 is disabled on arm6vl - """ - assert not pep425tags.is_manylinux1_compatible() - - @patch('setuptools.pep425tags.get_platform', lambda: 'linux_x86_64') - @patch('setuptools.glibc.have_compatible_glibc', - lambda major, minor: True) - @patch('sys.platform', 'linux2') - def test_manylinux1_tag_is_first(self): - """ - Test that the more specific tag manylinux1 comes first. - """ - groups = {} - for pyimpl, abi, arch in pep425tags.get_supported(): - groups.setdefault((pyimpl, abi), []).append(arch) - - for arches in groups.values(): - if arches == ['any']: - continue - # Expect the most specific arch first: - if len(arches) == 3: - assert arches == ['manylinux1_x86_64', 'linux_x86_64', 'any'] - else: - assert arches == ['manylinux1_x86_64', 'linux_x86_64'] diff --git a/setuptools/tests/test_register.py b/setuptools/tests/test_register.py index 96114595..98605806 100644 --- a/setuptools/tests/test_register.py +++ b/setuptools/tests/test_register.py @@ -1,43 +1,22 @@ -import mock -from distutils import log - -import pytest - from setuptools.command.register import register from setuptools.dist import Distribution +from setuptools.errors import RemovedCommandError +try: + from unittest import mock +except ImportError: + import mock -class TestRegisterTest: - def test_warns_deprecation(self): - dist = Distribution() - - cmd = register(dist) - cmd.run_command = mock.Mock() - cmd.send_metadata = mock.Mock() - cmd.announce = mock.Mock() - - cmd.run() +import pytest - cmd.announce.assert_called_with( - "WARNING: Registering is deprecated, use twine to upload instead " - "(https://pypi.org/p/twine/)", - log.WARN - ) - def test_warns_deprecation_when_raising(self): +class TestRegister: + def test_register_exception(self): + """Ensure that the register command has been properly removed.""" dist = Distribution() + dist.dist_files = [(mock.Mock(), mock.Mock(), mock.Mock())] cmd = register(dist) - cmd.run_command = mock.Mock() - cmd.send_metadata = mock.Mock() - cmd.send_metadata.side_effect = Exception - cmd.announce = mock.Mock() - with pytest.raises(Exception): + with pytest.raises(RemovedCommandError): cmd.run() - - cmd.announce.assert_called_with( - "WARNING: Registering is deprecated, use twine to upload instead " - "(https://pypi.org/p/twine/)", - log.WARN - ) diff --git a/setuptools/tests/test_sdist.py b/setuptools/tests/test_sdist.py index d2c4e0cf..0bea53df 100644 --- a/setuptools/tests/test_sdist.py +++ b/setuptools/tests/test_sdist.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- """sdist tests""" +from __future__ import print_function, unicode_literals + import os -import shutil import sys import tempfile import unicodedata @@ -50,7 +51,7 @@ def quiet(): # Convert to POSIX path def posix(path): - if six.PY3 and not isinstance(path, str): + if not six.PY2 and not isinstance(path, str): return path.replace(os.sep.encode('ascii'), b'/') else: return path.replace(os.sep, '/') @@ -89,30 +90,28 @@ fail_on_latin1_encoded_filenames = pytest.mark.xfail( ) +def touch(path): + path.write_text('', encoding='utf-8') + + class TestSdistTest: - def setup_method(self, method): - self.temp_dir = tempfile.mkdtemp() - with open(os.path.join(self.temp_dir, 'setup.py'), 'w') as f: - f.write(SETUP_PY) + @pytest.fixture(autouse=True) + def source_dir(self, tmpdir): + (tmpdir / 'setup.py').write_text(SETUP_PY, encoding='utf-8') # Set up the rest of the test package - test_pkg = os.path.join(self.temp_dir, 'sdist_test') - os.mkdir(test_pkg) - data_folder = os.path.join(self.temp_dir, "d") - os.mkdir(data_folder) + test_pkg = tmpdir / 'sdist_test' + test_pkg.mkdir() + data_folder = tmpdir / 'd' + data_folder.mkdir() # *.rst was not included in package_data, so c.rst should not be # automatically added to the manifest when not under version control - for fname in ['__init__.py', 'a.txt', 'b.txt', 'c.rst', - os.path.join(data_folder, "e.dat")]: - # Just touch the files; their contents are irrelevant - open(os.path.join(test_pkg, fname), 'w').close() - - self.old_cwd = os.getcwd() - os.chdir(self.temp_dir) + for fname in ['__init__.py', 'a.txt', 'b.txt', 'c.rst']: + touch(test_pkg / fname) + touch(data_folder / 'e.dat') - def teardown_method(self, method): - os.chdir(self.old_cwd) - shutil.rmtree(self.temp_dir) + with tmpdir.as_cwd(): + yield def test_package_data_in_sdist(self): """Regression test for pull request #4: ensures that files listed in @@ -175,14 +174,14 @@ class TestSdistTest: manifest = cmd.filelist.files assert 'setup.py' not in manifest - def test_defaults_case_sensitivity(self): + def test_defaults_case_sensitivity(self, tmpdir): """ Make sure default files (README.*, etc.) are added in a case-sensitive way to avoid problems with packages built on Windows. """ - open(os.path.join(self.temp_dir, 'readme.rst'), 'w').close() - open(os.path.join(self.temp_dir, 'SETUP.cfg'), 'w').close() + touch(tmpdir / 'readme.rst') + touch(tmpdir / 'SETUP.cfg') dist = Distribution(SETUP_ATTRS) # the extension deliberately capitalized for this test @@ -230,10 +229,6 @@ class TestSdistTest: u_contents = contents.decode('UTF-8') # The manifest should contain the UTF-8 filename - if six.PY2: - fs_enc = sys.getfilesystemencoding() - filename = filename.decode(fs_enc) - assert posix(filename) in u_contents @py3_only @@ -334,7 +329,7 @@ class TestSdistTest: cmd.read_manifest() # The filelist should contain the UTF-8 filename - if six.PY3: + if not six.PY2: filename = filename.decode('utf-8') assert filename in cmd.filelist.files @@ -374,7 +369,7 @@ class TestSdistTest: @fail_on_latin1_encoded_filenames def test_sdist_with_utf8_encoded_filename(self): # Test for #303. - dist = Distribution(SETUP_ATTRS) + dist = Distribution(self.make_strings(SETUP_ATTRS)) dist.script_name = 'setup.py' cmd = sdist(dist) cmd.ensure_finalized() @@ -388,7 +383,7 @@ class TestSdistTest: if sys.platform == 'darwin': filename = decompose(filename) - if six.PY3: + if not six.PY2: fs_enc = sys.getfilesystemencoding() if sys.platform == 'win32': @@ -405,10 +400,19 @@ class TestSdistTest: else: assert filename in cmd.filelist.files + @classmethod + def make_strings(cls, item): + if isinstance(item, dict): + return { + key: cls.make_strings(value) for key, value in item.items()} + if isinstance(item, list): + return list(map(cls.make_strings, item)) + return str(item) + @fail_on_latin1_encoded_filenames def test_sdist_with_latin1_encoded_filename(self): # Test for #303. - dist = Distribution(SETUP_ATTRS) + dist = Distribution(self.make_strings(SETUP_ATTRS)) dist.script_name = 'setup.py' cmd = sdist(dist) cmd.ensure_finalized() @@ -421,7 +425,19 @@ class TestSdistTest: with quiet(): cmd.run() - if six.PY3: + if six.PY2: + # Under Python 2 there seems to be no decoded string in the + # filelist. However, due to decode and encoding of the + # file name to get utf-8 Manifest the latin1 maybe excluded + try: + # fs_enc should match how one is expect the decoding to + # be proformed for the manifest output. + fs_enc = sys.getfilesystemencoding() + filename.decode(fs_enc) + assert filename in cmd.filelist.files + except UnicodeDecodeError: + filename not in cmd.filelist.files + else: # not all windows systems have a default FS encoding of cp1252 if sys.platform == 'win32': # Latin-1 is similar to Windows-1252 however @@ -436,18 +452,36 @@ class TestSdistTest: # The Latin-1 filename should have been skipped filename = filename.decode('latin-1') filename not in cmd.filelist.files - else: - # Under Python 2 there seems to be no decoded string in the - # filelist. However, due to decode and encoding of the - # file name to get utf-8 Manifest the latin1 maybe excluded - try: - # fs_enc should match how one is expect the decoding to - # be proformed for the manifest output. - fs_enc = sys.getfilesystemencoding() - filename.decode(fs_enc) - assert filename in cmd.filelist.files - except UnicodeDecodeError: - filename not in cmd.filelist.files + + def test_pyproject_toml_in_sdist(self, tmpdir): + """ + Check if pyproject.toml is included in source distribution if present + """ + touch(tmpdir / 'pyproject.toml') + dist = Distribution(SETUP_ATTRS) + dist.script_name = 'setup.py' + cmd = sdist(dist) + cmd.ensure_finalized() + with quiet(): + cmd.run() + manifest = cmd.filelist.files + assert 'pyproject.toml' in manifest + + def test_pyproject_toml_excluded(self, tmpdir): + """ + Check that pyproject.toml can excluded even if present + """ + touch(tmpdir / 'pyproject.toml') + with open('MANIFEST.in', 'w') as mts: + print('exclude pyproject.toml', file=mts) + dist = Distribution(SETUP_ATTRS) + dist.script_name = 'setup.py' + cmd = sdist(dist) + cmd.ensure_finalized() + with quiet(): + cmd.run() + manifest = cmd.filelist.files + assert 'pyproject.toml' not in manifest def test_default_revctrl(): diff --git a/setuptools/tests/test_setopt.py b/setuptools/tests/test_setopt.py index 3fb04fb4..1b038954 100644 --- a/setuptools/tests/test_setopt.py +++ b/setuptools/tests/test_setopt.py @@ -15,7 +15,7 @@ class TestEdit: def parse_config(filename): parser = configparser.ConfigParser() with io.open(filename, encoding='utf-8') as reader: - (parser.read_file if six.PY3 else parser.readfp)(reader) + (parser.readfp if six.PY2 else parser.read_file)(reader) return parser @staticmethod diff --git a/setuptools/tests/test_setuptools.py b/setuptools/tests/test_setuptools.py index 5896a69a..bca69c30 100644 --- a/setuptools/tests/test_setuptools.py +++ b/setuptools/tests/test_setuptools.py @@ -108,6 +108,11 @@ class TestDepends: assert not req.is_present() assert not req.is_current() + @needs_bytecode + def test_require_present(self): + # In #1896, this test was failing for months with the only + # complaint coming from test runners (not end users). + # TODO: Evaluate if this code is needed at all. req = Require('Tests', None, 'tests', homepage="http://example.com") assert req.format is None assert req.attribute is None @@ -223,10 +228,10 @@ class TestFeatures: py_modules=['bar_et'], remove=['bar.ext'], ), 'baz': Feature( - "baz", optional=False, packages=['pkg.baz'], - scripts=['scripts/baz_it'], - libraries=[('libfoo', 'foo/foofoo.c')] - ), + "baz", optional=False, packages=['pkg.baz'], + scripts=['scripts/baz_it'], + libraries=[('libfoo', 'foo/foofoo.c')] + ), 'dwim': Feature("DWIM", available=False, remove='bazish'), }, script_args=['--without-bar', 'install'], diff --git a/setuptools/tests/test_test.py b/setuptools/tests/test_test.py index faaa6ba9..8ee70a7e 100644 --- a/setuptools/tests/test_test.py +++ b/setuptools/tests/test_test.py @@ -1,7 +1,8 @@ -# -*- coding: UTF-8 -*- +# -*- coding: utf-8 -*- from __future__ import unicode_literals +import mock from distutils import log import os @@ -11,7 +12,7 @@ from setuptools.command.test import test from setuptools.dist import Distribution from .textwrap import DALS -from . import contexts + SETUP_PY = DALS(""" from setuptools import setup @@ -85,9 +86,7 @@ def test_test(capfd): dist.script_name = 'setup.py' cmd = test(dist) cmd.ensure_finalized() - # The test runner calls sys.exit - with contexts.suppress_exceptions(SystemExit): - cmd.run() + cmd.run() out, err = capfd.readouterr() assert out == 'Foo\n' @@ -119,8 +118,55 @@ def test_tests_are_run_once(capfd): dist.script_name = 'setup.py' cmd = test(dist) cmd.ensure_finalized() - # The test runner calls sys.exit - with contexts.suppress_exceptions(SystemExit): - cmd.run() + cmd.run() out, err = capfd.readouterr() assert out == 'Foo\n' + + +@pytest.mark.usefixtures('sample_test') +def test_warns_deprecation(capfd): + params = dict( + name='foo', + packages=['name', 'name.space', 'name.space.tests'], + namespace_packages=['name'], + test_suite='name.space.tests.test_suite', + use_2to3=True + ) + dist = Distribution(params) + dist.script_name = 'setup.py' + cmd = test(dist) + cmd.ensure_finalized() + cmd.announce = mock.Mock() + cmd.run() + capfd.readouterr() + msg = ( + "WARNING: Testing via this command is deprecated and will be " + "removed in a future version. Users looking for a generic test " + "entry point independent of test runner are encouraged to use " + "tox." + ) + cmd.announce.assert_any_call(msg, log.WARN) + + +@pytest.mark.usefixtures('sample_test') +def test_deprecation_stderr(capfd): + params = dict( + name='foo', + packages=['name', 'name.space', 'name.space.tests'], + namespace_packages=['name'], + test_suite='name.space.tests.test_suite', + use_2to3=True + ) + dist = Distribution(params) + dist.script_name = 'setup.py' + cmd = test(dist) + cmd.ensure_finalized() + cmd.run() + out, err = capfd.readouterr() + msg = ( + "WARNING: Testing via this command is deprecated and will be " + "removed in a future version. Users looking for a generic test " + "entry point independent of test runner are encouraged to use " + "tox.\n" + ) + assert msg in err diff --git a/setuptools/tests/test_upload.py b/setuptools/tests/test_upload.py index 320c6959..7586cb26 100644 --- a/setuptools/tests/test_upload.py +++ b/setuptools/tests/test_upload.py @@ -1,213 +1,22 @@ -import mock -import os -import re - -from distutils import log -from distutils.errors import DistutilsError - -import pytest - from setuptools.command.upload import upload from setuptools.dist import Distribution -from setuptools.extern import six - - -def _parse_upload_body(body): - boundary = u'\r\n----------------GHSKFJDLGDS7543FJKLFHRE75642756743254' - entries = [] - name_re = re.compile(u'^Content-Disposition: form-data; name="([^\"]+)"') - - for entry in body.split(boundary): - pair = entry.split(u'\r\n\r\n') - if not len(pair) == 2: - continue - - key, value = map(six.text_type.strip, pair) - m = name_re.match(key) - if m is not None: - key = m.group(1) - - entries.append((key, value)) - - return entries - - -@pytest.fixture -def patched_upload(tmpdir): - class Fix: - def __init__(self, cmd, urlopen): - self.cmd = cmd - self.urlopen = urlopen - - def __iter__(self): - return iter((self.cmd, self.urlopen)) - - def get_uploaded_metadata(self): - request = self.urlopen.call_args_list[0][0][0] - body = request.data.decode('utf-8') - entries = dict(_parse_upload_body(body)) - - return entries +from setuptools.errors import RemovedCommandError - class ResponseMock(mock.Mock): - def getheader(self, name, default=None): - """Mocked getheader method for response object""" - return { - 'content-type': 'text/plain; charset=utf-8', - }.get(name.lower(), default) +try: + from unittest import mock +except ImportError: + import mock - with mock.patch('setuptools.command.upload.urlopen') as urlopen: - urlopen.return_value = ResponseMock() - urlopen.return_value.getcode.return_value = 200 - urlopen.return_value.read.return_value = b'' - - content = os.path.join(str(tmpdir), "content_data") - - with open(content, 'w') as f: - f.write("Some content") - - dist = Distribution() - dist.dist_files = [('sdist', '3.7.0', content)] - - cmd = upload(dist) - cmd.announce = mock.Mock() - cmd.username = 'user' - cmd.password = 'hunter2' - - yield Fix(cmd, urlopen) - - -class TestUploadTest: - def test_upload_metadata(self, patched_upload): - cmd, patch = patched_upload - - # Set the metadata version to 2.1 - cmd.distribution.metadata.metadata_version = '2.1' - - # Run the command - cmd.ensure_finalized() - cmd.run() - - # Make sure we did the upload - patch.assert_called_once() - - # Make sure the metadata version is correct in the headers - entries = patched_upload.get_uploaded_metadata() - assert entries['metadata_version'] == '2.1' - - def test_warns_deprecation(self): - dist = Distribution() - dist.dist_files = [(mock.Mock(), mock.Mock(), mock.Mock())] - - cmd = upload(dist) - cmd.upload_file = mock.Mock() - cmd.announce = mock.Mock() - - cmd.run() +import pytest - cmd.announce.assert_called_once_with( - "WARNING: Uploading via this command is deprecated, use twine to " - "upload instead (https://pypi.org/p/twine/)", - log.WARN - ) - def test_warns_deprecation_when_raising(self): +class TestUpload: + def test_upload_exception(self): + """Ensure that the register command has been properly removed.""" dist = Distribution() dist.dist_files = [(mock.Mock(), mock.Mock(), mock.Mock())] cmd = upload(dist) - cmd.upload_file = mock.Mock() - cmd.upload_file.side_effect = Exception - cmd.announce = mock.Mock() - - with pytest.raises(Exception): - cmd.run() - - cmd.announce.assert_called_once_with( - "WARNING: Uploading via this command is deprecated, use twine to " - "upload instead (https://pypi.org/p/twine/)", - log.WARN - ) - - @pytest.mark.parametrize('url', [ - 'https://example.com/a;parameter', # Has parameters - 'https://example.com/a?query', # Has query - 'https://example.com/a#fragment', # Has fragment - 'ftp://example.com', # Invalid scheme - - ]) - def test_upload_file_invalid_url(self, url, patched_upload): - patched_upload.urlopen.side_effect = Exception("Should not be reached") - - cmd = patched_upload.cmd - cmd.repository = url - - cmd.ensure_finalized() - with pytest.raises(AssertionError): - cmd.run() - - def test_upload_file_http_error(self, patched_upload): - patched_upload.urlopen.side_effect = six.moves.urllib.error.HTTPError( - 'https://example.com', - 404, - 'File not found', - None, - None - ) - - cmd = patched_upload.cmd - cmd.ensure_finalized() - with pytest.raises(DistutilsError): + with pytest.raises(RemovedCommandError): cmd.run() - - cmd.announce.assert_any_call( - 'Upload failed (404): File not found', - log.ERROR) - - def test_upload_file_os_error(self, patched_upload): - patched_upload.urlopen.side_effect = OSError("Invalid") - - cmd = patched_upload.cmd - cmd.ensure_finalized() - - with pytest.raises(OSError): - cmd.run() - - cmd.announce.assert_any_call('Invalid', log.ERROR) - - @mock.patch('setuptools.command.upload.spawn') - def test_upload_file_gpg(self, spawn, patched_upload): - cmd, urlopen = patched_upload - - cmd.sign = True - cmd.identity = "Alice" - cmd.dry_run = True - content_fname = cmd.distribution.dist_files[0][2] - signed_file = content_fname + '.asc' - - with open(signed_file, 'wb') as f: - f.write("signed-data".encode('utf-8')) - - cmd.ensure_finalized() - cmd.run() - - # Make sure that GPG was called - spawn.assert_called_once_with([ - "gpg", "--detach-sign", "--local-user", "Alice", "-a", - content_fname - ], dry_run=True) - - # Read the 'signed' data that was transmitted - entries = patched_upload.get_uploaded_metadata() - assert entries['gpg_signature'] == 'signed-data' - - def test_show_response_no_error(self, patched_upload): - # This test is just that show_response doesn't throw an error - # It is not really important what the printed response looks like - # in a deprecated command, but we don't want to introduce new - # errors when importing this function from distutils - - patched_upload.cmd.show_response = True - patched_upload.cmd.ensure_finalized() - patched_upload.cmd.run() diff --git a/setuptools/tests/test_virtualenv.py b/setuptools/tests/test_virtualenv.py index d7b98c77..6549a6c0 100644 --- a/setuptools/tests/test_virtualenv.py +++ b/setuptools/tests/test_virtualenv.py @@ -13,6 +13,17 @@ from .test_easy_install import make_nspkg_sdist @pytest.fixture(autouse=True) +def disable_requires_python(monkeypatch): + """ + Disable Requires-Python on Python 2.7 + """ + if sys.version_info > (3,): + return + + monkeypatch.setenv('PIP_IGNORE_REQUIRES_PYTHON', 'true') + + +@pytest.fixture(autouse=True) def pytest_virtualenv_works(virtualenv): """ pytest_virtualenv may not work. if it doesn't, skip these @@ -62,7 +73,7 @@ def _get_pip_versions(): from urllib.request import urlopen from urllib.error import URLError except ImportError: - from urllib2 import urlopen, URLError # Python 2.7 compat + from urllib2 import urlopen, URLError # Python 2.7 compat try: urlopen('https://pypi.org', timeout=1) @@ -116,14 +127,12 @@ def test_pip_upgrade_from_source(pip_version, virtualenv): virtualenv.run('pip install --no-cache-dir --upgrade ' + sdist) -def test_test_command_install_requirements(bare_virtualenv, tmpdir): +def _check_test_command_install_requirements(virtualenv, tmpdir): """ Check the test command will install all required dependencies. """ - bare_virtualenv.run(' && '.join(( - 'cd {source}', - 'python setup.py develop', - )).format(source=SOURCE_DIR)) + # Install setuptools. + virtualenv.run('python setup.py develop', cd=SOURCE_DIR) def sdist(distname, version): dist_path = tmpdir.join('%s-%s.tar.gz' % (distname, version)) @@ -174,13 +183,25 @@ def test_test_command_install_requirements(bare_virtualenv, tmpdir): open('success', 'w').close() ''')) # Run test command for test package. - bare_virtualenv.run(' && '.join(( + virtualenv.run(' && '.join(( 'cd {tmpdir}', 'python setup.py test -s test', )).format(tmpdir=tmpdir)) assert tmpdir.join('success').check() +def test_test_command_install_requirements(virtualenv, tmpdir): + # Ensure pip/wheel packages are installed. + virtualenv.run( + "python -c \"__import__('pkg_resources').require(['pip', 'wheel'])\"") + _check_test_command_install_requirements(virtualenv, tmpdir) + + +def test_test_command_install_requirements_when_using_easy_install( + bare_virtualenv, tmpdir): + _check_test_command_install_requirements(bare_virtualenv, tmpdir) + + def test_no_missing_dependencies(bare_virtualenv): """ Quick and dirty test to ensure all external dependencies are vendored. diff --git a/setuptools/tests/test_wheel.py b/setuptools/tests/test_wheel.py index e85a4a7e..f72ccbbf 100644 --- a/setuptools/tests/test_wheel.py +++ b/setuptools/tests/test_wheel.py @@ -18,6 +18,7 @@ import pytest from pkg_resources import Distribution, PathMetadata, PY_MAJOR from setuptools.extern.packaging.utils import canonicalize_name +from setuptools.extern.packaging.tags import parse_tag from setuptools.wheel import Wheel from .contexts import tempdir @@ -124,11 +125,12 @@ def flatten_tree(tree): def format_install_tree(tree): - return {x.format( - py_version=PY_MAJOR, - platform=get_platform(), - shlib_ext=get_config_var('EXT_SUFFIX') or get_config_var('SO')) - for x in tree} + return { + x.format( + py_version=PY_MAJOR, + platform=get_platform(), + shlib_ext=get_config_var('EXT_SUFFIX') or get_config_var('SO')) + for x in tree} def _check_wheel_install(filename, install_dir, install_tree_includes, @@ -451,6 +453,35 @@ WHEEL_INSTALL_TESTS = ( ), dict( + id='empty_namespace_package', + file_defs={ + 'foobar': { + '__init__.py': + "__import__('pkg_resources').declare_namespace(__name__)", + }, + }, + setup_kwargs=dict( + namespace_packages=['foobar'], + packages=['foobar'], + ), + install_tree=flatten_tree({ + 'foo-1.0-py{py_version}.egg': [ + 'foo-1.0-py{py_version}-nspkg.pth', + {'EGG-INFO': [ + 'PKG-INFO', + 'RECORD', + 'WHEEL', + 'namespace_packages.txt', + 'top_level.txt', + ]}, + {'foobar': [ + '__init__.py', + ]}, + ] + }), + ), + + dict( id='data_in_package', file_defs={ 'foo': { @@ -543,3 +574,12 @@ def test_wheel_no_dist_dir(): _check_wheel_install(wheel_path, install_dir, None, project_name, version, None) + + +def test_wheel_is_compatible(monkeypatch): + def sys_tags(): + for t in parse_tag('cp36-cp36m-manylinux1_x86_64'): + yield t + monkeypatch.setattr('setuptools.wheel.sys_tags', sys_tags) + assert Wheel( + 'onnxruntime-0.1.2-cp36-cp36m-manylinux1_x86_64.whl').is_compatible() diff --git a/setuptools/wheel.py b/setuptools/wheel.py index e11f0a1d..ec1106a7 100644 --- a/setuptools/wheel.py +++ b/setuptools/wheel.py @@ -1,6 +1,7 @@ """Wheels support.""" from distutils.util import get_platform +from distutils import log import email import itertools import os @@ -11,9 +12,9 @@ import zipfile import pkg_resources import setuptools from pkg_resources import parse_version +from setuptools.extern.packaging.tags import sys_tags from setuptools.extern.packaging.utils import canonicalize_name from setuptools.extern.six import PY3 -from setuptools import pep425tags from setuptools.command.egg_info import write_requirements @@ -76,7 +77,8 @@ class Wheel: def is_compatible(self): '''Is the wheel is compatible with the current platform?''' - supported_tags = pep425tags.get_supported() + supported_tags = set( + (t.interpreter, t.abi, t.platform) for t in sys_tags()) return next((True for t in self.tags() if t in supported_tags), False) def egg_name(self): @@ -162,11 +164,17 @@ class Wheel: extras_require=extras_require, ), ) - write_requirements( - setup_dist.get_command_obj('egg_info'), - None, - os.path.join(egg_info, 'requires.txt'), - ) + # Temporarily disable info traces. + log_threshold = log._global_log.threshold + log.set_threshold(log.WARN) + try: + write_requirements( + setup_dist.get_command_obj('egg_info'), + None, + os.path.join(egg_info, 'requires.txt'), + ) + finally: + log.set_threshold(log_threshold) @staticmethod def _move_data_entries(destination_eggdir, dist_data): @@ -206,6 +214,8 @@ class Wheel: for mod in namespace_packages: mod_dir = os.path.join(destination_eggdir, *mod.split('.')) mod_init = os.path.join(mod_dir, '__init__.py') - if os.path.exists(mod_dir) and not os.path.exists(mod_init): + if not os.path.exists(mod_dir): + os.mkdir(mod_dir) + if not os.path.exists(mod_init): with open(mod_init, 'w') as fp: fp.write(NAMESPACE_PACKAGE_INIT) |