diff options
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 ------- @@ -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() @@ -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] @@ -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') |