From f14930e66601b462699c44384c482cd966f53b8f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 9 Dec 2016 08:16:33 -0500 Subject: [PATCH 1/7] Drop support for Python 2.6, removing lots of compatibility code for a leaner, cleaner codebase. Fixes #878. --- .travis.yml | 1 - CHANGES.rst | 6 ++++++ README.rst | 4 +--- docs/easy_install.txt | 12 +++++------ docs/formats.txt | 2 +- docs/pkg_resources.txt | 18 +++------------- docs/setuptools.txt | 2 +- pkg_resources/__init__.py | 24 ++------------------- pkg_resources/api_tests.txt | 4 ++-- pkg_resources/tests/test_resources.py | 28 +++++++++--------------- setup.py | 3 +-- setuptools/archive_util.py | 4 ++-- setuptools/command/egg_info.py | 4 +--- setuptools/command/sdist.py | 7 ------ setuptools/command/test.py | 7 +++--- setuptools/config.py | 2 +- setuptools/monkey.py | 2 +- setuptools/package_index.py | 3 +-- setuptools/py26compat.py | 31 --------------------------- setuptools/py31compat.py | 15 ------------- setuptools/sandbox.py | 4 ---- setuptools/tests/py26compat.py | 16 -------------- setuptools/tests/test_easy_install.py | 7 +++--- setuptools/tests/test_egg_info.py | 8 ------- 24 files changed, 44 insertions(+), 170 deletions(-) delete mode 100644 setuptools/py26compat.py delete mode 100644 setuptools/tests/py26compat.py diff --git a/.travis.yml b/.travis.yml index 2f9b6a7a06..9277af80d9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: python python: - - 2.6 - 2.7 - 3.3 - 3.4 diff --git a/CHANGES.rst b/CHANGES.rst index 248a52c6ac..50bdd3ab18 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +v31.0.0 +------- + +* #878: Drop support for Python 2.6. Python 2.6 users should + rely on 'setuptools < 31dev'. + v30.3.0 ------- diff --git a/README.rst b/README.rst index 2cf762d7d6..85b1a9932b 100755 --- a/README.rst +++ b/README.rst @@ -17,9 +17,7 @@ The recommended way to bootstrap setuptools on any system is to download operating systems have different recommended techniques to accomplish this basic routine, so below are some examples to get you started. -Setuptools requires Python 2.6 or later. To install setuptools -on Python 2.4 or Python 2.5, use the `bootstrap script for Setuptools 1.x -`_. +Setuptools requires Python 3.3 or later (or Python 2.7). The link provided to ez_setup.py is a bookmark to bootstrap script for the latest known stable release. diff --git a/docs/easy_install.txt b/docs/easy_install.txt index bd9f0e863d..56b1667226 100644 --- a/docs/easy_install.txt +++ b/docs/easy_install.txt @@ -35,7 +35,7 @@ Please see the `setuptools PyPI page `_ for download links and basic installation instructions for each of the supported platforms. -You will need at least Python 2.6. An ``easy_install`` script will be +You will need at least Python 3.3 or 2.7. An ``easy_install`` script will be installed in the normal location for Python scripts on your platform. Note that the instructions on the setuptools PyPI page assume that you are @@ -305,8 +305,7 @@ Regardless of the technique used, the script(s) will be installed to a Scripts directory (by default in the Python installation directory). It is recommended for EasyInstall that you ensure this directory is in the PATH environment variable. The easiest way to ensure the Scripts directory is in the PATH is -to run ``Tools\Scripts\win_add2path.py`` from the Python directory (requires -Python 2.6 or later). +to run ``Tools\Scripts\win_add2path.py`` from the Python directory. Note that instead of changing your ``PATH`` to include the Python scripts directory, you can also retarget the installation location for scripts so they @@ -987,21 +986,20 @@ The following section lists only the easiest and most relevant approaches [1]_. `Use "virtualenv"`_ -.. [1] There are older ways to achieve custom installation using various ``easy_install`` and ``setup.py install`` options, combined with ``PYTHONPATH`` and/or ``PYTHONUSERBASE`` alterations, but all of these are effectively deprecated by the User scheme brought in by `PEP-370`_ in Python 2.6. +.. [1] There are older ways to achieve custom installation using various ``easy_install`` and ``setup.py install`` options, combined with ``PYTHONPATH`` and/or ``PYTHONUSERBASE`` alterations, but all of these are effectively deprecated by the User scheme brought in by `PEP-370`_. .. _PEP-370: http://www.python.org/dev/peps/pep-0370/ Use the "--user" option ~~~~~~~~~~~~~~~~~~~~~~~ -With Python 2.6 came the User scheme for installation, which means that all -python distributions support an alternative install location that is specific to a user [2]_ [3]_. +Python provides a User scheme for installation, which means that all +python distributions support an alternative install location that is specific to a user [3]_. The Default location for each OS is explained in the python documentation for the ``site.USER_BASE`` variable. This mode of installation can be turned on by specifying the ``--user`` option to ``setup.py install`` or ``easy_install``. This approach serves the need to have a user-specific stash of packages. -.. [2] Prior to Python2.6, Mac OS X offered a form of the User scheme. That is now subsumed into the User scheme introduced in Python 2.6. .. [3] Prior to the User scheme, there was the Home scheme, which is still available, but requires more effort than the User scheme to get packages recognized. Use the "--user" option and customize "PYTHONUSERBASE" diff --git a/docs/formats.txt b/docs/formats.txt index 9e6fe72787..a182eb99fc 100644 --- a/docs/formats.txt +++ b/docs/formats.txt @@ -110,7 +110,7 @@ the need to create a directory just to store one file. This option is other metadata. (In fact, setuptools itself never generates ``.egg-info`` files, either; the support for using files was added so that the requirement could easily be satisfied by other tools, such -as the distutils in Python 2.5). +as distutils). In addition to the ``PKG-INFO`` file, an egg's metadata directory may also include files and directories representing various forms of diff --git a/docs/pkg_resources.txt b/docs/pkg_resources.txt index e8412b3371..c504412d8b 100644 --- a/docs/pkg_resources.txt +++ b/docs/pkg_resources.txt @@ -622,8 +622,8 @@ Requirements Parsing The "markers" in a requirement are used to specify when a requirement should be installed -- the requirement will be installed if the marker evaluates as true in the current environment. For example, specifying - ``argparse;python_version<"2.7"`` will not install in an Python 2.7 or 3.3 - environment, but will in a Python 2.6 environment. + ``argparse;python_version<"3.0"`` will not install in an Python 3 + environment, but will in a Python 2 environment. ``Requirement`` Methods and Attributes -------------------------------------- @@ -1660,19 +1660,7 @@ PEP 302 Utilities ----------------- ``get_importer(path_item)`` - Retrieve a PEP 302 "importer" for the given path item (which need not - actually be on ``sys.path``). This routine simulates the PEP 302 protocol - for obtaining an "importer" object. It first checks for an importer for - the path item in ``sys.path_importer_cache``, and if not found it calls - each of the ``sys.path_hooks`` and caches the result if a good importer is - found. If no importer is found, this routine returns an ``ImpWrapper`` - instance that wraps the builtin import machinery as a PEP 302-compliant - "importer" object. This ``ImpWrapper`` is *not* cached; instead a new - instance is returned each time. - - (Note: When run under Python 2.5, this function is simply an alias for - ``pkgutil.get_importer()``, and instead of ``pkg_resources.ImpWrapper`` - instances, it may return ``pkgutil.ImpImporter`` instances.) + A deprecated alias for ``pkgutil.get_importer()`` File/Path Utilities diff --git a/docs/setuptools.txt b/docs/setuptools.txt index 77de255b90..1b0be77d8e 100644 --- a/docs/setuptools.txt +++ b/docs/setuptools.txt @@ -3,7 +3,7 @@ Building and Distributing Packages with Setuptools ================================================== ``Setuptools`` is a collection of enhancements to the Python ``distutils`` -(for Python 2.6 and up) that allow developers to more easily build and +that allow developers to more easily build and distribute Python packages, especially ones that have dependencies on other packages. diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index 92503288e7..d8000b001a 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -1611,7 +1611,7 @@ def build(cls, path): Use a platform-specific path separator (os.sep) for the path keys for compatibility with pypy on Windows. """ - with ContextualZipFile(path) as zfile: + with zipfile.ZipFile(path) as zfile: items = ( ( name.replace('/', os.sep), @@ -1644,26 +1644,6 @@ def load(self, path): return self[path].manifest -class ContextualZipFile(zipfile.ZipFile): - """ - Supplement ZipFile class to support context manager for Python 2.6 - """ - - def __enter__(self): - return self - - def __exit__(self, type, value, traceback): - self.close() - - def __new__(cls, *args, **kwargs): - """ - Construct a ZipFile or ContextualZipFile as appropriate - """ - if hasattr(zipfile.ZipFile, '__exit__'): - return zipfile.ZipFile(*args, **kwargs) - return super(ContextualZipFile, cls).__new__(cls) - - class ZipProvider(EggProvider): """Resource support for zips and eggs""" @@ -1861,7 +1841,7 @@ def get_metadata(self, name): return metadata def _warn_on_replacement(self, metadata): - # Python 2.6 and 3.2 compat for: replacement_char = '�' + # Python 2.7 compat for: replacement_char = '�' replacement_char = b'\xef\xbf\xbd'.decode('utf-8') if replacement_char in metadata: tmpl = "{self.path} could not be properly decoded in UTF-8" diff --git a/pkg_resources/api_tests.txt b/pkg_resources/api_tests.txt index 4fbd3d235d..0a75170e4f 100644 --- a/pkg_resources/api_tests.txt +++ b/pkg_resources/api_tests.txt @@ -385,10 +385,10 @@ Environment Markers >>> em("sys_platform=='win32'") == (sys.platform=='win32') True - >>> em("python_version >= '2.6'") + >>> em("python_version >= '2.7'") True - >>> em("python_version > '2.5'") + >>> em("python_version > '2.6'") True >>> im("implementation_name=='cpython'") diff --git a/pkg_resources/tests/test_resources.py b/pkg_resources/tests/test_resources.py index 00ca74262a..8223963ced 100644 --- a/pkg_resources/tests/test_resources.py +++ b/pkg_resources/tests/test_resources.py @@ -206,12 +206,10 @@ def test_marker_evaluation_with_extras(self): """Extras are also evaluated as markers at resolution time.""" ad = pkg_resources.Environment([]) ws = WorkingSet([]) - # Metadata needs to be native strings due to cStringIO behaviour in - # 2.6, so use str(). Foo = Distribution.from_filename( "/foo_dir/Foo-1.2.dist-info", - metadata=Metadata(("METADATA", str("Provides-Extra: baz\n" - "Requires-Dist: quux; extra=='baz'"))) + metadata=Metadata(("METADATA", "Provides-Extra: baz\n" + "Requires-Dist: quux; extra=='baz'")) ) ad.add(Foo) assert list(ws.resolve(parse_requirements("Foo"), ad)) == [Foo] @@ -224,12 +222,10 @@ def test_marker_evaluation_with_extras_normlized(self): """Extras are also evaluated as markers at resolution time.""" ad = pkg_resources.Environment([]) ws = WorkingSet([]) - # Metadata needs to be native strings due to cStringIO behaviour in - # 2.6, so use str(). Foo = Distribution.from_filename( "/foo_dir/Foo-1.2.dist-info", - metadata=Metadata(("METADATA", str("Provides-Extra: baz-lightyear\n" - "Requires-Dist: quux; extra=='baz-lightyear'"))) + metadata=Metadata(("METADATA", "Provides-Extra: baz-lightyear\n" + "Requires-Dist: quux; extra=='baz-lightyear'")) ) ad.add(Foo) assert list(ws.resolve(parse_requirements("Foo"), ad)) == [Foo] @@ -241,14 +237,12 @@ def test_marker_evaluation_with_extras_normlized(self): def test_marker_evaluation_with_multiple_extras(self): ad = pkg_resources.Environment([]) ws = WorkingSet([]) - # Metadata needs to be native strings due to cStringIO behaviour in - # 2.6, so use str(). Foo = Distribution.from_filename( "/foo_dir/Foo-1.2.dist-info", - metadata=Metadata(("METADATA", str("Provides-Extra: baz\n" + metadata=Metadata(("METADATA", "Provides-Extra: baz\n" "Requires-Dist: quux; extra=='baz'\n" "Provides-Extra: bar\n" - "Requires-Dist: fred; extra=='bar'\n"))) + "Requires-Dist: fred; extra=='bar'\n")) ) ad.add(Foo) quux = Distribution.from_filename("/foo_dir/quux-1.0.dist-info") @@ -261,22 +255,20 @@ def test_marker_evaluation_with_multiple_extras(self): def test_marker_evaluation_with_extras_loop(self): ad = pkg_resources.Environment([]) ws = WorkingSet([]) - # Metadata needs to be native strings due to cStringIO behaviour in - # 2.6, so use str(). a = Distribution.from_filename( "/foo_dir/a-0.2.dist-info", - metadata=Metadata(("METADATA", str("Requires-Dist: c[a]"))) + metadata=Metadata(("METADATA", "Requires-Dist: c[a]")) ) b = Distribution.from_filename( "/foo_dir/b-0.3.dist-info", - metadata=Metadata(("METADATA", str("Requires-Dist: c[b]"))) + metadata=Metadata(("METADATA", "Requires-Dist: c[b]")) ) c = Distribution.from_filename( "/foo_dir/c-1.0.dist-info", - metadata=Metadata(("METADATA", str("Provides-Extra: a\n" + metadata=Metadata(("METADATA", "Provides-Extra: a\n" "Requires-Dist: b;extra=='a'\n" "Provides-Extra: b\n" - "Requires-Dist: foo;extra=='b'"))) + "Requires-Dist: foo;extra=='b'")) ) foo = Distribution.from_filename("/foo_dir/foo-0.1.dist-info") for dist in (a, b, c, foo): diff --git a/setup.py b/setup.py index da2c6473b6..5b2e8a5b3e 100755 --- a/setup.py +++ b/setup.py @@ -145,7 +145,6 @@ def pypi_link(pkg_filename): Intended Audience :: Developers License :: OSI Approved :: MIT License Operating System :: OS Independent - Programming Language :: Python :: 2.6 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.3 @@ -156,7 +155,7 @@ def pypi_link(pkg_filename): Topic :: System :: Systems Administration Topic :: Utilities """).strip().splitlines(), - python_requires='>=2.6,!=3.0.*,!=3.1.*,!=3.2.*', + python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*', extras_require={ "ssl:sys_platform=='win32'": "wincertstore==0.2", "certs": "certifi==2016.9.26", diff --git a/setuptools/archive_util.py b/setuptools/archive_util.py index cc82b3da36..81436044d9 100755 --- a/setuptools/archive_util.py +++ b/setuptools/archive_util.py @@ -8,7 +8,7 @@ import contextlib from distutils.errors import DistutilsError -from pkg_resources import ensure_directory, ContextualZipFile +from pkg_resources import ensure_directory __all__ = [ "unpack_archive", "unpack_zipfile", "unpack_tarfile", "default_filter", @@ -98,7 +98,7 @@ def unpack_zipfile(filename, extract_dir, progress_filter=default_filter): if not zipfile.is_zipfile(filename): raise UnrecognizedFormat("%s is not a zip file" % (filename,)) - with ContextualZipFile(filename) as z: + with zipfile.ZipFile(filename) as z: for info in z.infolist(): name = info.filename diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index 8a06e496d7..40cea9bf44 100755 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -158,9 +158,7 @@ def save_version_info(self, filename): build tag. Install these keys in a deterministic order to avoid arbitrary reordering on subsequent builds. """ - # python 2.6 compatibility - odict = getattr(collections, 'OrderedDict', dict) - egg_info = odict() + egg_info = collections.OrderedDict() # follow the order these keys would have been added # when PYTHONHASHSEED=0 egg_info['tag_build'] = self.tags() diff --git a/setuptools/command/sdist.py b/setuptools/command/sdist.py index 84e29a1b7d..39e29d7316 100755 --- a/setuptools/command/sdist.py +++ b/setuptools/command/sdist.py @@ -50,13 +50,6 @@ def run(self): for cmd_name in self.get_sub_commands(): self.run_command(cmd_name) - # Call check_metadata only if no 'check' command - # (distutils <= 2.6) - import distutils.command - - if 'check' not in distutils.command.__all__: - self.check_metadata() - self.make_distribution() dist_files = getattr(self.distribution, 'dist_files', []) diff --git a/setuptools/command/test.py b/setuptools/command/test.py index 9a5117be00..9931565be6 100644 --- a/setuptools/command/test.py +++ b/setuptools/command/test.py @@ -3,6 +3,7 @@ import sys import contextlib import itertools +import unittest from distutils.errors import DistutilsOptionError from unittest import TestLoader @@ -13,7 +14,6 @@ working_set, _namespace_packages, add_activation_listener, require, EntryPoint) from setuptools import Command -from setuptools.py31compat import unittest_main class ScanningLoader(TestLoader): @@ -225,12 +225,11 @@ def run_tests(self): del_modules.append(name) list(map(sys.modules.__delitem__, del_modules)) - exit_kwarg = {} if sys.version_info < (2, 7) else {"exit": False} - unittest_main( + unittest.main( None, None, self._argv, testLoader=self._resolve_as_ep(self.test_loader), testRunner=self._resolve_as_ep(self.test_runner), - **exit_kwarg + exit=False, ) @property diff --git a/setuptools/config.py b/setuptools/config.py index 889dc683d3..eb19c895a7 100644 --- a/setuptools/config.py +++ b/setuptools/config.py @@ -4,9 +4,9 @@ import sys from collections import defaultdict from functools import partial +from importlib import import_module from distutils.errors import DistutilsOptionError, DistutilsFileError -from setuptools.py26compat import import_module from setuptools.extern.six import string_types diff --git a/setuptools/monkey.py b/setuptools/monkey.py index aabc280f6b..09f208b189 100644 --- a/setuptools/monkey.py +++ b/setuptools/monkey.py @@ -7,8 +7,8 @@ import platform import types import functools +from importlib import import_module -from .py26compat import import_module from setuptools.extern import six import setuptools diff --git a/setuptools/package_index.py b/setuptools/package_index.py index d80d43bc79..d2f27ca6a0 100755 --- a/setuptools/package_index.py +++ b/setuptools/package_index.py @@ -27,7 +27,6 @@ from distutils import log from distutils.errors import DistutilsError from fnmatch import translate -from setuptools.py26compat import strip_fragment from setuptools.py27compat import get_all_headers EGG_FRAGMENT = re.compile(r'^egg=([-A-Za-z0-9_.]+)$') @@ -707,7 +706,7 @@ def _download_to(self, url, filename): fp, info = None, None try: checker = HashChecker.from_url(url) - fp = self.open_url(strip_fragment(url)) + fp = self.open_url(url) if isinstance(fp, urllib.error.HTTPError): raise DistutilsError( "Can't download %s: %s %s" % (url, fp.code, fp.msg) diff --git a/setuptools/py26compat.py b/setuptools/py26compat.py deleted file mode 100644 index 4d3add8ca8..0000000000 --- a/setuptools/py26compat.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -Compatibility Support for Python 2.6 and earlier -""" - -import sys - -try: - from urllib.parse import splittag -except ImportError: - from urllib import splittag - - -def strip_fragment(url): - """ - In `Python 8280 `_, Python 2.7 and - later was patched to disregard the fragment when making URL requests. - Do the same for Python 2.6 and earlier. - """ - url, fragment = splittag(url) - return url - - -if sys.version_info >= (2, 7): - strip_fragment = lambda x: x - -try: - from importlib import import_module -except ImportError: - - def import_module(module_name): - return __import__(module_name, fromlist=['__name__']) diff --git a/setuptools/py31compat.py b/setuptools/py31compat.py index 44b025d4b2..4ea953201f 100644 --- a/setuptools/py31compat.py +++ b/setuptools/py31compat.py @@ -1,6 +1,3 @@ -import sys -import unittest - __all__ = ['get_config_vars', 'get_path'] try: @@ -42,15 +39,3 @@ def __exit__(self, exctype, excvalue, exctrace): except OSError: # removal errors are not the only possible pass self.name = None - - -unittest_main = unittest.main - -_PY31 = (3, 1) <= sys.version_info[:2] < (3, 2) -if _PY31: - # on Python 3.1, translate testRunner==None to TextTestRunner - # for compatibility with Python 2.6, 2.7, and 3.2+ - def unittest_main(*args, **kwargs): - if 'testRunner' in kwargs and kwargs['testRunner'] is None: - kwargs['testRunner'] = unittest.TextTestRunner - return unittest.main(*args, **kwargs) diff --git a/setuptools/sandbox.py b/setuptools/sandbox.py index d882d71539..640691d853 100755 --- a/setuptools/sandbox.py +++ b/setuptools/sandbox.py @@ -37,10 +37,6 @@ def _execfile(filename, globals, locals=None): mode = 'rb' with open(filename, mode) as stream: script = stream.read() - # compile() function in Python 2.6 and 3.1 requires LF line endings. - if sys.version_info[:2] < (2, 7) or sys.version_info[:2] >= (3, 0) and sys.version_info[:2] < (3, 2): - script = script.replace(b'\r\n', b'\n') - script = script.replace(b'\r', b'\n') if locals is None: locals = globals code = compile(script, filename, 'exec') diff --git a/setuptools/tests/py26compat.py b/setuptools/tests/py26compat.py deleted file mode 100644 index 18cece051c..0000000000 --- a/setuptools/tests/py26compat.py +++ /dev/null @@ -1,16 +0,0 @@ -import sys -import tarfile -import contextlib - - -def _tarfile_open_ex(*args, **kwargs): - """ - Extend result as a context manager. - """ - return contextlib.closing(tarfile.open(*args, **kwargs)) - - -if sys.version_info[:2] < (2, 7) or (3, 0) <= sys.version_info[:2] < (3, 2): - tarfile_open = _tarfile_open_ex -else: - tarfile_open = tarfile.open diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py index 209e6b78ff..6f9bc8e103 100644 --- a/setuptools/tests/test_easy_install.py +++ b/setuptools/tests/test_easy_install.py @@ -35,7 +35,6 @@ import setuptools.tests.server import pkg_resources -from .py26compat import tarfile_open from . import contexts from .textwrap import DALS @@ -428,7 +427,7 @@ def test_setup_requires_override_nspkg(self): # extracted path to sys.path so foo.bar v0.1 is importable foobar_1_dir = os.path.join(temp_dir, 'foo.bar-0.1') os.mkdir(foobar_1_dir) - with tarfile_open(foobar_1_archive) as tf: + with tarfile.open(foobar_1_archive) as tf: tf.extractall(foobar_1_dir) sys.path.insert(1, foobar_1_dir) @@ -526,7 +525,7 @@ def make_sdist(dist_path, files): listed in ``files`` as ``(filename, content)`` tuples. """ - with tarfile_open(dist_path, 'w:gz') as dist: + with tarfile.open(dist_path, 'w:gz') as dist: for filename, content in files: file_bytes = io.BytesIO(content.encode('utf-8')) file_info = tarfile.TarInfo(name=filename) @@ -580,7 +579,7 @@ def make_trivial_sdist(dist_path, setup_py): setup_py_file = tarfile.TarInfo(name='setup.py') setup_py_bytes = io.BytesIO(setup_py.encode('utf-8')) setup_py_file.size = len(setup_py_bytes.getvalue()) - with tarfile_open(dist_path, 'w:gz') as dist: + with tarfile.open(dist_path, 'w:gz') as dist: dist.addfile(setup_py_file, fileobj=setup_py_bytes) diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index 75ae18dffd..7bf6b68a93 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -2,7 +2,6 @@ import glob import re import stat -import sys from setuptools.command.egg_info import egg_info, manifest_maker from setuptools.dist import Distribution @@ -63,12 +62,6 @@ def env(self): }) yield env - dict_order_fails = pytest.mark.skipif( - sys.version_info < (2,7), - reason="Intermittent failures on Python 2.6", - ) - - @dict_order_fails def test_egg_info_save_version_info_setup_empty(self, tmpdir_cwd, env): """ When the egg_info section is empty or not present, running @@ -104,7 +97,6 @@ def _validate_content_order(content, expected): flags = re.MULTILINE | re.DOTALL assert re.search(pattern, content, flags) - @dict_order_fails def test_egg_info_save_version_info_setup_defaults(self, tmpdir_cwd, env): """ When running save_version_info on an existing setup.cfg From cb98462202bb9b23fc15f4b81b127766700f58ad Mon Sep 17 00:00:00 2001 From: idle sign Date: Sat, 10 Dec 2016 12:06:26 +0700 Subject: [PATCH 2/7] `read_configuration()` now accepts `ignore_option_errors`. --- docs/setuptools.txt | 8 ++++++- setuptools/config.py | 40 +++++++++++++++++++++++++++------ setuptools/tests/test_config.py | 16 +++++++++++++ 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/docs/setuptools.txt b/docs/setuptools.txt index 1b0be77d8e..948e2a9a89 100644 --- a/docs/setuptools.txt +++ b/docs/setuptools.txt @@ -2543,7 +2543,7 @@ zip_safe bool setup_requires list-semi install_requires list-semi extras_require section -entry_points file, section +entry_points file:, section use_2to3 bool use_2to3_fixers list-comma use_2to3_exclude_fixers list-comma @@ -2582,6 +2582,12 @@ in the first argument. To include values from other configuration files which could be in various places set `find_others` function 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. + -------------------------------- Extending and Reusing Setuptools diff --git a/setuptools/config.py b/setuptools/config.py index eb19c895a7..e8877033c3 100644 --- a/setuptools/config.py +++ b/setuptools/config.py @@ -10,7 +10,8 @@ from setuptools.extern.six import string_types -def read_configuration(filepath, find_others=False): +def read_configuration( + filepath, find_others=False, ignore_option_errors=False): """Read given configuration file and returns options from it as a dict. :param str|unicode filepath: Path to configuration file @@ -19,6 +20,11 @@ def read_configuration(filepath, find_others=False): :param bool find_others: Whether to search for other configuration files which could be on in various places. + :param bool ignore_option_errors: Whether to silently ignore + options, values of which could not be resolved (e.g. due to exceptions + in directives such as file:, attr:, etc.). + If False exceptions are propagated as expected. + :rtype: dict """ from setuptools.dist import Distribution, _Distribution @@ -40,7 +46,9 @@ def read_configuration(filepath, find_others=False): _Distribution.parse_config_files(dist, filenames=filenames) - handlers = parse_configuration(dist, dist.command_options) + handlers = parse_configuration( + dist, dist.command_options, + ignore_option_errors=ignore_option_errors) os.chdir(current_directory) @@ -76,7 +84,8 @@ def configuration_to_dict(handlers): return config_dict -def parse_configuration(distribution, command_options): +def parse_configuration( + distribution, command_options, ignore_option_errors=False): """Performs additional parsing of configuration options for a distribution. @@ -84,12 +93,18 @@ def parse_configuration(distribution, command_options): :param Distribution distribution: :param dict command_options: + :param bool ignore_option_errors: Whether to silently ignore + options, values of which could not be resolved (e.g. due to exceptions + in directives such as file:, attr:, etc.). + If False exceptions are propagated as expected. :rtype: list """ - meta = ConfigMetadataHandler(distribution.metadata, command_options) + meta = ConfigMetadataHandler( + distribution.metadata, command_options, ignore_option_errors) meta.parse() - options = ConfigOptionsHandler(distribution, command_options) + options = ConfigOptionsHandler( + distribution, command_options, ignore_option_errors) options.parse() return [meta, options] @@ -111,7 +126,7 @@ class ConfigHandler(object): """ - def __init__(self, target_obj, options): + def __init__(self, target_obj, options, ignore_option_errors=False): sections = {} section_prefix = self.section_prefix @@ -122,6 +137,7 @@ def __init__(self, target_obj, options): section_name = section_name.replace(section_prefix, '').strip('.') sections[section_name] = section_options + self.ignore_option_errors = ignore_option_errors self.target_obj = target_obj self.sections = sections self.set_options = [] @@ -148,9 +164,19 @@ def __setitem__(self, option_name, value): # Already inhabited. Skipping. return + skip_option = False parser = self.parsers.get(option_name) if parser: - value = parser(value) + try: + value = parser(value) + + except Exception: + skip_option = True + if not self.ignore_option_errors: + raise + + if skip_option: + return setter = getattr(target_obj, 'set_%s' % option_name, None) if setter is None: diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index 2148772095..aaf78aefed 100644 --- a/setuptools/tests/test_config.py +++ b/setuptools/tests/test_config.py @@ -73,6 +73,22 @@ def test_no_config(self, tmpdir): with pytest.raises(DistutilsFileError): read_configuration('%s' % tmpdir.join('setup.cfg')) + def test_ignore_errors(self, tmpdir): + fake_env( + tmpdir, + '[metadata]\n' + 'version = attr: none.VERSION\n' + 'keywords = one, two\n' + ) + with pytest.raises(ImportError): + read_configuration('%s' % tmpdir.join('setup.cfg')) + + config_dict = read_configuration( + '%s' % tmpdir.join('setup.cfg'), ignore_option_errors=True) + + assert config_dict['metadata']['keywords'] == ['one', 'two'] + assert 'version' not in config_dict['metadata'] + class TestMetadata: From cb22f3bca2c8d995c78ddf46b147afe046246f1d Mon Sep 17 00:00:00 2001 From: idle sign Date: Sat, 10 Dec 2016 12:30:24 +0700 Subject: [PATCH 3/7] config tests refactored. --- setuptools/tests/test_config.py | 43 ++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index aaf78aefed..35bdbad108 100644 --- a/setuptools/tests/test_config.py +++ b/setuptools/tests/test_config.py @@ -9,6 +9,13 @@ class ErrConfigHandler(ConfigHandler): """Erroneous handler. Fails to implement required methods.""" +def make_package_dir(name, base_dir): + dir_package = base_dir.mkdir(name) + init_file = dir_package.join('__init__.py') + init_file.write('') + return dir_package, init_file + + def fake_env(tmpdir, setup_cfg, setup_py=None): if setup_py is None: @@ -18,11 +25,12 @@ def fake_env(tmpdir, setup_cfg, setup_py=None): ) tmpdir.join('setup.py').write(setup_py) - tmpdir.join('setup.cfg').write(setup_cfg) + config = tmpdir.join('setup.cfg') + config.write(setup_cfg) + + package_dir, init_file = make_package_dir('fake_package', tmpdir) - package_name = 'fake_package' - dir_package = tmpdir.mkdir(package_name) - dir_package.join('__init__.py').write( + init_file.write( 'VERSION = (1, 2, 3)\n' '\n' 'VERSION_MAJOR = 1' @@ -31,6 +39,7 @@ def fake_env(tmpdir, setup_cfg, setup_py=None): ' return [3, 4, 5, "dev"]\n' '\n' ) + return package_dir, config @contextlib.contextmanager @@ -55,7 +64,7 @@ def test_parsers_implemented(): class TestConfigurationReader: def test_basic(self, tmpdir): - fake_env( + _, config = fake_env( tmpdir, '[metadata]\n' 'version = 10.1.1\n' @@ -64,7 +73,7 @@ def test_basic(self, tmpdir): '[options]\n' 'scripts = bin/a.py, bin/b.py\n' ) - config_dict = read_configuration('%s' % tmpdir.join('setup.cfg')) + config_dict = read_configuration('%s' % config) assert config_dict['metadata']['version'] == '10.1.1' assert config_dict['metadata']['keywords'] == ['one', 'two'] assert config_dict['options']['scripts'] == ['bin/a.py', 'bin/b.py'] @@ -74,17 +83,17 @@ def test_no_config(self, tmpdir): read_configuration('%s' % tmpdir.join('setup.cfg')) def test_ignore_errors(self, tmpdir): - fake_env( + _, config = fake_env( tmpdir, '[metadata]\n' 'version = attr: none.VERSION\n' 'keywords = one, two\n' ) with pytest.raises(ImportError): - read_configuration('%s' % tmpdir.join('setup.cfg')) + read_configuration('%s' % config) config_dict = read_configuration( - '%s' % tmpdir.join('setup.cfg'), ignore_option_errors=True) + '%s' % config, ignore_option_errors=True) assert config_dict['metadata']['keywords'] == ['one', 'two'] assert 'version' not in config_dict['metadata'] @@ -188,7 +197,7 @@ def test_multiline(self, tmpdir): def test_version(self, tmpdir): - fake_env( + _, config = fake_env( tmpdir, '[metadata]\n' 'version = attr: fake_package.VERSION\n' @@ -196,14 +205,14 @@ def test_version(self, tmpdir): with get_dist(tmpdir) as dist: assert dist.metadata.version == '1.2.3' - tmpdir.join('setup.cfg').write( + config.write( '[metadata]\n' 'version = attr: fake_package.get_version\n' ) with get_dist(tmpdir) as dist: assert dist.metadata.version == '3.4.5.dev' - tmpdir.join('setup.cfg').write( + config.write( '[metadata]\n' 'version = attr: fake_package.VERSION_MAJOR\n' ) @@ -214,7 +223,7 @@ def test_version(self, tmpdir): subpack.join('__init__.py').write('') subpack.join('submodule.py').write('VERSION = (2016, 11, 26)') - tmpdir.join('setup.cfg').write( + config.write( '[metadata]\n' 'version = attr: fake_package.subpackage.submodule.VERSION\n' ) @@ -250,7 +259,7 @@ def test_classifiers(self, tmpdir): ]) # From file. - fake_env( + _, config = fake_env( tmpdir, '[metadata]\n' 'classifiers = file: classifiers\n' @@ -265,7 +274,7 @@ def test_classifiers(self, tmpdir): assert set(dist.metadata.classifiers) == expected # From section. - tmpdir.join('setup.cfg').write( + config.write( '[metadata.classifiers]\n' 'Framework :: Django\n' 'Programming Language :: Python :: 3.5\n' @@ -454,7 +463,7 @@ def test_extras_require(self, tmpdir): } def test_entry_points(self, tmpdir): - fake_env( + _, config = fake_env( tmpdir, '[options.entry_points]\n' 'group1 = point1 = pack.module:func, ' @@ -479,7 +488,7 @@ def test_entry_points(self, tmpdir): tmpdir.join('entry_points').write(expected) # From file. - tmpdir.join('setup.cfg').write( + config.write( '[options]\n' 'entry_points = file: entry_points\n' ) From 833dc2f816a284b7f189d779773f1aeabbfe6af1 Mon Sep 17 00:00:00 2001 From: idle sign Date: Sat, 10 Dec 2016 13:33:57 +0700 Subject: [PATCH 4/7] Implemented find() configuration support for `packages`. --- docs/setuptools.txt | 20 ++++++++++++++--- setuptools/config.py | 33 ++++++++++++++++++++++++++-- setuptools/tests/test_config.py | 38 +++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 5 deletions(-) diff --git a/docs/setuptools.txt b/docs/setuptools.txt index 948e2a9a89..614bf2c617 100644 --- a/docs/setuptools.txt +++ b/docs/setuptools.txt @@ -2448,6 +2448,11 @@ boilerplate code in some cases. pdf = ReportLab>=1.2; RXP rest = docutils>=0.3; pack ==1.1, ==1.3 + [options.packages.find] + exclude = + src.subpackage1 + src.subpackage2 + Metadata and options could be set in sections with the same names. @@ -2486,13 +2491,13 @@ Type names used below: * ``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 Special directives: * ``attr:`` - value could be read from module attribute * ``file:`` - value could be read from a file -* ``section:`` - values could be read from a dedicated (sub)section .. note:: @@ -2529,8 +2534,10 @@ requires list-comma obsoletes list-comma ================= ================= ===== -**version** - ``attr:`` supports callables; supports iterables; -unsupported types are casted using ``str()``. +.. note:: + + **version** - ``attr:`` supports callables; supports iterables; + unsupported types are casted using ``str()``. Options @@ -2560,6 +2567,13 @@ exclude_package_data section namespace_packages 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`. + Configuration API ================= diff --git a/setuptools/config.py b/setuptools/config.py index e8877033c3..144ff5024e 100644 --- a/setuptools/config.py +++ b/setuptools/config.py @@ -361,7 +361,10 @@ def parse(self): method_postfix = '_%s' % section_name section_parser_method = getattr( - self, 'parse_section%s' % method_postfix, None) + self, + # Dots in section names are tranlsated into dunderscores. + ('parse_section%s' % method_postfix).replace('.', '__'), + None) if section_parser_method is None: raise DistutilsOptionError( @@ -481,8 +484,34 @@ def _parse_packages(self, value): if not value.startswith(find_directive): return self._parse_list(value) + # Read function arguments from a dedicated section. + find_kwargs = self.parse_section_packages__find( + self.sections.get('packages.find', {})) + from setuptools import find_packages - return find_packages() + + return find_packages(**find_kwargs) + + def parse_section_packages__find(self, section_options): + """Parses `packages.find` configuration file section. + + To be used in conjunction with _parse_packages(). + + :param dict section_options: + """ + section_data = self._parse_section_to_dict( + section_options, self._parse_list) + + valid_keys = ['where', 'include', 'exclude'] + + find_kwargs = dict( + [(k, v) for k, v in section_data.items() if k in valid_keys and v]) + + where = find_kwargs.get('where') + if where is not None: + find_kwargs['where'] = where[0] # cast list to single val + + return find_kwargs def parse_section_entry_points(self, section_options): """Parses `entry_points` configuration file section. diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index 35bdbad108..08e398b3de 100644 --- a/setuptools/tests/test_config.py +++ b/setuptools/tests/test_config.py @@ -446,6 +446,44 @@ def test_packages(self, tmpdir): with get_dist(tmpdir) as dist: assert dist.packages == ['fake_package'] + def test_find_directive(self, tmpdir): + dir_package, config = fake_env( + tmpdir, + '[options]\n' + 'packages = find:\n' + ) + + dir_sub_one, _ = make_package_dir('sub_one', dir_package) + dir_sub_two, _ = make_package_dir('sub_two', dir_package) + + with get_dist(tmpdir) as dist: + assert dist.packages == [ + 'fake_package', 'fake_package.sub_two', 'fake_package.sub_one'] + + config.write( + '[options]\n' + 'packages = find:\n' + '\n' + '[options.packages.find]\n' + 'where = .\n' + 'include =\n' + ' fake_package.sub_one\n' + ' two\n' + ) + with get_dist(tmpdir) as dist: + assert dist.packages == ['fake_package.sub_one'] + + config.write( + '[options]\n' + 'packages = find:\n' + '\n' + '[options.packages.find]\n' + 'exclude =\n' + ' fake_package.sub_one\n' + ) + with get_dist(tmpdir) as dist: + assert dist.packages == ['fake_package', 'fake_package.sub_two'] + def test_extras_require(self, tmpdir): fake_env( tmpdir, From 489353999c3d6b0bce2ad4821743de239927b3c3 Mon Sep 17 00:00:00 2001 From: idle sign Date: Sat, 10 Dec 2016 13:48:03 +0700 Subject: [PATCH 5/7] Fixed test for `find()` results. --- setuptools/tests/test_config.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index 08e398b3de..677ccf2c03 100644 --- a/setuptools/tests/test_config.py +++ b/setuptools/tests/test_config.py @@ -457,8 +457,9 @@ def test_find_directive(self, tmpdir): dir_sub_two, _ = make_package_dir('sub_two', dir_package) with get_dist(tmpdir) as dist: - assert dist.packages == [ - 'fake_package', 'fake_package.sub_two', 'fake_package.sub_one'] + assert set(dist.packages) == set([ + 'fake_package', 'fake_package.sub_two', 'fake_package.sub_one' + ]) config.write( '[options]\n' @@ -482,7 +483,8 @@ def test_find_directive(self, tmpdir): ' fake_package.sub_one\n' ) with get_dist(tmpdir) as dist: - assert dist.packages == ['fake_package', 'fake_package.sub_two'] + assert set(dist.packages) == set( + ['fake_package', 'fake_package.sub_two']) def test_extras_require(self, tmpdir): fake_env( From cd374a1069327e9a0eb4e51985fc870683b8fb00 Mon Sep 17 00:00:00 2001 From: idle sign Date: Sat, 10 Dec 2016 15:23:49 +0700 Subject: [PATCH 6/7] `test_ignore_errors` side effect mitigated. --- setuptools/tests/test_config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index 677ccf2c03..fa8d523b47 100644 --- a/setuptools/tests/test_config.py +++ b/setuptools/tests/test_config.py @@ -98,6 +98,8 @@ def test_ignore_errors(self, tmpdir): assert config_dict['metadata']['keywords'] == ['one', 'two'] assert 'version' not in config_dict['metadata'] + config.remove() + class TestMetadata: From f59ab870d82334e06cd9ddbf0dcda6fd38cf4b9f Mon Sep 17 00:00:00 2001 From: idle sign Date: Sat, 10 Dec 2016 22:24:01 +0700 Subject: [PATCH 7/7] Proper finalization for `read_configuration()`. --- setuptools/config.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/setuptools/config.py b/setuptools/config.py index 144ff5024e..a65b63b8a9 100644 --- a/setuptools/config.py +++ b/setuptools/config.py @@ -38,19 +38,21 @@ def read_configuration( current_directory = os.getcwd() os.chdir(os.path.dirname(filepath)) - dist = Distribution() + try: + dist = Distribution() - filenames = dist.find_config_files() if find_others else [] - if filepath not in filenames: - filenames.append(filepath) + filenames = dist.find_config_files() if find_others else [] + if filepath not in filenames: + filenames.append(filepath) - _Distribution.parse_config_files(dist, filenames=filenames) + _Distribution.parse_config_files(dist, filenames=filenames) - handlers = parse_configuration( - dist, dist.command_options, - ignore_option_errors=ignore_option_errors) + handlers = parse_configuration( + dist, dist.command_options, + ignore_option_errors=ignore_option_errors) - os.chdir(current_directory) + finally: + os.chdir(current_directory) return configuration_to_dict(handlers)