aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml1
-rw-r--r--CHANGES.rst55
-rwxr-xr-xREADME.rst11
-rw-r--r--docs/setuptools.txt92
-rw-r--r--pkg_resources/__init__.py236
-rw-r--r--pkg_resources/tests/test_pkg_resources.py4
-rwxr-xr-xsetup.cfg5
-rwxr-xr-xsetup.py2
-rw-r--r--setuptools/__init__.py22
-rw-r--r--setuptools/build_meta.py172
-rw-r--r--setuptools/command/__init__.py1
-rw-r--r--setuptools/command/bdist_egg.py18
-rwxr-xr-xsetuptools/command/develop.py4
-rw-r--r--setuptools/command/dist_info.py36
-rwxr-xr-xsetuptools/command/easy_install.py2
-rw-r--r--setuptools/command/test.py11
-rw-r--r--setuptools/dist.py40
-rwxr-xr-xsetuptools/package_index.py45
-rw-r--r--setuptools/ssl_support.py11
-rw-r--r--setuptools/tests/__init__.py320
-rw-r--r--setuptools/tests/test_bdist_egg.py26
-rw-r--r--setuptools/tests/test_build_meta.py126
-rw-r--r--setuptools/tests/test_develop.py4
-rw-r--r--setuptools/tests/test_dist.py1
-rw-r--r--setuptools/tests/test_easy_install.py85
-rw-r--r--setuptools/tests/test_egg_info.py6
-rw-r--r--setuptools/tests/test_namespaces.py12
-rw-r--r--setuptools/tests/test_sdist.py67
-rw-r--r--setuptools/tests/test_setuptools.py320
-rw-r--r--setuptools/tests/test_test.py90
-rw-r--r--setuptools/tests/test_virtualenv.py4
-rw-r--r--setuptools/tests/text.py9
32 files changed, 1279 insertions, 559 deletions
diff --git a/.travis.yml b/.travis.yml
index 8558159f..1999d2f9 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -17,6 +17,7 @@ jobs:
- python: *latest_py2
env: LANG=C
- stage: deploy (to PyPI for tagged commits)
+ if: tag IS present
python: *latest_py3
install: skip
script: skip
diff --git a/CHANGES.rst b/CHANGES.rst
index cf596cf2..f75bb62a 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -4,6 +4,61 @@ v37.0.0
* #878: Drop support for Python 2.6. Python 2.6 users should
rely on 'setuptools < 37dev'.
+v36.8.0
+-------
+
+* #1190: In SSL support for package index operations, use SNI
+ where available.
+
+v36.7.3
+-------
+
+* #1175: Bug fixes to ``build_meta`` module.
+
+v36.7.2
+-------
+
+* #701: Fixed duplicate test discovery on Python 3.
+
+v36.7.1
+-------
+
+* #1193: Avoid test failures in bdist_egg when
+ PYTHONDONTWRITEBYTECODE is set.
+
+v36.7.0
+-------
+
+* #1054: Support ``setup_requires`` in ``setup.cfg`` files.
+
+v36.6.1
+-------
+
+* #1132: Removed redundant and costly serialization/parsing step
+ in ``EntryPoint.__init__``.
+
+* #844: ``bdist_egg --exclude-source-files`` now tested and works
+ on Python 3.
+
+v36.6.0
+-------
+
+* #1143: Added ``setuptools.build_meta`` module, an implementation
+ of PEP-517 for Setuptools-defined packages.
+
+* #1143: Added ``dist_info`` command for producing dist_info
+ metadata.
+
+v36.5.0
+-------
+
+* #170: When working with Mercurial checkouts, use Windows-friendly
+ syntax for suppressing output.
+
+* Inspired by #1134, performed substantial refactoring of
+ ``pkg_resources.find_on_path`` to facilitate an optimization
+ for paths with many non-version entries.
+
v36.4.0
-------
diff --git a/README.rst b/README.rst
index 2f55bce1..1c6ee603 100755
--- a/README.rst
+++ b/README.rst
@@ -1,6 +1,17 @@
+.. image:: https://img.shields.io/pypi/v/setuptools.svg
+ :target: https://pypi.org/project/setuptools
+
.. image:: https://readthedocs.org/projects/setuptools/badge/?version=latest
:target: https://setuptools.readthedocs.io
+.. image:: https://img.shields.io/travis/pypa/setuptools/master.svg?label=Linux%20build%20%40%20Travis%20CI
+ :target: http://travis-ci.org/pypa/setuptools
+
+.. image:: https://img.shields.io/appveyor/ci/jaraco/setuptools/master.svg?label=Windows%20build%20%40%20Appveyor
+ :target: https://ci.appveyor.com/project/jaraco/setuptools/branch/master
+
+.. image:: https://img.shields.io/pypi/pyversions/setuptools.svg
+
See the `Installation Instructions
<https://packaging.python.org/installing/>`_ in the Python Packaging
User's Guide for instructions on installing, upgrading, and uninstalling
diff --git a/docs/setuptools.txt b/docs/setuptools.txt
index a9242a51..c2822c4f 100644
--- a/docs/setuptools.txt
+++ b/docs/setuptools.txt
@@ -2281,21 +2281,23 @@ Configuring setup() using setup.cfg files
.. note:: New in 30.3.0 (8 Dec 2016).
-.. important:: ``setup.py`` with ``setup()`` function call is still required even
- if your configuration resides in ``setup.cfg``.
+.. important::
+ A ``setup.py`` file containing a ``setup()`` function call is still
+ required even if your configuration resides in ``setup.cfg``.
-``Setuptools`` allows using configuration files (usually `setup.cfg`)
-to define package’s metadata and other options which are normally supplied
-to ``setup()`` function.
+``Setuptools`` allows using configuration files (usually :file:`setup.cfg`)
+to define a package’s metadata and other options that are normally supplied
+to the ``setup()`` function.
-This approach not only allows automation scenarios, but also reduces
+This approach not only allows automation scenarios but also reduces
boilerplate code in some cases.
.. note::
- Implementation presents limited compatibility with distutils2-like
- ``setup.cfg`` sections (used by ``pbr`` and ``d2to1`` packages).
- Namely: only metadata related keys from ``metadata`` section are supported
+ This implementation has limited compatibility with the distutils2-like
+ ``setup.cfg`` sections used by the ``pbr`` and ``d2to1`` packages.
+
+ Namely: only metadata-related keys from ``metadata`` section are supported
(except for ``description-file``); keys from ``files``, ``entry_points``
and ``backwards_compat`` are not supported.
@@ -2336,12 +2338,13 @@ boilerplate code in some cases.
src.subpackage2
-Metadata and options could be set in sections with the same names.
+Metadata and options are set in the config sections of the same name.
-* Keys are the same as keyword arguments one provides to ``setup()`` function.
+* Keys are the same as the keyword arguments one provides to the ``setup()``
+ function.
-* Complex values could be placed comma-separated or one per line
- in *dangling* sections. The following are the same:
+* Complex values can be written comma-separated or placed one per line
+ in *dangling* config values. The following are equivalent:
.. code-block:: ini
@@ -2353,10 +2356,11 @@ Metadata and options could be set in sections with the same names.
one
two
-* In some cases complex values could be provided in subsections for clarity.
+* In some cases, complex values can be provided in dedicated subsections for
+ clarity.
-* Some keys allow ``file:``, ``attr:`` and ``find:`` directives to cover
- common usecases.
+* Some keys allow ``file:``, ``attr:``, and ``find:`` directives in order to
+ cover common usecases.
* Unknown keys are ignored.
@@ -2369,33 +2373,34 @@ Some values are treated as simple strings, some allow more logic.
Type names used below:
* ``str`` - simple string
-* ``list-comma`` - dangling list or comma-separated values string
-* ``list-semi`` - dangling list or semicolon-separated values string
-* ``bool`` - ``True`` is 1, yes, true
-* ``dict`` - list-comma where keys from values are separated by =
-* ``section`` - values could be read from a dedicated (sub)section
+* ``list-comma`` - dangling list or string of comma-separated values
+* ``list-semi`` - dangling list or string of semicolon-separated values
+* ``bool`` - ``True`` is 1, yes, true
+* ``dict`` - list-comma where keys are separated from values by ``=``
+* ``section`` - values are read from a dedicated (sub)section
Special directives:
-* ``attr:`` - value could be read from module attribute
-* ``file:`` - value could be read from a list of files and then concatenated
+* ``attr:`` - Value is read from a module attribute. ``attr:`` supports
+ callables and iterables; unsupported types are cast using ``str()``.
+* ``file:`` - Value is read from a list of files and then concatenated
.. note::
- ``file:`` directive is sandboxed and won't reach anything outside
- directory with ``setup.py``.
+ The ``file:`` directive is sandboxed and won't reach anything outside
+ the directory containing ``setup.py``.
Metadata
--------
.. note::
- Aliases given below are supported for compatibility reasons,
- but not advised.
+ The aliases given below are supported for compatibility reasons,
+ but their use is not advised.
============================== ================= =====
-Key Aliases Accepted value type
+Key Aliases Type
============================== ================= =====
name str
version attr:, str
@@ -2417,17 +2422,12 @@ requires list-comma
obsoletes list-comma
============================== ================= =====
-.. note::
-
- **version** - ``attr:`` supports callables; supports iterables;
- unsupported types are casted using ``str()``.
-
Options
-------
======================= =====
-Key Accepted value type
+Key Type
======================= =====
zip_safe bool
setup_requires list-semi
@@ -2454,10 +2454,10 @@ py_modules list-comma
.. note::
- **packages** - ``find:`` directive can be further configured
- in a dedicated subsection `options.packages.find`. This subsection
- accepts the same keys as `setuptools.find` function:
- `where`, `include`, `exclude`.
+ **packages** - The ``find:`` directive can be further configured
+ in a dedicated subsection ``options.packages.find``. This subsection
+ accepts the same keys as the `setuptools.find` function:
+ ``where``, ``include``, and ``exclude``.
Configuration API
@@ -2465,7 +2465,7 @@ Configuration API
Some automation tools may wish to access data from a configuration file.
-``Setuptools`` exposes ``read_configuration()`` function allowing
+``Setuptools`` exposes a ``read_configuration()`` function for
parsing ``metadata`` and ``options`` sections into a dictionary.
@@ -2476,16 +2476,16 @@ parsing ``metadata`` and ``options`` sections into a dictionary.
conf_dict = read_configuration('/home/user/dev/package/setup.cfg')
-By default ``read_configuration()`` will read only file provided
+By default, ``read_configuration()`` will read only the file provided
in the first argument. To include values from other configuration files
-which could be in various places set `find_others` function argument
+which could be in various places, set the ``find_others`` keyword argument
to ``True``.
-If you have only a configuration file but not the whole package you can still
-try to get data out of it with the help of `ignore_option_errors` function
-argument. When it is set to ``True`` all options with errors possibly produced
-by directives, such as ``attr:`` and others will be silently ignored.
-As a consequence the resulting dictionary will include no such options.
+If you have only a configuration file but not the whole package, you can still
+try to get data out of it with the help of the ``ignore_option_errors`` keyword
+argument. When it is set to ``True``, all options with errors possibly produced
+by directives, such as ``attr:`` and others, will be silently ignored.
+As a consequence, the resulting dictionary will include no such options.
--------------------------------
diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py
index 497448de..c06cf4b1 100644
--- a/pkg_resources/__init__.py
+++ b/pkg_resources/__init__.py
@@ -480,8 +480,10 @@ def get_build_platform():
try:
version = _macosx_vers()
machine = os.uname()[4].replace(" ", "_")
- return "macosx-%d.%d-%s" % (int(version[0]), int(version[1]),
- _macosx_arch(machine))
+ return "macosx-%d.%d-%s" % (
+ int(version[0]), int(version[1]),
+ _macosx_arch(machine),
+ )
except ValueError:
# if someone is running a non-Mac darwin system, this will fall
# through to the default implementation
@@ -806,7 +808,8 @@ class WorkingSet(object):
already-installed distribution; it should return a ``Distribution`` or
``None``.
- Unless `replace_conflicting=True`, raises a VersionConflict exception if
+ Unless `replace_conflicting=True`, raises a VersionConflict exception
+ if
any requirements are found on the path that have the correct name but
the wrong version. Otherwise, if an `installer` is supplied it will be
invoked to obtain the correct version of the requirement and activate
@@ -885,8 +888,8 @@ class WorkingSet(object):
# return list of distros to activate
return to_activate
- def find_plugins(self, plugin_env, full_env=None, installer=None,
- fallback=True):
+ def find_plugins(
+ self, plugin_env, full_env=None, installer=None, fallback=True):
"""Find all activatable distributions in `plugin_env`
Example usage::
@@ -1040,7 +1043,8 @@ class _ReqExtras(dict):
class Environment(object):
"""Searchable snapshot of distributions on a search path"""
- def __init__(self, search_path=None, platform=get_supported_platform(),
+ def __init__(
+ self, search_path=None, platform=get_supported_platform(),
python=PY_MAJOR):
"""Snapshot distributions available on a search path
@@ -1113,7 +1117,8 @@ class Environment(object):
dists.append(dist)
dists.sort(key=operator.attrgetter('hashcmp'), reverse=True)
- def best_match(self, req, working_set, installer=None, replace_conflicting=False):
+ def best_match(
+ self, req, working_set, installer=None, replace_conflicting=False):
"""Find distribution best matching `req` and usable on `working_set`
This calls the ``find(req)`` method of the `working_set` to see if a
@@ -1248,8 +1253,8 @@ class ResourceManager:
tmpl = textwrap.dedent("""
Can't extract file(s) to egg cache
- The following error occurred while trying to extract file(s) to the Python egg
- cache:
+ The following error occurred while trying to extract file(s)
+ to the Python egg cache:
{old_exc}
@@ -1257,9 +1262,9 @@ class ResourceManager:
{cache_path}
- Perhaps your account does not have write access to this directory? You can
- change the cache directory by setting the PYTHON_EGG_CACHE environment
- variable to point to an accessible directory.
+ Perhaps your account does not have write access to this directory?
+ You can change the cache directory by setting the PYTHON_EGG_CACHE
+ environment variable to point to an accessible directory.
""").lstrip()
err = ExtractionError(tmpl.format(**locals()))
err.manager = self
@@ -1309,11 +1314,13 @@ class ResourceManager:
return
mode = os.stat(path).st_mode
if mode & stat.S_IWOTH or mode & stat.S_IWGRP:
- msg = ("%s is writable by group/others and vulnerable to attack "
+ msg = (
+ "%s is writable by group/others and vulnerable to attack "
"when "
"used with get_resource_filename. Consider a more secure "
"location (set with .set_extraction_path or the "
- "PYTHON_EGG_CACHE environment variable)." % path)
+ "PYTHON_EGG_CACHE environment variable)." % path
+ )
warnings.warn(msg, UserWarning)
def postprocess(self, tempname, filename):
@@ -1597,8 +1604,11 @@ class DefaultProvider(EggProvider):
@classmethod
def _register(cls):
- loader_cls = getattr(importlib_machinery, 'SourceFileLoader',
- type(None))
+ loader_cls = getattr(
+ importlib_machinery,
+ 'SourceFileLoader',
+ type(None),
+ )
register_loader_type(loader_cls, cls)
@@ -1746,7 +1756,10 @@ class ZipProvider(EggProvider):
if self._is_current(real_path, zip_path):
return real_path
- outf, tmpnam = _mkstemp(".$extract", dir=os.path.dirname(real_path))
+ outf, tmpnam = _mkstemp(
+ ".$extract",
+ dir=os.path.dirname(real_path),
+ )
os.write(outf, self.loader.get_data(zip_path))
os.close(outf)
utime(tmpnam, (timestamp, timestamp))
@@ -1952,7 +1965,8 @@ def find_eggs_in_zip(importer, path_item, only=False):
for subitem in metadata.resource_listdir('/'):
if _is_egg_path(subitem):
subpath = os.path.join(path_item, subitem)
- for dist in find_eggs_in_zip(zipimport.zipimporter(subpath), subpath):
+ dists = find_eggs_in_zip(zipimport.zipimporter(subpath), subpath)
+ for dist in dists:
yield dist
elif subitem.lower().endswith('.dist-info'):
subpath = os.path.join(path_item, subitem)
@@ -1961,7 +1975,6 @@ def find_eggs_in_zip(importer, path_item, only=False):
yield Distribution.from_location(path_item, subitem, submeta)
-
register_finder(zipimport.zipimporter, find_eggs_in_zip)
@@ -2008,51 +2021,121 @@ def find_on_path(importer, path_item, only=False):
path_item, os.path.join(path_item, 'EGG-INFO')
)
)
- else:
- try:
- entries = os.listdir(path_item)
- except (PermissionError, NotADirectoryError):
- return
- except OSError as e:
- # Ignore the directory if does not exist, not a directory or we
- # don't have permissions
- if (e.errno in (errno.ENOTDIR, errno.EACCES, errno.ENOENT)
- # Python 2 on Windows needs to be handled this way :(
- or hasattr(e, "winerror") and e.winerror == 267):
- return
+ return
+
+ entries = safe_listdir(path_item)
+
+ # for performance, before sorting by version,
+ # screen entries for only those that will yield
+ # distributions
+ filtered = (
+ entry
+ for entry in entries
+ if dist_factory(path_item, entry, only)
+ )
+
+ # scan for .egg and .egg-info in directory
+ path_item_entries = _by_version_descending(filtered)
+ for entry in path_item_entries:
+ fullpath = os.path.join(path_item, entry)
+ factory = dist_factory(path_item, entry, only)
+ for dist in factory(fullpath):
+ yield dist
+
+
+def dist_factory(path_item, entry, only):
+ """
+ Return a dist_factory for a path_item and entry
+ """
+ lower = entry.lower()
+ is_meta = any(map(lower.endswith, ('.egg-info', '.dist-info')))
+ return (
+ distributions_from_metadata
+ if is_meta else
+ find_distributions
+ if not only and _is_egg_path(entry) else
+ resolve_egg_link
+ if not only and lower.endswith('.egg-link') else
+ NoDists()
+ )
+
+
+class NoDists:
+ """
+ >>> bool(NoDists())
+ False
+
+ >>> list(NoDists()('anything'))
+ []
+ """
+ def __bool__(self):
+ return False
+ if six.PY2:
+ __nonzero__ = __bool__
+
+ def __call__(self, fullpath):
+ return iter(())
+
+
+def safe_listdir(path):
+ """
+ Attempt to list contents of path, but suppress some exceptions.
+ """
+ try:
+ return os.listdir(path)
+ except (PermissionError, NotADirectoryError):
+ pass
+ except OSError as e:
+ # Ignore the directory if does not exist, not a directory or
+ # permission denied
+ ignorable = (
+ e.errno in (errno.ENOTDIR, errno.EACCES, errno.ENOENT)
+ # Python 2 on Windows needs to be handled this way :(
+ or getattr(e, "winerror", None) == 267
+ )
+ if not ignorable:
raise
- # scan for .egg and .egg-info in directory
- path_item_entries = _by_version_descending(entries)
- for entry in path_item_entries:
- lower = entry.lower()
- if lower.endswith('.egg-info') or lower.endswith('.dist-info'):
- fullpath = os.path.join(path_item, entry)
- if os.path.isdir(fullpath):
- # egg-info directory, allow getting metadata
- if len(os.listdir(fullpath)) == 0:
- # Empty egg directory, skip.
- continue
- metadata = PathMetadata(path_item, fullpath)
- else:
- metadata = FileMetadata(fullpath)
- yield Distribution.from_location(
- path_item, entry, metadata, precedence=DEVELOP_DIST
- )
- elif not only and _is_egg_path(entry):
- dists = find_distributions(os.path.join(path_item, entry))
- for dist in dists:
- yield dist
- elif not only and lower.endswith('.egg-link'):
- with open(os.path.join(path_item, entry)) as entry_file:
- entry_lines = entry_file.readlines()
- for line in entry_lines:
- if not line.strip():
- continue
- path = os.path.join(path_item, line.rstrip())
- dists = find_distributions(path)
- for item in dists:
- yield item
- break
+ return ()
+
+
+def distributions_from_metadata(path):
+ root = os.path.dirname(path)
+ if os.path.isdir(path):
+ if len(os.listdir(path)) == 0:
+ # empty metadata dir; skip
+ return
+ metadata = PathMetadata(root, path)
+ else:
+ metadata = FileMetadata(path)
+ entry = os.path.basename(path)
+ yield Distribution.from_location(
+ root, entry, metadata, precedence=DEVELOP_DIST,
+ )
+
+
+def non_empty_lines(path):
+ """
+ Yield non-empty lines from file at path
+ """
+ with open(path) as f:
+ for line in f:
+ line = line.strip()
+ if line:
+ yield line
+
+
+def resolve_egg_link(path):
+ """
+ Given a path to an .egg-link, resolve distributions
+ present in the referenced path.
+ """
+ referenced_paths = non_empty_lines(path)
+ resolved_paths = (
+ os.path.join(os.path.dirname(path), ref)
+ for ref in referenced_paths
+ )
+ dist_groups = map(find_distributions, resolved_paths)
+ return next(dist_groups, ())
register_finder(pkgutil.ImpImporter, find_on_path)
@@ -2230,9 +2313,7 @@ def _is_egg_path(path):
"""
Determine if given path appears to be an egg.
"""
- return (
- path.lower().endswith('.egg')
- )
+ return path.lower().endswith('.egg')
def _is_unpacked_egg(path):
@@ -2291,7 +2372,7 @@ class EntryPoint(object):
self.name = name
self.module_name = module_name
self.attrs = tuple(attrs)
- self.extras = Requirement.parse(("x[%s]" % ','.join(extras))).extras
+ self.extras = tuple(extras)
self.dist = dist
def __str__(self):
@@ -2439,7 +2520,8 @@ class Distribution(object):
"""Wrap an actual or potential sys.path entry w/metadata"""
PKG_INFO = 'PKG-INFO'
- def __init__(self, location=None, metadata=None, project_name=None,
+ def __init__(
+ self, location=None, metadata=None, project_name=None,
version=None, py_version=PY_MAJOR, platform=None,
precedence=EGG_DIST):
self.project_name = safe_name(project_name or 'Unknown')
@@ -2715,7 +2797,8 @@ class Distribution(object):
if replace:
break
else:
- # don't modify path (even removing duplicates) if found and not replace
+ # don't modify path (even removing duplicates) if
+ # found and not replace
return
elif item == bdir and self.precedence == EGG_DIST:
# if it's an .egg, give it precedence over its directory
@@ -2812,7 +2895,10 @@ class EggInfoDistribution(Distribution):
class DistInfoDistribution(Distribution):
- """Wrap an actual or potential sys.path entry w/metadata, .dist-info style"""
+ """
+ Wrap an actual or potential sys.path entry
+ w/metadata, .dist-info style.
+ """
PKG_INFO = 'METADATA'
EQEQ = re.compile(r"([\(,])\s*(\d.*?)\s*([,\)])")
@@ -2862,7 +2948,7 @@ _distributionImpl = {
'.egg': Distribution,
'.egg-info': EggInfoDistribution,
'.dist-info': DistInfoDistribution,
- }
+}
def issue_warning(*args, **kw):
@@ -2947,7 +3033,8 @@ class Requirement(packaging.requirements.Requirement):
def __hash__(self):
return self.__hash
- def __repr__(self): return "Requirement.parse(%r)" % str(self)
+ def __repr__(self):
+ return "Requirement.parse(%r)" % str(self)
@staticmethod
def parse(s):
@@ -3081,7 +3168,10 @@ def _initialize_master_working_set():
dist.activate(replace=False)
for dist in working_set
)
- add_activation_listener(lambda dist: dist.activate(replace=True), existing=False)
+ add_activation_listener(
+ lambda dist: dist.activate(replace=True),
+ existing=False,
+ )
working_set.entries = []
# match order
list(map(working_set.add_entry, sys.path))
diff --git a/pkg_resources/tests/test_pkg_resources.py b/pkg_resources/tests/test_pkg_resources.py
index 49bf7a04..c6a7ac97 100644
--- a/pkg_resources/tests/test_pkg_resources.py
+++ b/pkg_resources/tests/test_pkg_resources.py
@@ -92,8 +92,8 @@ class TestZipProvider(object):
ts = timestamp(self.ref_time)
os.utime(filename, (ts, ts))
filename = zp.get_resource_filename(manager, 'data.dat')
- f = open(filename)
- assert f.read() == 'hello, world!'
+ with open(filename) as f:
+ assert f.read() == 'hello, world!'
manager.cleanup_resources()
diff --git a/setup.cfg b/setup.cfg
index ea649893..8da91de2 100755
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 36.4.0
+current_version = 36.8.0
commit = True
tag = True
@@ -22,5 +22,8 @@ formats = zip
[bdist_wheel]
universal = 1
+[metadata]
+license_file = LICENSE
+
[bumpversion:file:setup.py]
diff --git a/setup.py b/setup.py
index 3be4b303..492b96d8 100755
--- a/setup.py
+++ b/setup.py
@@ -89,7 +89,7 @@ def pypi_link(pkg_filename):
setup_params = dict(
name="setuptools",
- version="36.4.0",
+ version="36.8.0",
description="Easily download, build, install, upgrade, and uninstall "
"Python packages",
author="Python Packaging Authority",
diff --git a/setuptools/__init__.py b/setuptools/__init__.py
index 04f76740..7da47fbe 100644
--- a/setuptools/__init__.py
+++ b/setuptools/__init__.py
@@ -109,7 +109,27 @@ class PEP420PackageFinder(PackageFinder):
find_packages = PackageFinder.find
-setup = distutils.core.setup
+
+def _install_setup_requires(attrs):
+ # Note: do not use `setuptools.Distribution` directly, as
+ # our PEP 517 backend patch `distutils.core.Distribution`.
+ dist = distutils.core.Distribution(dict(
+ (k, v) for k, v in attrs.items()
+ if k in ('dependency_links', 'setup_requires')
+ ))
+ # Honor setup.cfg's options.
+ dist.parse_config_files(ignore_option_errors=True)
+ if dist.setup_requires:
+ dist.fetch_build_eggs(dist.setup_requires)
+
+
+def setup(**attrs):
+ # Make sure we have any requirements needed to interpret 'attrs'.
+ _install_setup_requires(attrs)
+ return distutils.core.setup(**attrs)
+
+setup.__doc__ = distutils.core.setup.__doc__
+
_Command = monkey.get_unpatched(distutils.core.Command)
diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py
new file mode 100644
index 00000000..609ea1e5
--- /dev/null
+++ b/setuptools/build_meta.py
@@ -0,0 +1,172 @@
+"""A PEP 517 interface to setuptools
+
+Previously, when a user or a command line tool (let's call it a "frontend")
+needed to make a request of setuptools to take a certain action, for
+example, generating a list of installation requirements, the frontend would
+would call "setup.py egg_info" or "setup.py bdist_wheel" on the command line.
+
+PEP 517 defines a different method of interfacing with setuptools. Rather
+than calling "setup.py" directly, the frontend should:
+
+ 1. Set the current directory to the directory with a setup.py file
+ 2. Import this module into a safe python interpreter (one in which
+ setuptools can potentially set global variables or crash hard).
+ 3. Call one of the functions defined in PEP 517.
+
+What each function does is defined in PEP 517. However, here is a "casual"
+definition of the functions (this definition should not be relied on for
+bug reports or API stability):
+
+ - `build_wheel`: build a wheel in the folder and return the basename
+ - `get_requires_for_build_wheel`: get the `setup_requires` to build
+ - `prepare_metadata_for_build_wheel`: get the `install_requires`
+ - `build_sdist`: build an sdist in the folder and return the basename
+ - `get_requires_for_build_sdist`: get the `setup_requires` to build
+
+Again, this is not a formal definition! Just a "taste" of the module.
+"""
+
+import os
+import sys
+import tokenize
+import shutil
+import contextlib
+
+import setuptools
+import distutils
+
+
+class SetupRequirementsError(BaseException):
+ def __init__(self, specifiers):
+ self.specifiers = specifiers
+
+
+class Distribution(setuptools.dist.Distribution):
+ def fetch_build_eggs(self, specifiers):
+ raise SetupRequirementsError(specifiers)
+
+ @classmethod
+ @contextlib.contextmanager
+ def patch(cls):
+ """
+ Replace
+ distutils.dist.Distribution with this class
+ for the duration of this context.
+ """
+ orig = distutils.core.Distribution
+ distutils.core.Distribution = cls
+ try:
+ yield
+ finally:
+ distutils.core.Distribution = orig
+
+
+def _run_setup(setup_script='setup.py'):
+ # Note that we can reuse our build directory between calls
+ # Correctness comes first, then optimization later
+ __file__ = setup_script
+ __name__ = '__main__'
+ f = getattr(tokenize, 'open', open)(__file__)
+ code = f.read().replace('\\r\\n', '\\n')
+ f.close()
+ exec(compile(code, __file__, 'exec'), locals())
+
+
+def _fix_config(config_settings):
+ config_settings = config_settings or {}
+ config_settings.setdefault('--global-option', [])
+ return config_settings
+
+
+def _get_build_requires(config_settings):
+ config_settings = _fix_config(config_settings)
+ requirements = ['setuptools', 'wheel']
+
+ sys.argv = sys.argv[:1] + ['egg_info'] + \
+ config_settings["--global-option"]
+ try:
+ with Distribution.patch():
+ _run_setup()
+ except SetupRequirementsError as e:
+ requirements += e.specifiers
+
+ return requirements
+
+
+def _get_immediate_subdirectories(a_dir):
+ return [name for name in os.listdir(a_dir)
+ if os.path.isdir(os.path.join(a_dir, name))]
+
+
+def get_requires_for_build_wheel(config_settings=None):
+ config_settings = _fix_config(config_settings)
+ return _get_build_requires(config_settings)
+
+
+def get_requires_for_build_sdist(config_settings=None):
+ config_settings = _fix_config(config_settings)
+ return _get_build_requires(config_settings)
+
+
+def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None):
+ sys.argv = sys.argv[:1] + ['dist_info', '--egg-base', metadata_directory]
+ _run_setup()
+
+ dist_info_directory = metadata_directory
+ while True:
+ 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:
+ dist_info_directory = os.path.join(
+ dist_info_directory, os.listdir(dist_info_directory)[0])
+ continue
+
+ assert len(dist_infos) == 1
+ break
+
+ # PEP 517 requires that the .dist-info directory be placed in the
+ # metadata_directory. To comply, we MUST copy the directory to the root
+ if dist_info_directory != metadata_directory:
+ shutil.move(
+ os.path.join(dist_info_directory, dist_infos[0]),
+ metadata_directory)
+ shutil.rmtree(dist_info_directory, ignore_errors=True)
+
+ return dist_infos[0]
+
+
+def build_wheel(wheel_directory, config_settings=None,
+ metadata_directory=None):
+ config_settings = _fix_config(config_settings)
+ wheel_directory = os.path.abspath(wheel_directory)
+ sys.argv = sys.argv[:1] + ['bdist_wheel'] + \
+ config_settings["--global-option"]
+ _run_setup()
+ if wheel_directory != 'dist':
+ shutil.rmtree(wheel_directory)
+ shutil.copytree('dist', wheel_directory)
+
+ wheels = [f for f in os.listdir(wheel_directory)
+ if f.endswith('.whl')]
+
+ assert len(wheels) == 1
+ return wheels[0]
+
+
+def build_sdist(sdist_directory, config_settings=None):
+ config_settings = _fix_config(config_settings)
+ sdist_directory = os.path.abspath(sdist_directory)
+ sys.argv = sys.argv[:1] + ['sdist'] + \
+ config_settings["--global-option"]
+ _run_setup()
+ if sdist_directory != 'dist':
+ shutil.rmtree(sdist_directory)
+ shutil.copytree('dist', sdist_directory)
+
+ sdists = [f for f in os.listdir(sdist_directory)
+ if f.endswith('.tar.gz')]
+
+ assert len(sdists) == 1
+ return sdists[0]
diff --git a/setuptools/command/__init__.py b/setuptools/command/__init__.py
index c96d33c2..fe619e2e 100644
--- a/setuptools/command/__init__.py
+++ b/setuptools/command/__init__.py
@@ -3,6 +3,7 @@ __all__ = [
'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',
]
from distutils.command.bdist import bdist
diff --git a/setuptools/command/bdist_egg.py b/setuptools/command/bdist_egg.py
index 51755d52..5fdb62d9 100644
--- a/setuptools/command/bdist_egg.py
+++ b/setuptools/command/bdist_egg.py
@@ -8,6 +8,7 @@ from distutils import log
from types import CodeType
import sys
import os
+import re
import textwrap
import marshal
@@ -240,11 +241,26 @@ class bdist_egg(Command):
log.info("Removing .py files from temporary directory")
for base, dirs, files in walk_egg(self.bdist_dir):
for name in files:
+ path = os.path.join(base, name)
+
if name.endswith('.py'):
- path = os.path.join(base, name)
log.debug("Deleting %s", path)
os.unlink(path)
+ if base.endswith('__pycache__'):
+ path_old = path
+
+ pattern = r'(?P<name>.+)\.(?P<magic>[^.]+)\.pyc'
+ m = re.match(pattern, name)
+ path_new = os.path.join(base, os.pardir, m.group('name') + '.pyc')
+ log.info("Renaming file from [%s] to [%s]" % (path_old, path_new))
+ try:
+ os.remove(path_new)
+ except OSError:
+ pass
+ os.rename(path_old, path_new)
+
+
def zip_safe(self):
safe = getattr(self.distribution, 'zip_safe', None)
if safe is not None:
diff --git a/setuptools/command/develop.py b/setuptools/command/develop.py
index 85b23c60..959c932a 100755
--- a/setuptools/command/develop.py
+++ b/setuptools/command/develop.py
@@ -95,7 +95,9 @@ class develop(namespaces.DevelopInstaller, easy_install):
path_to_setup = egg_base.replace(os.sep, '/').rstrip('/')
if path_to_setup != os.curdir:
path_to_setup = '../' * (path_to_setup.count('/') + 1)
- resolved = normalize_path(os.path.join(install_dir, egg_path, path_to_setup))
+ resolved = normalize_path(
+ os.path.join(install_dir, egg_path, path_to_setup)
+ )
if resolved != normalize_path(os.curdir):
raise DistutilsOptionError(
"Can't get a consistent path to setup script from"
diff --git a/setuptools/command/dist_info.py b/setuptools/command/dist_info.py
new file mode 100644
index 00000000..c45258fa
--- /dev/null
+++ b/setuptools/command/dist_info.py
@@ -0,0 +1,36 @@
+"""
+Create a dist_info directory
+As defined in the wheel specification
+"""
+
+import os
+
+from distutils.core import Command
+from distutils import log
+
+
+class dist_info(Command):
+
+ description = 'create a .dist-info directory'
+
+ user_options = [
+ ('egg-base=', 'e', "directory containing .egg-info directories"
+ " (default: top of the source tree)"),
+ ]
+
+ def initialize_options(self):
+ self.egg_base = None
+
+ def finalize_options(self):
+ pass
+
+ def run(self):
+ egg_info = self.get_finalized_command('egg_info')
+ egg_info.egg_base = self.egg_base
+ egg_info.finalize_options()
+ egg_info.run()
+ dist_info_dir = egg_info.egg_info[:-len('.egg-info')] + '.dist-info'
+ log.info("creating '{}'".format(os.path.abspath(dist_info_dir)))
+
+ bdist_wheel = self.get_finalized_command('bdist_wheel')
+ bdist_wheel.egg2dist(egg_info.egg_info, dist_info_dir)
diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py
index 8fba7b41..71991efa 100755
--- a/setuptools/command/easy_install.py
+++ b/setuptools/command/easy_install.py
@@ -1817,7 +1817,7 @@ def _update_zipimporter_cache(normalized_path, cache, updater=None):
# get/del patterns instead. For more detailed information see the
# following links:
# https://github.com/pypa/setuptools/issues/202#issuecomment-202913420
- # https://bitbucket.org/pypy/pypy/src/dd07756a34a41f674c0cacfbc8ae1d4cc9ea2ae4/pypy/module/zipimport/interp_zipimport.py#cl-99
+ # http://bit.ly/2h9itJX
old_entry = cache[p]
del cache[p]
new_entry = updater and updater(p, old_entry)
diff --git a/setuptools/command/test.py b/setuptools/command/test.py
index f00d6794..51aee1f7 100644
--- a/setuptools/command/test.py
+++ b/setuptools/command/test.py
@@ -18,6 +18,11 @@ from setuptools import Command
class ScanningLoader(TestLoader):
+
+ def __init__(self):
+ TestLoader.__init__(self)
+ self._visited = set()
+
def loadTestsFromModule(self, module, pattern=None):
"""Return a suite of all tests cases contained in the given module
@@ -25,6 +30,10 @@ class ScanningLoader(TestLoader):
If the module has an ``additional_tests`` function, call it and add
the return value to the tests.
"""
+ if module in self._visited:
+ return None
+ self._visited.add(module)
+
tests = []
tests.append(TestLoader.loadTestsFromModule(self, module))
@@ -101,6 +110,8 @@ class test(Command):
return list(self._test_args())
def _test_args(self):
+ if not self.test_suite and sys.version_info >= (2, 7):
+ yield 'discover'
if self.verbose:
yield '--verbose'
if self.test_suite:
diff --git a/setuptools/dist.py b/setuptools/dist.py
index a2ca8795..aa304500 100644
--- a/setuptools/dist.py
+++ b/setuptools/dist.py
@@ -316,23 +316,19 @@ class Distribution(Distribution_parse_config_files, _Distribution):
have_package_data = hasattr(self, "package_data")
if not have_package_data:
self.package_data = {}
- _attrs_dict = attrs or {}
- if 'features' in _attrs_dict or 'require_features' in _attrs_dict:
+ attrs = attrs or {}
+ if 'features' in attrs or 'require_features' in attrs:
Feature.warn_deprecated()
self.require_features = []
self.features = {}
self.dist_files = []
- self.src_root = attrs and attrs.pop("src_root", None)
+ self.src_root = attrs.pop("src_root", None)
self.patch_missing_pkg_info(attrs)
- self.long_description_content_type = _attrs_dict.get(
+ self.long_description_content_type = attrs.get(
'long_description_content_type'
)
- # Make sure we have any eggs needed to interpret 'attrs'
- if attrs is not None:
- self.dependency_links = attrs.pop('dependency_links', [])
- assert_string_list(self, 'dependency_links', self.dependency_links)
- if attrs and 'setup_requires' in attrs:
- self.fetch_build_eggs(attrs['setup_requires'])
+ self.dependency_links = attrs.pop('dependency_links', [])
+ self.setup_requires = attrs.pop('setup_requires', [])
for ep in pkg_resources.iter_entry_points('distutils.setup_keywords'):
vars(self).setdefault(ep.name, None)
_Distribution.__init__(self, attrs)
@@ -427,14 +423,15 @@ class Distribution(Distribution_parse_config_files, _Distribution):
req.marker = None
return req
- def parse_config_files(self, filenames=None):
+ def parse_config_files(self, filenames=None, ignore_option_errors=False):
"""Parses configuration files from various levels
and loads configuration.
"""
_Distribution.parse_config_files(self, filenames=filenames)
- parse_configuration(self, self.command_options)
+ parse_configuration(self, self.command_options,
+ ignore_option_errors=ignore_option_errors)
self._finalize_requires()
def parse_command_line(self):
@@ -497,19 +494,20 @@ class Distribution(Distribution_parse_config_files, _Distribution):
"""Fetch an egg needed for building"""
from setuptools.command.easy_install import easy_install
dist = self.__class__({'script_args': ['easy_install']})
- dist.parse_config_files()
opts = dist.get_option_dict('easy_install')
- keep = (
- 'find_links', 'site_dirs', 'index_url', 'optimize',
- 'site_dirs', 'allow_hosts'
- )
- for key in list(opts):
- if key not in keep:
- del opts[key] # don't use any other settings
+ 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].split() + links
+ links = opts['find_links'][1] + links
opts['find_links'] = ('setup', links)
install_dir = self.get_egg_cache_dir()
cmd = easy_install(
diff --git a/setuptools/package_index.py b/setuptools/package_index.py
index 4f610e0e..fe2ef50f 100755
--- a/setuptools/package_index.py
+++ b/setuptools/package_index.py
@@ -140,7 +140,7 @@ def distros_for_filename(filename, metadata=None):
def interpret_distro_name(
location, basename, metadata, py_version=None, precedence=SOURCE_DIST,
platform=None
- ):
+):
"""Generate alternative interpretations of a source distro name
Note: if `location` is a filesystem filename, you should call
@@ -291,7 +291,7 @@ class PackageIndex(Environment):
def __init__(
self, index_url="https://pypi.python.org/simple", hosts=('*',),
ca_bundle=None, verify_ssl=True, *args, **kw
- ):
+ ):
Environment.__init__(self, *args, **kw)
self.index_url = index_url + "/" [:not index_url.endswith('/')]
self.scanned_urls = {}
@@ -345,7 +345,8 @@ class PackageIndex(Environment):
base = f.url # handle redirects
page = f.read()
- if not isinstance(page, str): # We are in Python 3 and got bytes. We want str.
+ if not isinstance(page, str):
+ # In Python 3 and got bytes but want str.
if isinstance(f, urllib.error.HTTPError):
# Errors have no charset, assume latin1:
charset = 'latin-1'
@@ -380,8 +381,9 @@ class PackageIndex(Environment):
is_file = s and s.group(1).lower() == 'file'
if is_file or self.allows(urllib.parse.urlparse(url)[1]):
return True
- msg = ("\nNote: Bypassing %s (disallowed host; see "
- "http://bit.ly/1dg9ijs for details).\n")
+ msg = (
+ "\nNote: Bypassing %s (disallowed host; see "
+ "http://bit.ly/2hrImnY for details).\n")
if fatal:
raise DistutilsError(msg % url)
else:
@@ -499,15 +501,16 @@ class PackageIndex(Environment):
"""
checker is a ContentChecker
"""
- checker.report(self.debug,
+ checker.report(
+ self.debug,
"Validating %%s checksum for %s" % filename)
if not checker.is_valid():
tfp.close()
os.unlink(filename)
raise DistutilsError(
"%s validation failed for %s; "
- "possible download problem?" % (
- checker.hash.name, os.path.basename(filename))
+ "possible download problem?"
+ % (checker.hash.name, os.path.basename(filename))
)
def add_find_links(self, urls):
@@ -535,7 +538,8 @@ class PackageIndex(Environment):
if self[requirement.key]: # we've seen at least one distro
meth, msg = self.info, "Couldn't retrieve index page for %r"
else: # no distros seen for this name, might be misspelled
- meth, msg = (self.warn,
+ meth, msg = (
+ self.warn,
"Couldn't find index page for %r (maybe misspelled?)")
meth(msg, requirement.unsafe_name)
self.scan_all()
@@ -576,8 +580,7 @@ class PackageIndex(Environment):
def fetch_distribution(
self, requirement, tmpdir, force_scan=False, source=False,
- develop_ok=False, local_index=None
- ):
+ develop_ok=False, local_index=None):
"""Obtain a distribution suitable for fulfilling `requirement`
`requirement` must be a ``pkg_resources.Requirement`` instance.
@@ -608,12 +611,19 @@ class PackageIndex(Environment):
if dist.precedence == DEVELOP_DIST and not develop_ok:
if dist not in skipped:
- self.warn("Skipping development or system egg: %s", dist)
+ self.warn(
+ "Skipping development or system egg: %s", dist,
+ )
skipped[dist] = 1
continue
- if dist in req and (dist.precedence <= SOURCE_DIST or not source):
- dist.download_location = self.download(dist.location, tmpdir)
+ test = (
+ dist in req
+ and (dist.precedence <= SOURCE_DIST or not source)
+ )
+ if test:
+ loc = self.download(dist.location, tmpdir)
+ dist.download_location = loc
if os.path.exists(dist.download_location):
return dist
@@ -703,7 +713,7 @@ class PackageIndex(Environment):
def _download_to(self, url, filename):
self.info("Downloading %s", url)
# Download the file
- fp, info = None, None
+ fp = None
try:
checker = HashChecker.from_url(url)
fp = self.open_url(url)
@@ -892,7 +902,7 @@ class PackageIndex(Environment):
if rev is not None:
self.info("Updating to %s", rev)
- os.system("(cd %s && hg up -C -r %s >&-)" % (
+ os.system("(cd %s && hg up -C -r %s -q)" % (
filename,
rev,
))
@@ -1102,7 +1112,8 @@ def local_open(url):
f += '/'
files.append('<a href="{name}">{name}</a>'.format(name=f))
else:
- tmpl = ("<html><head><title>{url}</title>"
+ tmpl = (
+ "<html><head><title>{url}</title>"
"</head><body>{files}</body></html>")
body = tmpl.format(url=url, files='\n'.join(files))
status, message = 200, "OK"
diff --git a/setuptools/ssl_support.py b/setuptools/ssl_support.py
index 72b18ef2..6362f1f4 100644
--- a/setuptools/ssl_support.py
+++ b/setuptools/ssl_support.py
@@ -186,9 +186,14 @@ class VerifyingHTTPSConn(HTTPSConnection):
else:
actual_host = self.host
- self.sock = ssl.wrap_socket(
- sock, cert_reqs=ssl.CERT_REQUIRED, ca_certs=self.ca_bundle
- )
+ if hasattr(ssl, 'create_default_context'):
+ ctx = ssl.create_default_context(cafile=self.ca_bundle)
+ self.sock = ctx.wrap_socket(sock, server_hostname=actual_host)
+ else:
+ # This is for python < 2.7.9 and < 3.4?
+ self.sock = ssl.wrap_socket(
+ sock, cert_reqs=ssl.CERT_REQUIRED, ca_certs=self.ca_bundle
+ )
try:
match_hostname(self.sock.getpeercert(), actual_host)
except CertificateError:
diff --git a/setuptools/tests/__init__.py b/setuptools/tests/__init__.py
index 8ae4402d..54dd7d2b 100644
--- a/setuptools/tests/__init__.py
+++ b/setuptools/tests/__init__.py
@@ -1,326 +1,6 @@
-"""Tests for the 'setuptools' package"""
import locale
-import sys
-import os
-import distutils.core
-import distutils.cmd
-from distutils.errors import DistutilsOptionError, DistutilsPlatformError
-from distutils.errors import DistutilsSetupError
-from distutils.core import Extension
-from distutils.version import LooseVersion
-from setuptools.extern import six
import pytest
-import setuptools.dist
-import setuptools.depends as dep
-from setuptools import Feature
-from setuptools.depends import Require
-
is_ascii = locale.getpreferredencoding() == 'ANSI_X3.4-1968'
fail_on_ascii = pytest.mark.xfail(is_ascii, reason="Test fails in this locale")
-
-
-def makeSetup(**args):
- """Return distribution from 'setup(**args)', without executing commands"""
-
- distutils.core._setup_stop_after = "commandline"
-
- # Don't let system command line leak into tests!
- args.setdefault('script_args', ['install'])
-
- try:
- return setuptools.setup(**args)
- finally:
- distutils.core._setup_stop_after = None
-
-
-needs_bytecode = pytest.mark.skipif(
- not hasattr(dep, 'get_module_constant'),
- reason="bytecode support not available",
-)
-
-
-class TestDepends:
- def testExtractConst(self):
- if not hasattr(dep, 'extract_constant'):
- # skip on non-bytecode platforms
- return
-
- def f1():
- global x, y, z
- x = "test"
- y = z
-
- fc = six.get_function_code(f1)
-
- # unrecognized name
- assert dep.extract_constant(fc, 'q', -1) is None
-
- # constant assigned
- dep.extract_constant(fc, 'x', -1) == "test"
-
- # expression assigned
- dep.extract_constant(fc, 'y', -1) == -1
-
- # recognized name, not assigned
- dep.extract_constant(fc, 'z', -1) is None
-
- def testFindModule(self):
- with pytest.raises(ImportError):
- dep.find_module('no-such.-thing')
- with pytest.raises(ImportError):
- dep.find_module('setuptools.non-existent')
- f, p, i = dep.find_module('setuptools.tests')
- f.close()
-
- @needs_bytecode
- def testModuleExtract(self):
- from email import __version__
- assert dep.get_module_constant('email', '__version__') == __version__
- assert dep.get_module_constant('sys', 'version') == sys.version
- assert dep.get_module_constant('setuptools.tests', '__doc__') == __doc__
-
- @needs_bytecode
- def testRequire(self):
- req = Require('Email', '1.0.3', 'email')
-
- assert req.name == 'Email'
- assert req.module == 'email'
- assert req.requested_version == '1.0.3'
- assert req.attribute == '__version__'
- assert req.full_name() == 'Email-1.0.3'
-
- from email import __version__
- assert req.get_version() == __version__
- assert req.version_ok('1.0.9')
- assert not req.version_ok('0.9.1')
- assert not req.version_ok('unknown')
-
- assert req.is_present()
- assert req.is_current()
-
- req = Require('Email 3000', '03000', 'email', format=LooseVersion)
- assert req.is_present()
- assert not req.is_current()
- assert not req.version_ok('unknown')
-
- req = Require('Do-what-I-mean', '1.0', 'd-w-i-m')
- assert not req.is_present()
- assert not req.is_current()
-
- req = Require('Tests', None, 'tests', homepage="http://example.com")
- assert req.format is None
- assert req.attribute is None
- assert req.requested_version is None
- assert req.full_name() == 'Tests'
- assert req.homepage == 'http://example.com'
-
- paths = [os.path.dirname(p) for p in __path__]
- assert req.is_present(paths)
- assert req.is_current(paths)
-
-
-class TestDistro:
- def setup_method(self, method):
- self.e1 = Extension('bar.ext', ['bar.c'])
- self.e2 = Extension('c.y', ['y.c'])
-
- self.dist = makeSetup(
- packages=['a', 'a.b', 'a.b.c', 'b', 'c'],
- py_modules=['b.d', 'x'],
- ext_modules=(self.e1, self.e2),
- package_dir={},
- )
-
- def testDistroType(self):
- assert isinstance(self.dist, setuptools.dist.Distribution)
-
- def testExcludePackage(self):
- self.dist.exclude_package('a')
- assert self.dist.packages == ['b', 'c']
-
- self.dist.exclude_package('b')
- assert self.dist.packages == ['c']
- assert self.dist.py_modules == ['x']
- assert self.dist.ext_modules == [self.e1, self.e2]
-
- self.dist.exclude_package('c')
- assert self.dist.packages == []
- assert self.dist.py_modules == ['x']
- assert self.dist.ext_modules == [self.e1]
-
- # test removals from unspecified options
- makeSetup().exclude_package('x')
-
- def testIncludeExclude(self):
- # remove an extension
- self.dist.exclude(ext_modules=[self.e1])
- assert self.dist.ext_modules == [self.e2]
-
- # add it back in
- self.dist.include(ext_modules=[self.e1])
- assert self.dist.ext_modules == [self.e2, self.e1]
-
- # should not add duplicate
- self.dist.include(ext_modules=[self.e1])
- assert self.dist.ext_modules == [self.e2, self.e1]
-
- def testExcludePackages(self):
- self.dist.exclude(packages=['c', 'b', 'a'])
- assert self.dist.packages == []
- assert self.dist.py_modules == ['x']
- assert self.dist.ext_modules == [self.e1]
-
- def testEmpty(self):
- dist = makeSetup()
- dist.include(packages=['a'], py_modules=['b'], ext_modules=[self.e2])
- dist = makeSetup()
- dist.exclude(packages=['a'], py_modules=['b'], ext_modules=[self.e2])
-
- def testContents(self):
- assert self.dist.has_contents_for('a')
- self.dist.exclude_package('a')
- assert not self.dist.has_contents_for('a')
-
- assert self.dist.has_contents_for('b')
- self.dist.exclude_package('b')
- assert not self.dist.has_contents_for('b')
-
- assert self.dist.has_contents_for('c')
- self.dist.exclude_package('c')
- assert not self.dist.has_contents_for('c')
-
- def testInvalidIncludeExclude(self):
- with pytest.raises(DistutilsSetupError):
- self.dist.include(nonexistent_option='x')
- with pytest.raises(DistutilsSetupError):
- self.dist.exclude(nonexistent_option='x')
- with pytest.raises(DistutilsSetupError):
- self.dist.include(packages={'x': 'y'})
- with pytest.raises(DistutilsSetupError):
- self.dist.exclude(packages={'x': 'y'})
- with pytest.raises(DistutilsSetupError):
- self.dist.include(ext_modules={'x': 'y'})
- with pytest.raises(DistutilsSetupError):
- self.dist.exclude(ext_modules={'x': 'y'})
-
- with pytest.raises(DistutilsSetupError):
- self.dist.include(package_dir=['q'])
- with pytest.raises(DistutilsSetupError):
- self.dist.exclude(package_dir=['q'])
-
-
-class TestFeatures:
- def setup_method(self, method):
- self.req = Require('Distutils', '1.0.3', 'distutils')
- self.dist = makeSetup(
- features={
- 'foo': Feature("foo", standard=True, require_features=['baz', self.req]),
- 'bar': Feature("bar", standard=True, packages=['pkg.bar'],
- py_modules=['bar_et'], remove=['bar.ext'],
- ),
- 'baz': Feature(
- "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'],
- packages=['pkg.bar', 'pkg.foo'],
- py_modules=['bar_et', 'bazish'],
- ext_modules=[Extension('bar.ext', ['bar.c'])]
- )
-
- def testDefaults(self):
- assert not Feature(
- "test", standard=True, remove='x', available=False
- ).include_by_default()
- assert Feature("test", standard=True, remove='x').include_by_default()
- # Feature must have either kwargs, removes, or require_features
- with pytest.raises(DistutilsSetupError):
- Feature("test")
-
- def testAvailability(self):
- with pytest.raises(DistutilsPlatformError):
- self.dist.features['dwim'].include_in(self.dist)
-
- def testFeatureOptions(self):
- dist = self.dist
- assert (
- ('with-dwim', None, 'include DWIM') in dist.feature_options
- )
- assert (
- ('without-dwim', None, 'exclude DWIM (default)') in dist.feature_options
- )
- assert (
- ('with-bar', None, 'include bar (default)') in dist.feature_options
- )
- assert (
- ('without-bar', None, 'exclude bar') in dist.feature_options
- )
- assert dist.feature_negopt['without-foo'] == 'with-foo'
- assert dist.feature_negopt['without-bar'] == 'with-bar'
- assert dist.feature_negopt['without-dwim'] == 'with-dwim'
- assert ('without-baz' not in dist.feature_negopt)
-
- def testUseFeatures(self):
- dist = self.dist
- assert dist.with_foo == 1
- assert dist.with_bar == 0
- assert dist.with_baz == 1
- assert ('bar_et' not in dist.py_modules)
- assert ('pkg.bar' not in dist.packages)
- assert ('pkg.baz' in dist.packages)
- assert ('scripts/baz_it' in dist.scripts)
- assert (('libfoo', 'foo/foofoo.c') in dist.libraries)
- assert dist.ext_modules == []
- assert dist.require_features == [self.req]
-
- # If we ask for bar, it should fail because we explicitly disabled
- # it on the command line
- with pytest.raises(DistutilsOptionError):
- dist.include_feature('bar')
-
- def testFeatureWithInvalidRemove(self):
- with pytest.raises(SystemExit):
- makeSetup(features={'x': Feature('x', remove='y')})
-
-
-class TestCommandTests:
- def testTestIsCommand(self):
- test_cmd = makeSetup().get_command_obj('test')
- assert (isinstance(test_cmd, distutils.cmd.Command))
-
- def testLongOptSuiteWNoDefault(self):
- ts1 = makeSetup(script_args=['test', '--test-suite=foo.tests.suite'])
- ts1 = ts1.get_command_obj('test')
- ts1.ensure_finalized()
- assert ts1.test_suite == 'foo.tests.suite'
-
- def testDefaultSuite(self):
- ts2 = makeSetup(test_suite='bar.tests.suite').get_command_obj('test')
- ts2.ensure_finalized()
- assert ts2.test_suite == 'bar.tests.suite'
-
- def testDefaultWModuleOnCmdLine(self):
- ts3 = makeSetup(
- test_suite='bar.tests',
- script_args=['test', '-m', 'foo.tests']
- ).get_command_obj('test')
- ts3.ensure_finalized()
- assert ts3.test_module == 'foo.tests'
- assert ts3.test_suite == 'foo.tests.test_suite'
-
- def testConflictingOptions(self):
- ts4 = makeSetup(
- script_args=['test', '-m', 'bar.tests', '-s', 'foo.tests.suite']
- ).get_command_obj('test')
- with pytest.raises(DistutilsOptionError):
- ts4.ensure_finalized()
-
- def testNoSuite(self):
- ts5 = makeSetup().get_command_obj('test')
- ts5.ensure_finalized()
- assert ts5.test_suite is None
diff --git a/setuptools/tests/test_bdist_egg.py b/setuptools/tests/test_bdist_egg.py
index d24aa366..54742aa6 100644
--- a/setuptools/tests/test_bdist_egg.py
+++ b/setuptools/tests/test_bdist_egg.py
@@ -2,6 +2,7 @@
"""
import os
import re
+import zipfile
import pytest
@@ -16,7 +17,7 @@ setup(name='foo', py_modules=['hi'])
"""
-@pytest.yield_fixture
+@pytest.fixture(scope='function')
def setup_context(tmpdir):
with (tmpdir / 'setup.py').open('w') as f:
f.write(SETUP_PY)
@@ -32,7 +33,7 @@ class Test:
script_name='setup.py',
script_args=['bdist_egg'],
name='foo',
- py_modules=['hi']
+ py_modules=['hi'],
))
os.makedirs(os.path.join('build', 'src'))
with contexts.quiet():
@@ -42,3 +43,24 @@ 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)
+
+ @pytest.mark.xfail(
+ os.environ.get('PYTHONDONTWRITEBYTECODE'),
+ reason="Byte code disabled",
+ )
+ def test_exclude_source_files(self, setup_context, user_override):
+ dist = Distribution(dict(
+ script_name='setup.py',
+ script_args=['bdist_egg', '--exclude-source-files'],
+ name='foo',
+ py_modules=['hi'],
+ ))
+ with contexts.quiet():
+ dist.parse_command_line()
+ dist.run_commands()
+ [dist_name] = os.listdir('dist')
+ dist_filename = os.path.join('dist', dist_name)
+ zip = zipfile.ZipFile(dist_filename)
+ names = list(zi.filename for zi in zip.filelist)
+ assert 'hi.pyc' in names
+ assert 'hi.py' not in names
diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py
new file mode 100644
index 00000000..659c1a65
--- /dev/null
+++ b/setuptools/tests/test_build_meta.py
@@ -0,0 +1,126 @@
+import os
+
+import pytest
+
+from .files import build_files
+from .textwrap import DALS
+
+
+futures = pytest.importorskip('concurrent.futures')
+importlib = pytest.importorskip('importlib')
+
+
+class BuildBackendBase(object):
+ def __init__(self, cwd=None, env={}, backend_name='setuptools.build_meta'):
+ self.cwd = cwd
+ self.env = env
+ self.backend_name = backend_name
+
+
+class BuildBackend(BuildBackendBase):
+ """PEP 517 Build Backend"""
+ def __init__(self, *args, **kwargs):
+ super(BuildBackend, self).__init__(*args, **kwargs)
+ self.pool = futures.ProcessPoolExecutor()
+
+ def __getattr__(self, name):
+ """Handles aribrary function invocations on the build backend."""
+ def method(*args, **kw):
+ root = os.path.abspath(self.cwd)
+ caller = BuildBackendCaller(root, self.env, self.backend_name)
+ return self.pool.submit(caller, name, *args, **kw).result()
+
+ return method
+
+
+class BuildBackendCaller(BuildBackendBase):
+ def __call__(self, name, *args, **kw):
+ """Handles aribrary function invocations on the build backend."""
+ os.chdir(self.cwd)
+ os.environ.update(self.env)
+ mod = importlib.import_module(self.backend_name)
+ return getattr(mod, name)(*args, **kw)
+
+
+defns = [{
+ 'setup.py': DALS("""
+ __import__('setuptools').setup(
+ name='foo',
+ py_modules=['hello'],
+ setup_requires=['six'],
+ )
+ """),
+ 'hello.py': DALS("""
+ def run():
+ print('hello')
+ """),
+ },
+ {
+ 'setup.py': DALS("""
+ assert __name__ == '__main__'
+ __import__('setuptools').setup(
+ name='foo',
+ py_modules=['hello'],
+ setup_requires=['six'],
+ )
+ """),
+ 'hello.py': DALS("""
+ def run():
+ print('hello')
+ """),
+ },
+ {
+ 'setup.py': DALS("""
+ variable = True
+ def function():
+ return variable
+ assert variable
+ __import__('setuptools').setup(
+ name='foo',
+ py_modules=['hello'],
+ setup_requires=['six'],
+ )
+ """),
+ 'hello.py': DALS("""
+ def run():
+ print('hello')
+ """),
+ }]
+
+
+@pytest.fixture(params=defns)
+def build_backend(tmpdir, request):
+ build_files(request.param, prefix=str(tmpdir))
+ with tmpdir.as_cwd():
+ yield BuildBackend(cwd='.')
+
+
+def test_get_requires_for_build_wheel(build_backend):
+ actual = build_backend.get_requires_for_build_wheel()
+ expected = ['six', 'setuptools', 'wheel']
+ assert sorted(actual) == sorted(expected)
+
+
+def test_build_wheel(build_backend):
+ dist_dir = os.path.abspath('pip-wheel')
+ os.makedirs(dist_dir)
+ wheel_name = build_backend.build_wheel(dist_dir)
+
+ assert os.path.isfile(os.path.join(dist_dir, wheel_name))
+
+
+def test_build_sdist(build_backend):
+ dist_dir = os.path.abspath('pip-sdist')
+ os.makedirs(dist_dir)
+ sdist_name = build_backend.build_sdist(dist_dir)
+
+ assert os.path.isfile(os.path.join(dist_dir, sdist_name))
+
+
+def test_prepare_metadata_for_build_wheel(build_backend):
+ dist_dir = os.path.abspath('pip-dist-info')
+ os.makedirs(dist_dir)
+
+ dist_info = build_backend.prepare_metadata_for_build_wheel(dist_dir)
+
+ assert os.path.isfile(os.path.join(dist_dir, dist_info, 'METADATA'))
diff --git a/setuptools/tests/test_develop.py b/setuptools/tests/test_develop.py
index ad7cfa05..cb4ff4b4 100644
--- a/setuptools/tests/test_develop.py
+++ b/setuptools/tests/test_develop.py
@@ -167,7 +167,9 @@ class TestNamespaces:
target = tmpdir / 'packages'
# use pip to install to the target directory
install_cmd = [
- 'pip',
+ sys.executable,
+ '-m',
+ 'pip.__main__',
'install',
str(pkg_A),
'-t', str(target),
diff --git a/setuptools/tests/test_dist.py b/setuptools/tests/test_dist.py
index 435ffec0..c4c9bd03 100644
--- a/setuptools/tests/test_dist.py
+++ b/setuptools/tests/test_dist.py
@@ -39,6 +39,7 @@ def test_dist_fetch_build_egg(tmpdir):
'''.split()
with tmpdir.as_cwd():
dist = Distribution()
+ dist.parse_config_files()
resolved_dists = [
dist.fetch_build_egg(r)
for r in reqs
diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py
index e4ed556f..834710ef 100644
--- a/setuptools/tests/test_easy_install.py
+++ b/setuptools/tests/test_easy_install.py
@@ -380,7 +380,15 @@ class TestSetupRequires:
"""))])
yield dist_path
- def test_setup_requires_overrides_version_conflict(self):
+ use_setup_cfg = (
+ (),
+ ('dependency_links',),
+ ('setup_requires',),
+ ('dependency_links', 'setup_requires'),
+ )
+
+ @pytest.mark.parametrize('use_setup_cfg', use_setup_cfg)
+ def test_setup_requires_overrides_version_conflict(self, use_setup_cfg):
"""
Regression test for distribution issue 323:
https://bitbucket.org/tarek/distribute/issues/323
@@ -396,7 +404,7 @@ class TestSetupRequires:
with contexts.save_pkg_resources_state():
with contexts.tempdir() as temp_dir:
- test_pkg = create_setup_requires_package(temp_dir)
+ test_pkg = create_setup_requires_package(temp_dir, use_setup_cfg=use_setup_cfg)
test_setup_py = os.path.join(test_pkg, 'setup.py')
with contexts.quiet() as (stdout, stderr):
# Don't even need to install the package, just
@@ -405,9 +413,10 @@ class TestSetupRequires:
lines = stdout.readlines()
assert len(lines) > 0
- assert lines[-1].strip(), 'test_pkg'
+ assert lines[-1].strip() == 'test_pkg'
- def test_setup_requires_override_nspkg(self):
+ @pytest.mark.parametrize('use_setup_cfg', use_setup_cfg)
+ def test_setup_requires_override_nspkg(self, use_setup_cfg):
"""
Like ``test_setup_requires_overrides_version_conflict`` but where the
``setup_requires`` package is part of a namespace package that has
@@ -445,7 +454,8 @@ class TestSetupRequires:
""")
test_pkg = create_setup_requires_package(
- temp_dir, 'foo.bar', '0.2', make_nspkg_sdist, template)
+ temp_dir, 'foo.bar', '0.2', make_nspkg_sdist, template,
+ use_setup_cfg=use_setup_cfg)
test_setup_py = os.path.join(test_pkg, 'setup.py')
@@ -463,6 +473,38 @@ class TestSetupRequires:
assert len(lines) > 0
assert lines[-1].strip() == 'test_pkg'
+ @pytest.mark.parametrize('use_setup_cfg', use_setup_cfg)
+ def test_setup_requires_with_attr_version(self, use_setup_cfg):
+ def make_dependency_sdist(dist_path, distname, version):
+ make_sdist(dist_path, [
+ ('setup.py',
+ DALS("""
+ import setuptools
+ setuptools.setup(
+ name={name!r},
+ version={version!r},
+ py_modules=[{name!r}],
+ )
+ """.format(name=distname, version=version))),
+ (distname + '.py',
+ DALS("""
+ version = 42
+ """
+ ))])
+ with contexts.save_pkg_resources_state():
+ with contexts.tempdir() as temp_dir:
+ 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',),
+ )
+ test_setup_py = os.path.join(test_pkg, 'setup.py')
+ with contexts.quiet() as (stdout, stderr):
+ run_setup(test_setup_py, ['--version'])
+ lines = stdout.readlines()
+ assert len(lines) > 0
+ assert lines[-1].strip() == '42'
+
def make_trivial_sdist(dist_path, distname, version):
"""
@@ -531,7 +573,8 @@ def make_sdist(dist_path, files):
def create_setup_requires_package(path, distname='foobar', version='0.1',
make_package=make_trivial_sdist,
- setup_py_template=None):
+ setup_py_template=None, setup_attrs={},
+ use_setup_cfg=()):
"""Creates a source tree under path for a trivial test package that has a
single requirement in setup_requires--a tarball for that requirement is
also created and added to the dependency_links argument.
@@ -546,11 +589,39 @@ def create_setup_requires_package(path, distname='foobar', version='0.1',
'setup_requires': ['%s==%s' % (distname, version)],
'dependency_links': [os.path.abspath(path)]
}
+ test_setup_attrs.update(setup_attrs)
test_pkg = os.path.join(path, 'test_pkg')
- test_setup_py = os.path.join(test_pkg, 'setup.py')
os.mkdir(test_pkg)
+ if use_setup_cfg:
+ test_setup_cfg = os.path.join(test_pkg, 'setup.cfg')
+ options = []
+ metadata = []
+ for name in use_setup_cfg:
+ value = test_setup_attrs.pop(name)
+ if name in 'name version'.split():
+ section = metadata
+ else:
+ section = options
+ 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')
+
if setup_py_template is None:
setup_py_template = DALS("""\
import setuptools
diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py
index 4c04d298..a97d0c84 100644
--- a/setuptools/tests/test_egg_info.py
+++ b/setuptools/tests/test_egg_info.py
@@ -157,7 +157,8 @@ class TestEggInfo(object):
self._run_install_command(tmpdir_cwd, env)
egg_info_dir = self._find_egg_info_files(env.paths['lib']).base
sources_txt = os.path.join(egg_info_dir, 'SOURCES.txt')
- assert 'docs/usage.rst' in open(sources_txt).read().split('\n')
+ with open(sources_txt) as f:
+ assert 'docs/usage.rst' in f.read().split('\n')
def _setup_script_with_requires(self, requires, use_setup_cfg=False):
setup_script = DALS(
@@ -440,7 +441,8 @@ class TestEggInfo(object):
self._run_install_command(tmpdir_cwd, env)
egg_info_dir = self._find_egg_info_files(env.paths['lib']).base
pkginfo = os.path.join(egg_info_dir, 'PKG-INFO')
- assert 'Requires-Python: >=1.2.3' in open(pkginfo).read().split('\n')
+ with open(pkginfo) as f:
+ assert 'Requires-Python: >=1.2.3' in f.read().split('\n')
def test_manifest_maker_warning_suppression(self):
fixtures = [
diff --git a/setuptools/tests/test_namespaces.py b/setuptools/tests/test_namespaces.py
index 721cad1e..1ac1b35e 100644
--- a/setuptools/tests/test_namespaces.py
+++ b/setuptools/tests/test_namespaces.py
@@ -30,7 +30,9 @@ class TestNamespaces:
targets = site_packages, path_packages
# use pip to install to the target directory
install_cmd = [
- 'pip',
+ sys.executable,
+ '-m',
+ 'pip.__main__',
'install',
str(pkg_A),
'-t', str(site_packages),
@@ -38,7 +40,9 @@ class TestNamespaces:
subprocess.check_call(install_cmd)
namespaces.make_site_dir(site_packages)
install_cmd = [
- 'pip',
+ sys.executable,
+ '-m',
+ 'pip.__main__',
'install',
str(pkg_B),
'-t', str(path_packages),
@@ -88,7 +92,9 @@ class TestNamespaces:
target = tmpdir / 'packages'
# use pip to install to the target directory
install_cmd = [
- 'pip',
+ sys.executable,
+ '-m',
+ 'pip.__main__',
'install',
str(pkg_A),
'-t', str(target),
diff --git a/setuptools/tests/test_sdist.py b/setuptools/tests/test_sdist.py
index f34068dc..02222da5 100644
--- a/setuptools/tests/test_sdist.py
+++ b/setuptools/tests/test_sdist.py
@@ -19,6 +19,7 @@ from setuptools.command.sdist import sdist
from setuptools.command.egg_info import manifest_maker
from setuptools.dist import Distribution
from setuptools.tests import fail_on_ascii
+from .text import Filenames
py3_only = pytest.mark.xfail(six.PY2, reason="Test runs on Python 3 only")
@@ -36,13 +37,7 @@ from setuptools import setup
setup(**%r)
""" % SETUP_ATTRS
-if six.PY3:
- LATIN1_FILENAME = 'smörbröd.py'.encode('latin-1')
-else:
- LATIN1_FILENAME = 'sm\xf6rbr\xf6d.py'
-
-# Cannot use context manager because of Python 2.4
@contextlib.contextmanager
def quiet():
old_stdout, old_stderr = sys.stdout, sys.stderr
@@ -53,17 +48,10 @@ def quiet():
sys.stdout, sys.stderr = old_stdout, old_stderr
-# Fake byte literals for Python <= 2.5
-def b(s, encoding='utf-8'):
- if six.PY3:
- return s.encode(encoding)
- return s
-
-
# Convert to POSIX path
def posix(path):
if six.PY3 and not isinstance(path, str):
- return path.replace(os.sep.encode('ascii'), b('/'))
+ return path.replace(os.sep.encode('ascii'), b'/')
else:
return path.replace(os.sep, '/')
@@ -86,6 +74,21 @@ def read_all_bytes(filename):
return fp.read()
+def latin1_fail():
+ try:
+ desc, filename = tempfile.mkstemp(suffix=Filenames.latin_1)
+ os.close(desc)
+ os.remove(filename)
+ except Exception:
+ return True
+
+
+fail_on_latin1_encoded_filenames = pytest.mark.xfail(
+ latin1_fail(),
+ reason="System does not support latin-1 filenames",
+)
+
+
class TestSdistTest:
def setup_method(self, method):
self.temp_dir = tempfile.mkdtemp()
@@ -134,8 +137,8 @@ class TestSdistTest:
def test_defaults_case_sensitivity(self):
"""
- Make sure default files (README.*, etc.) are added in a case-sensitive
- way to avoid problems with packages built on Windows.
+ 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()
@@ -152,7 +155,9 @@ class TestSdistTest:
with quiet():
cmd.run()
- # lowercase all names so we can test in a case-insensitive way to make sure the files are not included
+ # lowercase all names so we can test in a
+ # case-insensitive way to make sure the files
+ # are not included.
manifest = map(lambda x: x.lower(), cmd.filelist.files)
assert 'readme.rst' not in manifest, manifest
assert 'setup.py' not in manifest, manifest
@@ -201,8 +206,7 @@ class TestSdistTest:
mm.manifest = os.path.join('sdist_test.egg-info', 'SOURCES.txt')
os.mkdir('sdist_test.egg-info')
- # UTF-8 filename
- filename = os.path.join(b('sdist_test'), b('smörbröd.py'))
+ filename = os.path.join(b'sdist_test', Filenames.utf_8)
# Must touch the file or risk removal
open(filename, "w").close()
@@ -241,7 +245,7 @@ class TestSdistTest:
os.mkdir('sdist_test.egg-info')
# Latin-1 filename
- filename = os.path.join(b('sdist_test'), LATIN1_FILENAME)
+ filename = os.path.join(b'sdist_test', Filenames.latin_1)
# Add filename with surrogates and write manifest
with quiet():
@@ -275,10 +279,10 @@ class TestSdistTest:
cmd.run()
# Add UTF-8 filename to manifest
- filename = os.path.join(b('sdist_test'), b('smörbröd.py'))
+ filename = os.path.join(b'sdist_test', Filenames.utf_8)
cmd.manifest = os.path.join('sdist_test.egg-info', 'SOURCES.txt')
manifest = open(cmd.manifest, 'ab')
- manifest.write(b('\n') + filename)
+ manifest.write(b'\n' + filename)
manifest.close()
# The file must exist to be included in the filelist
@@ -295,6 +299,7 @@ class TestSdistTest:
assert filename in cmd.filelist.files
@py3_only
+ @fail_on_latin1_encoded_filenames
def test_read_manifest_skips_non_utf8_filenames(self):
# Test for #303.
dist = Distribution(SETUP_ATTRS)
@@ -307,10 +312,10 @@ class TestSdistTest:
cmd.run()
# Add Latin-1 filename to manifest
- filename = os.path.join(b('sdist_test'), LATIN1_FILENAME)
+ filename = os.path.join(b'sdist_test', Filenames.latin_1)
cmd.manifest = os.path.join('sdist_test.egg-info', 'SOURCES.txt')
manifest = open(cmd.manifest, 'ab')
- manifest.write(b('\n') + filename)
+ manifest.write(b'\n' + filename)
manifest.close()
# The file must exist to be included in the filelist
@@ -326,6 +331,7 @@ class TestSdistTest:
assert filename not in cmd.filelist.files
@fail_on_ascii
+ @fail_on_latin1_encoded_filenames
def test_sdist_with_utf8_encoded_filename(self):
# Test for #303.
dist = Distribution(SETUP_ATTRS)
@@ -333,8 +339,7 @@ class TestSdistTest:
cmd = sdist(dist)
cmd.ensure_finalized()
- # UTF-8 filename
- filename = os.path.join(b('sdist_test'), b('smörbröd.py'))
+ filename = os.path.join(b'sdist_test', Filenames.utf_8)
open(filename, 'w').close()
with quiet():
@@ -360,6 +365,7 @@ class TestSdistTest:
else:
assert filename in cmd.filelist.files
+ @fail_on_latin1_encoded_filenames
def test_sdist_with_latin1_encoded_filename(self):
# Test for #303.
dist = Distribution(SETUP_ATTRS)
@@ -368,7 +374,7 @@ class TestSdistTest:
cmd.ensure_finalized()
# Latin-1 filename
- filename = os.path.join(b('sdist_test'), LATIN1_FILENAME)
+ filename = os.path.join(b'sdist_test', Filenames.latin_1)
open(filename, 'w').close()
assert os.path.isfile(filename)
@@ -381,10 +387,9 @@ class TestSdistTest:
# Latin-1 is similar to Windows-1252 however
# on mbcs filesys it is not in latin-1 encoding
fs_enc = sys.getfilesystemencoding()
- if fs_enc == 'mbcs':
- filename = filename.decode('mbcs')
- else:
- filename = filename.decode('latin-1')
+ if fs_enc != 'mbcs':
+ fs_enc = 'latin-1'
+ filename = filename.decode(fs_enc)
assert filename in cmd.filelist.files
else:
diff --git a/setuptools/tests/test_setuptools.py b/setuptools/tests/test_setuptools.py
index e59800d2..26e37a6c 100644
--- a/setuptools/tests/test_setuptools.py
+++ b/setuptools/tests/test_setuptools.py
@@ -1,8 +1,328 @@
+"""Tests for the 'setuptools' package"""
+
+import sys
import os
+import distutils.core
+import distutils.cmd
+from distutils.errors import DistutilsOptionError, DistutilsPlatformError
+from distutils.errors import DistutilsSetupError
+from distutils.core import Extension
+from distutils.version import LooseVersion
import pytest
import setuptools
+import setuptools.dist
+import setuptools.depends as dep
+from setuptools import Feature
+from setuptools.depends import Require
+from setuptools.extern import six
+
+
+def makeSetup(**args):
+ """Return distribution from 'setup(**args)', without executing commands"""
+
+ distutils.core._setup_stop_after = "commandline"
+
+ # Don't let system command line leak into tests!
+ args.setdefault('script_args', ['install'])
+
+ try:
+ return setuptools.setup(**args)
+ finally:
+ distutils.core._setup_stop_after = None
+
+
+needs_bytecode = pytest.mark.skipif(
+ not hasattr(dep, 'get_module_constant'),
+ reason="bytecode support not available",
+)
+
+
+class TestDepends:
+ def testExtractConst(self):
+ if not hasattr(dep, 'extract_constant'):
+ # skip on non-bytecode platforms
+ return
+
+ def f1():
+ global x, y, z
+ x = "test"
+ y = z
+
+ fc = six.get_function_code(f1)
+
+ # unrecognized name
+ assert dep.extract_constant(fc, 'q', -1) is None
+
+ # constant assigned
+ dep.extract_constant(fc, 'x', -1) == "test"
+
+ # expression assigned
+ dep.extract_constant(fc, 'y', -1) == -1
+
+ # recognized name, not assigned
+ dep.extract_constant(fc, 'z', -1) is None
+
+ def testFindModule(self):
+ with pytest.raises(ImportError):
+ dep.find_module('no-such.-thing')
+ with pytest.raises(ImportError):
+ dep.find_module('setuptools.non-existent')
+ f, p, i = dep.find_module('setuptools.tests')
+ f.close()
+
+ @needs_bytecode
+ def testModuleExtract(self):
+ from json import __version__
+ assert dep.get_module_constant('json', '__version__') == __version__
+ assert dep.get_module_constant('sys', 'version') == sys.version
+ assert dep.get_module_constant('setuptools.tests.test_setuptools', '__doc__') == __doc__
+
+ @needs_bytecode
+ def testRequire(self):
+ req = Require('Json', '1.0.3', 'json')
+
+ assert req.name == 'Json'
+ assert req.module == 'json'
+ assert req.requested_version == '1.0.3'
+ assert req.attribute == '__version__'
+ assert req.full_name() == 'Json-1.0.3'
+
+ from json import __version__
+ assert req.get_version() == __version__
+ assert req.version_ok('1.0.9')
+ assert not req.version_ok('0.9.1')
+ assert not req.version_ok('unknown')
+
+ assert req.is_present()
+ assert req.is_current()
+
+ req = Require('Json 3000', '03000', 'json', format=LooseVersion)
+ assert req.is_present()
+ assert not req.is_current()
+ assert not req.version_ok('unknown')
+
+ req = Require('Do-what-I-mean', '1.0', 'd-w-i-m')
+ assert not req.is_present()
+ assert not req.is_current()
+
+ req = Require('Tests', None, 'tests', homepage="http://example.com")
+ assert req.format is None
+ assert req.attribute is None
+ assert req.requested_version is None
+ assert req.full_name() == 'Tests'
+ assert req.homepage == 'http://example.com'
+
+ from setuptools.tests import __path__
+ paths = [os.path.dirname(p) for p in __path__]
+ assert req.is_present(paths)
+ assert req.is_current(paths)
+
+
+class TestDistro:
+ def setup_method(self, method):
+ self.e1 = Extension('bar.ext', ['bar.c'])
+ self.e2 = Extension('c.y', ['y.c'])
+
+ self.dist = makeSetup(
+ packages=['a', 'a.b', 'a.b.c', 'b', 'c'],
+ py_modules=['b.d', 'x'],
+ ext_modules=(self.e1, self.e2),
+ package_dir={},
+ )
+
+ def testDistroType(self):
+ assert isinstance(self.dist, setuptools.dist.Distribution)
+
+ def testExcludePackage(self):
+ self.dist.exclude_package('a')
+ assert self.dist.packages == ['b', 'c']
+
+ self.dist.exclude_package('b')
+ assert self.dist.packages == ['c']
+ assert self.dist.py_modules == ['x']
+ assert self.dist.ext_modules == [self.e1, self.e2]
+
+ self.dist.exclude_package('c')
+ assert self.dist.packages == []
+ assert self.dist.py_modules == ['x']
+ assert self.dist.ext_modules == [self.e1]
+
+ # test removals from unspecified options
+ makeSetup().exclude_package('x')
+
+ def testIncludeExclude(self):
+ # remove an extension
+ self.dist.exclude(ext_modules=[self.e1])
+ assert self.dist.ext_modules == [self.e2]
+
+ # add it back in
+ self.dist.include(ext_modules=[self.e1])
+ assert self.dist.ext_modules == [self.e2, self.e1]
+
+ # should not add duplicate
+ self.dist.include(ext_modules=[self.e1])
+ assert self.dist.ext_modules == [self.e2, self.e1]
+
+ def testExcludePackages(self):
+ self.dist.exclude(packages=['c', 'b', 'a'])
+ assert self.dist.packages == []
+ assert self.dist.py_modules == ['x']
+ assert self.dist.ext_modules == [self.e1]
+
+ def testEmpty(self):
+ dist = makeSetup()
+ dist.include(packages=['a'], py_modules=['b'], ext_modules=[self.e2])
+ dist = makeSetup()
+ dist.exclude(packages=['a'], py_modules=['b'], ext_modules=[self.e2])
+
+ def testContents(self):
+ assert self.dist.has_contents_for('a')
+ self.dist.exclude_package('a')
+ assert not self.dist.has_contents_for('a')
+
+ assert self.dist.has_contents_for('b')
+ self.dist.exclude_package('b')
+ assert not self.dist.has_contents_for('b')
+
+ assert self.dist.has_contents_for('c')
+ self.dist.exclude_package('c')
+ assert not self.dist.has_contents_for('c')
+
+ def testInvalidIncludeExclude(self):
+ with pytest.raises(DistutilsSetupError):
+ self.dist.include(nonexistent_option='x')
+ with pytest.raises(DistutilsSetupError):
+ self.dist.exclude(nonexistent_option='x')
+ with pytest.raises(DistutilsSetupError):
+ self.dist.include(packages={'x': 'y'})
+ with pytest.raises(DistutilsSetupError):
+ self.dist.exclude(packages={'x': 'y'})
+ with pytest.raises(DistutilsSetupError):
+ self.dist.include(ext_modules={'x': 'y'})
+ with pytest.raises(DistutilsSetupError):
+ self.dist.exclude(ext_modules={'x': 'y'})
+
+ with pytest.raises(DistutilsSetupError):
+ self.dist.include(package_dir=['q'])
+ with pytest.raises(DistutilsSetupError):
+ self.dist.exclude(package_dir=['q'])
+
+
+class TestFeatures:
+ def setup_method(self, method):
+ self.req = Require('Distutils', '1.0.3', 'distutils')
+ self.dist = makeSetup(
+ features={
+ 'foo': Feature("foo", standard=True, require_features=['baz', self.req]),
+ 'bar': Feature("bar", standard=True, packages=['pkg.bar'],
+ py_modules=['bar_et'], remove=['bar.ext'],
+ ),
+ 'baz': Feature(
+ "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'],
+ packages=['pkg.bar', 'pkg.foo'],
+ py_modules=['bar_et', 'bazish'],
+ ext_modules=[Extension('bar.ext', ['bar.c'])]
+ )
+
+ def testDefaults(self):
+ assert not Feature(
+ "test", standard=True, remove='x', available=False
+ ).include_by_default()
+ assert Feature("test", standard=True, remove='x').include_by_default()
+ # Feature must have either kwargs, removes, or require_features
+ with pytest.raises(DistutilsSetupError):
+ Feature("test")
+
+ def testAvailability(self):
+ with pytest.raises(DistutilsPlatformError):
+ self.dist.features['dwim'].include_in(self.dist)
+
+ def testFeatureOptions(self):
+ dist = self.dist
+ assert (
+ ('with-dwim', None, 'include DWIM') in dist.feature_options
+ )
+ assert (
+ ('without-dwim', None, 'exclude DWIM (default)') in dist.feature_options
+ )
+ assert (
+ ('with-bar', None, 'include bar (default)') in dist.feature_options
+ )
+ assert (
+ ('without-bar', None, 'exclude bar') in dist.feature_options
+ )
+ assert dist.feature_negopt['without-foo'] == 'with-foo'
+ assert dist.feature_negopt['without-bar'] == 'with-bar'
+ assert dist.feature_negopt['without-dwim'] == 'with-dwim'
+ assert ('without-baz' not in dist.feature_negopt)
+
+ def testUseFeatures(self):
+ dist = self.dist
+ assert dist.with_foo == 1
+ assert dist.with_bar == 0
+ assert dist.with_baz == 1
+ assert ('bar_et' not in dist.py_modules)
+ assert ('pkg.bar' not in dist.packages)
+ assert ('pkg.baz' in dist.packages)
+ assert ('scripts/baz_it' in dist.scripts)
+ assert (('libfoo', 'foo/foofoo.c') in dist.libraries)
+ assert dist.ext_modules == []
+ assert dist.require_features == [self.req]
+
+ # If we ask for bar, it should fail because we explicitly disabled
+ # it on the command line
+ with pytest.raises(DistutilsOptionError):
+ dist.include_feature('bar')
+
+ def testFeatureWithInvalidRemove(self):
+ with pytest.raises(SystemExit):
+ makeSetup(features={'x': Feature('x', remove='y')})
+
+
+class TestCommandTests:
+ def testTestIsCommand(self):
+ test_cmd = makeSetup().get_command_obj('test')
+ assert (isinstance(test_cmd, distutils.cmd.Command))
+
+ def testLongOptSuiteWNoDefault(self):
+ ts1 = makeSetup(script_args=['test', '--test-suite=foo.tests.suite'])
+ ts1 = ts1.get_command_obj('test')
+ ts1.ensure_finalized()
+ assert ts1.test_suite == 'foo.tests.suite'
+
+ def testDefaultSuite(self):
+ ts2 = makeSetup(test_suite='bar.tests.suite').get_command_obj('test')
+ ts2.ensure_finalized()
+ assert ts2.test_suite == 'bar.tests.suite'
+
+ def testDefaultWModuleOnCmdLine(self):
+ ts3 = makeSetup(
+ test_suite='bar.tests',
+ script_args=['test', '-m', 'foo.tests']
+ ).get_command_obj('test')
+ ts3.ensure_finalized()
+ assert ts3.test_module == 'foo.tests'
+ assert ts3.test_suite == 'foo.tests.test_suite'
+
+ def testConflictingOptions(self):
+ ts4 = makeSetup(
+ script_args=['test', '-m', 'bar.tests', '-s', 'foo.tests.suite']
+ ).get_command_obj('test')
+ with pytest.raises(DistutilsOptionError):
+ ts4.ensure_finalized()
+
+ def testNoSuite(self):
+ ts5 = makeSetup().get_command_obj('test')
+ ts5.ensure_finalized()
+ assert ts5.test_suite is None
@pytest.fixture
diff --git a/setuptools/tests/test_test.py b/setuptools/tests/test_test.py
index 7ea43c57..960527bc 100644
--- a/setuptools/tests/test_test.py
+++ b/setuptools/tests/test_test.py
@@ -2,9 +2,9 @@
from __future__ import unicode_literals
+from distutils import log
import os
-import site
-from distutils.errors import DistutilsError
+import sys
import pytest
@@ -66,26 +66,66 @@ def sample_test(tmpdir_cwd):
f.write(TEST_PY)
-@pytest.mark.skipif('hasattr(sys, "real_prefix")')
-@pytest.mark.usefixtures('user_override')
-@pytest.mark.usefixtures('sample_test')
-class TestTestTest:
- def test_test(self):
- 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.user = 1
- cmd.ensure_finalized()
- cmd.install_dir = site.USER_SITE
- cmd.user = 1
- with contexts.quiet():
- # The test runner calls sys.exit
- with contexts.suppress_exceptions(SystemExit):
- cmd.run()
+@pytest.fixture
+def quiet_log():
+ # Running some of the other tests will automatically
+ # change the log level to info, messing our output.
+ log.set_verbosity(0)
+
+
+@pytest.mark.usefixtures('sample_test', 'quiet_log')
+def test_test(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()
+ # The test runner calls sys.exit
+ with contexts.suppress_exceptions(SystemExit):
+ cmd.run()
+ out, err = capfd.readouterr()
+ assert out == 'Foo\n'
+
+
+@pytest.mark.xfail(
+ sys.version_info < (2, 7),
+ reason="No discover support for unittest on Python 2.6",
+)
+@pytest.mark.usefixtures('tmpdir_cwd', 'quiet_log')
+def test_tests_are_run_once(capfd):
+ params = dict(
+ name='foo',
+ packages=['dummy'],
+ )
+ with open('setup.py', 'wt') as f:
+ f.write('from setuptools import setup; setup(\n')
+ for k, v in sorted(params.items()):
+ f.write(' %s=%r,\n' % (k, v))
+ f.write(')\n')
+ os.makedirs('dummy')
+ with open('dummy/__init__.py', 'wt'):
+ pass
+ with open('dummy/test_dummy.py', 'wt') as f:
+ f.write(DALS(
+ """
+ from __future__ import print_function
+ import unittest
+ class TestTest(unittest.TestCase):
+ def test_test(self):
+ print('Foo')
+ """))
+ dist = Distribution(params)
+ dist.script_name = 'setup.py'
+ cmd = test(dist)
+ cmd.ensure_finalized()
+ # The test runner calls sys.exit
+ with contexts.suppress_exceptions(SystemExit):
+ cmd.run()
+ out, err = capfd.readouterr()
+ assert out == 'Foo\n'
diff --git a/setuptools/tests/test_virtualenv.py b/setuptools/tests/test_virtualenv.py
index 17b8793c..9dbd3c86 100644
--- a/setuptools/tests/test_virtualenv.py
+++ b/setuptools/tests/test_virtualenv.py
@@ -1,5 +1,6 @@
import glob
import os
+import sys
from pytest import yield_fixture
from pytest_fixture_config import yield_requires_config
@@ -39,6 +40,9 @@ def test_pip_upgrade_from_source(virtualenv):
Check pip can upgrade setuptools from source.
"""
dist_dir = virtualenv.workspace
+ if sys.version_info < (2, 7):
+ # Python 2.6 support was dropped in wheel 0.30.0.
+ virtualenv.run('pip install -U "wheel<0.30.0"')
# Generate source distribution / wheel.
virtualenv.run(' && '.join((
'cd {source}',
diff --git a/setuptools/tests/text.py b/setuptools/tests/text.py
new file mode 100644
index 00000000..ad2c6249
--- /dev/null
+++ b/setuptools/tests/text.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import unicode_literals
+
+
+class Filenames:
+ unicode = 'smörbröd.py'
+ latin_1 = unicode.encode('latin-1')
+ utf_8 = unicode.encode('utf-8')