From 3686dedb4bfbd0e6630c10119c8fe7af9369248e Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Wed, 25 Oct 2017 17:50:22 +0200 Subject: add test to ensure `setup.cfg` interpolation behavior remain unchanged --- setuptools/tests/test_config.py | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'setuptools') diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index cdfa5af4..2494a0bc 100644 --- a/setuptools/tests/test_config.py +++ b/setuptools/tests/test_config.py @@ -3,6 +3,7 @@ import pytest from distutils.errors import DistutilsOptionError, DistutilsFileError from setuptools.dist import Distribution from setuptools.config import ConfigHandler, read_configuration +from setuptools.extern.six.moves.configparser import InterpolationMissingOptionError class ErrConfigHandler(ConfigHandler): @@ -306,6 +307,15 @@ class TestMetadata: with get_dist(tmpdir) as dist: assert set(dist.metadata.classifiers) == expected + def test_interpolation(self, tmpdir): + fake_env( + tmpdir, + '[metadata]\n' + 'description = %(message)s\n' + ) + with pytest.raises(InterpolationMissingOptionError): + with get_dist(tmpdir): + pass class TestOptions: -- cgit v1.2.3 From 2c897b5b877d401e13b661f2a0a14e99a1aabdc8 Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Wed, 25 Oct 2017 17:55:26 +0200 Subject: improve encoding handling for `setup.cfg` Support the same mechanism as for Python sources for declaring the encoding to be used when reading `setup.cfg` (see PEP 263), and return the results of reading it as Unicode. Fix #1062 and #1136. --- setuptools/__init__.py | 34 ++++++++++++++++++++ setuptools/dist.py | 2 +- setuptools/py36compat.py | 37 ++++++++++++++-------- setuptools/tests/test_config.py | 65 +++++++++++++++++++++++++++++++++++++-- setuptools/tests/test_egg_info.py | 46 +++++++++++++++++++++++++++ 5 files changed, 169 insertions(+), 15 deletions(-) (limited to 'setuptools') diff --git a/setuptools/__init__.py b/setuptools/__init__.py index 04f76740..77b4a374 100644 --- a/setuptools/__init__.py +++ b/setuptools/__init__.py @@ -4,9 +4,12 @@ import os import functools import distutils.core import distutils.filelist +import re +from distutils.errors import DistutilsOptionError from distutils.util import convert_path from fnmatch import fnmatchcase +from setuptools.extern.six import string_types from setuptools.extern.six.moves import filter, map import setuptools.version @@ -127,6 +130,37 @@ class Command(_Command): _Command.__init__(self, dist) vars(self).update(kw) + def _ensure_stringlike(self, option, what, default=None): + val = getattr(self, option) + if val is None: + setattr(self, option, default) + return default + elif not isinstance(val, string_types): + raise DistutilsOptionError("'%s' must be a %s (got `%s`)" + % (option, what, val)) + return val + + def ensure_string_list(self, option): + r"""Ensure that 'option' is a list of strings. If 'option' is + currently a string, we split it either on /,\s*/ or /\s+/, so + "foo bar baz", "foo,bar,baz", and "foo, bar baz" all become + ["foo", "bar", "baz"]. + """ + val = getattr(self, option) + if val is None: + return + elif isinstance(val, string_types): + setattr(self, option, re.split(r',\s*|\s+', val)) + else: + if isinstance(val, list): + ok = all(isinstance(v, string_types) for v in val) + else: + ok = False + if not ok: + raise DistutilsOptionError( + "'%s' must be a list of strings (got %r)" + % (option, val)) + def reinitialize_command(self, command, reinit_subcommands=0, **kw): cmd = _Command.reinitialize_command(self, command, reinit_subcommands) vars(cmd).update(kw) diff --git a/setuptools/dist.py b/setuptools/dist.py index a2ca8795..b10bd6f7 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -432,7 +432,7 @@ class Distribution(Distribution_parse_config_files, _Distribution): and loads configuration. """ - _Distribution.parse_config_files(self, filenames=filenames) + Distribution_parse_config_files.parse_config_files(self, filenames=filenames) parse_configuration(self, self.command_options) self._finalize_requires() diff --git a/setuptools/py36compat.py b/setuptools/py36compat.py index f5279696..3d3c34ec 100644 --- a/setuptools/py36compat.py +++ b/setuptools/py36compat.py @@ -1,7 +1,21 @@ +import io +import re import sys from distutils.errors import DistutilsOptionError from distutils.util import strtobool from distutils.debug import DEBUG +from setuptools.extern import six + + +CODING_RE = re.compile(br'^[ \t\f]*#.*?coding[:=][ \t]*([-\w.]+)') + +def detect_encoding(fp): + first_line = fp.readline() + fp.seek(0) + m = CODING_RE.match(first_line) + if m is None: + return None + return m.group(1).decode('ascii') class Distribution_parse_config_files: @@ -13,10 +27,10 @@ class Distribution_parse_config_files: as implemented in distutils. """ def parse_config_files(self, filenames=None): - from configparser import ConfigParser + from setuptools.extern.six.moves.configparser import ConfigParser # Ignore install directory options if we have a venv - if sys.prefix != sys.base_prefix: + if six.PY3 and sys.prefix != sys.base_prefix: ignore_options = [ 'install-base', 'install-platbase', 'install-lib', 'install-platlib', 'install-purelib', 'install-headers', @@ -33,11 +47,16 @@ class Distribution_parse_config_files: if DEBUG: self.announce("Distribution.parse_config_files():") - parser = ConfigParser(interpolation=None) + parser = ConfigParser() for filename in filenames: - if DEBUG: - self.announce(" reading %s" % filename) - parser.read(filename) + with io.open(filename, 'rb') as fp: + encoding = detect_encoding(fp) + if DEBUG: + self.announce(" reading %s [%s]" % ( + filename, encoding or 'locale') + ) + reader = io.TextIOWrapper(fp, encoding=encoding) + (parser.read_file if six.PY3 else parser.readfp)(reader) for section in parser.sections(): options = parser.options(section) opt_dict = self.get_option_dict(section) @@ -69,12 +88,6 @@ class Distribution_parse_config_files: raise DistutilsOptionError(msg) -if sys.version_info < (3,): - # Python 2 behavior is sufficient - class Distribution_parse_config_files: - pass - - if False: # When updated behavior is available upstream, # disable override here. diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index 2494a0bc..89fde257 100644 --- a/setuptools/tests/test_config.py +++ b/setuptools/tests/test_config.py @@ -1,9 +1,13 @@ +# -*- coding: UTF-8 -*- +from __future__ import unicode_literals + import contextlib import pytest from distutils.errors import DistutilsOptionError, DistutilsFileError from setuptools.dist import Distribution from setuptools.config import ConfigHandler, read_configuration from setuptools.extern.six.moves.configparser import InterpolationMissingOptionError +from setuptools.tests import is_ascii class ErrConfigHandler(ConfigHandler): @@ -17,7 +21,7 @@ def make_package_dir(name, base_dir): return dir_package, init_file -def fake_env(tmpdir, setup_cfg, setup_py=None): +def fake_env(tmpdir, setup_cfg, setup_py=None, encoding='ascii'): if setup_py is None: setup_py = ( @@ -27,7 +31,7 @@ def fake_env(tmpdir, setup_cfg, setup_py=None): tmpdir.join('setup.py').write(setup_py) config = tmpdir.join('setup.cfg') - config.write(setup_cfg) + config.write(setup_cfg.encode(encoding), mode='wb') package_dir, init_file = make_package_dir('fake_package', tmpdir) @@ -317,6 +321,63 @@ class TestMetadata: with get_dist(tmpdir): pass + skip_if_not_ascii = pytest.mark.skipif(not is_ascii, reason='Test not supported with this locale') + + @skip_if_not_ascii + def test_non_ascii_1(self, tmpdir): + fake_env( + tmpdir, + '[metadata]\n' + 'description = éàïôñ\n', + encoding='utf-8' + ) + with pytest.raises(UnicodeDecodeError): + with get_dist(tmpdir): + pass + + def test_non_ascii_2(self, tmpdir): + fake_env( + tmpdir, + '# -*- coding: invalid\n' + ) + with pytest.raises(LookupError): + with get_dist(tmpdir): + pass + + def test_non_ascii_3(self, tmpdir): + fake_env( + tmpdir, + '\n' + '# -*- coding: invalid\n' + ) + with get_dist(tmpdir): + pass + + @skip_if_not_ascii + def test_non_ascii_4(self, tmpdir): + fake_env( + tmpdir, + '# -*- coding: utf-8\n' + '[metadata]\n' + 'description = éàïôñ\n', + encoding='utf-8' + ) + with get_dist(tmpdir) as dist: + assert dist.metadata.description == 'éàïôñ' + + @skip_if_not_ascii + def test_non_ascii_5(self, tmpdir): + fake_env( + tmpdir, + '# vim: set fileencoding=iso-8859-15 :\n' + '[metadata]\n' + 'description = éàïôñ\n', + encoding='iso-8859-15' + ) + with get_dist(tmpdir) as dist: + assert dist.metadata.description == 'éàïôñ' + + class TestOptions: def test_basic(self, tmpdir): diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index 1411f93c..5196f32e 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -497,3 +497,49 @@ class TestEggInfo(object): # expect exactly one result result, = results return result + + EGG_INFO_TESTS = ( + # Check for issue #1136: invalid string type when + # reading declarative `setup.cfg` under Python 2. + { + 'setup.py': DALS( + """ + from setuptools import setup + setup( + name="foo", + ) + """), + 'setup.cfg': DALS( + """ + [options] + package_dir = + = src + """), + 'src': {}, + }, + # Check Unicode can be used in `setup.py` under Python 2. + { + 'setup.py': DALS( + """ + # -*- coding: utf-8 -*- + from __future__ import unicode_literals + from setuptools import setup, find_packages + setup( + name="foo", + package_dir={'': 'src'}, + ) + """), + 'src': {}, + } + ) + + @pytest.mark.parametrize('package_files', EGG_INFO_TESTS) + def test_egg_info(self, tmpdir_cwd, env, package_files): + """ + """ + build_files(package_files) + code, data = environment.run_setup_py( + cmd=['egg_info'], + data_stream=1, + ) + assert not code, data -- cgit v1.2.3 From ca0760af2071c333cda28d18279db95455ffa2de Mon Sep 17 00:00:00 2001 From: Deniz Taneli <7292227+dtaneli@users.noreply.github.com> Date: Sat, 27 Oct 2018 13:19:22 +0100 Subject: Setuptools will install licenses if included in setup.cfg Addressing #357 `python setup.py sdist` now includes the license file if `license_file` is included in `setup.cfg` unless it is explicitly excluded in `MANIFEST.in`. Co-Authored-By: Poyzan Nur Taneli <31743851+ptaneli@users.noreply.github.com> --- setuptools/command/egg_info.py | 1 + setuptools/command/sdist.py | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) (limited to 'setuptools') diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index bd116e1f..93100ab9 100644 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -568,6 +568,7 @@ class manifest_maker(sdist): def add_defaults(self): sdist.add_defaults(self) + self.check_license() self.filelist.append(self.template) self.filelist.append(self.manifest) rcfiles = list(walk_revctrl()) diff --git a/setuptools/command/sdist.py b/setuptools/command/sdist.py index bcfae4d8..a1b20733 100644 --- a/setuptools/command/sdist.py +++ b/setuptools/command/sdist.py @@ -6,6 +6,7 @@ import io import contextlib from setuptools.extern import six +from setuptools.extern.six.moves import configparser from .py36compat import sdist_add_defaults @@ -198,3 +199,27 @@ class sdist(sdist_add_defaults, orig.sdist): continue self.filelist.append(line) manifest.close() + + def check_license(self): + """Read the setup configuration file ('setup.cfg') and use it to find + if a license is defined with the 'license_file' attribute. + If the license is declared and exists, it will be added to + 'self.filelist'. + """ + + cfg_file = 'setup.cfg' + log.debug("Reading configuration from %s", cfg_file) + parser = configparser.RawConfigParser() + parser.read(cfg_file) + try: + license_file = parser.get('metadata', 'license_file') + + if not os.path.exists(license_file): + log.warn("warning: Failed to find license file '%s' in setup.cfg", + license_file) + return + + self.filelist.append(license_file) + except configparser.Error: + log.debug("license_file attribute is not defined in setup.cfg") + return -- cgit v1.2.3 From 00b172d6b4a4f3fb1546dd0c2fc53ec2d4087b00 Mon Sep 17 00:00:00 2001 From: Deniz Taneli <7292227+dtaneli@users.noreply.github.com> Date: Sat, 27 Oct 2018 16:32:24 +0100 Subject: Unit tests for installing licenses from setup.cfg (#357) Co-Authored-By: Poyzan Nur Taneli <31743851+ptaneli@users.noreply.github.com> --- setuptools/tests/test_egg_info.py | 92 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) (limited to 'setuptools') diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index 59ffb16d..2ca91e85 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -486,6 +486,98 @@ class TestEggInfo: pkg_info_text = pkginfo_file.read() assert 'Provides-Extra:' not in pkg_info_text + def test_setup_cfg_with_license(self, tmpdir_cwd, env): + self._create_project() + build_files({ + 'setup.cfg': DALS(""" + [metadata] + license_file = LICENSE + """), + 'LICENSE': DALS("Test license") + }) + environ = os.environ.copy().update( + HOME=env.paths['home'], + ) + environment.run_setup_py( + cmd=['egg_info'], + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), + data_stream=1, + env=environ, + ) + egg_info_dir = os.path.join('.', 'foo.egg-info') + with open(os.path.join(egg_info_dir, 'SOURCES.txt')) as sources_file: + sources_text = sources_file.read() + assert 'LICENSE' in sources_text + + def test_setup_cfg_with_invalid_license(self, tmpdir_cwd, env): + self._create_project() + build_files({ + 'setup.cfg': DALS(""" + [metadata] + license_file = INVALID_LICENSE + """), + 'LICENSE': DALS("Test license") + }) + environ = os.environ.copy().update( + HOME=env.paths['home'], + ) + environment.run_setup_py( + cmd=['egg_info'], + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), + data_stream=1, + env=environ, + ) + egg_info_dir = os.path.join('.', 'foo.egg-info') + with open(os.path.join(egg_info_dir, 'SOURCES.txt')) as sources_file: + sources_text = sources_file.read() + assert 'LICENSE' not in sources_text + assert 'INVALID_LICENSE' not in sources_text + + def test_setup_cfg_with_no_license(self, tmpdir_cwd, env): + self._create_project() + build_files({ + 'setup.cfg': DALS(""" + """), + 'LICENSE': DALS("Test license") + }) + environ = os.environ.copy().update( + HOME=env.paths['home'], + ) + environment.run_setup_py( + cmd=['egg_info'], + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), + data_stream=1, + env=environ, + ) + egg_info_dir = os.path.join('.', 'foo.egg-info') + with open(os.path.join(egg_info_dir, 'SOURCES.txt')) as sources_file: + sources_text = sources_file.read() + assert 'LICENSE' not in sources_text + + def test_setup_cfg_with_license_excluded(self, tmpdir_cwd, env): + self._create_project() + build_files({ + 'setup.cfg': DALS(""" + [metadata] + license_file = LICENSE + """), + 'MANIFEST.in': DALS("exclude LICENSE"), + 'LICENSE': DALS("Test license") + }) + environ = os.environ.copy().update( + HOME=env.paths['home'], + ) + environment.run_setup_py( + cmd=['egg_info'], + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), + data_stream=1, + env=environ, + ) + egg_info_dir = os.path.join('.', 'foo.egg-info') + with open(os.path.join(egg_info_dir, 'SOURCES.txt')) as sources_file: + sources_text = sources_file.read() + assert 'LICENSE' not in sources_text + def test_long_description_content_type(self, tmpdir_cwd, env): # Test that specifying a `long_description_content_type` keyword arg to # the `setup` function results in writing a `Description-Content-Type` -- cgit v1.2.3 From 08bff86fe64cfb05d9ecd6dbf436fa706ecc8440 Mon Sep 17 00:00:00 2001 From: Deniz Taneli <7292227+dtaneli@users.noreply.github.com> Date: Sun, 28 Oct 2018 11:25:18 +0000 Subject: Make the new tests parametrized --- setuptools/tests/test_egg_info.py | 79 ++++++++++----------------------------- 1 file changed, 19 insertions(+), 60 deletions(-) (limited to 'setuptools') diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index 2ca91e85..76c31ada 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -486,84 +486,39 @@ class TestEggInfo: pkg_info_text = pkginfo_file.read() assert 'Provides-Extra:' not in pkg_info_text - def test_setup_cfg_with_license(self, tmpdir_cwd, env): - self._create_project() - build_files({ + @pytest.mark.parametrize("files, license_in_sources", [ + ({ 'setup.cfg': DALS(""" [metadata] license_file = LICENSE """), 'LICENSE': DALS("Test license") - }) - environ = os.environ.copy().update( - HOME=env.paths['home'], - ) - environment.run_setup_py( - cmd=['egg_info'], - pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), - data_stream=1, - env=environ, - ) - egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'SOURCES.txt')) as sources_file: - sources_text = sources_file.read() - assert 'LICENSE' in sources_text - - def test_setup_cfg_with_invalid_license(self, tmpdir_cwd, env): - self._create_project() - build_files({ + }, True), # with license + ({ 'setup.cfg': DALS(""" [metadata] license_file = INVALID_LICENSE """), 'LICENSE': DALS("Test license") - }) - environ = os.environ.copy().update( - HOME=env.paths['home'], - ) - environment.run_setup_py( - cmd=['egg_info'], - pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), - data_stream=1, - env=environ, - ) - egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'SOURCES.txt')) as sources_file: - sources_text = sources_file.read() - assert 'LICENSE' not in sources_text - assert 'INVALID_LICENSE' not in sources_text - - def test_setup_cfg_with_no_license(self, tmpdir_cwd, env): - self._create_project() - build_files({ + }, False), # with an invalid license + ({ 'setup.cfg': DALS(""" """), 'LICENSE': DALS("Test license") - }) - environ = os.environ.copy().update( - HOME=env.paths['home'], - ) - environment.run_setup_py( - cmd=['egg_info'], - pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), - data_stream=1, - env=environ, - ) - egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'SOURCES.txt')) as sources_file: - sources_text = sources_file.read() - assert 'LICENSE' not in sources_text - - def test_setup_cfg_with_license_excluded(self, tmpdir_cwd, env): - self._create_project() - build_files({ + }, False), # no license_file attribute + ({ 'setup.cfg': DALS(""" [metadata] license_file = LICENSE """), 'MANIFEST.in': DALS("exclude LICENSE"), 'LICENSE': DALS("Test license") - }) + }, False) # license file is manually excluded + ]) + def test_setup_cfg_license_file( + self, tmpdir_cwd, env, files, license_in_sources): + self._create_project() + build_files(files) environ = os.environ.copy().update( HOME=env.paths['home'], ) @@ -576,7 +531,11 @@ class TestEggInfo: egg_info_dir = os.path.join('.', 'foo.egg-info') with open(os.path.join(egg_info_dir, 'SOURCES.txt')) as sources_file: sources_text = sources_file.read() - assert 'LICENSE' not in sources_text + if license_in_sources: + assert 'LICENSE' in sources_text + else: + assert 'LICENSE' not in sources_text + assert 'INVALID_LICENSE' not in sources_text # for invalid license test def test_long_description_content_type(self, tmpdir_cwd, env): # Test that specifying a `long_description_content_type` keyword arg to -- cgit v1.2.3 From 4e4efa77722cc2e99171a2396252a4ddc98450e3 Mon Sep 17 00:00:00 2001 From: Deniz Taneli <7292227+dtaneli@users.noreply.github.com> Date: Sun, 28 Oct 2018 15:36:02 +0000 Subject: `check_license` no longer needs to parse `setup.cfg` --- setuptools/command/sdist.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) (limited to 'setuptools') diff --git a/setuptools/command/sdist.py b/setuptools/command/sdist.py index a1b20733..347f8817 100644 --- a/setuptools/command/sdist.py +++ b/setuptools/command/sdist.py @@ -201,25 +201,21 @@ class sdist(sdist_add_defaults, orig.sdist): manifest.close() def check_license(self): - """Read the setup configuration file ('setup.cfg') and use it to find - if a license is defined with the 'license_file' attribute. - If the license is declared and exists, it will be added to - 'self.filelist'. + """Checks if license_file' is configured and adds it to + 'self.filelist' if the value contains a valid path. """ - cfg_file = 'setup.cfg' - log.debug("Reading configuration from %s", cfg_file) - parser = configparser.RawConfigParser() - parser.read(cfg_file) + opts = self.distribution.get_option_dict('metadata') try: - license_file = parser.get('metadata', 'license_file') - - if not os.path.exists(license_file): - log.warn("warning: Failed to find license file '%s' in setup.cfg", - license_file) - return + # ignore the source of the value + _, license_file = opts.get('license_file') + except TypeError: + log.debug("'license_file' attribute is not defined") + return - self.filelist.append(license_file) - except configparser.Error: - log.debug("license_file attribute is not defined in setup.cfg") + if not os.path.exists(license_file): + log.warn("warning: Failed to find the configured license file '%s'", + license_file) return + + self.filelist.append(license_file) -- cgit v1.2.3 From 1047052e341d69c799e26ff889359e101c5e0499 Mon Sep 17 00:00:00 2001 From: Deniz Taneli <7292227+dtaneli@users.noreply.github.com> Date: Sat, 10 Nov 2018 19:55:12 +0000 Subject: Address review comments --- setuptools/command/sdist.py | 12 ++++++------ setuptools/tests/test_egg_info.py | 2 ++ 2 files changed, 8 insertions(+), 6 deletions(-) (limited to 'setuptools') diff --git a/setuptools/command/sdist.py b/setuptools/command/sdist.py index 347f8817..dc253981 100644 --- a/setuptools/command/sdist.py +++ b/setuptools/command/sdist.py @@ -6,7 +6,6 @@ import io import contextlib from setuptools.extern import six -from setuptools.extern.six.moves import configparser from .py36compat import sdist_add_defaults @@ -206,11 +205,12 @@ class sdist(sdist_add_defaults, orig.sdist): """ opts = self.distribution.get_option_dict('metadata') - try: - # ignore the source of the value - _, license_file = opts.get('license_file') - except TypeError: - log.debug("'license_file' attribute is not defined") + + # ignore the source of the value + _, license_file = opts.get('license_file', (None, None)) + + if license_file is None: + log.debug("'license_file' option was not specified") return if not os.path.exists(license_file): diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index 76c31ada..04a17308 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -529,8 +529,10 @@ class TestEggInfo: env=environ, ) egg_info_dir = os.path.join('.', 'foo.egg-info') + with open(os.path.join(egg_info_dir, 'SOURCES.txt')) as sources_file: sources_text = sources_file.read() + if license_in_sources: assert 'LICENSE' in sources_text else: -- cgit v1.2.3 From 28872fc9e7d15a1acf3bc557795c76c5e64dbad3 Mon Sep 17 00:00:00 2001 From: Deniz Taneli <7292227+dtaneli@users.noreply.github.com> Date: Sat, 10 Nov 2018 20:00:45 +0000 Subject: Remove unnecessary parameters from the test --- setuptools/tests/test_egg_info.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) (limited to 'setuptools') diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index 04a17308..7c862e61 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -519,14 +519,10 @@ class TestEggInfo: self, tmpdir_cwd, env, files, license_in_sources): self._create_project() build_files(files) - environ = os.environ.copy().update( - HOME=env.paths['home'], - ) + environment.run_setup_py( cmd=['egg_info'], - pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), - data_stream=1, - env=environ, + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]) ) egg_info_dir = os.path.join('.', 'foo.egg-info') -- cgit v1.2.3 From 24be5abd4cbd9d84537c457456f841522d626e14 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 25 Jan 2019 16:11:07 -0500 Subject: Given that the config file parsing functionality is unlikely to change upstream, just incorporate the functionality directly. --- setuptools/dist.py | 78 +++++++++++++++++++++++++++++++++++-- setuptools/py36compat.py | 95 --------------------------------------------- setuptools/unicode_utils.py | 13 +++++++ 3 files changed, 87 insertions(+), 99 deletions(-) delete mode 100644 setuptools/py36compat.py (limited to 'setuptools') diff --git a/setuptools/dist.py b/setuptools/dist.py index a2600711..4cc3bdfe 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- __all__ = ['Distribution'] +import io +import sys import re import os import warnings @@ -9,9 +11,11 @@ import distutils.log import distutils.core import distutils.cmd import distutils.dist +from distutils.errors import DistutilsOptionError +from distutils.util import strtobool +from distutils.debug import DEBUG import itertools - from collections import defaultdict from email import message_from_file @@ -31,8 +35,8 @@ from setuptools.depends import Require from setuptools import windows_support from setuptools.monkey import get_unpatched from setuptools.config import parse_configuration +from .unicode_utils import detect_encoding import pkg_resources -from .py36compat import Distribution_parse_config_files __import__('setuptools.extern.packaging.specifiers') __import__('setuptools.extern.packaging.version') @@ -332,7 +336,7 @@ def check_packages(dist, attr, value): _Distribution = get_unpatched(distutils.core.Distribution) -class Distribution(Distribution_parse_config_files, _Distribution): +class Distribution(_Distribution): """Distribution with support for features, tests, and package data This is an enhanced version of 'distutils.dist.Distribution' that @@ -556,12 +560,78 @@ class Distribution(Distribution_parse_config_files, _Distribution): req.marker = None return req + def _parse_config_files(self, filenames=None): + """ + Adapted from distutils.dist.Distribution.parse_config_files, + this method provides the same functionality in subtly-improved + ways. + """ + from setuptools.extern.six.moves.configparser import ConfigParser + + # Ignore install directory options if we have a venv + if six.PY3 and sys.prefix != sys.base_prefix: + ignore_options = [ + 'install-base', 'install-platbase', 'install-lib', + 'install-platlib', 'install-purelib', 'install-headers', + 'install-scripts', 'install-data', 'prefix', 'exec-prefix', + 'home', 'user', 'root'] + else: + ignore_options = [] + + ignore_options = frozenset(ignore_options) + + if filenames is None: + filenames = self.find_config_files() + + if DEBUG: + self.announce("Distribution.parse_config_files():") + + parser = ConfigParser() + for filename in filenames: + with io.open(filename, 'rb') as fp: + encoding = detect_encoding(fp) + if DEBUG: + self.announce(" reading %s [%s]" % ( + filename, encoding or 'locale') + ) + reader = io.TextIOWrapper(fp, encoding=encoding) + (parser.read_file if six.PY3 else parser.readfp)(reader) + for section in parser.sections(): + options = parser.options(section) + opt_dict = self.get_option_dict(section) + + for opt in options: + if opt != '__name__' and opt not in ignore_options: + val = parser.get(section, opt) + opt = opt.replace('-', '_') + opt_dict[opt] = (filename, val) + + # Make the ConfigParser forget everything (so we retain + # the original filenames that options come from) + parser.__init__() + + # If there was a "global" section in the config file, use it + # to set Distribution options. + + if 'global' in self.command_options: + for (opt, (src, val)) in self.command_options['global'].items(): + alias = self.negative_opt.get(opt) + try: + if alias: + setattr(self, alias, not strtobool(val)) + elif opt in ('verbose', 'dry_run'): # ugh! + setattr(self, opt, strtobool(val)) + else: + setattr(self, opt, val) + except ValueError as msg: + raise DistutilsOptionError(msg) + def parse_config_files(self, filenames=None, ignore_option_errors=False): """Parses configuration files from various levels and loads configuration. """ - Distribution_parse_config_files.parse_config_files(self, filenames=filenames) + self._parse_config_files(filenames=filenames) parse_configuration(self, self.command_options, ignore_option_errors=ignore_option_errors) diff --git a/setuptools/py36compat.py b/setuptools/py36compat.py deleted file mode 100644 index 3d3c34ec..00000000 --- a/setuptools/py36compat.py +++ /dev/null @@ -1,95 +0,0 @@ -import io -import re -import sys -from distutils.errors import DistutilsOptionError -from distutils.util import strtobool -from distutils.debug import DEBUG -from setuptools.extern import six - - -CODING_RE = re.compile(br'^[ \t\f]*#.*?coding[:=][ \t]*([-\w.]+)') - -def detect_encoding(fp): - first_line = fp.readline() - fp.seek(0) - m = CODING_RE.match(first_line) - if m is None: - return None - return m.group(1).decode('ascii') - - -class Distribution_parse_config_files: - """ - Mix-in providing forward-compatibility for functionality to be - included by default on Python 3.7. - - Do not edit the code in this class except to update functionality - as implemented in distutils. - """ - def parse_config_files(self, filenames=None): - from setuptools.extern.six.moves.configparser import ConfigParser - - # Ignore install directory options if we have a venv - if six.PY3 and sys.prefix != sys.base_prefix: - ignore_options = [ - 'install-base', 'install-platbase', 'install-lib', - 'install-platlib', 'install-purelib', 'install-headers', - 'install-scripts', 'install-data', 'prefix', 'exec-prefix', - 'home', 'user', 'root'] - else: - ignore_options = [] - - ignore_options = frozenset(ignore_options) - - if filenames is None: - filenames = self.find_config_files() - - if DEBUG: - self.announce("Distribution.parse_config_files():") - - parser = ConfigParser() - for filename in filenames: - with io.open(filename, 'rb') as fp: - encoding = detect_encoding(fp) - if DEBUG: - self.announce(" reading %s [%s]" % ( - filename, encoding or 'locale') - ) - reader = io.TextIOWrapper(fp, encoding=encoding) - (parser.read_file if six.PY3 else parser.readfp)(reader) - for section in parser.sections(): - options = parser.options(section) - opt_dict = self.get_option_dict(section) - - for opt in options: - if opt != '__name__' and opt not in ignore_options: - val = parser.get(section,opt) - opt = opt.replace('-', '_') - opt_dict[opt] = (filename, val) - - # Make the ConfigParser forget everything (so we retain - # the original filenames that options come from) - parser.__init__() - - # If there was a "global" section in the config file, use it - # to set Distribution options. - - if 'global' in self.command_options: - for (opt, (src, val)) in self.command_options['global'].items(): - alias = self.negative_opt.get(opt) - try: - if alias: - setattr(self, alias, not strtobool(val)) - elif opt in ('verbose', 'dry_run'): # ugh! - setattr(self, opt, strtobool(val)) - else: - setattr(self, opt, val) - except ValueError as msg: - raise DistutilsOptionError(msg) - - -if False: - # When updated behavior is available upstream, - # disable override here. - class Distribution_parse_config_files: - pass diff --git a/setuptools/unicode_utils.py b/setuptools/unicode_utils.py index 7c63efd2..3b8179a8 100644 --- a/setuptools/unicode_utils.py +++ b/setuptools/unicode_utils.py @@ -1,5 +1,6 @@ import unicodedata import sys +import re from setuptools.extern import six @@ -42,3 +43,15 @@ def try_encode(string, enc): return string.encode(enc) except UnicodeEncodeError: return None + + +CODING_RE = re.compile(br'^[ \t\f]*#.*?coding[:=][ \t]*([-\w.]+)') + + +def detect_encoding(fp): + first_line = fp.readline() + fp.seek(0) + m = CODING_RE.match(first_line) + if m is None: + return None + return m.group(1).decode('ascii') -- cgit v1.2.3 From 5cd86987530892bfb01f68ad5f1a2b997a3d01e7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 26 Jan 2019 20:20:12 -0500 Subject: Feed the hobgoblins (delint). --- setuptools/tests/files.py | 9 +- setuptools/tests/server.py | 14 ++-- setuptools/tests/test_build_clib.py | 9 +- setuptools/tests/test_config.py | 16 ++-- setuptools/tests/test_depends.py | 18 ++-- setuptools/tests/test_dist.py | 15 ++-- setuptools/tests/test_easy_install.py | 6 +- setuptools/tests/test_egg_info.py | 5 +- setuptools/tests/test_find_packages.py | 11 +-- setuptools/tests/test_install_scripts.py | 6 +- setuptools/tests/test_integration.py | 23 ++++-- setuptools/tests/test_manifest.py | 133 +++++++++++++++--------------- setuptools/tests/test_msvc.py | 5 +- setuptools/tests/test_namespaces.py | 1 - setuptools/tests/test_packageindex.py | 17 ++-- setuptools/tests/test_pep425tags.py | 4 +- setuptools/tests/test_sandbox.py | 3 +- setuptools/tests/test_setuptools.py | 9 +- setuptools/tests/test_test.py | 1 - setuptools/tests/test_virtualenv.py | 3 +- setuptools/tests/test_wheel.py | 2 + setuptools/tests/test_windows_wrappers.py | 13 ++- 22 files changed, 183 insertions(+), 140 deletions(-) (limited to 'setuptools') diff --git a/setuptools/tests/files.py b/setuptools/tests/files.py index 465a6b41..bad2189d 100644 --- a/setuptools/tests/files.py +++ b/setuptools/tests/files.py @@ -6,10 +6,13 @@ import pkg_resources.py31compat def build_files(file_defs, prefix=""): """ - Build a set of files/directories, as described by the file_defs dictionary. + Build a set of files/directories, as described by the + file_defs dictionary. - Each key/value pair in the dictionary is interpreted as a filename/contents - pair. If the contents value is a dictionary, a directory is created, and the + Each key/value pair in the dictionary is interpreted as + a filename/contents + pair. If the contents value is a dictionary, a directory + is created, and the dictionary interpreted as the files within it, recursively. For example: diff --git a/setuptools/tests/server.py b/setuptools/tests/server.py index 35312120..fc3a5975 100644 --- a/setuptools/tests/server.py +++ b/setuptools/tests/server.py @@ -19,10 +19,11 @@ class IndexServer(BaseHTTPServer.HTTPServer): s.stop() """ - def __init__(self, server_address=('', 0), + def __init__( + self, server_address=('', 0), RequestHandlerClass=SimpleHTTPServer.SimpleHTTPRequestHandler): - BaseHTTPServer.HTTPServer.__init__(self, server_address, - RequestHandlerClass) + BaseHTTPServer.HTTPServer.__init__( + self, server_address, RequestHandlerClass) self._run = True def start(self): @@ -56,10 +57,11 @@ class MockServer(BaseHTTPServer.HTTPServer, threading.Thread): A simple HTTP Server that records the requests made to it. """ - def __init__(self, server_address=('', 0), + def __init__( + self, server_address=('', 0), RequestHandlerClass=RequestRecorder): - BaseHTTPServer.HTTPServer.__init__(self, server_address, - RequestHandlerClass) + BaseHTTPServer.HTTPServer.__init__( + self, server_address, RequestHandlerClass) threading.Thread.__init__(self) self.setDaemon(True) self.requests = [] diff --git a/setuptools/tests/test_build_clib.py b/setuptools/tests/test_build_clib.py index aebcc350..3779e679 100644 --- a/setuptools/tests/test_build_clib.py +++ b/setuptools/tests/test_build_clib.py @@ -1,6 +1,4 @@ import pytest -import os -import shutil import mock from distutils.errors import DistutilsSetupError @@ -40,13 +38,14 @@ class TestBuildCLib: # with that out of the way, let's see if the crude dependency # system works cmd.compiler = mock.MagicMock(spec=cmd.compiler) - mock_newer.return_value = ([],[]) + mock_newer.return_value = ([], []) obj_deps = {'': ('global.h',), 'example.c': ('example.h',)} - libs = [('example', {'sources': ['example.c'] ,'obj_deps': obj_deps})] + libs = [('example', {'sources': ['example.c'], 'obj_deps': obj_deps})] cmd.build_libraries(libs) - assert [['example.c', 'global.h', 'example.h']] in mock_newer.call_args[0] + assert [['example.c', 'global.h', 'example.h']] in \ + mock_newer.call_args[0] assert not cmd.compiler.compile.called assert cmd.compiler.create_static_lib.call_count == 1 diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index 53b8a956..a6b44b9f 100644 --- a/setuptools/tests/test_config.py +++ b/setuptools/tests/test_config.py @@ -8,6 +8,7 @@ from setuptools.config import ConfigHandler, read_configuration from . import py2_only, py3_only from .textwrap import DALS + class ErrConfigHandler(ConfigHandler): """Erroneous handler. Fails to implement required methods.""" @@ -18,8 +19,8 @@ def make_package_dir(name, base_dir, ns=False): dir_package = dir_package.mkdir(dir_name) init_file = None if not ns: - init_file = dir_package.join('__init__.py') - init_file.write('') + init_file = dir_package.join('__init__.py') + init_file.write('') return dir_package, init_file @@ -308,7 +309,7 @@ class TestMetadata: tmpdir.join('fake_package', 'version.txt').write('1.2.3\n4.5.6\n') with pytest.raises(DistutilsOptionError): with get_dist(tmpdir) as dist: - _ = dist.metadata.version + dist.metadata.version def test_version_with_package_dir_simple(self, tmpdir): @@ -451,7 +452,7 @@ class TestOptions: 'tests_require = mock==0.7.2; pytest\n' 'setup_requires = docutils>=0.3; spack ==1.1, ==1.3; there\n' 'dependency_links = http://some.com/here/1, ' - 'http://some.com/there/2\n' + 'http://some.com/there/2\n' 'python_requires = >=1.0, !=2.8\n' 'py_modules = module1, module2\n' ) @@ -659,7 +660,7 @@ class TestOptions: dir_sub_two, _ = make_package_dir('sub_two', dir_package, ns=True) with get_dist(tmpdir) as dist: - assert set(dist.packages) == { + assert set(dist.packages) == { 'fake_package', 'fake_package.sub_two', 'fake_package.sub_one' } @@ -711,7 +712,7 @@ class TestOptions: tmpdir, '[options.entry_points]\n' 'group1 = point1 = pack.module:func, ' - '.point2 = pack.module2:func_rest [rest]\n' + '.point2 = pack.module2:func_rest [rest]\n' 'group2 = point3 = pack.module:func2\n' ) @@ -757,7 +758,10 @@ class TestOptions: ] assert sorted(dist.data_files) == sorted(expected) + saved_dist_init = _Distribution.__init__ + + class TestExternalSetters: # During creation of the setuptools Distribution() object, we call # the init of the parent distutils Distribution object via diff --git a/setuptools/tests/test_depends.py b/setuptools/tests/test_depends.py index e0cfa880..bff1dfb1 100644 --- a/setuptools/tests/test_depends.py +++ b/setuptools/tests/test_depends.py @@ -5,12 +5,12 @@ from setuptools import depends class TestGetModuleConstant: - def test_basic(self): - """ - Invoke get_module_constant on a module in - the test package. - """ - mod_name = 'setuptools.tests.mod_with_constant' - val = depends.get_module_constant(mod_name, 'value') - assert val == 'three, sir!' - assert 'setuptools.tests.mod_with_constant' not in sys.modules + def test_basic(self): + """ + Invoke get_module_constant on a module in + the test package. + """ + mod_name = 'setuptools.tests.mod_with_constant' + val = depends.get_module_constant(mod_name, 'value') + assert val == 'three, sir!' + assert 'setuptools.tests.mod_with_constant' not in sys.modules diff --git a/setuptools/tests/test_dist.py b/setuptools/tests/test_dist.py index cf830b43..390c3dfc 100644 --- a/setuptools/tests/test_dist.py +++ b/setuptools/tests/test_dist.py @@ -14,6 +14,7 @@ from .test_easy_install import make_nspkg_sdist import pytest + def test_dist_fetch_build_egg(tmpdir): """ Check multiple calls to `Distribution.fetch_build_egg` work as expected. @@ -90,30 +91,32 @@ def __read_test_cases(): 'classifiers': [ 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.7', - 'License :: OSI Approved :: MIT License' - ]})), + 'License :: OSI Approved :: MIT License', + ]})), ('Metadata version 1.1: Download URL', merge_dicts(base_attrs, { 'download_url': 'https://example.com' })), ('Metadata Version 1.2: Requires-Python', merge_dicts(base_attrs, { 'python_requires': '>=3.7' })), - pytest.param('Metadata Version 1.2: Project-Url', + pytest.param( + 'Metadata Version 1.2: Project-Url', merge_dicts(base_attrs, { 'project_urls': { 'Foo': 'https://example.bar' } }), marks=pytest.mark.xfail( reason="Issue #1578: project_urls not read" - )), + )), ('Metadata Version 2.1: Long Description Content Type', merge_dicts(base_attrs, { 'long_description_content_type': 'text/x-rst; charset=UTF-8' })), - pytest.param('Metadata Version 2.1: Provides Extra', + pytest.param( + 'Metadata Version 2.1: Provides Extra', merge_dicts(base_attrs, { 'provides_extras': ['foo', 'bar'] - }), marks=pytest.mark.xfail(reason="provides_extras not read")), + }), marks=pytest.mark.xfail(reason="provides_extras not read")), ('Missing author, missing author e-mail', {'name': 'foo', 'version': '1.0.0'}), ('Missing author', diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py index 2cf65ae7..c3fd1c6e 100644 --- a/setuptools/tests/test_easy_install.py +++ b/setuptools/tests/test_easy_install.py @@ -15,7 +15,9 @@ import distutils.errors import io import zipfile import mock -from setuptools.command.easy_install import EasyInstallDeprecationWarning, ScriptWriter, WindowsScriptWriter +from setuptools.command.easy_install import ( + EasyInstallDeprecationWarning, ScriptWriter, WindowsScriptWriter, +) import time from setuptools.extern import six from setuptools.extern.six.moves import urllib @@ -287,7 +289,6 @@ class TestEasyInstallTest: cmd.easy_install(sdist_script) assert (target / 'mypkg_script').exists() - def test_dist_get_script_args_deprecated(self): with pytest.warns(EasyInstallDeprecationWarning): ScriptWriter.get_script_args(None, None) @@ -304,6 +305,7 @@ class TestEasyInstallTest: with pytest.warns(EasyInstallDeprecationWarning): WindowsScriptWriter.get_writer() + @pytest.mark.filterwarnings('ignore:Unbuilt egg') class TestPTHFileWriter: def test_add_from_cwd_site_sets_dirty(self): diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index 979ff18e..571e6054 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -1,4 +1,3 @@ -import datetime import sys import ast import os @@ -7,7 +6,9 @@ import re import stat import time -from setuptools.command.egg_info import egg_info, manifest_maker, EggInfoDeprecationWarning, get_pkg_info_revision +from setuptools.command.egg_info import ( + egg_info, manifest_maker, EggInfoDeprecationWarning, get_pkg_info_revision, +) from setuptools.dist import Distribution from setuptools.extern.six.moves import map diff --git a/setuptools/tests/test_find_packages.py b/setuptools/tests/test_find_packages.py index b08f91c7..ab26b4f1 100644 --- a/setuptools/tests/test_find_packages.py +++ b/setuptools/tests/test_find_packages.py @@ -12,10 +12,10 @@ from . import py3_only from setuptools.extern.six import PY3 from setuptools import find_packages if PY3: - from setuptools import find_namespace_packages + from setuptools import find_namespace_packages -# modeled after CPython's test.support.can_symlink +# modeled after CPython's test.support.can_symlink def can_symlink(): TESTFN = tempfile.mktemp() symlink_path = TESTFN + "can_symlink" @@ -164,12 +164,14 @@ class TestFindPackages: def test_pep420_ns_package_no_includes(self): packages = find_namespace_packages( self.dist_dir, exclude=['pkg.subpkg.assets']) - self._assert_packages(packages, ['docs', 'pkg', 'pkg.nspkg', 'pkg.subpkg']) + self._assert_packages( + packages, ['docs', 'pkg', 'pkg.nspkg', 'pkg.subpkg']) @py3_only def test_pep420_ns_package_no_includes_or_excludes(self): packages = find_namespace_packages(self.dist_dir) - expected = ['docs', 'pkg', 'pkg.nspkg', 'pkg.subpkg', 'pkg.subpkg.assets'] + expected = [ + 'docs', 'pkg', 'pkg.nspkg', 'pkg.subpkg', 'pkg.subpkg.assets'] self._assert_packages(packages, expected) @py3_only @@ -185,4 +187,3 @@ class TestFindPackages: shutil.rmtree(os.path.join(self.dist_dir, 'pkg/subpkg/assets')) packages = find_namespace_packages(self.dist_dir) self._assert_packages(packages, ['pkg', 'pkg.nspkg', 'pkg.subpkg']) - diff --git a/setuptools/tests/test_install_scripts.py b/setuptools/tests/test_install_scripts.py index 727ad65b..4338c792 100644 --- a/setuptools/tests/test_install_scripts.py +++ b/setuptools/tests/test_install_scripts.py @@ -64,7 +64,8 @@ class TestInstallScripts: @pytest.mark.skipif(sys.platform == 'win32', reason='non-Windows only') def test_executable_with_spaces_escaping_unix(self, tmpdir): """ - Ensure that shebang on Unix is not quoted, even when a value with spaces + Ensure that shebang on Unix is not quoted, even when + a value with spaces is specified using --executable. """ expected = '#!%s\n' % self.unix_spaces_exe @@ -77,7 +78,8 @@ class TestInstallScripts: @pytest.mark.skipif(sys.platform != 'win32', reason='Windows only') def test_executable_arg_escaping_win32(self, tmpdir): """ - Ensure that shebang on Windows is quoted when getting a path with spaces + Ensure that shebang on Windows is quoted when + getting a path with spaces from --executable, that is itself properly quoted. """ expected = '#!"%s"\n' % self.win32_exe diff --git a/setuptools/tests/test_integration.py b/setuptools/tests/test_integration.py index 3a9a6c50..e54f3209 100644 --- a/setuptools/tests/test_integration.py +++ b/setuptools/tests/test_integration.py @@ -6,6 +6,11 @@ Try to install a few packages. import glob import os import sys +import re +import subprocess +import functools +import tarfile +import zipfile from setuptools.extern.six.moves import urllib import pytest @@ -114,15 +119,12 @@ def test_pyuri(install_context): assert os.path.exists(os.path.join(pyuri.location, 'pyuri', 'uri.regex')) -import re -import subprocess -import functools -import tarfile, zipfile +build_deps = ['appdirs', 'packaging', 'pyparsing', 'six'] -build_deps = ['appdirs', 'packaging', 'pyparsing', 'six'] @pytest.mark.parametrize("build_dep", build_deps) -@pytest.mark.skipif(sys.version_info < (3, 6), reason='run only on late versions') +@pytest.mark.skipif( + sys.version_info < (3, 6), reason='run only on late versions') def test_build_deps_on_distutils(request, tmpdir_factory, build_dep): """ All setuptools build dependencies must build without @@ -149,13 +151,16 @@ def install(pkg_dir, install_dir): breaker.write('raise ImportError()') cmd = [sys.executable, 'setup.py', 'install', '--prefix', install_dir] env = dict(os.environ, PYTHONPATH=pkg_dir) - output = subprocess.check_output(cmd, cwd=pkg_dir, env=env, stderr=subprocess.STDOUT) + output = subprocess.check_output( + cmd, cwd=pkg_dir, env=env, stderr=subprocess.STDOUT) return output.decode('utf-8') def download_and_extract(request, req, target): - cmd = [sys.executable, '-m', 'pip', 'download', '--no-deps', - '--no-binary', ':all:', req] + cmd = [ + sys.executable, '-m', 'pip', 'download', '--no-deps', + '--no-binary', ':all:', req, + ] output = subprocess.check_output(cmd, encoding='utf-8') filename = re.search('Saved (.*)', output).group(1) request.addfinalizer(functools.partial(os.remove, filename)) diff --git a/setuptools/tests/test_manifest.py b/setuptools/tests/test_manifest.py index 5edfbea0..2a0e9c86 100644 --- a/setuptools/tests/test_manifest.py +++ b/setuptools/tests/test_manifest.py @@ -15,7 +15,6 @@ from setuptools.command.egg_info import FileList, egg_info, translate_pattern from setuptools.dist import Distribution from setuptools.extern import six from setuptools.tests.textwrap import DALS -from . import py3_only import pytest @@ -74,7 +73,9 @@ translate_specs = [ # Glob matching ('*.txt', ['foo.txt', 'bar.txt'], ['foo/foo.txt']), - ('dir/*.txt', ['dir/foo.txt', 'dir/bar.txt', 'dir/.txt'], ['notdir/foo.txt']), + ( + 'dir/*.txt', + ['dir/foo.txt', 'dir/bar.txt', 'dir/.txt'], ['notdir/foo.txt']), ('*/*.py', ['bin/start.py'], []), ('docs/page-?.txt', ['docs/page-9.txt'], ['docs/page-10.txt']), @@ -243,77 +244,77 @@ class TestManifestTest(TempDirTestCase): def test_exclude(self): """Include everything in app/ except the text files""" - l = make_local_path + ml = make_local_path self.make_manifest( """ include app/* exclude app/*.txt """) - files = default_files | set([l('app/c.rst')]) + files = default_files | set([ml('app/c.rst')]) assert files == self.get_files() def test_include_multiple(self): """Include with multiple patterns.""" - l = make_local_path + ml = make_local_path self.make_manifest("include app/*.txt app/static/*") files = default_files | set([ - l('app/a.txt'), l('app/b.txt'), - l('app/static/app.js'), l('app/static/app.js.map'), - l('app/static/app.css'), l('app/static/app.css.map')]) + ml('app/a.txt'), ml('app/b.txt'), + ml('app/static/app.js'), ml('app/static/app.js.map'), + ml('app/static/app.css'), ml('app/static/app.css.map')]) assert files == self.get_files() def test_graft(self): """Include the whole app/static/ directory.""" - l = make_local_path + ml = make_local_path self.make_manifest("graft app/static") files = default_files | set([ - l('app/static/app.js'), l('app/static/app.js.map'), - l('app/static/app.css'), l('app/static/app.css.map')]) + ml('app/static/app.js'), ml('app/static/app.js.map'), + ml('app/static/app.css'), ml('app/static/app.css.map')]) assert files == self.get_files() def test_graft_glob_syntax(self): """Include the whole app/static/ directory.""" - l = make_local_path + ml = make_local_path self.make_manifest("graft */static") files = default_files | set([ - l('app/static/app.js'), l('app/static/app.js.map'), - l('app/static/app.css'), l('app/static/app.css.map')]) + ml('app/static/app.js'), ml('app/static/app.js.map'), + ml('app/static/app.css'), ml('app/static/app.css.map')]) assert files == self.get_files() def test_graft_global_exclude(self): """Exclude all *.map files in the project.""" - l = make_local_path + ml = make_local_path self.make_manifest( """ graft app/static global-exclude *.map """) files = default_files | set([ - l('app/static/app.js'), l('app/static/app.css')]) + ml('app/static/app.js'), ml('app/static/app.css')]) assert files == self.get_files() def test_global_include(self): """Include all *.rst, *.js, and *.css files in the whole tree.""" - l = make_local_path + ml = make_local_path self.make_manifest( """ global-include *.rst *.js *.css """) files = default_files | set([ - '.hidden.rst', 'testing.rst', l('app/c.rst'), - l('app/static/app.js'), l('app/static/app.css')]) + '.hidden.rst', 'testing.rst', ml('app/c.rst'), + ml('app/static/app.js'), ml('app/static/app.css')]) assert files == self.get_files() def test_graft_prune(self): """Include all files in app/, except for the whole app/static/ dir.""" - l = make_local_path + ml = make_local_path self.make_manifest( """ graft app prune app/static """) files = default_files | set([ - l('app/a.txt'), l('app/b.txt'), l('app/c.rst')]) + ml('app/a.txt'), ml('app/b.txt'), ml('app/c.rst')]) assert files == self.get_files() @@ -369,7 +370,7 @@ class TestFileListTest(TempDirTestCase): def test_process_template_line(self): # testing all MANIFEST.in template patterns file_list = FileList() - l = make_local_path + ml = make_local_path # simulated file list self.make_files([ @@ -377,16 +378,16 @@ class TestFileListTest(TempDirTestCase): 'buildout.cfg', # filelist does not filter out VCS directories, # it's sdist that does - l('.hg/last-message.txt'), - l('global/one.txt'), - l('global/two.txt'), - l('global/files.x'), - l('global/here.tmp'), - l('f/o/f.oo'), - l('dir/graft-one'), - l('dir/dir2/graft2'), - l('dir3/ok'), - l('dir3/sub/ok.txt'), + ml('.hg/last-message.txt'), + ml('global/one.txt'), + ml('global/two.txt'), + ml('global/files.x'), + ml('global/here.tmp'), + ml('f/o/f.oo'), + ml('dir/graft-one'), + ml('dir/dir2/graft2'), + ml('dir3/ok'), + ml('dir3/sub/ok.txt'), ]) MANIFEST_IN = DALS("""\ @@ -413,12 +414,12 @@ class TestFileListTest(TempDirTestCase): 'buildout.cfg', 'four.txt', 'ok', - l('.hg/last-message.txt'), - l('dir/graft-one'), - l('dir/dir2/graft2'), - l('f/o/f.oo'), - l('global/one.txt'), - l('global/two.txt'), + ml('.hg/last-message.txt'), + ml('dir/graft-one'), + ml('dir/dir2/graft2'), + ml('f/o/f.oo'), + ml('global/one.txt'), + ml('global/two.txt'), ] file_list.sort() @@ -475,10 +476,10 @@ class TestFileListTest(TempDirTestCase): assert False, "Should have thrown an error" def test_include(self): - l = make_local_path + ml = make_local_path # include file_list = FileList() - self.make_files(['a.py', 'b.txt', l('d/c.py')]) + self.make_files(['a.py', 'b.txt', ml('d/c.py')]) file_list.process_template_line('include *.py') file_list.sort() @@ -491,42 +492,42 @@ class TestFileListTest(TempDirTestCase): self.assertWarnings() def test_exclude(self): - l = make_local_path + ml = make_local_path # exclude file_list = FileList() - file_list.files = ['a.py', 'b.txt', l('d/c.py')] + file_list.files = ['a.py', 'b.txt', ml('d/c.py')] file_list.process_template_line('exclude *.py') file_list.sort() - assert file_list.files == ['b.txt', l('d/c.py')] + assert file_list.files == ['b.txt', ml('d/c.py')] self.assertNoWarnings() file_list.process_template_line('exclude *.rb') file_list.sort() - assert file_list.files == ['b.txt', l('d/c.py')] + assert file_list.files == ['b.txt', ml('d/c.py')] self.assertWarnings() def test_global_include(self): - l = make_local_path + ml = make_local_path # global-include file_list = FileList() - self.make_files(['a.py', 'b.txt', l('d/c.py')]) + self.make_files(['a.py', 'b.txt', ml('d/c.py')]) file_list.process_template_line('global-include *.py') file_list.sort() - assert file_list.files == ['a.py', l('d/c.py')] + assert file_list.files == ['a.py', ml('d/c.py')] self.assertNoWarnings() file_list.process_template_line('global-include *.rb') file_list.sort() - assert file_list.files == ['a.py', l('d/c.py')] + assert file_list.files == ['a.py', ml('d/c.py')] self.assertWarnings() def test_global_exclude(self): - l = make_local_path + ml = make_local_path # global-exclude file_list = FileList() - file_list.files = ['a.py', 'b.txt', l('d/c.py')] + file_list.files = ['a.py', 'b.txt', ml('d/c.py')] file_list.process_template_line('global-exclude *.py') file_list.sort() @@ -539,65 +540,65 @@ class TestFileListTest(TempDirTestCase): self.assertWarnings() def test_recursive_include(self): - l = make_local_path + ml = make_local_path # recursive-include file_list = FileList() - self.make_files(['a.py', l('d/b.py'), l('d/c.txt'), l('d/d/e.py')]) + self.make_files(['a.py', ml('d/b.py'), ml('d/c.txt'), ml('d/d/e.py')]) file_list.process_template_line('recursive-include d *.py') file_list.sort() - assert file_list.files == [l('d/b.py'), l('d/d/e.py')] + assert file_list.files == [ml('d/b.py'), ml('d/d/e.py')] self.assertNoWarnings() file_list.process_template_line('recursive-include e *.py') file_list.sort() - assert file_list.files == [l('d/b.py'), l('d/d/e.py')] + assert file_list.files == [ml('d/b.py'), ml('d/d/e.py')] self.assertWarnings() def test_recursive_exclude(self): - l = make_local_path + ml = make_local_path # recursive-exclude file_list = FileList() - file_list.files = ['a.py', l('d/b.py'), l('d/c.txt'), l('d/d/e.py')] + file_list.files = ['a.py', ml('d/b.py'), ml('d/c.txt'), ml('d/d/e.py')] file_list.process_template_line('recursive-exclude d *.py') file_list.sort() - assert file_list.files == ['a.py', l('d/c.txt')] + assert file_list.files == ['a.py', ml('d/c.txt')] self.assertNoWarnings() file_list.process_template_line('recursive-exclude e *.py') file_list.sort() - assert file_list.files == ['a.py', l('d/c.txt')] + assert file_list.files == ['a.py', ml('d/c.txt')] self.assertWarnings() def test_graft(self): - l = make_local_path + ml = make_local_path # graft file_list = FileList() - self.make_files(['a.py', l('d/b.py'), l('d/d/e.py'), l('f/f.py')]) + self.make_files(['a.py', ml('d/b.py'), ml('d/d/e.py'), ml('f/f.py')]) file_list.process_template_line('graft d') file_list.sort() - assert file_list.files == [l('d/b.py'), l('d/d/e.py')] + assert file_list.files == [ml('d/b.py'), ml('d/d/e.py')] self.assertNoWarnings() file_list.process_template_line('graft e') file_list.sort() - assert file_list.files == [l('d/b.py'), l('d/d/e.py')] + assert file_list.files == [ml('d/b.py'), ml('d/d/e.py')] self.assertWarnings() def test_prune(self): - l = make_local_path + ml = make_local_path # prune file_list = FileList() - file_list.files = ['a.py', l('d/b.py'), l('d/d/e.py'), l('f/f.py')] + file_list.files = ['a.py', ml('d/b.py'), ml('d/d/e.py'), ml('f/f.py')] file_list.process_template_line('prune d') file_list.sort() - assert file_list.files == ['a.py', l('f/f.py')] + assert file_list.files == ['a.py', ml('f/f.py')] self.assertNoWarnings() file_list.process_template_line('prune e') file_list.sort() - assert file_list.files == ['a.py', l('f/f.py')] + assert file_list.files == ['a.py', ml('f/f.py')] self.assertWarnings() diff --git a/setuptools/tests/test_msvc.py b/setuptools/tests/test_msvc.py index 32d7a907..24e38ea8 100644 --- a/setuptools/tests/test_msvc.py +++ b/setuptools/tests/test_msvc.py @@ -49,7 +49,8 @@ def mock_reg(hkcu=None, hklm=None): for k in hive if k.startswith(key.lower()) ) - return mock.patch.multiple(distutils.msvc9compiler.Reg, + return mock.patch.multiple( + distutils.msvc9compiler.Reg, read_keys=read_keys, read_values=read_values) @@ -61,7 +62,7 @@ class TestModulePatch: """ key_32 = r'software\microsoft\devdiv\vcforpython\9.0\installdir' - key_64 = r'software\wow6432node\microsoft\devdiv\vcforpython\9.0\installdir' + key_64 = key_32.replace(r'\microsoft', r'\wow6432node\microsoft') def test_patched(self): "Test the module is actually patched" diff --git a/setuptools/tests/test_namespaces.py b/setuptools/tests/test_namespaces.py index 670ccee9..f937d981 100644 --- a/setuptools/tests/test_namespaces.py +++ b/setuptools/tests/test_namespaces.py @@ -1,6 +1,5 @@ from __future__ import absolute_import, unicode_literals -import os import sys import subprocess diff --git a/setuptools/tests/test_packageindex.py b/setuptools/tests/test_packageindex.py index 13cffb7e..ab371884 100644 --- a/setuptools/tests/test_packageindex.py +++ b/setuptools/tests/test_packageindex.py @@ -44,7 +44,10 @@ class TestPackageIndex: hosts=('www.example.com',) ) - url = 'url:%20https://svn.plone.org/svn/collective/inquant.contentmirror.plone/trunk' + url = ( + 'url:%20https://svn.plone.org/svn' + '/collective/inquant.contentmirror.plone/trunk' + ) try: v = index.open_url(url) except Exception as v: @@ -63,9 +66,9 @@ class TestPackageIndex: index.opener = _urlopen url = 'http://example.com' try: - v = index.open_url(url) - except Exception as v: - assert 'line' in str(v) + index.open_url(url) + except Exception as exc: + assert 'line' in str(exc) else: raise AssertionError('Should have raise here!') @@ -83,7 +86,11 @@ class TestPackageIndex: index.open_url(url) except distutils.errors.DistutilsError as error: msg = six.text_type(error) - assert 'nonnumeric port' in msg or 'getaddrinfo failed' in msg or 'Name or service not known' in msg + assert ( + 'nonnumeric port' in msg + or 'getaddrinfo failed' in msg + or 'Name or service not known' in msg + ) return raise RuntimeError("Did not raise") diff --git a/setuptools/tests/test_pep425tags.py b/setuptools/tests/test_pep425tags.py index f558a0d8..30afdec7 100644 --- a/setuptools/tests/test_pep425tags.py +++ b/setuptools/tests/test_pep425tags.py @@ -32,7 +32,9 @@ class TestPEP425Tags: if sys.version_info < (3, 3): config_vars.update({'Py_UNICODE_SIZE': 2}) mock_gcf = self.mock_get_config_var(**config_vars) - with patch('setuptools.pep425tags.sysconfig.get_config_var', mock_gcf): + with patch( + 'setuptools.pep425tags.sysconfig.get_config_var', + mock_gcf): abi_tag = pep425tags.get_abi_tag() assert abi_tag == base + flags diff --git a/setuptools/tests/test_sandbox.py b/setuptools/tests/test_sandbox.py index d8675422..99398cdb 100644 --- a/setuptools/tests/test_sandbox.py +++ b/setuptools/tests/test_sandbox.py @@ -26,7 +26,8 @@ class TestSandbox: """ It should be possible to execute a setup.py with a Byte Order Mark """ - target = pkg_resources.resource_filename(__name__, + target = pkg_resources.resource_filename( + __name__, 'script-with-bom.py') namespace = types.ModuleType('namespace') setuptools.sandbox._execfile(target, vars(namespace)) diff --git a/setuptools/tests/test_setuptools.py b/setuptools/tests/test_setuptools.py index 7aae3a16..5896a69a 100644 --- a/setuptools/tests/test_setuptools.py +++ b/setuptools/tests/test_setuptools.py @@ -77,7 +77,8 @@ class TestDepends: 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__ + assert dep.get_module_constant( + 'setuptools.tests.test_setuptools', '__doc__') == __doc__ @needs_bytecode def testRequire(self): @@ -216,7 +217,8 @@ class TestFeatures: self.req = Require('Distutils', '1.0.3', 'distutils') self.dist = makeSetup( features={ - 'foo': Feature("foo", standard=True, require_features=['baz', self.req]), + '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'], ), @@ -252,7 +254,8 @@ class TestFeatures: ('with-dwim', None, 'include DWIM') in dist.feature_options ) assert ( - ('without-dwim', None, 'exclude DWIM (default)') in dist.feature_options + ('without-dwim', None, 'exclude DWIM (default)') + in dist.feature_options ) assert ( ('with-bar', None, 'include bar (default)') in dist.feature_options diff --git a/setuptools/tests/test_test.py b/setuptools/tests/test_test.py index 8d1425e1..faaa6ba9 100644 --- a/setuptools/tests/test_test.py +++ b/setuptools/tests/test_test.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals from distutils import log import os -import sys import pytest diff --git a/setuptools/tests/test_virtualenv.py b/setuptools/tests/test_virtualenv.py index 7b5fea17..3d5c84b0 100644 --- a/setuptools/tests/test_virtualenv.py +++ b/setuptools/tests/test_virtualenv.py @@ -1,4 +1,3 @@ -import distutils.command import glob import os import sys @@ -141,7 +140,7 @@ def test_no_missing_dependencies(bare_virtualenv): """ Quick and dirty test to ensure all external dependencies are vendored. """ - for command in ('upload',):#sorted(distutils.command.__all__): + for command in ('upload',): # sorted(distutils.command.__all__): bare_virtualenv.run(' && '.join(( 'cd {source}', 'python setup.py {command} -h', diff --git a/setuptools/tests/test_wheel.py b/setuptools/tests/test_wheel.py index 6db5fa11..e85a4a7e 100644 --- a/setuptools/tests/test_wheel.py +++ b/setuptools/tests/test_wheel.py @@ -63,6 +63,7 @@ WHEEL_INFO_TESTS = ( }), ) + @pytest.mark.parametrize( ('filename', 'info'), WHEEL_INFO_TESTS, ids=[t[0] for t in WHEEL_INFO_TESTS] @@ -487,6 +488,7 @@ WHEEL_INSTALL_TESTS = ( ) + @pytest.mark.parametrize( 'params', WHEEL_INSTALL_TESTS, ids=list(params['id'] for params in WHEEL_INSTALL_TESTS), diff --git a/setuptools/tests/test_windows_wrappers.py b/setuptools/tests/test_windows_wrappers.py index d2871c0f..2553394a 100644 --- a/setuptools/tests/test_windows_wrappers.py +++ b/setuptools/tests/test_windows_wrappers.py @@ -97,7 +97,8 @@ class TestCLI(WrapperTester): 'arg 4\\', 'arg5 a\\\\b', ] - proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE) + proc = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE) stdout, stderr = proc.communicate('hello\nworld\n'.encode('ascii')) actual = stdout.decode('ascii').replace('\r\n', '\n') expected = textwrap.dedent(r""" @@ -134,7 +135,11 @@ class TestCLI(WrapperTester): with (tmpdir / 'foo-script.py').open('w') as f: f.write(self.prep_script(tmpl)) cmd = [str(tmpdir / 'foo.exe')] - proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT) + proc = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + stderr=subprocess.STDOUT) stdout, stderr = proc.communicate() actual = stdout.decode('ascii').replace('\r\n', '\n') expected = textwrap.dedent(r""" @@ -172,7 +177,9 @@ class TestGUI(WrapperTester): str(tmpdir / 'test_output.txt'), 'Test Argument', ] - proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT) + proc = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, + stderr=subprocess.STDOUT) stdout, stderr = proc.communicate() assert not stdout assert not stderr -- cgit v1.2.3 From b1615d12435289c21853be2f4f40e523317998da Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 26 Jan 2019 21:45:33 -0500 Subject: Adopt distutils.dist.Distribution._set_command_options to support better string detection. --- setuptools/dist.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) (limited to 'setuptools') diff --git a/setuptools/dist.py b/setuptools/dist.py index 4cc3bdfe..a7ebe73b 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -14,6 +14,7 @@ import distutils.dist from distutils.errors import DistutilsOptionError from distutils.util import strtobool from distutils.debug import DEBUG +from distutils.fancy_getopt import translate_longopt import itertools from collections import defaultdict @@ -626,6 +627,53 @@ class Distribution(_Distribution): except ValueError as msg: raise DistutilsOptionError(msg) + def _set_command_options(self, command_obj, option_dict=None): + """ + Set the options for 'command_obj' from 'option_dict'. Basically + this means copying elements of a dictionary ('option_dict') to + attributes of an instance ('command'). + + 'command_obj' must be a Command instance. If 'option_dict' is not + supplied, uses the standard option dictionary for this command + (from 'self.command_options'). + + (Adopted from distutils.dist.Distribution._set_command_options) + """ + command_name = command_obj.get_command_name() + if option_dict is None: + option_dict = self.get_option_dict(command_name) + + if DEBUG: + self.announce(" setting options for '%s' command:" % command_name) + for (option, (source, value)) in option_dict.items(): + if DEBUG: + self.announce(" %s = %s (from %s)" % (option, value, + source)) + try: + bool_opts = [translate_longopt(o) + for o in command_obj.boolean_options] + except AttributeError: + bool_opts = [] + try: + neg_opt = command_obj.negative_opt + except AttributeError: + neg_opt = {} + + try: + is_string = isinstance(value, str) + if option in neg_opt and is_string: + setattr(command_obj, neg_opt[option], not strtobool(value)) + elif option in bool_opts and is_string: + setattr(command_obj, option, strtobool(value)) + elif hasattr(command_obj, option): + setattr(command_obj, option, value) + else: + raise DistutilsOptionError( + "error in %s: command '%s' has no such option '%s'" + % (source, command_name, option)) + except ValueError as msg: + raise DistutilsOptionError(msg) + def parse_config_files(self, filenames=None, ignore_option_errors=False): """Parses configuration files from various levels and loads configuration. -- cgit v1.2.3 From 249f24a1f04ce390a9e48a4d8a5bff7982714c86 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 26 Jan 2019 21:48:38 -0500 Subject: Fix test failure by better detecting string options from an updated ConfigParser. --- setuptools/dist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'setuptools') diff --git a/setuptools/dist.py b/setuptools/dist.py index a7ebe73b..b8551228 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -660,7 +660,7 @@ class Distribution(_Distribution): neg_opt = {} try: - is_string = isinstance(value, str) + is_string = isinstance(value, six.string_types) if option in neg_opt and is_string: setattr(command_obj, neg_opt[option], not strtobool(value)) elif option in bool_opts and is_string: -- cgit v1.2.3 From 3ac3b67e2c8a776f410eae49472a0f8266e29612 Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Mon, 28 Jan 2019 23:04:09 +0100 Subject: tests: minor cleanup --- setuptools/tests/test_build_py.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) (limited to 'setuptools') diff --git a/setuptools/tests/test_build_py.py b/setuptools/tests/test_build_py.py index cc701ae6..b3a99f56 100644 --- a/setuptools/tests/test_build_py.py +++ b/setuptools/tests/test_build_py.py @@ -1,17 +1,9 @@ import os -import pytest - from setuptools.dist import Distribution -@pytest.yield_fixture -def tmpdir_as_cwd(tmpdir): - with tmpdir.as_cwd(): - yield tmpdir - - -def test_directories_in_package_data_glob(tmpdir_as_cwd): +def test_directories_in_package_data_glob(tmpdir_cwd): """ Directories matching the glob in package_data should not be included in the package data. -- cgit v1.2.3 From f7447817b65c12dfe508249cea019c41ce0dc23f Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Mon, 28 Jan 2019 22:57:20 +0100 Subject: test: add a simple regression test for `build_ext` --- setuptools/tests/environment.py | 2 ++ setuptools/tests/test_build_ext.py | 70 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) (limited to 'setuptools') diff --git a/setuptools/tests/environment.py b/setuptools/tests/environment.py index c67898ca..bd3119ef 100644 --- a/setuptools/tests/environment.py +++ b/setuptools/tests/environment.py @@ -46,6 +46,8 @@ def run_setup_py(cmd, pypath=None, path=None, cmd, stdout=_PIPE, stderr=_PIPE, shell=shell, env=env, ) + if isinstance(data_stream, tuple): + data_stream = slice(*data_stream) data = proc.communicate()[data_stream] except OSError: return 1, '' diff --git a/setuptools/tests/test_build_ext.py b/setuptools/tests/test_build_ext.py index 60257154..3dc87ca3 100644 --- a/setuptools/tests/test_build_ext.py +++ b/setuptools/tests/test_build_ext.py @@ -8,6 +8,10 @@ from setuptools.command.build_ext import build_ext, get_abi3_suffix from setuptools.dist import Distribution from setuptools.extension import Extension +from . import environment +from .files import build_files +from .textwrap import DALS + class TestBuildExt: def test_get_ext_filename(self): @@ -43,3 +47,69 @@ class TestBuildExt: assert res.endswith('eggs.pyd') else: assert 'abi3' in res + + +def test_build_ext_config_handling(tmpdir_cwd): + files = { + 'setup.py': DALS( + """ + from setuptools import Extension, setup + setup( + name='foo', + version='0.0.0', + ext_modules=[Extension('foo', ['foo.c'])], + ) + """), + 'foo.c': DALS( + """ + #include "Python.h" + + #if PY_MAJOR_VERSION >= 3 + + static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "foo", + NULL, + 0, + NULL, + NULL, + NULL, + NULL, + NULL + }; + + #define INITERROR return NULL + + PyMODINIT_FUNC PyInit_foo(void) + + #else + + #define INITERROR return + + void initfoo(void) + + #endif + { + #if PY_MAJOR_VERSION >= 3 + PyObject *module = PyModule_Create(&moduledef); + #else + PyObject *module = Py_InitModule("extension", NULL); + #endif + if (module == NULL) + INITERROR; + #if PY_MAJOR_VERSION >= 3 + return module; + #endif + } + """), + 'setup.cfg': DALS( + """ + [build] + build-base = foo_build + """), + } + build_files(files) + code, output = environment.run_setup_py( + cmd=['build'], data_stream=(0, 2), + ) + assert code == 0, '\nSTDOUT:\n%s\nSTDERR:\n%s' % output -- cgit v1.2.3 From 9150b6b7272130f11a71190905e6bd3db31afd81 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 28 Jan 2019 17:10:37 -0500 Subject: Prefer native strings on Python 2 when reading config files. Fixes #1653. --- setuptools/dist.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) (limited to 'setuptools') diff --git a/setuptools/dist.py b/setuptools/dist.py index b8551228..ddb1787a 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -603,7 +603,7 @@ class Distribution(_Distribution): for opt in options: if opt != '__name__' and opt not in ignore_options: - val = parser.get(section, opt) + val = self._try_str(parser.get(section, opt)) opt = opt.replace('-', '_') opt_dict[opt] = (filename, val) @@ -627,6 +627,26 @@ class Distribution(_Distribution): except ValueError as msg: raise DistutilsOptionError(msg) + @staticmethod + def _try_str(val): + """ + On Python 2, much of distutils relies on string values being of + type 'str' (bytes) and not unicode text. If the value can be safely + encoded to bytes using the default encoding, prefer that. + + Why the default encoding? Because that value can be implicitly + decoded back to text if needed. + + Ref #1653 + """ + if six.PY3: + return val + try: + return val.encode() + except UnicodeEncodeError: + pass + return val + def _set_command_options(self, command_obj, option_dict=None): """ Set the options for 'command_obj' from 'option_dict'. Basically -- cgit v1.2.3 From 260bbe545e15ea75782c540c421da6c0a67abfd5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 31 Jan 2019 20:46:48 -0500 Subject: Ensure a specified port in package_index isn't lost in the parse/unparse of the URL when auth is present. Fixes #1663. --- setuptools/package_index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'setuptools') diff --git a/setuptools/package_index.py b/setuptools/package_index.py index 7e9517ce..ea76c005 100644 --- a/setuptools/package_index.py +++ b/setuptools/package_index.py @@ -1072,7 +1072,7 @@ def open_with_auth(url, opener=urllib.request.urlopen): if auth: auth = "Basic " + _encode_auth(auth) - parts = scheme, parsed.hostname, path, params, query, frag + parts = scheme, netloc, path, params, query, frag new_url = urllib.parse.urlunparse(parts) request = urllib.request.Request(new_url) request.add_header("Authorization", auth) -- cgit v1.2.3 From 0830a69efde3d561c2025bdfa1fae10dcbbcc8ed Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 3 Feb 2019 09:22:19 -0500 Subject: Revert to using a copy of splituser from Python 3.8. Using urllib.parse.urlparse is clumsy and causes problems as reported in #1663 and #1668. Alternative to #1499 and fixes #1668. --- setuptools/package_index.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) (limited to 'setuptools') diff --git a/setuptools/package_index.py b/setuptools/package_index.py index ea76c005..05c9e341 100644 --- a/setuptools/package_index.py +++ b/setuptools/package_index.py @@ -850,16 +850,13 @@ class PackageIndex(Environment): def _download_svn(self, url, filename): warnings.warn("SVN download support is deprecated", UserWarning) - def splituser(host): - user, delim, host = host.rpartition('@') - return user, host url = url.split('#', 1)[0] # remove any fragment for svn's sake creds = '' if url.lower().startswith('svn:') and '@' in url: scheme, netloc, path, p, q, f = urllib.parse.urlparse(url) if not netloc and path.startswith('//') and '/' in path[2:]: netloc, path = path[2:].split('/', 1) - auth, host = splituser(netloc) + auth, host = _splituser(netloc) if auth: if ':' in auth: user, pw = auth.split(':', 1) @@ -1058,8 +1055,8 @@ def open_with_auth(url, opener=urllib.request.urlopen): if netloc.endswith(':'): raise http_client.InvalidURL("nonnumeric port: ''") - if scheme in ('http', 'https') and parsed.username: - auth = ':'.join((parsed.username, parsed.password)) + if scheme in ('http', 'https'): + auth, address = _splituser(netloc) else: auth = None @@ -1072,7 +1069,7 @@ def open_with_auth(url, opener=urllib.request.urlopen): if auth: auth = "Basic " + _encode_auth(auth) - parts = scheme, netloc, path, params, query, frag + parts = scheme, address, path, params, query, frag new_url = urllib.parse.urlunparse(parts) request = urllib.request.Request(new_url) request.add_header("Authorization", auth) @@ -1093,6 +1090,13 @@ def open_with_auth(url, opener=urllib.request.urlopen): return fp +# copy of urllib.parse._splituser from Python 3.8 +def _splituser(host): + """splituser('user[:passwd]@host[:port]') --> 'user[:passwd]', 'host[:port]'.""" + user, delim, host = host.rpartition('@') + return (user if delim else None), host + + # adding a timeout to avoid freezing package_index open_with_auth = socket_timeout(_SOCKET_TIMEOUT)(open_with_auth) -- cgit v1.2.3 From be840c2fe49766e3311475441b123a4eb3ba473a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 3 Feb 2019 10:17:02 -0500 Subject: Also restore port consideration when re-injecting credentials for found links. --- setuptools/package_index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'setuptools') diff --git a/setuptools/package_index.py b/setuptools/package_index.py index 05c9e341..705a47cf 100644 --- a/setuptools/package_index.py +++ b/setuptools/package_index.py @@ -1083,7 +1083,7 @@ def open_with_auth(url, opener=urllib.request.urlopen): # Put authentication info back into request URL if same host, # so that links found on the page will work s2, h2, path2, param2, query2, frag2 = urllib.parse.urlparse(fp.url) - if s2 == scheme and h2 == parsed.hostname: + if s2 == scheme and h2 == address: parts = s2, netloc, path2, param2, query2, frag2 fp.url = urllib.parse.urlunparse(parts) -- cgit v1.2.3 From 74c323d1658f554b879ef6ac2867faf77776a2ac Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Sun, 27 Jan 2019 11:45:10 -0500 Subject: Wrap build_meta backend in a class In order to support both the `build_meta` and `build_meta_legacy` backends, the core functionality is wrapped in a class with methods to be overridden in build_meta_legacy. The class is an implementation detail and should remain private. --- setuptools/build_meta.py | 187 ++++++++++++++++++++++++----------------------- 1 file changed, 97 insertions(+), 90 deletions(-) (limited to 'setuptools') diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index c883d92f..f40549e7 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -74,81 +74,11 @@ def _to_str(s): return s -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, requirements): - config_settings = _fix_config(config_settings) - - 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, requirements=['wheel']) - - -def get_requires_for_build_sdist(config_settings=None): - config_settings = _fix_config(config_settings) - return _get_build_requires(config_settings, requirements=[]) - - -def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None): - sys.argv = sys.argv[:1] + ['dist_info', '--egg-base', _to_str(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 _file_with_extension(directory, extension): matching = ( f for f in os.listdir(directory) @@ -158,26 +88,103 @@ def _file_with_extension(directory, extension): return file -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) +class _BuildMetaBackend(object): - return _file_with_extension(wheel_directory, '.whl') + def _fix_config(self, config_settings): + config_settings = config_settings or {} + config_settings.setdefault('--global-option', []) + return config_settings + def _get_build_requires(self, config_settings, requirements): + config_settings = self._fix_config(config_settings) -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', '--formats', 'gztar'] + \ - config_settings["--global-option"] + \ - ["--dist-dir", sdist_directory] - _run_setup() - - return _file_with_extension(sdist_directory, '.tar.gz') + sys.argv = sys.argv[:1] + ['egg_info'] + \ + config_settings["--global-option"] + try: + with Distribution.patch(): + self.run_setup() + except SetupRequirementsError as e: + requirements += e.specifiers + + return requirements + + def run_setup(self, 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 get_requires_for_build_wheel(self, config_settings=None): + config_settings = self._fix_config(config_settings) + return self._get_build_requires(config_settings, requirements=['wheel']) + + def get_requires_for_build_sdist(self, config_settings=None): + config_settings = self._fix_config(config_settings) + return self._get_build_requires(config_settings, requirements=[]) + + def prepare_metadata_for_build_wheel(self, metadata_directory, + config_settings=None): + sys.argv = sys.argv[:1] + ['dist_info', '--egg-base', + _to_str(metadata_directory)] + self.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(self, wheel_directory, config_settings=None, + metadata_directory=None): + config_settings = self._fix_config(config_settings) + wheel_directory = os.path.abspath(wheel_directory) + sys.argv = sys.argv[:1] + ['bdist_wheel'] + \ + config_settings["--global-option"] + self.run_setup() + if wheel_directory != 'dist': + shutil.rmtree(wheel_directory) + shutil.copytree('dist', wheel_directory) + + return _file_with_extension(wheel_directory, '.whl') + + def build_sdist(self, sdist_directory, config_settings=None): + config_settings = self._fix_config(config_settings) + sdist_directory = os.path.abspath(sdist_directory) + sys.argv = sys.argv[:1] + ['sdist', '--formats', 'gztar'] + \ + config_settings["--global-option"] + \ + ["--dist-dir", sdist_directory] + self.run_setup() + + return _file_with_extension(sdist_directory, '.tar.gz') + + +_BACKEND = _BuildMetaBackend() + +get_requires_for_build_wheel = _BACKEND.get_requires_for_build_wheel +get_requires_for_build_sdist = _BACKEND.get_requires_for_build_sdist +prepare_metadata_for_build_wheel = _BACKEND.prepare_metadata_for_build_wheel +build_wheel = _BACKEND.build_wheel +build_sdist = _BACKEND.build_sdist -- cgit v1.2.3 From f40a47a776904b09747502a1f210af9fc92ec542 Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Sun, 27 Jan 2019 11:46:21 -0500 Subject: Add __all__ to setuptools.build_meta --- setuptools/build_meta.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'setuptools') diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index f40549e7..8e31a04d 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -35,6 +35,12 @@ import contextlib import setuptools import distutils +__all__ = ['get_requires_for_build_sdist', + 'get_requires_for_build_wheel', + 'prepare_metadata_for_build_wheel', + 'build_wheel', + 'build_sdist', + 'SetupRequirementsError'] class SetupRequirementsError(BaseException): def __init__(self, specifiers): -- cgit v1.2.3 From 6d0daf14277223da4e32093c00157ae5b26d1ece Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Sun, 27 Jan 2019 12:11:21 -0500 Subject: Wrap build_meta tests in a reusable test class --- setuptools/tests/test_build_meta.py | 268 ++++++++++++++++++------------------ 1 file changed, 134 insertions(+), 134 deletions(-) (limited to 'setuptools') diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index 82b44c89..a9865382 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -6,7 +6,6 @@ import tarfile import pytest -from setuptools.build_meta import build_sdist from .files import build_files from .textwrap import DALS from . import py2_only @@ -103,136 +102,137 @@ defns = [ ] -@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', 'wheel'] - assert sorted(actual) == sorted(expected) - - -def test_get_requires_for_build_sdist(build_backend): - actual = build_backend.get_requires_for_build_sdist() - expected = ['six'] - 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')) - - -@py2_only -def test_prepare_metadata_for_build_wheel_with_str(build_backend): - dist_dir = os.path.abspath(str('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')) - - -def test_build_sdist_explicit_dist(build_backend): - # explicitly specifying the dist folder should work - # the folder sdist_directory and the ``--dist-dir`` can be the same - dist_dir = os.path.abspath('dist') - sdist_name = build_backend.build_sdist(dist_dir) - assert os.path.isfile(os.path.join(dist_dir, sdist_name)) - - -def test_build_sdist_version_change(build_backend): - sdist_into_directory = os.path.abspath("out_sdist") - os.makedirs(sdist_into_directory) - - sdist_name = build_backend.build_sdist(sdist_into_directory) - assert os.path.isfile(os.path.join(sdist_into_directory, sdist_name)) - - # if the setup.py changes subsequent call of the build meta - # should still succeed, given the - # sdist_directory the frontend specifies is empty - with open(os.path.abspath("setup.py"), 'rt') as file_handler: - content = file_handler.read() - with open(os.path.abspath("setup.py"), 'wt') as file_handler: - file_handler.write( - content.replace("version='0.0.0'", "version='0.0.1'")) - - shutil.rmtree(sdist_into_directory) - os.makedirs(sdist_into_directory) - - sdist_name = build_backend.build_sdist("out_sdist") - assert os.path.isfile( - os.path.join(os.path.abspath("out_sdist"), sdist_name)) - - -def test_build_sdist_setup_py_exists(tmpdir_cwd): - # If build_sdist is called from a script other than setup.py, - # ensure setup.py is include - build_files(defns[0]) - targz_path = build_sdist("temp") - with tarfile.open(os.path.join("temp", targz_path)) as tar: - assert any('setup.py' in name for name in tar.getnames()) - - -def test_build_sdist_setup_py_manifest_excluded(tmpdir_cwd): - # Ensure that MANIFEST.in can exclude setup.py - files = { - 'setup.py': DALS(""" - __import__('setuptools').setup( - name='foo', - version='0.0.0', - py_modules=['hello'] - )"""), - 'hello.py': '', - 'MANIFEST.in': DALS(""" - exclude setup.py - """) - } - - build_files(files) - targz_path = build_sdist("temp") - with tarfile.open(os.path.join("temp", targz_path)) as tar: - assert not any('setup.py' in name for name in tar.getnames()) - - -def test_build_sdist_builds_targz_even_if_zip_indicated(tmpdir_cwd): - files = { - 'setup.py': DALS(""" - __import__('setuptools').setup( - name='foo', - version='0.0.0', - py_modules=['hello'] - )"""), - 'hello.py': '', - 'setup.cfg': DALS(""" - [sdist] - formats=zip - """) - } - - build_files(files) - build_sdist("temp") +class TestBuildMetaBackend: + backend_name = 'setuptools.build_meta' + + def get_build_backend(self): + return BuildBackend(cwd='.', backend_name=self.backend_name) + + @pytest.fixture(params=defns) + def build_backend(self, tmpdir, request): + build_files(request.param, prefix=str(tmpdir)) + with tmpdir.as_cwd(): + yield self.get_build_backend() + + def test_get_requires_for_build_wheel(self, build_backend): + actual = build_backend.get_requires_for_build_wheel() + expected = ['six', 'wheel'] + assert sorted(actual) == sorted(expected) + + def test_get_requires_for_build_sdist(self, build_backend): + actual = build_backend.get_requires_for_build_sdist() + expected = ['six'] + assert sorted(actual) == sorted(expected) + + def test_build_wheel(self, 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(self, 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(self, 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')) + + @py2_only + def test_prepare_metadata_for_build_wheel_with_str(self, build_backend): + dist_dir = os.path.abspath(str('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')) + + def test_build_sdist_explicit_dist(self, build_backend): + # explicitly specifying the dist folder should work + # the folder sdist_directory and the ``--dist-dir`` can be the same + dist_dir = os.path.abspath('dist') + sdist_name = build_backend.build_sdist(dist_dir) + assert os.path.isfile(os.path.join(dist_dir, sdist_name)) + + def test_build_sdist_version_change(self, build_backend): + sdist_into_directory = os.path.abspath("out_sdist") + os.makedirs(sdist_into_directory) + + sdist_name = build_backend.build_sdist(sdist_into_directory) + assert os.path.isfile(os.path.join(sdist_into_directory, sdist_name)) + + # if the setup.py changes subsequent call of the build meta + # should still succeed, given the + # sdist_directory the frontend specifies is empty + with open(os.path.abspath("setup.py"), 'rt') as file_handler: + content = file_handler.read() + with open(os.path.abspath("setup.py"), 'wt') as file_handler: + file_handler.write( + content.replace("version='0.0.0'", "version='0.0.1'")) + + shutil.rmtree(sdist_into_directory) + os.makedirs(sdist_into_directory) + + sdist_name = build_backend.build_sdist("out_sdist") + assert os.path.isfile( + os.path.join(os.path.abspath("out_sdist"), sdist_name)) + + def test_build_sdist_setup_py_exists(self, tmpdir_cwd): + # If build_sdist is called from a script other than setup.py, + # ensure setup.py is included + build_files(defns[0]) + + build_backend = self.get_build_backend() + targz_path = build_backend.build_sdist("temp") + with tarfile.open(os.path.join("temp", targz_path)) as tar: + assert any('setup.py' in name for name in tar.getnames()) + + def test_build_sdist_setup_py_manifest_excluded(self, tmpdir_cwd): + # Ensure that MANIFEST.in can exclude setup.py + files = { + 'setup.py': DALS(""" + __import__('setuptools').setup( + name='foo', + version='0.0.0', + py_modules=['hello'] + )"""), + 'hello.py': '', + 'MANIFEST.in': DALS(""" + exclude setup.py + """) + } + + build_files(files) + + build_backend = self.get_build_backend() + targz_path = build_backend.build_sdist("temp") + with tarfile.open(os.path.join("temp", targz_path)) as tar: + assert not any('setup.py' in name for name in tar.getnames()) + + def test_build_sdist_builds_targz_even_if_zip_indicated(self, tmpdir_cwd): + files = { + 'setup.py': DALS(""" + __import__('setuptools').setup( + name='foo', + version='0.0.0', + py_modules=['hello'] + )"""), + 'hello.py': '', + 'setup.cfg': DALS(""" + [sdist] + formats=zip + """) + } + + build_files(files) + + build_backend = self.get_build_backend() + build_backend.build_sdist("temp") -- cgit v1.2.3 From bd800f4c793fdcff2347a263c39c4256107b417f Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Sun, 27 Jan 2019 12:24:15 -0500 Subject: Add test for relative path imports in build_meta Failing test adapted from PR #1643 Co-authored-by: Tzu-ping Chung --- setuptools/tests/test_build_meta.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) (limited to 'setuptools') diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index a9865382..8222b8a2 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -236,3 +236,23 @@ class TestBuildMetaBackend: build_backend = self.get_build_backend() build_backend.build_sdist("temp") + + _relative_path_import_files = { + 'setup.py': DALS(""" + __import__('setuptools').setup( + name='foo', + version=__import__('hello').__version__, + py_modules=['hello'] + )"""), + 'hello.py': '__version__ = "0.0.0"', + 'setup.cfg': DALS(""" + [sdist] + formats=zip + """) + } + + def test_build_sdist_relative_path_import(self, tmpdir_cwd): + build_files(self._relative_path_import_files) + build_backend = self.get_build_backend() + with pytest.raises(ImportError): + build_backend.build_sdist("temp") -- cgit v1.2.3 From a114112ea5e6df87a826e0fa2927cffd2472aec1 Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Sun, 27 Jan 2019 12:16:14 -0500 Subject: Add failing test suite for build_meta_legacy This runs all build_meta tests, plus a test that it is possible to import from the directory containing `setup.py` when using the build_meta_legacy backend. --- setuptools/tests/test_build_meta.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'setuptools') diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index 8222b8a2..5d97ebce 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -256,3 +256,16 @@ class TestBuildMetaBackend: build_backend = self.get_build_backend() with pytest.raises(ImportError): build_backend.build_sdist("temp") + + +@pytest.mark.xfail +class TestBuildMetaLegacyBackend(TestBuildMetaBackend): + backend_name = 'setuptools.build_meta_legacy' + + # build_meta_legacy-specific tests + def test_build_sdist_relative_path_import(self, tmpdir_cwd): + # This must fail in build_meta, but must pass in build_meta_legacy + build_files(self._relative_path_import_files) + + build_backend = self.get_build_backend() + build_backend.build_sdist("temp") -- cgit v1.2.3 From fd3b06dae1a33b3131cf96ddb9d7938430b9d8b5 Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Sun, 27 Jan 2019 12:31:28 -0500 Subject: Add build_meta_legacy backend This is part of the solution to GH #1642, it is a backwards-compatibility backend that can be used as a default PEP 517 backend for projects that use setuptools but haven't opted in to build_meta. --- setuptools/build_meta_legacy.py | 44 +++++++++++++++++++++++++++++++++++++ setuptools/tests/test_build_meta.py | 1 - 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 setuptools/build_meta_legacy.py (limited to 'setuptools') diff --git a/setuptools/build_meta_legacy.py b/setuptools/build_meta_legacy.py new file mode 100644 index 00000000..7eed41f1 --- /dev/null +++ b/setuptools/build_meta_legacy.py @@ -0,0 +1,44 @@ +"""Compatibility backend for setuptools + +This is a version of setuptools.build_meta that endeavors to maintain backwards +compatibility with pre-PEP 517 modes of invocation. It exists as a temporary +bridge between the old packaging mechanism and the new packaging mechanism, +and will eventually be removed. +""" + +import sys + +from setuptools.build_meta import _BuildMetaBackend +from setuptools.build_meta import SetupRequirementsError + + +__all__ = ['get_requires_for_build_sdist', + 'get_requires_for_build_wheel', + 'prepare_metadata_for_build_wheel', + 'build_wheel', + 'build_sdist', + 'SetupRequirementsError'] + + +class _BuildMetaLegacyBackend(_BuildMetaBackend): + def run_setup(self, setup_script='setup.py'): + # In order to maintain compatibility with scripts assuming that + # the setup.py script is in a directory on the PYTHONPATH, inject + # '' into sys.path. (pypa/setuptools#1642) + sys_path = list(sys.path) # Save the old path + if '' not in sys.path: + sys.path.insert(0, '') + + super(_BuildMetaLegacyBackend, + self).run_setup(setup_script=setup_script) + + sys.path = sys_path # Restore the old path + + +_BACKEND = _BuildMetaLegacyBackend() + +get_requires_for_build_wheel = _BACKEND.get_requires_for_build_wheel +get_requires_for_build_sdist = _BACKEND.get_requires_for_build_sdist +prepare_metadata_for_build_wheel = _BACKEND.prepare_metadata_for_build_wheel +build_wheel = _BACKEND.build_wheel +build_sdist = _BACKEND.build_sdist diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index 5d97ebce..b29d6f29 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -258,7 +258,6 @@ class TestBuildMetaBackend: build_backend.build_sdist("temp") -@pytest.mark.xfail class TestBuildMetaLegacyBackend(TestBuildMetaBackend): backend_name = 'setuptools.build_meta_legacy' -- cgit v1.2.3 From 49d17725a0ad02552babbdc79c737cfb27430f4f Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Mon, 28 Jan 2019 09:22:49 -0500 Subject: Set sys.path in try/finally block with comment Per Nick Coghlan's suggestion on PR #1652, a try/finally block ensures that the path is restored even in the event of an error. --- setuptools/build_meta_legacy.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) (limited to 'setuptools') diff --git a/setuptools/build_meta_legacy.py b/setuptools/build_meta_legacy.py index 7eed41f1..03492da9 100644 --- a/setuptools/build_meta_legacy.py +++ b/setuptools/build_meta_legacy.py @@ -6,6 +6,7 @@ bridge between the old packaging mechanism and the new packaging mechanism, and will eventually be removed. """ +import os import sys from setuptools.build_meta import _BuildMetaBackend @@ -25,14 +26,21 @@ class _BuildMetaLegacyBackend(_BuildMetaBackend): # In order to maintain compatibility with scripts assuming that # the setup.py script is in a directory on the PYTHONPATH, inject # '' into sys.path. (pypa/setuptools#1642) - sys_path = list(sys.path) # Save the old path - if '' not in sys.path: - sys.path.insert(0, '') - - super(_BuildMetaLegacyBackend, - self).run_setup(setup_script=setup_script) - - sys.path = sys_path # Restore the old path + sys_path = list(sys.path) # Save the original path + + try: + if '' not in sys.path: + sys.path.insert(0, '') + + super(_BuildMetaLegacyBackend, + self).run_setup(setup_script=setup_script) + finally: + # While PEP 517 frontends should be calling each hook in a fresh + # subprocess according to the standard (and thus it should not be + # strictly necessary to restore the old sys.path), we'll restore + # the original path so that the path manipulation does not persist + # within the hook after run_setup is called. + sys.path[:] = sys_path _BACKEND = _BuildMetaLegacyBackend() -- cgit v1.2.3 From db590baf81ebcb23605da54118905437412228ce Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Mon, 28 Jan 2019 09:25:03 -0500 Subject: Use absolute path in build_meta_legacy Using the absolute path to the directory of the setup script better mimics the semantics of a direct invocation of python setup.py. --- setuptools/build_meta_legacy.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'setuptools') diff --git a/setuptools/build_meta_legacy.py b/setuptools/build_meta_legacy.py index 03492da9..3d209711 100644 --- a/setuptools/build_meta_legacy.py +++ b/setuptools/build_meta_legacy.py @@ -28,10 +28,11 @@ class _BuildMetaLegacyBackend(_BuildMetaBackend): # '' into sys.path. (pypa/setuptools#1642) sys_path = list(sys.path) # Save the original path - try: - if '' not in sys.path: - sys.path.insert(0, '') + script_dir = os.path.dirname(os.path.abspath(setup_script)) + if script_dir not in sys.path: + sys.path.insert(0, script_dir) + try: super(_BuildMetaLegacyBackend, self).run_setup(setup_script=setup_script) finally: -- cgit v1.2.3 From 11fb3f38d23ff1e0d81e64ba3b68b3de2d2b990a Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Sun, 3 Feb 2019 12:17:46 -0500 Subject: Move build_meta_legacy to build_meta:legacy Rather than exposing a top-level module for the legacy backend, we will move the legacy backend into the `setuptools.build_meta` module and specify it using the module:object syntax. --- setuptools/build_meta.py | 35 ++++++++++++++++++++++++ setuptools/build_meta_legacy.py | 53 ------------------------------------- setuptools/tests/test_build_meta.py | 17 +++++++++--- 3 files changed, 49 insertions(+), 56 deletions(-) delete mode 100644 setuptools/build_meta_legacy.py (limited to 'setuptools') diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index 8e31a04d..e16f319e 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -40,6 +40,7 @@ __all__ = ['get_requires_for_build_sdist', 'prepare_metadata_for_build_wheel', 'build_wheel', 'build_sdist', + 'legacy', 'SetupRequirementsError'] class SetupRequirementsError(BaseException): @@ -187,6 +188,36 @@ class _BuildMetaBackend(object): return _file_with_extension(sdist_directory, '.tar.gz') +class _BuildMetaLegacyBackend(_BuildMetaBackend): + """Compatibility backend for setuptools + + This is a version of setuptools.build_meta that endeavors to maintain backwards + compatibility with pre-PEP 517 modes of invocation. It exists as a temporary + bridge between the old packaging mechanism and the new packaging mechanism, + and will eventually be removed. + """ + def run_setup(self, setup_script='setup.py'): + # In order to maintain compatibility with scripts assuming that + # the setup.py script is in a directory on the PYTHONPATH, inject + # '' into sys.path. (pypa/setuptools#1642) + sys_path = list(sys.path) # Save the original path + + script_dir = os.path.dirname(os.path.abspath(setup_script)) + if script_dir not in sys.path: + sys.path.insert(0, script_dir) + + try: + super(_BuildMetaLegacyBackend, + self).run_setup(setup_script=setup_script) + finally: + # While PEP 517 frontends should be calling each hook in a fresh + # subprocess according to the standard (and thus it should not be + # strictly necessary to restore the old sys.path), we'll restore + # the original path so that the path manipulation does not persist + # within the hook after run_setup is called. + sys.path[:] = sys_path + +# The primary backend _BACKEND = _BuildMetaBackend() get_requires_for_build_wheel = _BACKEND.get_requires_for_build_wheel @@ -194,3 +225,7 @@ get_requires_for_build_sdist = _BACKEND.get_requires_for_build_sdist prepare_metadata_for_build_wheel = _BACKEND.prepare_metadata_for_build_wheel build_wheel = _BACKEND.build_wheel build_sdist = _BACKEND.build_sdist + + +# The legacy backend +legacy = _BuildMetaLegacyBackend() diff --git a/setuptools/build_meta_legacy.py b/setuptools/build_meta_legacy.py deleted file mode 100644 index 3d209711..00000000 --- a/setuptools/build_meta_legacy.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Compatibility backend for setuptools - -This is a version of setuptools.build_meta that endeavors to maintain backwards -compatibility with pre-PEP 517 modes of invocation. It exists as a temporary -bridge between the old packaging mechanism and the new packaging mechanism, -and will eventually be removed. -""" - -import os -import sys - -from setuptools.build_meta import _BuildMetaBackend -from setuptools.build_meta import SetupRequirementsError - - -__all__ = ['get_requires_for_build_sdist', - 'get_requires_for_build_wheel', - 'prepare_metadata_for_build_wheel', - 'build_wheel', - 'build_sdist', - 'SetupRequirementsError'] - - -class _BuildMetaLegacyBackend(_BuildMetaBackend): - def run_setup(self, setup_script='setup.py'): - # In order to maintain compatibility with scripts assuming that - # the setup.py script is in a directory on the PYTHONPATH, inject - # '' into sys.path. (pypa/setuptools#1642) - sys_path = list(sys.path) # Save the original path - - script_dir = os.path.dirname(os.path.abspath(setup_script)) - if script_dir not in sys.path: - sys.path.insert(0, script_dir) - - try: - super(_BuildMetaLegacyBackend, - self).run_setup(setup_script=setup_script) - finally: - # While PEP 517 frontends should be calling each hook in a fresh - # subprocess according to the standard (and thus it should not be - # strictly necessary to restore the old sys.path), we'll restore - # the original path so that the path manipulation does not persist - # within the hook after run_setup is called. - sys.path[:] = sys_path - - -_BACKEND = _BuildMetaLegacyBackend() - -get_requires_for_build_wheel = _BACKEND.get_requires_for_build_wheel -get_requires_for_build_sdist = _BACKEND.get_requires_for_build_sdist -prepare_metadata_for_build_wheel = _BACKEND.prepare_metadata_for_build_wheel -build_wheel = _BACKEND.build_wheel -build_sdist = _BACKEND.build_sdist diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index b29d6f29..42e3098a 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -23,7 +23,6 @@ class BuildBackendBase: self.env = env self.backend_name = backend_name - class BuildBackend(BuildBackendBase): """PEP 517 Build Backend""" @@ -43,12 +42,24 @@ class BuildBackend(BuildBackendBase): class BuildBackendCaller(BuildBackendBase): + def __init__(self, *args, **kwargs): + super(BuildBackendCaller, self).__init__(*args, **kwargs) + + (self.backend_name, _, + self.backend_obj) = self.backend_name.partition(':') + 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) + + if self.backend_obj: + backend = getattr(mod, self.backend_obj) + else: + backend = mod + + return getattr(backend, name)(*args, **kw) defns = [ @@ -259,7 +270,7 @@ class TestBuildMetaBackend: class TestBuildMetaLegacyBackend(TestBuildMetaBackend): - backend_name = 'setuptools.build_meta_legacy' + backend_name = 'setuptools.build_meta:legacy' # build_meta_legacy-specific tests def test_build_sdist_relative_path_import(self, tmpdir_cwd): -- cgit v1.2.3 From e04a41e3129fa9945e15b16fd6d65cc212c1d946 Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Tue, 5 Feb 2019 08:42:36 -0500 Subject: Rename build_meta:legacy to build_meta:__legacy__ --- setuptools/build_meta.py | 4 ++-- setuptools/tests/test_build_meta.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'setuptools') diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index e16f319e..70b7ab23 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -40,7 +40,7 @@ __all__ = ['get_requires_for_build_sdist', 'prepare_metadata_for_build_wheel', 'build_wheel', 'build_sdist', - 'legacy', + '__legacy__', 'SetupRequirementsError'] class SetupRequirementsError(BaseException): @@ -228,4 +228,4 @@ build_sdist = _BACKEND.build_sdist # The legacy backend -legacy = _BuildMetaLegacyBackend() +__legacy__ = _BuildMetaLegacyBackend() diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index 42e3098a..6236b9f4 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -270,7 +270,7 @@ class TestBuildMetaBackend: class TestBuildMetaLegacyBackend(TestBuildMetaBackend): - backend_name = 'setuptools.build_meta:legacy' + backend_name = 'setuptools.build_meta:__legacy__' # build_meta_legacy-specific tests def test_build_sdist_relative_path_import(self, tmpdir_cwd): -- cgit v1.2.3 From 179115b198387a21202433ea61cc53f2efd383fc Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Thu, 31 Jan 2019 08:29:36 -0500 Subject: Add support for setup.cfg-only projects Many projects can get away with an empty `setup.py` and use *only* the declarative `setup.cfg`. With the new PEP 517 backend, we can supply a default empty `setup.py` if one is not provided. --- setuptools/build_meta.py | 16 +++++++++++++--- setuptools/tests/test_build_meta.py | 23 +++++++++++++++++++++-- 2 files changed, 34 insertions(+), 5 deletions(-) (limited to 'setuptools') diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index 70b7ab23..047cc07b 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -26,6 +26,7 @@ bug reports or API stability): Again, this is not a formal definition! Just a "taste" of the module. """ +import io import os import sys import tokenize @@ -95,6 +96,14 @@ def _file_with_extension(directory, extension): return file +def _open_setup_script(setup_script): + if not os.path.exists(setup_script): + # Supply a default setup.py + return io.StringIO(u"from setuptools import setup; setup()") + + return getattr(tokenize, 'open', open)(setup_script) + + class _BuildMetaBackend(object): def _fix_config(self, config_settings): @@ -120,9 +129,10 @@ class _BuildMetaBackend(object): # 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() + + with _open_setup_script(__file__) as f: + code = f.read().replace(r'\r\n', r'\n') + exec(compile(code, __file__, 'exec'), locals()) def get_requires_for_build_wheel(self, config_settings=None): diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index 6236b9f4..74969322 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -110,6 +110,21 @@ defns = [ print('hello') """), }, + { + 'setup.cfg': DALS(""" + [metadata] + name = foo + version='0.0.0' + + [options] + py_modules=hello + setup_requires=six + """), + 'hello.py': DALS(""" + def run(): + print('hello') + """) + }, ] @@ -183,9 +198,13 @@ class TestBuildMetaBackend: # if the setup.py changes subsequent call of the build meta # should still succeed, given the # sdist_directory the frontend specifies is empty - with open(os.path.abspath("setup.py"), 'rt') as file_handler: + setup_loc = os.path.abspath("setup.py") + if not os.path.exists(setup_loc): + setup_loc = os.path.abspath("setup.cfg") + + with open(setup_loc, 'rt') as file_handler: content = file_handler.read() - with open(os.path.abspath("setup.py"), 'wt') as file_handler: + with open(setup_loc, 'wt') as file_handler: file_handler.write( content.replace("version='0.0.0'", "version='0.0.1'")) -- cgit v1.2.3 From 4a6b8ba7ced6bb841000a59bdef7f9879fb6578d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 17 Feb 2019 15:42:23 -0500 Subject: Add test capturing expectation that provides_extras are ordered. --- setuptools/tests/test_dist.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'setuptools') diff --git a/setuptools/tests/test_dist.py b/setuptools/tests/test_dist.py index 390c3dfc..e349d068 100644 --- a/setuptools/tests/test_dist.py +++ b/setuptools/tests/test_dist.py @@ -263,3 +263,16 @@ def test_maintainer_author(name, attrs, tmpdir): else: line = '%s: %s' % (fkey, val) assert line in pkg_lines_set + + +def test_provides_extras_deterministic_order(): + attrs = dict(extras_require=dict( + a=['foo'], + b=['bar'], + )) + dist = Distribution(attrs) + assert dist.metadata.provides_extras == ['a', 'b'] + attrs['extras_require'] = dict( + reversed(list(attrs['extras_require'].items()))) + dist = Distribution(attrs) + assert dist.metadata.provides_extras == ['b', 'a'] -- cgit v1.2.3 From 3d289a7015b9ba4f8dd29594309e472fd880841e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 17 Feb 2019 17:01:57 -0500 Subject: Add 'ordered_set' as a vendored package --- setuptools/_vendor/ordered_set.py | 488 ++++++++++++++++++++++++++++++++++++++ setuptools/_vendor/vendored.txt | 1 + setuptools/extern/__init__.py | 2 +- 3 files changed, 490 insertions(+), 1 deletion(-) create mode 100644 setuptools/_vendor/ordered_set.py (limited to 'setuptools') diff --git a/setuptools/_vendor/ordered_set.py b/setuptools/_vendor/ordered_set.py new file mode 100644 index 00000000..d257470b --- /dev/null +++ b/setuptools/_vendor/ordered_set.py @@ -0,0 +1,488 @@ +""" +An OrderedSet is a custom MutableSet that remembers its order, so that every +entry has an index that can be looked up. + +Based on a recipe originally posted to ActiveState Recipes by Raymond Hettiger, +and released under the MIT license. +""" +import itertools as it +from collections import deque + +try: + # Python 3 + from collections.abc import MutableSet, Sequence +except ImportError: + # Python 2.7 + from collections import MutableSet, Sequence + +SLICE_ALL = slice(None) +__version__ = "3.1" + + +def is_iterable(obj): + """ + Are we being asked to look up a list of things, instead of a single thing? + We check for the `__iter__` attribute so that this can cover types that + don't have to be known by this module, such as NumPy arrays. + + Strings, however, should be considered as atomic values to look up, not + iterables. The same goes for tuples, since they are immutable and therefore + valid entries. + + We don't need to check for the Python 2 `unicode` type, because it doesn't + have an `__iter__` attribute anyway. + """ + return ( + hasattr(obj, "__iter__") + and not isinstance(obj, str) + and not isinstance(obj, tuple) + ) + + +class OrderedSet(MutableSet, Sequence): + """ + An OrderedSet is a custom MutableSet that remembers its order, so that + every entry has an index that can be looked up. + + Example: + >>> OrderedSet([1, 1, 2, 3, 2]) + OrderedSet([1, 2, 3]) + """ + + def __init__(self, iterable=None): + self.items = [] + self.map = {} + if iterable is not None: + self |= iterable + + def __len__(self): + """ + Returns the number of unique elements in the ordered set + + Example: + >>> len(OrderedSet([])) + 0 + >>> len(OrderedSet([1, 2])) + 2 + """ + return len(self.items) + + def __getitem__(self, index): + """ + Get the item at a given index. + + If `index` is a slice, you will get back that slice of items, as a + new OrderedSet. + + If `index` is a list or a similar iterable, you'll get a list of + items corresponding to those indices. This is similar to NumPy's + "fancy indexing". The result is not an OrderedSet because you may ask + for duplicate indices, and the number of elements returned should be + the number of elements asked for. + + Example: + >>> oset = OrderedSet([1, 2, 3]) + >>> oset[1] + 2 + """ + if isinstance(index, slice) and index == SLICE_ALL: + return self.copy() + elif hasattr(index, "__index__") or isinstance(index, slice): + result = self.items[index] + if isinstance(result, list): + return self.__class__(result) + else: + return result + elif is_iterable(index): + return [self.items[i] for i in index] + else: + raise TypeError("Don't know how to index an OrderedSet by %r" % index) + + def copy(self): + """ + Return a shallow copy of this object. + + Example: + >>> this = OrderedSet([1, 2, 3]) + >>> other = this.copy() + >>> this == other + True + >>> this is other + False + """ + return self.__class__(self) + + def __getstate__(self): + if len(self) == 0: + # The state can't be an empty list. + # We need to return a truthy value, or else __setstate__ won't be run. + # + # This could have been done more gracefully by always putting the state + # in a tuple, but this way is backwards- and forwards- compatible with + # previous versions of OrderedSet. + return (None,) + else: + return list(self) + + def __setstate__(self, state): + if state == (None,): + self.__init__([]) + else: + self.__init__(state) + + def __contains__(self, key): + """ + Test if the item is in this ordered set + + Example: + >>> 1 in OrderedSet([1, 3, 2]) + True + >>> 5 in OrderedSet([1, 3, 2]) + False + """ + return key in self.map + + def add(self, key): + """ + Add `key` as an item to this OrderedSet, then return its index. + + If `key` is already in the OrderedSet, return the index it already + had. + + Example: + >>> oset = OrderedSet() + >>> oset.append(3) + 0 + >>> print(oset) + OrderedSet([3]) + """ + if key not in self.map: + self.map[key] = len(self.items) + self.items.append(key) + return self.map[key] + + append = add + + def update(self, sequence): + """ + Update the set with the given iterable sequence, then return the index + of the last element inserted. + + Example: + >>> oset = OrderedSet([1, 2, 3]) + >>> oset.update([3, 1, 5, 1, 4]) + 4 + >>> print(oset) + OrderedSet([1, 2, 3, 5, 4]) + """ + item_index = None + try: + for item in sequence: + item_index = self.add(item) + except TypeError: + raise ValueError( + "Argument needs to be an iterable, got %s" % type(sequence) + ) + return item_index + + def index(self, key): + """ + Get the index of a given entry, raising an IndexError if it's not + present. + + `key` can be an iterable of entries that is not a string, in which case + this returns a list of indices. + + Example: + >>> oset = OrderedSet([1, 2, 3]) + >>> oset.index(2) + 1 + """ + if is_iterable(key): + return [self.index(subkey) for subkey in key] + return self.map[key] + + # Provide some compatibility with pd.Index + get_loc = index + get_indexer = index + + def pop(self): + """ + Remove and return the last element from the set. + + Raises KeyError if the set is empty. + + Example: + >>> oset = OrderedSet([1, 2, 3]) + >>> oset.pop() + 3 + """ + if not self.items: + raise KeyError("Set is empty") + + elem = self.items[-1] + del self.items[-1] + del self.map[elem] + return elem + + def discard(self, key): + """ + Remove an element. Do not raise an exception if absent. + + The MutableSet mixin uses this to implement the .remove() method, which + *does* raise an error when asked to remove a non-existent item. + + Example: + >>> oset = OrderedSet([1, 2, 3]) + >>> oset.discard(2) + >>> print(oset) + OrderedSet([1, 3]) + >>> oset.discard(2) + >>> print(oset) + OrderedSet([1, 3]) + """ + if key in self: + i = self.map[key] + del self.items[i] + del self.map[key] + for k, v in self.map.items(): + if v >= i: + self.map[k] = v - 1 + + def clear(self): + """ + Remove all items from this OrderedSet. + """ + del self.items[:] + self.map.clear() + + def __iter__(self): + """ + Example: + >>> list(iter(OrderedSet([1, 2, 3]))) + [1, 2, 3] + """ + return iter(self.items) + + def __reversed__(self): + """ + Example: + >>> list(reversed(OrderedSet([1, 2, 3]))) + [3, 2, 1] + """ + return reversed(self.items) + + def __repr__(self): + if not self: + return "%s()" % (self.__class__.__name__,) + return "%s(%r)" % (self.__class__.__name__, list(self)) + + def __eq__(self, other): + """ + Returns true if the containers have the same items. If `other` is a + Sequence, then order is checked, otherwise it is ignored. + + Example: + >>> oset = OrderedSet([1, 3, 2]) + >>> oset == [1, 3, 2] + True + >>> oset == [1, 2, 3] + False + >>> oset == [2, 3] + False + >>> oset == OrderedSet([3, 2, 1]) + False + """ + # In Python 2 deque is not a Sequence, so treat it as one for + # consistent behavior with Python 3. + if isinstance(other, (Sequence, deque)): + # Check that this OrderedSet contains the same elements, in the + # same order, as the other object. + return list(self) == list(other) + try: + other_as_set = set(other) + except TypeError: + # If `other` can't be converted into a set, it's not equal. + return False + else: + return set(self) == other_as_set + + def union(self, *sets): + """ + Combines all unique items. + Each items order is defined by its first appearance. + + Example: + >>> oset = OrderedSet.union(OrderedSet([3, 1, 4, 1, 5]), [1, 3], [2, 0]) + >>> print(oset) + OrderedSet([3, 1, 4, 5, 2, 0]) + >>> oset.union([8, 9]) + OrderedSet([3, 1, 4, 5, 2, 0, 8, 9]) + >>> oset | {10} + OrderedSet([3, 1, 4, 5, 2, 0, 10]) + """ + cls = self.__class__ if isinstance(self, OrderedSet) else OrderedSet + containers = map(list, it.chain([self], sets)) + items = it.chain.from_iterable(containers) + return cls(items) + + def __and__(self, other): + # the parent implementation of this is backwards + return self.intersection(other) + + def intersection(self, *sets): + """ + Returns elements in common between all sets. Order is defined only + by the first set. + + Example: + >>> oset = OrderedSet.intersection(OrderedSet([0, 1, 2, 3]), [1, 2, 3]) + >>> print(oset) + OrderedSet([1, 2, 3]) + >>> oset.intersection([2, 4, 5], [1, 2, 3, 4]) + OrderedSet([2]) + >>> oset.intersection() + OrderedSet([1, 2, 3]) + """ + cls = self.__class__ if isinstance(self, OrderedSet) else OrderedSet + if sets: + common = set.intersection(*map(set, sets)) + items = (item for item in self if item in common) + else: + items = self + return cls(items) + + def difference(self, *sets): + """ + Returns all elements that are in this set but not the others. + + Example: + >>> OrderedSet([1, 2, 3]).difference(OrderedSet([2])) + OrderedSet([1, 3]) + >>> OrderedSet([1, 2, 3]).difference(OrderedSet([2]), OrderedSet([3])) + OrderedSet([1]) + >>> OrderedSet([1, 2, 3]) - OrderedSet([2]) + OrderedSet([1, 3]) + >>> OrderedSet([1, 2, 3]).difference() + OrderedSet([1, 2, 3]) + """ + cls = self.__class__ + if sets: + other = set.union(*map(set, sets)) + items = (item for item in self if item not in other) + else: + items = self + return cls(items) + + def issubset(self, other): + """ + Report whether another set contains this set. + + Example: + >>> OrderedSet([1, 2, 3]).issubset({1, 2}) + False + >>> OrderedSet([1, 2, 3]).issubset({1, 2, 3, 4}) + True + >>> OrderedSet([1, 2, 3]).issubset({1, 4, 3, 5}) + False + """ + if len(self) > len(other): # Fast check for obvious cases + return False + return all(item in other for item in self) + + def issuperset(self, other): + """ + Report whether this set contains another set. + + Example: + >>> OrderedSet([1, 2]).issuperset([1, 2, 3]) + False + >>> OrderedSet([1, 2, 3, 4]).issuperset({1, 2, 3}) + True + >>> OrderedSet([1, 4, 3, 5]).issuperset({1, 2, 3}) + False + """ + if len(self) < len(other): # Fast check for obvious cases + return False + return all(item in self for item in other) + + def symmetric_difference(self, other): + """ + Return the symmetric difference of two OrderedSets as a new set. + That is, the new set will contain all elements that are in exactly + one of the sets. + + Their order will be preserved, with elements from `self` preceding + elements from `other`. + + Example: + >>> this = OrderedSet([1, 4, 3, 5, 7]) + >>> other = OrderedSet([9, 7, 1, 3, 2]) + >>> this.symmetric_difference(other) + OrderedSet([4, 5, 9, 2]) + """ + cls = self.__class__ if isinstance(self, OrderedSet) else OrderedSet + diff1 = cls(self).difference(other) + diff2 = cls(other).difference(self) + return diff1.union(diff2) + + def _update_items(self, items): + """ + Replace the 'items' list of this OrderedSet with a new one, updating + self.map accordingly. + """ + self.items = items + self.map = {item: idx for (idx, item) in enumerate(items)} + + def difference_update(self, *sets): + """ + Update this OrderedSet to remove items from one or more other sets. + + Example: + >>> this = OrderedSet([1, 2, 3]) + >>> this.difference_update(OrderedSet([2, 4])) + >>> print(this) + OrderedSet([1, 3]) + + >>> this = OrderedSet([1, 2, 3, 4, 5]) + >>> this.difference_update(OrderedSet([2, 4]), OrderedSet([1, 4, 6])) + >>> print(this) + OrderedSet([3, 5]) + """ + items_to_remove = set() + for other in sets: + items_to_remove |= set(other) + self._update_items([item for item in self.items if item not in items_to_remove]) + + def intersection_update(self, other): + """ + Update this OrderedSet to keep only items in another set, preserving + their order in this set. + + Example: + >>> this = OrderedSet([1, 4, 3, 5, 7]) + >>> other = OrderedSet([9, 7, 1, 3, 2]) + >>> this.intersection_update(other) + >>> print(this) + OrderedSet([1, 3, 7]) + """ + other = set(other) + self._update_items([item for item in self.items if item in other]) + + def symmetric_difference_update(self, other): + """ + Update this OrderedSet to remove items from another set, then + add items from the other set that were not present in this set. + + Example: + >>> this = OrderedSet([1, 4, 3, 5, 7]) + >>> other = OrderedSet([9, 7, 1, 3, 2]) + >>> this.symmetric_difference_update(other) + >>> print(this) + OrderedSet([4, 5, 9, 2]) + """ + items_to_add = [item for item in other if item not in self] + items_to_remove = set(other) + self._update_items( + [item for item in self.items if item not in items_to_remove] + items_to_add + ) diff --git a/setuptools/_vendor/vendored.txt b/setuptools/_vendor/vendored.txt index 7d77863d..379aae56 100644 --- a/setuptools/_vendor/vendored.txt +++ b/setuptools/_vendor/vendored.txt @@ -1,3 +1,4 @@ packaging==16.8 pyparsing==2.2.1 six==1.10.0 +ordered-set diff --git a/setuptools/extern/__init__.py b/setuptools/extern/__init__.py index cb2fa329..e8c616f9 100644 --- a/setuptools/extern/__init__.py +++ b/setuptools/extern/__init__.py @@ -69,5 +69,5 @@ class VendorImporter: sys.meta_path.append(self) -names = 'six', 'packaging', 'pyparsing', +names = 'six', 'packaging', 'pyparsing', 'ordered_set', VendorImporter(__name__, names, 'setuptools._vendor').install() -- cgit v1.2.3 From 636070d9922f1443ae98b0fdd45b6c74c3c9af11 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 17 Feb 2019 17:09:26 -0500 Subject: Use an ordered set when constructing 'extras provided'. Ref #1305. --- setuptools/dist.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'setuptools') diff --git a/setuptools/dist.py b/setuptools/dist.py index ddb1787a..ce37761e 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -28,6 +28,7 @@ from distutils.version import StrictVersion from setuptools.extern import six from setuptools.extern import packaging +from setuptools.extern import ordered_set from setuptools.extern.six.moves import map, filter, filterfalse from . import SetuptoolsDeprecationWarning @@ -408,7 +409,7 @@ class Distribution(_Distribution): _DISTUTILS_UNSUPPORTED_METADATA = { 'long_description_content_type': None, 'project_urls': dict, - 'provides_extras': set, + 'provides_extras': ordered_set.OrderedSet, } _patched_dist = None -- cgit v1.2.3 From 7f7780e5b572d6fcacd63bf99389dd9f48c5345c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 17 Feb 2019 17:37:01 -0500 Subject: In tests, force deterministic ordering on extras_require so tests pass. --- setuptools/tests/test_dist.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) (limited to 'setuptools') diff --git a/setuptools/tests/test_dist.py b/setuptools/tests/test_dist.py index e349d068..0c0b9d66 100644 --- a/setuptools/tests/test_dist.py +++ b/setuptools/tests/test_dist.py @@ -3,6 +3,8 @@ from __future__ import unicode_literals import io +import collections + from setuptools.dist import DistDeprecationWarning, _get_unpatched from setuptools import Distribution from setuptools.extern.six.moves.urllib.request import pathname2url @@ -266,13 +268,13 @@ def test_maintainer_author(name, attrs, tmpdir): def test_provides_extras_deterministic_order(): - attrs = dict(extras_require=dict( - a=['foo'], - b=['bar'], - )) + extras = collections.OrderedDict() + extras['a'] = ['foo'] + extras['b'] = ['bar'] + attrs = dict(extras_require=extras) dist = Distribution(attrs) assert dist.metadata.provides_extras == ['a', 'b'] - attrs['extras_require'] = dict( + attrs['extras_require'] = collections.OrderedDict( reversed(list(attrs['extras_require'].items()))) dist = Distribution(attrs) assert dist.metadata.provides_extras == ['b', 'a'] -- cgit v1.2.3 From 82db4c621974659513effae337d47a05e31fe7a5 Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Mon, 28 Jan 2019 23:07:59 +0100 Subject: tests: improve `test_pip_upgrade_from_source` Parametrize the test to check different versions of pip (including master) are correctly supported. --- setuptools/tests/test_virtualenv.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) (limited to 'setuptools') diff --git a/setuptools/tests/test_virtualenv.py b/setuptools/tests/test_virtualenv.py index 3d5c84b0..bd89fd64 100644 --- a/setuptools/tests/test_virtualenv.py +++ b/setuptools/tests/test_virtualenv.py @@ -52,10 +52,24 @@ def test_clean_env_install(bare_virtualenv): )).format(source=SOURCE_DIR)) -def test_pip_upgrade_from_source(virtualenv): +@pytest.mark.parametrize('pip_version', ( + 'pip==9.0.3', + 'pip==10.0.1', + 'pip==18.1', + 'pip==19.0.1', + 'https://github.com/pypa/pip/archive/master.zip', +)) +def test_pip_upgrade_from_source(virtualenv, pip_version): """ Check pip can upgrade setuptools from source. """ + # Install pip/wheel, and remove setuptools (as it + # should not be needed for bootstraping from source) + virtualenv.run(' && '.join(( + 'pip uninstall -y setuptools', + 'pip install -U wheel', + 'python -m pip install {pip_version}', + )).format(pip_version=pip_version)) dist_dir = virtualenv.workspace # Generate source distribution / wheel. virtualenv.run(' && '.join(( -- cgit v1.2.3 From b224605a8c16b2a713120bf0d484fa12ce781f02 Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Thu, 7 Feb 2019 09:28:29 -0500 Subject: Automatically skip tests that require network --- setuptools/tests/test_virtualenv.py | 51 +++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 10 deletions(-) (limited to 'setuptools') diff --git a/setuptools/tests/test_virtualenv.py b/setuptools/tests/test_virtualenv.py index bd89fd64..d7b98c77 100644 --- a/setuptools/tests/test_virtualenv.py +++ b/setuptools/tests/test_virtualenv.py @@ -52,24 +52,55 @@ def test_clean_env_install(bare_virtualenv): )).format(source=SOURCE_DIR)) -@pytest.mark.parametrize('pip_version', ( - 'pip==9.0.3', - 'pip==10.0.1', - 'pip==18.1', - 'pip==19.0.1', - 'https://github.com/pypa/pip/archive/master.zip', -)) -def test_pip_upgrade_from_source(virtualenv, pip_version): +def _get_pip_versions(): + # This fixture will attempt to detect if tests are being run without + # network connectivity and if so skip some tests + + network = True + if not os.environ.get('NETWORK_REQUIRED', False): # pragma: nocover + try: + from urllib.request import urlopen + from urllib.error import URLError + except ImportError: + from urllib2 import urlopen, URLError # Python 2.7 compat + + try: + urlopen('https://pypi.org', timeout=1) + except URLError: + # No network, disable most of these tests + network = False + + network_versions = [ + 'pip==9.0.3', + 'pip==10.0.1', + 'pip==18.1', + 'pip==19.0.1', + 'https://github.com/pypa/pip/archive/master.zip', + ] + + versions = [None] + [ + pytest.param(v, **({} if network else {'marks': pytest.mark.skip})) + for v in network_versions + ] + + return versions + + +@pytest.mark.parametrize('pip_version', _get_pip_versions()) +def test_pip_upgrade_from_source(pip_version, virtualenv): """ Check pip can upgrade setuptools from source. """ # Install pip/wheel, and remove setuptools (as it # should not be needed for bootstraping from source) + if pip_version is None: + upgrade_pip = () + else: + upgrade_pip = ('python -m pip install -U {pip_version} --retries=1',) virtualenv.run(' && '.join(( 'pip uninstall -y setuptools', 'pip install -U wheel', - 'python -m pip install {pip_version}', - )).format(pip_version=pip_version)) + ) + upgrade_pip).format(pip_version=pip_version)) dist_dir = virtualenv.workspace # Generate source distribution / wheel. virtualenv.run(' && '.join(( -- cgit v1.2.3 From 5b2175ebd9f4a669097e8309a53e3b843dcbb218 Mon Sep 17 00:00:00 2001 From: robnagler Date: Tue, 26 Feb 2019 17:10:49 +0000 Subject: uniquify paths in PYTHONPATH When running in a complex environment with lots of installed packages, PYTHONPATH gets way too long. Instead, just make sure that paths_on_pythonpath doesn't contain duplicates --- setuptools/command/test.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) (limited to 'setuptools') diff --git a/setuptools/command/test.py b/setuptools/command/test.py index dde0118c..997fd8b0 100644 --- a/setuptools/command/test.py +++ b/setuptools/command/test.py @@ -186,11 +186,12 @@ class test(Command): orig_pythonpath = os.environ.get('PYTHONPATH', nothing) current_pythonpath = os.environ.get('PYTHONPATH', '') try: - prefix = os.pathsep.join(paths) - to_join = filter(None, [prefix, current_pythonpath]) - new_path = os.pathsep.join(to_join) - if new_path: - os.environ['PYTHONPATH'] = new_path + to_join = [] + for x in list(paths) + current_pythonpath.split(os.pathsep): + if x not in to_join: + to_join.append(x) + if to_join: + os.environ['PYTHONPATH'] = os.pathsep.join(to_join) yield finally: if orig_pythonpath is nothing: -- cgit v1.2.3 From 16e452a42a3dbbb0ab3d3146ffa3b743cdca2539 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Chapoton?= Date: Wed, 6 Mar 2019 15:22:28 +0100 Subject: Remove duplicate import io (#1713) Found by lgtm --- setuptools/dist.py | 1 - 1 file changed, 1 deletion(-) (limited to 'setuptools') diff --git a/setuptools/dist.py b/setuptools/dist.py index ddb1787a..6233d5dc 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -1103,7 +1103,6 @@ class Distribution(_Distribution): return _Distribution.handle_display_options(self, option_order) # Stdout may be StringIO (e.g. in tests) - import io if not isinstance(sys.stdout, io.TextIOWrapper): return _Distribution.handle_display_options(self, option_order) -- cgit v1.2.3 From 1aa781cd8ee638e7b403ebbd1caa82f8c7d4e6cd Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Sat, 16 Mar 2019 12:28:17 -0400 Subject: Add failing test for setup_requires Per GH #1682, with setuptools.build_meta we are not properly handling the situation where setup_requires is actually a newline-delimited string rather than a list, which is supported by setup.py interface. This adds several failing (and some passing) tests for how setup_requires is handled by setuptools.build_meta. --- setuptools/tests/test_build_meta.py | 46 +++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) (limited to 'setuptools') diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index 74969322..d9df8b2c 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -287,6 +287,52 @@ class TestBuildMetaBackend: with pytest.raises(ImportError): build_backend.build_sdist("temp") + @pytest.mark.parametrize('setup_literal, requirements', [ + pytest.param("'foo'", ['foo'], marks=pytest.mark.xfail), + ("['foo']", ['foo']), + pytest.param(r"'foo\n'", ['foo'], marks=pytest.mark.xfail), + pytest.param(r"'foo\n\n'", ['foo'], marks=pytest.mark.xfail), + ("['foo', 'bar']", ['foo', 'bar']), + pytest.param(r"'# Has a comment line\nfoo'", + ['foo'], marks=pytest.mark.xfail), + pytest.param(r"'foo # Has an inline comment'", + ['foo'], marks=pytest.mark.xfail), + pytest.param(r"'foo \\\n >=3.0'", + ['foo>=3.0'], marks=pytest.mark.xfail), + pytest.param(r"'foo\nbar'", ['foo', 'bar'], marks=pytest.mark.xfail), + pytest.param(r"'foo\nbar\n'", ['foo', 'bar'], marks=pytest.mark.xfail), + pytest.param(r"['foo\n', 'bar\n']", + ['foo', 'bar'], marks=pytest.mark.xfail), + ]) + def test_setup_requires(self, setup_literal, requirements, tmpdir_cwd): + + files = { + 'setup.py': DALS(""" + from setuptools import setup + + setup( + name="qux", + version="0.0.0", + py_modules=["hello.py"], + setup_requires={setup_literal}, + ) + """).format(setup_literal=setup_literal), + 'hello.py': DALS(""" + def run(): + print('hello') + """), + } + + build_files(files) + + build_backend = self.get_build_backend() + + # Ensure that the build requirements are properly parsed + expected = sorted(['wheel'] + requirements) + actual = build_backend.get_requires_for_build_wheel() + + assert expected == sorted(actual) + class TestBuildMetaLegacyBackend(TestBuildMetaBackend): backend_name = 'setuptools.build_meta:__legacy__' -- cgit v1.2.3 From 318f739d14a810042e6803fa3eb4c4e140f0ef88 Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Sat, 16 Mar 2019 12:53:05 -0400 Subject: Add requirement parsing in setuptools.build_meta This fixes GH #1682 by porting the pkg_resources requirement parsing logic into setuptools.build_meta, so that all valid requirement specifiers passed to setup_requires will be added to the get_requires_for_build_* function outputs. Fixes GH #1682 --- setuptools/build_meta.py | 45 ++++++++++++++++++++++++++++++++++++- setuptools/tests/test_build_meta.py | 22 ++++++++---------- 2 files changed, 53 insertions(+), 14 deletions(-) (limited to 'setuptools') diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index 047cc07b..fb37c02a 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -36,6 +36,8 @@ import contextlib import setuptools import distutils +from setuptools._vendor import six + __all__ = ['get_requires_for_build_sdist', 'get_requires_for_build_wheel', 'prepare_metadata_for_build_wheel', @@ -51,7 +53,9 @@ class SetupRequirementsError(BaseException): class Distribution(setuptools.dist.Distribution): def fetch_build_eggs(self, specifiers): - raise SetupRequirementsError(specifiers) + specifier_list = self._parse_requirements(specifiers) + + raise SetupRequirementsError(specifier_list) @classmethod @contextlib.contextmanager @@ -68,6 +72,45 @@ class Distribution(setuptools.dist.Distribution): finally: distutils.core.Distribution = orig + def _yield_lines(self, strs): + """Yield non-empty/non-comment lines of a string or sequence""" + if isinstance(strs, six.string_types): + for s in strs.splitlines(): + s = s.strip() + # skip blank lines/comments + if s and not s.startswith('#'): + yield s + else: + for ss in strs: + for s in self._yield_lines(ss): + yield s + + def _parse_requirements(self, strs): + """Parse requirement specifiers into a list of requirement strings + + This is forked from pkg_resources.parse_requirements. + + `strs` must be a string, or a (possibly-nested) iterable thereof. + """ + # create a steppable iterator, so we can handle \-continuations + lines = iter(self._yield_lines(strs)) + + requirements = [] + + for line in lines: + # Drop comments -- a hash without a space may be in a URL. + if ' #' in line: + line = line[:line.find(' #')] + # If there is a line continuation, drop it, and append the next line. + if line.endswith('\\'): + line = line[:-2].strip() + try: + line += next(lines) + except StopIteration: + return + requirements.append(line) + + return requirements def _to_str(s): """ diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index d9df8b2c..a14a3c7a 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -288,21 +288,17 @@ class TestBuildMetaBackend: build_backend.build_sdist("temp") @pytest.mark.parametrize('setup_literal, requirements', [ - pytest.param("'foo'", ['foo'], marks=pytest.mark.xfail), + ("'foo'", ['foo']), ("['foo']", ['foo']), - pytest.param(r"'foo\n'", ['foo'], marks=pytest.mark.xfail), - pytest.param(r"'foo\n\n'", ['foo'], marks=pytest.mark.xfail), + (r"'foo\n'", ['foo']), + (r"'foo\n\n'", ['foo']), ("['foo', 'bar']", ['foo', 'bar']), - pytest.param(r"'# Has a comment line\nfoo'", - ['foo'], marks=pytest.mark.xfail), - pytest.param(r"'foo # Has an inline comment'", - ['foo'], marks=pytest.mark.xfail), - pytest.param(r"'foo \\\n >=3.0'", - ['foo>=3.0'], marks=pytest.mark.xfail), - pytest.param(r"'foo\nbar'", ['foo', 'bar'], marks=pytest.mark.xfail), - pytest.param(r"'foo\nbar\n'", ['foo', 'bar'], marks=pytest.mark.xfail), - pytest.param(r"['foo\n', 'bar\n']", - ['foo', 'bar'], marks=pytest.mark.xfail), + (r"'# Has a comment line\nfoo'", ['foo']), + (r"'foo # Has an inline comment'", ['foo']), + (r"'foo \\\n >=3.0'", ['foo>=3.0']), + (r"'foo\nbar'", ['foo', 'bar']), + (r"'foo\nbar\n'", ['foo', 'bar']), + (r"['foo\n', 'bar\n']", ['foo', 'bar']), ]) def test_setup_requires(self, setup_literal, requirements, tmpdir_cwd): -- cgit v1.2.3 From b54d4c699fc4e1692dadd19bdd7cbcde1c844976 Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Sat, 16 Mar 2019 12:57:57 -0400 Subject: Extend requirement parsing tests to sdists --- setuptools/tests/test_build_meta.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) (limited to 'setuptools') diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index a14a3c7a..0bdea2d6 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -300,7 +300,9 @@ class TestBuildMetaBackend: (r"'foo\nbar\n'", ['foo', 'bar']), (r"['foo\n', 'bar\n']", ['foo', 'bar']), ]) - def test_setup_requires(self, setup_literal, requirements, tmpdir_cwd): + @pytest.mark.parametrize('use_wheel', [True, False]) + def test_setup_requires(self, setup_literal, requirements, use_wheel, + tmpdir_cwd): files = { 'setup.py': DALS(""" @@ -323,9 +325,16 @@ class TestBuildMetaBackend: build_backend = self.get_build_backend() + if use_wheel: + base_requirements = ['wheel'] + get_requires = build_backend.get_requires_for_build_wheel + else: + base_requirements = [] + get_requires = build_backend.get_requires_for_build_sdist + # Ensure that the build requirements are properly parsed - expected = sorted(['wheel'] + requirements) - actual = build_backend.get_requires_for_build_wheel() + expected = sorted(base_requirements + requirements) + actual = get_requires() assert expected == sorted(actual) -- cgit v1.2.3 From c27c705f6a326e4820f1a34d6ce1db101dad3a30 Mon Sep 17 00:00:00 2001 From: Dustin Ingram Date: Sat, 16 Mar 2019 10:06:53 -0700 Subject: Fix typo in docstring (#1718) --- setuptools/dist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'setuptools') diff --git a/setuptools/dist.py b/setuptools/dist.py index 6233d5dc..e6d08b92 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -885,7 +885,7 @@ class Distribution(_Distribution): def include(self, **attrs): """Add items to distribution that are named in keyword arguments - For example, 'dist.exclude(py_modules=["x"])' would add 'x' to + For example, 'dist.include(py_modules=["x"])' would add 'x' to the distribution's 'py_modules' attribute, if it was not already there. -- cgit v1.2.3 From 5efdf816fddcd8fbc9c3d1e6867a25848b1f9a06 Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Sat, 16 Mar 2019 13:24:36 -0400 Subject: Use pkg_resources.parse_requirements in build_meta Since pkg_resources is imported elsewhere anyway, we don't get much value out of porting the requirement parser locally. --- setuptools/build_meta.py | 43 ++----------------------------------------- 1 file changed, 2 insertions(+), 41 deletions(-) (limited to 'setuptools') diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index fb37c02a..47cbcbf6 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -36,7 +36,7 @@ import contextlib import setuptools import distutils -from setuptools._vendor import six +from pkg_resources import parse_requirements __all__ = ['get_requires_for_build_sdist', 'get_requires_for_build_wheel', @@ -53,7 +53,7 @@ class SetupRequirementsError(BaseException): class Distribution(setuptools.dist.Distribution): def fetch_build_eggs(self, specifiers): - specifier_list = self._parse_requirements(specifiers) + specifier_list = list(map(str, parse_requirements(specifiers))) raise SetupRequirementsError(specifier_list) @@ -72,45 +72,6 @@ class Distribution(setuptools.dist.Distribution): finally: distutils.core.Distribution = orig - def _yield_lines(self, strs): - """Yield non-empty/non-comment lines of a string or sequence""" - if isinstance(strs, six.string_types): - for s in strs.splitlines(): - s = s.strip() - # skip blank lines/comments - if s and not s.startswith('#'): - yield s - else: - for ss in strs: - for s in self._yield_lines(ss): - yield s - - def _parse_requirements(self, strs): - """Parse requirement specifiers into a list of requirement strings - - This is forked from pkg_resources.parse_requirements. - - `strs` must be a string, or a (possibly-nested) iterable thereof. - """ - # create a steppable iterator, so we can handle \-continuations - lines = iter(self._yield_lines(strs)) - - requirements = [] - - for line in lines: - # Drop comments -- a hash without a space may be in a URL. - if ' #' in line: - line = line[:line.find(' #')] - # If there is a line continuation, drop it, and append the next line. - if line.endswith('\\'): - line = line[:-2].strip() - try: - line += next(lines) - except StopIteration: - return - requirements.append(line) - - return requirements def _to_str(s): """ -- cgit v1.2.3 From 393809a02ed4d0f07faec5c1f23384233e6cd68e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 5 Apr 2019 09:27:23 -0400 Subject: Feed the hobgoblins (delint). --- setuptools/dist.py | 5 ++--- setuptools/tests/test_config.py | 11 +++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) (limited to 'setuptools') diff --git a/setuptools/dist.py b/setuptools/dist.py index e6d08b92..ae380290 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -11,7 +11,6 @@ import distutils.log import distutils.core import distutils.cmd import distutils.dist -from distutils.errors import DistutilsOptionError from distutils.util import strtobool from distutils.debug import DEBUG from distutils.fancy_getopt import translate_longopt @@ -135,7 +134,6 @@ def write_pkg_file(self, file): def write_field(key, value): file.write("%s: %s\n" % (key, value)) - write_field('Metadata-Version', str(version)) write_field('Name', self.get_name()) write_field('Version', self.get_version()) @@ -1281,4 +1279,5 @@ class Feature: class DistDeprecationWarning(SetuptoolsDeprecationWarning): - """Class for warning about deprecations in dist in setuptools. Not ignored by default, unlike DeprecationWarning.""" + """Class for warning about deprecations in dist in + setuptools. Not ignored by default, unlike DeprecationWarning.""" diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index 6b177709..4daf1df1 100644 --- a/setuptools/tests/test_config.py +++ b/setuptools/tests/test_config.py @@ -8,7 +8,7 @@ from distutils.errors import DistutilsOptionError, DistutilsFileError from mock import patch from setuptools.dist import Distribution, _Distribution from setuptools.config import ConfigHandler, read_configuration -from setuptools.extern.six.moves.configparser import InterpolationMissingOptionError +from setuptools.extern.six.moves import configparser from setuptools.tests import is_ascii from . import py2_only, py3_only from .textwrap import DALS @@ -29,7 +29,9 @@ def make_package_dir(name, base_dir, ns=False): return dir_package, init_file -def fake_env(tmpdir, setup_cfg, setup_py=None, encoding='ascii', package_path='fake_package'): +def fake_env( + tmpdir, setup_cfg, setup_py=None, + encoding='ascii', package_path='fake_package'): if setup_py is None: setup_py = ( @@ -440,11 +442,12 @@ class TestMetadata: '[metadata]\n' 'description = %(message)s\n' ) - with pytest.raises(InterpolationMissingOptionError): + with pytest.raises(configparser.InterpolationMissingOptionError): with get_dist(tmpdir): pass - skip_if_not_ascii = pytest.mark.skipif(not is_ascii, reason='Test not supported with this locale') + skip_if_not_ascii = pytest.mark.skipif( + not is_ascii, reason='Test not supported with this locale') @skip_if_not_ascii def test_non_ascii_1(self, tmpdir): -- cgit v1.2.3 From 85fa4a6bc506be12fdbd0f4cff139e7c4e3bc6a8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 5 Apr 2019 10:04:50 -0400 Subject: When reading config files, require them to be encoded with UTF-8. Fixes #1702. --- setuptools/dist.py | 9 ++------- setuptools/tests/test_config.py | 31 +++++++++---------------------- 2 files changed, 11 insertions(+), 29 deletions(-) (limited to 'setuptools') diff --git a/setuptools/dist.py b/setuptools/dist.py index ae380290..9a165de0 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -35,7 +35,6 @@ from setuptools.depends import Require from setuptools import windows_support from setuptools.monkey import get_unpatched from setuptools.config import parse_configuration -from .unicode_utils import detect_encoding import pkg_resources __import__('setuptools.extern.packaging.specifiers') @@ -587,13 +586,9 @@ class Distribution(_Distribution): parser = ConfigParser() for filename in filenames: - with io.open(filename, 'rb') as fp: - encoding = detect_encoding(fp) + with io.open(filename, encoding='utf-8') as reader: if DEBUG: - self.announce(" reading %s [%s]" % ( - filename, encoding or 'locale') - ) - reader = io.TextIOWrapper(fp, encoding=encoding) + self.announce(" reading {filename}".format(**locals())) (parser.read_file if six.PY3 else parser.readfp)(reader) for section in parser.sections(): options = parser.options(section) diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index 4daf1df1..bc97664d 100644 --- a/setuptools/tests/test_config.py +++ b/setuptools/tests/test_config.py @@ -9,7 +9,6 @@ from mock import patch from setuptools.dist import Distribution, _Distribution from setuptools.config import ConfigHandler, read_configuration from setuptools.extern.six.moves import configparser -from setuptools.tests import is_ascii from . import py2_only, py3_only from .textwrap import DALS @@ -446,10 +445,6 @@ class TestMetadata: with get_dist(tmpdir): pass - skip_if_not_ascii = pytest.mark.skipif( - not is_ascii, reason='Test not supported with this locale') - - @skip_if_not_ascii def test_non_ascii_1(self, tmpdir): fake_env( tmpdir, @@ -457,18 +452,8 @@ class TestMetadata: 'description = éàïôñ\n', encoding='utf-8' ) - with pytest.raises(UnicodeDecodeError): - with get_dist(tmpdir): - pass - - def test_non_ascii_2(self, tmpdir): - fake_env( - tmpdir, - '# -*- coding: invalid\n' - ) - with pytest.raises(LookupError): - with get_dist(tmpdir): - pass + with get_dist(tmpdir): + pass def test_non_ascii_3(self, tmpdir): fake_env( @@ -479,7 +464,6 @@ class TestMetadata: with get_dist(tmpdir): pass - @skip_if_not_ascii def test_non_ascii_4(self, tmpdir): fake_env( tmpdir, @@ -491,8 +475,10 @@ class TestMetadata: with get_dist(tmpdir) as dist: assert dist.metadata.description == 'éàïôñ' - @skip_if_not_ascii - def test_non_ascii_5(self, tmpdir): + def test_not_utf8(self, tmpdir): + """ + Config files encoded not in UTF-8 will fail + """ fake_env( tmpdir, '# vim: set fileencoding=iso-8859-15 :\n' @@ -500,8 +486,9 @@ class TestMetadata: 'description = éàïôñ\n', encoding='iso-8859-15' ) - with get_dist(tmpdir) as dist: - assert dist.metadata.description == 'éàïôñ' + with pytest.raises(UnicodeDecodeError): + with get_dist(tmpdir): + pass class TestOptions: -- cgit v1.2.3 From 7b09ba64c0327ecea04cc95057ffa7d5c8d939c8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 5 Apr 2019 10:46:00 -0400 Subject: Add test for setopt to demonstrate that edit_config retains non-ASCII characters. --- setuptools/tests/test_setopt.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 setuptools/tests/test_setopt.py (limited to 'setuptools') diff --git a/setuptools/tests/test_setopt.py b/setuptools/tests/test_setopt.py new file mode 100644 index 00000000..2241ef73 --- /dev/null +++ b/setuptools/tests/test_setopt.py @@ -0,0 +1,36 @@ +# coding: utf-8 + +from __future__ import unicode_literals + +import io + +import six + +from setuptools.command import setopt +from setuptools.extern.six.moves import configparser + + +class TestEdit: + @staticmethod + def parse_config(filename): + parser = configparser.ConfigParser() + with io.open(filename, encoding='utf-8') as reader: + (parser.read_file if six.PY3 else parser.readfp)(reader) + return parser + + @staticmethod + def write_text(file, content): + with io.open(file, 'wb') as strm: + strm.write(content.encode('utf-8')) + + def test_utf8_encoding_retained(self, tmpdir): + """ + When editing a file, non-ASCII characters encoded in + UTF-8 should be retained. + """ + config = tmpdir.join('setup.cfg') + self.write_text(config, '[names]\njaraco=йарацо') + setopt.edit_config(str(config), dict(names=dict(other='yes'))) + parser = self.parse_config(str(config)) + assert parser['names']['jaraco'] == 'йарацо' + assert parser['names']['other'] == 'yes' -- cgit v1.2.3 From b336e83a63722b3a3e4d3f1779686149d5cef8d1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 5 Apr 2019 10:49:23 -0400 Subject: Add compatibility for Python 2 --- setuptools/tests/test_setopt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'setuptools') diff --git a/setuptools/tests/test_setopt.py b/setuptools/tests/test_setopt.py index 2241ef73..7c803500 100644 --- a/setuptools/tests/test_setopt.py +++ b/setuptools/tests/test_setopt.py @@ -29,8 +29,8 @@ class TestEdit: UTF-8 should be retained. """ config = tmpdir.join('setup.cfg') - self.write_text(config, '[names]\njaraco=йарацо') + self.write_text(str(config), '[names]\njaraco=йарацо') setopt.edit_config(str(config), dict(names=dict(other='yes'))) parser = self.parse_config(str(config)) - assert parser['names']['jaraco'] == 'йарацо' - assert parser['names']['other'] == 'yes' + assert parser.get('names', 'jaraco') == 'йарацо' + assert parser.get('names', 'other') == 'yes' -- cgit v1.2.3 From 7ed188bcaf38a25fb63fbb1ed3b070428ff95759 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 5 Apr 2019 11:07:02 -0400 Subject: Correct cyrillic to match preferred pronunciation. --- setuptools/tests/test_setopt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'setuptools') diff --git a/setuptools/tests/test_setopt.py b/setuptools/tests/test_setopt.py index 7c803500..3fb04fb4 100644 --- a/setuptools/tests/test_setopt.py +++ b/setuptools/tests/test_setopt.py @@ -29,8 +29,8 @@ class TestEdit: UTF-8 should be retained. """ config = tmpdir.join('setup.cfg') - self.write_text(str(config), '[names]\njaraco=йарацо') + self.write_text(str(config), '[names]\njaraco=джарако') setopt.edit_config(str(config), dict(names=dict(other='yes'))) parser = self.parse_config(str(config)) - assert parser.get('names', 'jaraco') == 'йарацо' + assert parser.get('names', 'jaraco') == 'джарако' assert parser.get('names', 'other') == 'yes' -- cgit v1.2.3 From f36781084f8f870ea747d477bd742057ea022421 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 5 Apr 2019 12:25:03 -0400 Subject: Remove detect_encoding, no longer used. --- setuptools/unicode_utils.py | 13 ------------- 1 file changed, 13 deletions(-) (limited to 'setuptools') diff --git a/setuptools/unicode_utils.py b/setuptools/unicode_utils.py index 3b8179a8..7c63efd2 100644 --- a/setuptools/unicode_utils.py +++ b/setuptools/unicode_utils.py @@ -1,6 +1,5 @@ import unicodedata import sys -import re from setuptools.extern import six @@ -43,15 +42,3 @@ def try_encode(string, enc): return string.encode(enc) except UnicodeEncodeError: return None - - -CODING_RE = re.compile(br'^[ \t\f]*#.*?coding[:=][ \t]*([-\w.]+)') - - -def detect_encoding(fp): - first_line = fp.readline() - fp.seek(0) - m = CODING_RE.match(first_line) - if m is None: - return None - return m.group(1).decode('ascii') -- cgit v1.2.3 From 8db41e478db4ded53b9836f62211f8c9371ec7c9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 5 Apr 2019 15:12:21 -0400 Subject: Rely on unique_everseen to avoid unnecessarily polluting the PYTHONPATH with duplicate entries. --- setuptools/command/test.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'setuptools') diff --git a/setuptools/command/test.py b/setuptools/command/test.py index 997fd8b0..973e4eb2 100644 --- a/setuptools/command/test.py +++ b/setuptools/command/test.py @@ -15,6 +15,7 @@ from pkg_resources import (resource_listdir, resource_exists, normalize_path, working_set, _namespace_packages, evaluate_marker, add_activation_listener, require, EntryPoint) from setuptools import Command +from .build_py import _unique_everseen __metaclass__ = type @@ -186,12 +187,11 @@ class test(Command): orig_pythonpath = os.environ.get('PYTHONPATH', nothing) current_pythonpath = os.environ.get('PYTHONPATH', '') try: - to_join = [] - for x in list(paths) + current_pythonpath.split(os.pathsep): - if x not in to_join: - to_join.append(x) - if to_join: - os.environ['PYTHONPATH'] = os.pathsep.join(to_join) + prefix = os.pathsep.join(_unique_everseen(paths)) + to_join = filter(None, [prefix, current_pythonpath]) + new_path = os.pathsep.join(to_join) + if new_path: + os.environ['PYTHONPATH'] = new_path yield finally: if orig_pythonpath is nothing: -- cgit v1.2.3 From 59aeb62614ab07acb4b9520d81179d6e647dfbb7 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Fri, 12 Apr 2019 00:32:12 +0200 Subject: FIX: git and hg revision checkout under Windows Windows does not change the working directory for a process via `cd .. && ` (see e.g. this question: https://stackoverflow.com/q/55641332/8575607 ). This commit replaces the use of `cd .. &&` with arguments provided by `git` and `hg` respectively. --- setuptools/package_index.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'setuptools') diff --git a/setuptools/package_index.py b/setuptools/package_index.py index 705a47cf..6b06f2ca 100644 --- a/setuptools/package_index.py +++ b/setuptools/package_index.py @@ -897,7 +897,7 @@ class PackageIndex(Environment): if rev is not None: self.info("Checking out %s", rev) - os.system("(cd %s && git checkout --quiet %s)" % ( + os.system("git -C %s checkout --quiet %s" % ( filename, rev, )) @@ -913,7 +913,7 @@ class PackageIndex(Environment): if rev is not None: self.info("Updating to %s", rev) - os.system("(cd %s && hg up -C -r %s -q)" % ( + os.system("hg --cwd %s up -C -r %s -q" % ( filename, rev, )) -- cgit v1.2.3 From 0259db3b7aaaff9d0dfdde48e9dbc0361d3ab47f Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Fri, 12 Apr 2019 01:10:33 +0200 Subject: Updated test to check for changed git rev checkout Checking for new implementation solving issue #1740 --- setuptools/tests/test_packageindex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'setuptools') diff --git a/setuptools/tests/test_packageindex.py b/setuptools/tests/test_packageindex.py index ab371884..60d968fd 100644 --- a/setuptools/tests/test_packageindex.py +++ b/setuptools/tests/test_packageindex.py @@ -249,7 +249,7 @@ class TestPackageIndex: first_call_args = os_system_mock.call_args_list[0][0] assert first_call_args == (expected,) - tmpl = '(cd {expected_dir} && git checkout --quiet master)' + tmpl = 'git -C {expected_dir} checkout --quiet master' expected = tmpl.format(**locals()) assert os_system_mock.call_args_list[1][0] == (expected,) assert result == expected_dir -- cgit v1.2.3 From 869c634880f24b918ca074588b625b9dce2038b2 Mon Sep 17 00:00:00 2001 From: Floris Lambrechts Date: Tue, 26 Mar 2019 09:08:33 +0100 Subject: Add test for pre-existing wheels in build_meta Currently, this will fail because setuptools.build_meta.build_wheel assumes that no wheels already exist in the `dist/` directory. See GH #1671 --- setuptools/tests/test_build_meta.py | 38 +++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) (limited to 'setuptools') diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index 0bdea2d6..90400afc 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -157,6 +157,44 @@ class TestBuildMetaBackend: assert os.path.isfile(os.path.join(dist_dir, wheel_name)) + @pytest.mark.xfail(reason="Known error, see GH #1671") + def test_build_wheel_with_existing_wheel_file_present(self, tmpdir_cwd): + # Building a wheel should still succeed if there's already a wheel + # in the wheel directory + files = { + 'setup.py': "from setuptools import setup\nsetup()", + 'VERSION': "0.0.1", + 'setup.cfg': DALS(""" + [metadata] + name = foo + version = file: VERSION + """), + 'pyproject.toml': DALS(""" + [build-system] + requires = ["setuptools", "wheel"] + build-backend = "setuptools.build_meta + """), + } + + build_files(files) + + dist_dir = os.path.abspath('pip-wheel-preexisting') + os.makedirs(dist_dir) + + # make first wheel + build_backend = self.get_build_backend() + wheel_one = build_backend.build_wheel(dist_dir) + + # change version + with open("VERSION", "wt") as version_file: + version_file.write("0.0.2") + + # make *second* wheel + wheel_two = self.get_build_backend().build_wheel(dist_dir) + + assert os.path.isfile(os.path.join(dist_dir, wheel_one)) + assert wheel_one != wheel_two + def test_build_sdist(self, build_backend): dist_dir = os.path.abspath('pip-sdist') os.makedirs(dist_dir) -- cgit v1.2.3 From 901f7cc2a036bfeb93bfbe480608e04c76c2c5ec Mon Sep 17 00:00:00 2001 From: Shashank Singh Date: Sat, 20 Apr 2019 23:24:41 -0400 Subject: Fix error when wheels already exist in dist/ `build_meta.build_wheel` assumes that the only wheel in its output directory is the one it builds, but prior to this, it also used the `dist/` folder as its working output directory. This commit uses a temporary directory instead, preventing an error that was triggered when previously-generated wheel files were still sitting in `dist/`. Fixes GH #1671 --- setuptools/build_meta.py | 23 ++++++++++++++++------- setuptools/py31compat.py | 4 ++-- setuptools/tests/test_build_meta.py | 7 ++++++- 3 files changed, 24 insertions(+), 10 deletions(-) (limited to 'setuptools') diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index 47cbcbf6..e40904a5 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -35,6 +35,7 @@ import contextlib import setuptools import distutils +from setuptools.py31compat import TemporaryDirectory from pkg_resources import parse_requirements @@ -182,14 +183,22 @@ class _BuildMetaBackend(object): metadata_directory=None): config_settings = self._fix_config(config_settings) wheel_directory = os.path.abspath(wheel_directory) - sys.argv = sys.argv[:1] + ['bdist_wheel'] + \ - config_settings["--global-option"] - self.run_setup() - if wheel_directory != 'dist': - shutil.rmtree(wheel_directory) - shutil.copytree('dist', wheel_directory) - return _file_with_extension(wheel_directory, '.whl') + # Build the wheel in a temporary directory, then copy to the target + with TemporaryDirectory(dir=wheel_directory) as tmp_dist_dir: + sys.argv = (sys.argv[:1] + + ['bdist_wheel', '--dist-dir', tmp_dist_dir] + + config_settings["--global-option"]) + self.run_setup() + + wheel_basename = _file_with_extension(tmp_dist_dir, '.whl') + wheel_path = os.path.join(wheel_directory, wheel_basename) + if os.path.exists(wheel_path): + # os.rename will fail overwriting on non-unix env + os.remove(wheel_path) + os.rename(os.path.join(tmp_dist_dir, wheel_basename), wheel_path) + + return wheel_basename def build_sdist(self, sdist_directory, config_settings=None): config_settings = self._fix_config(config_settings) diff --git a/setuptools/py31compat.py b/setuptools/py31compat.py index 1a0705ec..e1da7ee2 100644 --- a/setuptools/py31compat.py +++ b/setuptools/py31compat.py @@ -17,9 +17,9 @@ except ImportError: errors on deletion. """ - def __init__(self): + def __init__(self, **kwargs): self.name = None # Handle mkdtemp raising an exception - self.name = tempfile.mkdtemp() + self.name = tempfile.mkdtemp(**kwargs) def __enter__(self): return self.name diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index 90400afc..88e29ffe 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -157,7 +157,6 @@ class TestBuildMetaBackend: assert os.path.isfile(os.path.join(dist_dir, wheel_name)) - @pytest.mark.xfail(reason="Known error, see GH #1671") def test_build_wheel_with_existing_wheel_file_present(self, tmpdir_cwd): # Building a wheel should still succeed if there's already a wheel # in the wheel directory @@ -195,6 +194,12 @@ class TestBuildMetaBackend: assert os.path.isfile(os.path.join(dist_dir, wheel_one)) assert wheel_one != wheel_two + # and if rebuilding the same wheel? + open(os.path.join(dist_dir, wheel_two), 'w').close() + wheel_three = self.get_build_backend().build_wheel(dist_dir) + assert wheel_three == wheel_two + assert os.path.getsize(os.path.join(dist_dir, wheel_three)) > 0 + def test_build_sdist(self, build_backend): dist_dir = os.path.abspath('pip-sdist') os.makedirs(dist_dir) -- cgit v1.2.3 From 127c8c74dead715b67a66eed86c420fe7b31ea3b Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Mon, 22 Apr 2019 11:05:29 -0400 Subject: Limit workers in ProcessPoolExecutor As a mitigation for #1730, this commit limits the number of workers in the ProcessPoolExecutor to 1 (default is the number of CPUs). On PyPy, having a higher number of available workers dramatically increases the number of concurrent processes, leading to some resource exhaustion issues. This does not address the root issue, but should improve the situation until the root issue is addressed. --- setuptools/tests/test_build_meta.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'setuptools') diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index 90400afc..e32cfb92 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -28,7 +28,7 @@ class BuildBackend(BuildBackendBase): def __init__(self, *args, **kwargs): super(BuildBackend, self).__init__(*args, **kwargs) - self.pool = futures.ProcessPoolExecutor() + self.pool = futures.ProcessPoolExecutor(max_workers=1) def __getattr__(self, name): """Handles aribrary function invocations on the build backend.""" -- cgit v1.2.3 From 5f88c42f3b4529956e4d02453ae571e32bc4692a Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Mon, 22 Apr 2019 22:18:50 +0200 Subject: build_meta: fix 2 issues with `build_wheel` / `build_sdist` Fix the following cases: * `build_sdist` is called with another sdist already present in the destination directory * `build_wheel` is called with the destination directory not already created --- setuptools/build_meta.py | 47 ++++++++++++++++++++----------------- setuptools/tests/test_build_meta.py | 38 ++++++++++++++++-------------- 2 files changed, 46 insertions(+), 39 deletions(-) (limited to 'setuptools') diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index e40904a5..10c4b528 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -38,6 +38,7 @@ import distutils from setuptools.py31compat import TemporaryDirectory from pkg_resources import parse_requirements +from pkg_resources.py31compat import makedirs __all__ = ['get_requires_for_build_sdist', 'get_requires_for_build_wheel', @@ -179,36 +180,38 @@ class _BuildMetaBackend(object): return dist_infos[0] - def build_wheel(self, wheel_directory, config_settings=None, - metadata_directory=None): + def _build_with_temp_dir(self, setup_command, result_extension, + result_directory, config_settings): config_settings = self._fix_config(config_settings) - wheel_directory = os.path.abspath(wheel_directory) + result_directory = os.path.abspath(result_directory) - # Build the wheel in a temporary directory, then copy to the target - with TemporaryDirectory(dir=wheel_directory) as tmp_dist_dir: - sys.argv = (sys.argv[:1] + - ['bdist_wheel', '--dist-dir', tmp_dist_dir] + + # Build in a temporary directory, then copy to the target. + makedirs(result_directory, exist_ok=True) + with TemporaryDirectory(dir=result_directory) as tmp_dist_dir: + sys.argv = (sys.argv[:1] + setup_command + + ['--dist-dir', tmp_dist_dir] + config_settings["--global-option"]) self.run_setup() - wheel_basename = _file_with_extension(tmp_dist_dir, '.whl') - wheel_path = os.path.join(wheel_directory, wheel_basename) - if os.path.exists(wheel_path): - # os.rename will fail overwriting on non-unix env - os.remove(wheel_path) - os.rename(os.path.join(tmp_dist_dir, wheel_basename), wheel_path) + result_basename = _file_with_extension(tmp_dist_dir, result_extension) + result_path = os.path.join(result_directory, result_basename) + if os.path.exists(result_path): + # os.rename will fail overwriting on non-Unix. + os.remove(result_path) + os.rename(os.path.join(tmp_dist_dir, result_basename), result_path) - return wheel_basename + return result_basename - def build_sdist(self, sdist_directory, config_settings=None): - config_settings = self._fix_config(config_settings) - sdist_directory = os.path.abspath(sdist_directory) - sys.argv = sys.argv[:1] + ['sdist', '--formats', 'gztar'] + \ - config_settings["--global-option"] + \ - ["--dist-dir", sdist_directory] - self.run_setup() - return _file_with_extension(sdist_directory, '.tar.gz') + def build_wheel(self, wheel_directory, config_settings=None, + metadata_directory=None): + return self._build_with_temp_dir(['bdist_wheel'], '.whl', + wheel_directory, config_settings) + + def build_sdist(self, sdist_directory, config_settings=None): + return self._build_with_temp_dir(['sdist', '--formats', 'gztar'], + '.tar.gz', sdist_directory, + config_settings) class _BuildMetaLegacyBackend(_BuildMetaBackend): diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index 7612ebd7..e1efe561 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -157,9 +157,10 @@ class TestBuildMetaBackend: assert os.path.isfile(os.path.join(dist_dir, wheel_name)) - def test_build_wheel_with_existing_wheel_file_present(self, tmpdir_cwd): - # Building a wheel should still succeed if there's already a wheel - # in the wheel directory + @pytest.mark.parametrize('build_type', ('wheel', 'sdist')) + def test_build_with_existing_file_present(self, build_type, tmpdir_cwd): + # Building a sdist/wheel should still succeed if there's + # already a sdist/wheel in the destination directory. files = { 'setup.py': "from setuptools import setup\nsetup()", 'VERSION': "0.0.1", @@ -177,28 +178,31 @@ class TestBuildMetaBackend: build_files(files) - dist_dir = os.path.abspath('pip-wheel-preexisting') - os.makedirs(dist_dir) + dist_dir = os.path.abspath('preexisting-' + build_type) - # make first wheel build_backend = self.get_build_backend() - wheel_one = build_backend.build_wheel(dist_dir) + build_method = getattr(build_backend, 'build_' + build_type) + + # Build a first sdist/wheel. + # Note: this also check the destination directory is + # successfully created if it does not exist already. + first_result = build_method(dist_dir) - # change version + # Change version. with open("VERSION", "wt") as version_file: version_file.write("0.0.2") - # make *second* wheel - wheel_two = self.get_build_backend().build_wheel(dist_dir) + # Build a *second* sdist/wheel. + second_result = build_method(dist_dir) - assert os.path.isfile(os.path.join(dist_dir, wheel_one)) - assert wheel_one != wheel_two + assert os.path.isfile(os.path.join(dist_dir, first_result)) + assert first_result != second_result - # and if rebuilding the same wheel? - open(os.path.join(dist_dir, wheel_two), 'w').close() - wheel_three = self.get_build_backend().build_wheel(dist_dir) - assert wheel_three == wheel_two - assert os.path.getsize(os.path.join(dist_dir, wheel_three)) > 0 + # And if rebuilding the exact same sdist/wheel? + open(os.path.join(dist_dir, second_result), 'w').close() + third_result = build_method(dist_dir) + assert third_result == second_result + assert os.path.getsize(os.path.join(dist_dir, third_result)) > 0 def test_build_sdist(self, build_backend): dist_dir = os.path.abspath('pip-sdist') -- cgit v1.2.3 From f58549ab38eb6f5d1146510cbf15965aeb75c6fb Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Tue, 14 May 2019 15:31:27 +0200 Subject: tests: fix_test_build_deps_on_distutils * ignore distutils' warning (`Unknown distribution option: 'python_requires'`) * fix test on Windows --- setuptools/tests/test_integration.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'setuptools') diff --git a/setuptools/tests/test_integration.py b/setuptools/tests/test_integration.py index e54f3209..1e132188 100644 --- a/setuptools/tests/test_integration.py +++ b/setuptools/tests/test_integration.py @@ -141,6 +141,7 @@ def test_build_deps_on_distutils(request, tmpdir_factory, build_dep): allowed_unknowns = [ 'test_suite', 'tests_require', + 'python_requires', 'install_requires', ] assert not match or match.group(1).strip('"\'') in allowed_unknowns @@ -149,8 +150,8 @@ def test_build_deps_on_distutils(request, tmpdir_factory, build_dep): def install(pkg_dir, install_dir): with open(os.path.join(pkg_dir, 'setuptools.py'), 'w') as breaker: breaker.write('raise ImportError()') - cmd = [sys.executable, 'setup.py', 'install', '--prefix', install_dir] - env = dict(os.environ, PYTHONPATH=pkg_dir) + cmd = [sys.executable, 'setup.py', 'install', '--prefix', str(install_dir)] + env = dict(os.environ, PYTHONPATH=str(pkg_dir)) output = subprocess.check_output( cmd, cwd=pkg_dir, env=env, stderr=subprocess.STDOUT) return output.decode('utf-8') -- cgit v1.2.3 From 880ff4a3579bac7839f8f038085381ae14155f31 Mon Sep 17 00:00:00 2001 From: Bastian Venthur Date: Thu, 16 May 2019 12:46:50 +0200 Subject: Force metadata-version = 1.2 when project urls are present. Closes: #1756 --- setuptools/dist.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'setuptools') diff --git a/setuptools/dist.py b/setuptools/dist.py index 9a165de0..ea6411b1 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -54,7 +54,8 @@ def get_metadata_version(self): mv = StrictVersion('2.1') elif (self.maintainer is not None or self.maintainer_email is not None or - getattr(self, 'python_requires', None) is not None): + getattr(self, 'python_requires', None) is not None or + self.project_urls): mv = StrictVersion('1.2') elif (self.provides or self.requires or self.obsoletes or self.classifiers or self.download_url): -- cgit v1.2.3 From a64ddf0d2f2bcd6e9843fb55c94fba922f315722 Mon Sep 17 00:00:00 2001 From: Bastian Venthur Date: Thu, 16 May 2019 13:48:15 +0200 Subject: Added test for metadata-version 1.2 --- setuptools/tests/test_egg_info.py | 1 + 1 file changed, 1 insertion(+) (limited to 'setuptools') diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index db9c3873..316eb2ed 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -622,6 +622,7 @@ class TestEggInfo: assert expected_line in pkg_info_lines expected_line = 'Project-URL: Link Two, https://example.com/two/' assert expected_line in pkg_info_lines + assert 'Metadata-Version: 1.2' in pkg_info_lines def test_python_requires_egg_info(self, tmpdir_cwd, env): self._setup_script_with_requires( -- cgit v1.2.3 From 53b8db359378f436bfd88f90a90aaf01b650d3a6 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 18 Jun 2019 16:15:25 +0900 Subject: Stop using deprecated HTMLParser.unescape HTMLParser.unescape is accessed even when unused - this will cause an exception when `HTMLParser.unescape` is removed in Python 3.9. --- setuptools/py33compat.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'setuptools') diff --git a/setuptools/py33compat.py b/setuptools/py33compat.py index 87cf5398..cb694436 100644 --- a/setuptools/py33compat.py +++ b/setuptools/py33compat.py @@ -52,4 +52,8 @@ class Bytecode_compat: Bytecode = getattr(dis, 'Bytecode', Bytecode_compat) -unescape = getattr(html, 'unescape', html_parser.HTMLParser().unescape) +unescape = getattr(html, 'unescape', None) +if unescape is None: + # HTMLParser.unescape is deprecated since Python 3.4, and will be removed + # from 3.9. + unescape = html_parser.HTMLParser().unescape -- cgit v1.2.3 From 305bb1cefc3251c67b55149139a768ddf474f7b6 Mon Sep 17 00:00:00 2001 From: Daniel Himmelstein Date: Thu, 23 May 2019 14:15:19 -0400 Subject: fix assert_string_list docstring value=None raises TypeError DistutilsSetupError: 2 must be a list of strings (got None) --- setuptools/dist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'setuptools') diff --git a/setuptools/dist.py b/setuptools/dist.py index ea6411b1..1e1b83be 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -213,7 +213,7 @@ def check_importable(dist, attr, value): def assert_string_list(dist, attr, value): - """Verify that value is a string list or None""" + """Verify that value is a string list""" try: assert ''.join(value) != value except (TypeError, ValueError, AttributeError, AssertionError): -- cgit v1.2.3 From 8f848bd777278fc8dcb42dc45751cd8b95ec2a02 Mon Sep 17 00:00:00 2001 From: Daniel Himmelstein Date: Wed, 22 May 2019 17:45:44 -0400 Subject: improve `package_data` check Ensure the dictionary values are lists/tuples of strings. Fix #1459. --- setuptools/dist.py | 29 +++++++++++------------ setuptools/tests/test_dist.py | 53 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 15 deletions(-) (limited to 'setuptools') diff --git a/setuptools/dist.py b/setuptools/dist.py index 1e1b83be..f0f030b5 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -215,6 +215,10 @@ def check_importable(dist, attr, value): def assert_string_list(dist, attr, value): """Verify that value is a string list""" try: + # verify that value is a list or tuple to exclude unordered + # or single-use iterables + assert isinstance(value, (list, tuple)) + # verify that elements of value are strings assert ''.join(value) != value except (TypeError, ValueError, AttributeError, AssertionError): raise DistutilsSetupError( @@ -307,20 +311,17 @@ def check_test_suite(dist, attr, value): def check_package_data(dist, attr, value): """Verify that value is a dictionary of package names to glob lists""" - if isinstance(value, dict): - for k, v in value.items(): - if not isinstance(k, str): - break - try: - iter(v) - except TypeError: - break - else: - return - raise DistutilsSetupError( - attr + " must be a dictionary mapping package names to lists of " - "wildcard patterns" - ) + if not isinstance(value, dict): + raise DistutilsSetupError( + "{!r} must be a dictionary mapping package names to lists of " + "string wildcard patterns".format(attr)) + for k, v in value.items(): + if not isinstance(k, six.string_types): + raise DistutilsSetupError( + "keys of {!r} dict must be strings (got {!r})" + .format(attr, k) + ) + assert_string_list(dist, 'values of {!r} dict'.format(attr), v) def check_packages(dist, attr, value): diff --git a/setuptools/tests/test_dist.py b/setuptools/tests/test_dist.py index 390c3dfc..c771a19a 100644 --- a/setuptools/tests/test_dist.py +++ b/setuptools/tests/test_dist.py @@ -3,7 +3,13 @@ from __future__ import unicode_literals import io -from setuptools.dist import DistDeprecationWarning, _get_unpatched +import re +from distutils.errors import DistutilsSetupError +from setuptools.dist import ( + _get_unpatched, + check_package_data, + DistDeprecationWarning, +) from setuptools import Distribution from setuptools.extern.six.moves.urllib.request import pathname2url from setuptools.extern.six.moves.urllib_parse import urljoin @@ -263,3 +269,48 @@ def test_maintainer_author(name, attrs, tmpdir): else: line = '%s: %s' % (fkey, val) assert line in pkg_lines_set + + +CHECK_PACKAGE_DATA_TESTS = ( + # Valid. + ({ + '': ['*.txt', '*.rst'], + 'hello': ['*.msg'], + }, None), + # Not a dictionary. + (( + ('', ['*.txt', '*.rst']), + ('hello', ['*.msg']), + ), ( + "'package_data' must be a dictionary mapping package" + " names to lists of string wildcard patterns" + )), + # Invalid key type. + ({ + 400: ['*.txt', '*.rst'], + }, ( + "keys of 'package_data' dict must be strings (got 400)" + )), + # Invalid value type. + ({ + 'hello': str('*.msg'), + }, ( + "\"values of 'package_data' dict\" must be a list of strings (got '*.msg')" + )), + # Invalid value type (generators are single use) + ({ + 'hello': (x for x in "generator"), + }, ( + "\"values of 'package_data' dict\" must be a list of strings " + "(got Date: Tue, 23 Jul 2019 11:29:21 +0200 Subject: tests: fix `test_pip_upgrade_from_source` on Python 3.4 Do not test pip's master on 3.4, as support for it has been dropped. --- setuptools/tests/test_virtualenv.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'setuptools') diff --git a/setuptools/tests/test_virtualenv.py b/setuptools/tests/test_virtualenv.py index d7b98c77..74a1284c 100644 --- a/setuptools/tests/test_virtualenv.py +++ b/setuptools/tests/test_virtualenv.py @@ -8,6 +8,8 @@ from pytest_fixture_config import yield_requires_config import pytest_virtualenv +from setuptools.extern import six + from .textwrap import DALS from .test_easy_install import make_nspkg_sdist @@ -75,9 +77,12 @@ def _get_pip_versions(): 'pip==10.0.1', 'pip==18.1', 'pip==19.0.1', - 'https://github.com/pypa/pip/archive/master.zip', ] + # Pip's master dropped support for 3.4. + if not six.PY34: + network_versions.append('https://github.com/pypa/pip/archive/master.zip') + versions = [None] + [ pytest.param(v, **({} if network else {'marks': pytest.mark.skip})) for v in network_versions -- cgit v1.2.3 From 7e1b1934c7e0dcd400ff17a701601d912aa603bc Mon Sep 17 00:00:00 2001 From: jgoutin Date: Sat, 3 Aug 2019 10:41:34 +0200 Subject: Improve Visual C++ 14.X support Improve VC++14 support for VS 2017 and 2019. Separate VC from VS version (Miss match starting VS15). Improve docstrings args and returns information + fixe typos. Fix coding style and minor coding issues. Remove Microsoft "Windows SDK 7.0" dead link. --- setuptools/msvc.py | 1021 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 699 insertions(+), 322 deletions(-) (limited to 'setuptools') diff --git a/setuptools/msvc.py b/setuptools/msvc.py index b9c472f1..ffa7053b 100644 --- a/setuptools/msvc.py +++ b/setuptools/msvc.py @@ -11,13 +11,17 @@ Microsoft Visual C++ 9.0: Microsoft Visual C++ 10.0: Microsoft Windows SDK 7.1 (x86, x64, ia64) -Microsoft Visual C++ 14.0: +Microsoft Visual C++ 14.X: Microsoft Visual C++ Build Tools 2015 (x86, x64, arm) - Microsoft Visual Studio 2017 (x86, x64, arm, arm64) Microsoft Visual Studio Build Tools 2017 (x86, x64, arm, arm64) + Microsoft Visual Studio Build Tools 2019 (x86, x64, arm, arm64) + +This may also support compilers shipped with compatible Visual Studio versions. """ -import os +import json +from os import listdir, pathsep +from os.path import join, isfile, isdir, dirname import sys import platform import itertools @@ -30,12 +34,9 @@ from .monkey import get_unpatched if platform.system() == 'Windows': from setuptools.extern.six.moves import winreg - safe_env = os.environ + from os import environ else: - """ - Mock winreg and environ so the module can be imported - on this platform. - """ + # Mock winreg and environ so the module can be imported on this platform. class winreg: HKEY_USERS = None @@ -43,7 +44,7 @@ else: HKEY_LOCAL_MACHINE = None HKEY_CLASSES_ROOT = None - safe_env = dict() + environ = dict() _msvc9_suppress_errors = ( # msvc9compiler isn't available on some platforms @@ -63,15 +64,13 @@ except _msvc9_suppress_errors: def msvc9_find_vcvarsall(version): """ Patched "distutils.msvc9compiler.find_vcvarsall" to use the standalone - compiler build for Python (VCForPython). Fall back to original behavior - when the standalone compiler is not available. + compiler build for Python + (VCForPython / Microsoft Visual C++ Compiler for Python 2.7). - Redirect the path of "vcvarsall.bat". + Fall back to original behavior when the standalone compiler is not + available. - Known supported compilers - ------------------------- - Microsoft Visual C++ 9.0: - Microsoft Visual C++ Compiler for Python 2.7 (x86, amd64) + Redirect the path of "vcvarsall.bat". Parameters ---------- @@ -80,24 +79,25 @@ def msvc9_find_vcvarsall(version): Return ------ - vcvarsall.bat path: str + str + vcvarsall.bat path """ - VC_BASE = r'Software\%sMicrosoft\DevDiv\VCForPython\%0.1f' - key = VC_BASE % ('', version) + vc_base = r'Software\%sMicrosoft\DevDiv\VCForPython\%0.1f' + key = vc_base % ('', version) try: # Per-user installs register the compiler path here productdir = Reg.get_value(key, "installdir") except KeyError: try: # All-user installs on a 64-bit system register here - key = VC_BASE % ('Wow6432Node\\', version) + key = vc_base % ('Wow6432Node\\', version) productdir = Reg.get_value(key, "installdir") except KeyError: productdir = None if productdir: - vcvarsall = os.path.os.path.join(productdir, "vcvarsall.bat") - if os.path.isfile(vcvarsall): + vcvarsall = join(productdir, "vcvarsall.bat") + if isfile(vcvarsall): return vcvarsall return get_unpatched(msvc9_find_vcvarsall)(version) @@ -106,20 +106,10 @@ def msvc9_find_vcvarsall(version): def msvc9_query_vcvarsall(ver, arch='x86', *args, **kwargs): """ Patched "distutils.msvc9compiler.query_vcvarsall" for support extra - compilers. + Microsoft Visual C++ 9.0 and 10.0 compilers. Set environment without use of "vcvarsall.bat". - Known supported compilers - ------------------------- - Microsoft Visual C++ 9.0: - Microsoft Visual C++ Compiler for Python 2.7 (x86, amd64) - Microsoft Windows SDK 6.1 (x86, x64, ia64) - Microsoft Windows SDK 7.0 (x86, x64, ia64) - - Microsoft Visual C++ 10.0: - Microsoft Windows SDK 7.1 (x86, x64, ia64) - Parameters ---------- ver: float @@ -129,9 +119,10 @@ def msvc9_query_vcvarsall(ver, arch='x86', *args, **kwargs): Return ------ - environment: dict + dict + environment """ - # Try to get environement from vcvarsall.bat (Classical way) + # Try to get environment from vcvarsall.bat (Classical way) try: orig = get_unpatched(msvc9_query_vcvarsall) return orig(ver, arch, *args, **kwargs) @@ -153,17 +144,10 @@ def msvc9_query_vcvarsall(ver, arch='x86', *args, **kwargs): def msvc14_get_vc_env(plat_spec): """ Patched "distutils._msvccompiler._get_vc_env" for support extra - compilers. + Microsoft Visual C++ 14.X compilers. Set environment without use of "vcvarsall.bat". - Known supported compilers - ------------------------- - Microsoft Visual C++ 14.0: - Microsoft Visual C++ Build Tools 2015 (x86, x64, arm) - Microsoft Visual Studio 2017 (x86, x64, arm, arm64) - Microsoft Visual Studio Build Tools 2017 (x86, x64, arm, arm64) - Parameters ---------- plat_spec: str @@ -171,7 +155,8 @@ def msvc14_get_vc_env(plat_spec): Return ------ - environment: dict + dict + environment """ # Try to get environment from vcvarsall.bat (Classical way) try: @@ -217,9 +202,9 @@ def _augment_exception(exc, version, arch=''): if version == 9.0: if arch.lower().find('ia64') > -1: # For VC++ 9.0, if IA64 support is needed, redirect user - # to Windows SDK 7.0 - message += ' Get it with "Microsoft Windows SDK 7.0": ' - message += msdownload % 3138 + # to Windows SDK 7.0. + # Note: No download link available from Microsoft. + message += ' Get it with "Microsoft Windows SDK 7.0"' else: # For VC++ 9.0 redirect user to Vc++ for Python 2.7 : # This redirection link is maintained by Microsoft. @@ -230,8 +215,8 @@ def _augment_exception(exc, version, arch=''): message += ' Get it with "Microsoft Windows SDK 7.1": ' message += msdownload % 8279 elif version >= 14.0: - # For VC++ 14.0 Redirect user to Visual C++ Build Tools - message += (' Get it with "Microsoft Visual C++ Build Tools": ' + # For VC++ 14.X Redirect user to latest Visual C++ Build Tools + message += (' Get it with "Build Tools for Visual Studio": ' r'https://visualstudio.microsoft.com/downloads/') exc.args = (message, ) @@ -239,26 +224,50 @@ def _augment_exception(exc, version, arch=''): class PlatformInfo: """ - Current and Target Architectures informations. + Current and Target Architectures information. Parameters ---------- arch: str Target architecture. """ - current_cpu = safe_env.get('processor_architecture', '').lower() + current_cpu = environ.get('processor_architecture', '').lower() def __init__(self, arch): self.arch = arch.lower().replace('x64', 'amd64') @property def target_cpu(self): + """ + Return Target CPU architecture. + + Return + ------ + str + Target CPU + """ return self.arch[self.arch.find('_') + 1:] def target_is_x86(self): + """ + Return True if target CPU is x86 32 bits.. + + Return + ------ + bool + CPU is x86 32 bits + """ return self.target_cpu == 'x86' def current_is_x86(self): + """ + Return True if current CPU is x86 32 bits.. + + Return + ------ + bool + CPU is x86 32 bits + """ return self.current_cpu == 'x86' def current_dir(self, hidex86=False, x64=False): @@ -274,8 +283,8 @@ class PlatformInfo: Return ------ - subfolder: str - '\target', or '' (see hidex86 parameter) + str + subfolder: '\target', or '' (see hidex86 parameter) """ return ( '' if (self.current_cpu == 'x86' and hidex86) else @@ -296,8 +305,8 @@ class PlatformInfo: Return ------ - subfolder: str - '\current', or '' (see hidex86 parameter) + str + subfolder: '\current', or '' (see hidex86 parameter) """ return ( '' if (self.target_cpu == 'x86' and hidex86) else @@ -312,13 +321,13 @@ class PlatformInfo: Parameters ---------- forcex86: bool - Use 'x86' as current architecture even if current acritecture is + Use 'x86' as current architecture even if current architecture is not x86. Return ------ - subfolder: str - '' if target architecture is current architecture, + str + subfolder: '' if target architecture is current architecture, '\current_target' if not. """ current = 'x86' if forcex86 else self.current_cpu @@ -330,7 +339,7 @@ class PlatformInfo: class RegistryInfo: """ - Microsoft Visual Studio related registry informations. + Microsoft Visual Studio related registry information. Parameters ---------- @@ -349,6 +358,11 @@ class RegistryInfo: def visualstudio(self): """ Microsoft Visual Studio root registry key. + + Return + ------ + str + Registry key """ return 'VisualStudio' @@ -356,27 +370,47 @@ class RegistryInfo: def sxs(self): """ Microsoft Visual Studio SxS registry key. + + Return + ------ + str + Registry key """ - return os.path.join(self.visualstudio, 'SxS') + return join(self.visualstudio, 'SxS') @property def vc(self): """ Microsoft Visual C++ VC7 registry key. + + Return + ------ + str + Registry key """ - return os.path.join(self.sxs, 'VC7') + return join(self.sxs, 'VC7') @property def vs(self): """ Microsoft Visual Studio VS7 registry key. + + Return + ------ + str + Registry key """ - return os.path.join(self.sxs, 'VS7') + return join(self.sxs, 'VS7') @property def vc_for_python(self): """ Microsoft Visual C++ for Python registry key. + + Return + ------ + str + Registry key """ return r'DevDiv\VCForPython' @@ -384,6 +418,11 @@ class RegistryInfo: def microsoft_sdk(self): """ Microsoft SDK registry key. + + Return + ------ + str + Registry key """ return 'Microsoft SDKs' @@ -391,20 +430,35 @@ class RegistryInfo: def windows_sdk(self): """ Microsoft Windows/Platform SDK registry key. + + Return + ------ + str + Registry key """ - return os.path.join(self.microsoft_sdk, 'Windows') + return join(self.microsoft_sdk, 'Windows') @property def netfx_sdk(self): """ Microsoft .NET Framework SDK registry key. + + Return + ------ + str + Registry key """ - return os.path.join(self.microsoft_sdk, 'NETFXSDK') + return join(self.microsoft_sdk, 'NETFXSDK') @property def windows_kits_roots(self): """ Microsoft Windows Kits Roots registry key. + + Return + ------ + str + Registry key """ return r'Windows Kits\Installed Roots' @@ -421,10 +475,11 @@ class RegistryInfo: Return ------ - str: value + str + Registry key """ node64 = '' if self.pi.current_is_x86() or x86 else 'Wow6432Node' - return os.path.join('Software', node64, 'Microsoft', key) + return join('Software', node64, 'Microsoft', key) def lookup(self, key, name): """ @@ -439,18 +494,19 @@ class RegistryInfo: Return ------ - str: value + str + value """ - KEY_READ = winreg.KEY_READ + key_read = winreg.KEY_READ openkey = winreg.OpenKey ms = self.microsoft for hkey in self.HKEYS: try: - bkey = openkey(hkey, ms(key), 0, KEY_READ) + bkey = openkey(hkey, ms(key), 0, key_read) except (OSError, IOError): if not self.pi.current_is_x86(): try: - bkey = openkey(hkey, ms(key, True), 0, KEY_READ) + bkey = openkey(hkey, ms(key, True), 0, key_read) except (OSError, IOError): continue else: @@ -463,7 +519,7 @@ class RegistryInfo: class SystemInfo: """ - Microsoft Windows and Visual Studio related system inormations. + Microsoft Windows and Visual Studio related system information. Parameters ---------- @@ -474,30 +530,52 @@ class SystemInfo: """ # Variables and properties in this class use originals CamelCase variables - # names from Microsoft source files for more easy comparaison. - WinDir = safe_env.get('WinDir', '') - ProgramFiles = safe_env.get('ProgramFiles', '') - ProgramFilesx86 = safe_env.get('ProgramFiles(x86)', ProgramFiles) + # names from Microsoft source files for more easy comparison. + WinDir = environ.get('WinDir', '') + ProgramFiles = environ.get('ProgramFiles', '') + ProgramFilesx86 = environ.get('ProgramFiles(x86)', ProgramFiles) def __init__(self, registry_info, vc_ver=None): self.ri = registry_info self.pi = self.ri.pi - self.vc_ver = vc_ver or self._find_latest_available_vc_ver() - def _find_latest_available_vc_ver(self): - try: - return self.find_available_vc_vers()[-1] - except IndexError: - err = 'No Microsoft Visual C++ version found' - raise distutils.errors.DistutilsPlatformError(err) + self.known_vs_paths = self.find_programdata_vs_vers() + + # Except for VS15+, VC version is aligned with VS version + self.vs_ver = self.vc_ver = ( + vc_ver or self._find_latest_available_vs_ver()) + + def _find_latest_available_vs_ver(self): + """ + Find the latest VC version + + Return + ------ + float + version + """ + reg_vc_vers = self.find_reg_vs_vers() + + if not (reg_vc_vers or self.known_vs_paths): + raise distutils.errors.DistutilsPlatformError( + 'No Microsoft Visual C++ version found') + + vc_vers = set(reg_vc_vers) + vc_vers.update(self.known_vs_paths) + return sorted(vc_vers)[-1] - def find_available_vc_vers(self): + def find_reg_vs_vers(self): """ - Find all available Microsoft Visual C++ versions. + Find Microsoft Visual Studio versions available in registry. + + Return + ------ + list of float + Versions """ ms = self.ri.microsoft vckeys = (self.ri.vc, self.ri.vc_for_python, self.ri.vs) - vc_vers = [] + vs_vers = [] for hkey in self.ri.HKEYS: for key in vckeys: try: @@ -508,49 +586,108 @@ class SystemInfo: for i in range(values): try: ver = float(winreg.EnumValue(bkey, i)[0]) - if ver not in vc_vers: - vc_vers.append(ver) + if ver not in vs_vers: + vs_vers.append(ver) except ValueError: pass for i in range(subkeys): try: ver = float(winreg.EnumKey(bkey, i)) - if ver not in vc_vers: - vc_vers.append(ver) + if ver not in vs_vers: + vs_vers.append(ver) except ValueError: pass - return sorted(vc_vers) + return sorted(vs_vers) + + def find_programdata_vs_vers(self): + r""" + Find Visual studio 2017+ versions from information in + "C:\ProgramData\Microsoft\VisualStudio\Packages\_Instances". + + Return + ------ + dict + float version as key, path as value. + """ + vs_versions = {} + instances_dir = \ + r'C:\ProgramData\Microsoft\VisualStudio\Packages\_Instances' + + try: + hashed_names = listdir(instances_dir) + + except (OSError, IOError): + # Directory not exists with all Visual Studio versions + return vs_versions + + for name in hashed_names: + try: + # Get VS installation path from "state.json" file + state_path = join(instances_dir, name, 'state.json') + with open(state_path, 'rt', encoding='utf-8') as state_file: + state = json.load(state_file) + vs_path = state['installationPath'] + + # Raises OSError if this VS installation does not contain VC + listdir(join(vs_path, r'VC\Tools\MSVC')) + + # Store version and path + vs_versions[self._as_float_version( + state['installationVersion'])] = vs_path + + except (OSError, IOError, KeyError): + # Skip if "state.json" file is missing or bad format + continue + + return vs_versions + + @staticmethod + def _as_float_version(version): + """ + Return a string version as a simplified float version (major.minor) + + Parameters + ---------- + version: str + Version. + + Return + ------ + float + version + """ + return float('.'.join(version.split('.')[:2])) @property def VSInstallDir(self): """ Microsoft Visual Studio directory. + + Return + ------ + str + path """ # Default path - name = 'Microsoft Visual Studio %0.1f' % self.vc_ver - default = os.path.join(self.ProgramFilesx86, name) + default = join(self.ProgramFilesx86, + 'Microsoft Visual Studio %0.1f' % self.vs_ver) # Try to get path from registry, if fail use default path - return self.ri.lookup(self.ri.vs, '%0.1f' % self.vc_ver) or default + return self.ri.lookup(self.ri.vs, '%0.1f' % self.vs_ver) or default @property def VCInstallDir(self): """ Microsoft Visual C++ directory. - """ - self.VSInstallDir - - guess_vc = self._guess_vc() or self._guess_vc_legacy() - - # Try to get "VC++ for Python" path from registry as default path - reg_path = os.path.join(self.ri.vc_for_python, '%0.1f' % self.vc_ver) - python_vc = self.ri.lookup(reg_path, 'installdir') - default_vc = os.path.join(python_vc, 'VC') if python_vc else guess_vc - # Try to get path from registry, if fail use default path - path = self.ri.lookup(self.ri.vc, '%0.1f' % self.vc_ver) or default_vc + Return + ------ + str + path + """ + path = self._guess_vc() or self._guess_vc_legacy() - if not os.path.isdir(path): + if not isdir(path): msg = 'Microsoft Visual C++ directory not found' raise distutils.errors.DistutilsPlatformError(msg) @@ -558,186 +695,256 @@ class SystemInfo: def _guess_vc(self): """ - Locate Visual C for 2017 + Locate Visual C++ for VS2017+. + + Return + ------ + str + path """ - if self.vc_ver <= 14.0: - return + if self.vs_ver <= 14.0: + return '' + + try: + # First search in known VS paths + vs_dir = self.known_vs_paths[self.vs_ver] + except KeyError: + # Else, search with path from registry + vs_dir = self.VSInstallDir + + guess_vc = join(vs_dir, r'VC\Tools\MSVC') - default = r'VC\Tools\MSVC' - guess_vc = os.path.join(self.VSInstallDir, default) # Subdir with VC exact version as name try: - vc_exact_ver = os.listdir(guess_vc)[-1] - return os.path.join(guess_vc, vc_exact_ver) + # Update the VC version with real one instead of VS version + vc_ver = listdir(guess_vc)[-1] + self.vc_ver = self._as_float_version(vc_ver) + return join(guess_vc, vc_ver) except (OSError, IOError, IndexError): - pass + return '' def _guess_vc_legacy(self): """ - Locate Visual C for versions prior to 2017 + Locate Visual C++ for versions prior to 2017. + + Return + ------ + str + path """ - default = r'Microsoft Visual Studio %0.1f\VC' % self.vc_ver - return os.path.join(self.ProgramFilesx86, default) + default = join(self.ProgramFilesx86, + r'Microsoft Visual Studio %0.1f\VC' % self.vs_ver) + + # Try to get "VC++ for Python" path from registry as default path + reg_path = join(self.ri.vc_for_python, '%0.1f' % self.vs_ver) + python_vc = self.ri.lookup(reg_path, 'installdir') + default_vc = join(python_vc, 'VC') if python_vc else default + + # Try to get path from registry, if fail use default path + return self.ri.lookup(self.ri.vc, '%0.1f' % self.vs_ver) or default_vc @property def WindowsSdkVersion(self): """ Microsoft Windows SDK versions for specified MSVC++ version. - """ - if self.vc_ver <= 9.0: - return ('7.0', '6.1', '6.0a') - elif self.vc_ver == 10.0: - return ('7.1', '7.0a') - elif self.vc_ver == 11.0: - return ('8.0', '8.0a') - elif self.vc_ver == 12.0: - return ('8.1', '8.1a') - elif self.vc_ver >= 14.0: - return ('10.0', '8.1') + + Return + ------ + tuple of str + versions + """ + if self.vs_ver <= 9.0: + return '7.0', '6.1', '6.0a' + elif self.vs_ver == 10.0: + return '7.1', '7.0a' + elif self.vs_ver == 11.0: + return '8.0', '8.0a' + elif self.vs_ver == 12.0: + return '8.1', '8.1a' + elif self.vs_ver >= 14.0: + return '10.0', '8.1' @property def WindowsSdkLastVersion(self): """ - Microsoft Windows SDK last version + Microsoft Windows SDK last version. + + Return + ------ + str + version """ - return self._use_last_dir_name(os.path.join( - self.WindowsSdkDir, 'lib')) + return self._use_last_dir_name(join(self.WindowsSdkDir, 'lib')) @property def WindowsSdkDir(self): """ Microsoft Windows SDK directory. + + Return + ------ + str + path """ sdkdir = '' for ver in self.WindowsSdkVersion: # Try to get it from registry - loc = os.path.join(self.ri.windows_sdk, 'v%s' % ver) + loc = join(self.ri.windows_sdk, 'v%s' % ver) sdkdir = self.ri.lookup(loc, 'installationfolder') if sdkdir: break - if not sdkdir or not os.path.isdir(sdkdir): + if not sdkdir or not isdir(sdkdir): # Try to get "VC++ for Python" version from registry - path = os.path.join(self.ri.vc_for_python, '%0.1f' % self.vc_ver) + path = join(self.ri.vc_for_python, '%0.1f' % self.vc_ver) install_base = self.ri.lookup(path, 'installdir') if install_base: - sdkdir = os.path.join(install_base, 'WinSDK') - if not sdkdir or not os.path.isdir(sdkdir): + sdkdir = join(install_base, 'WinSDK') + if not sdkdir or not isdir(sdkdir): # If fail, use default new path for ver in self.WindowsSdkVersion: intver = ver[:ver.rfind('.')] - path = r'Microsoft SDKs\Windows Kits\%s' % (intver) - d = os.path.join(self.ProgramFiles, path) - if os.path.isdir(d): + path = r'Microsoft SDKs\Windows Kits\%s' % intver + d = join(self.ProgramFiles, path) + if isdir(d): sdkdir = d - if not sdkdir or not os.path.isdir(sdkdir): + if not sdkdir or not isdir(sdkdir): # If fail, use default old path for ver in self.WindowsSdkVersion: path = r'Microsoft SDKs\Windows\v%s' % ver - d = os.path.join(self.ProgramFiles, path) - if os.path.isdir(d): + d = join(self.ProgramFiles, path) + if isdir(d): sdkdir = d if not sdkdir: # If fail, use Platform SDK - sdkdir = os.path.join(self.VCInstallDir, 'PlatformSDK') + sdkdir = join(self.VCInstallDir, 'PlatformSDK') return sdkdir @property def WindowsSDKExecutablePath(self): """ Microsoft Windows SDK executable directory. + + Return + ------ + str + path """ # Find WinSDK NetFx Tools registry dir name - if self.vc_ver <= 11.0: + if self.vs_ver <= 11.0: netfxver = 35 arch = '' else: netfxver = 40 - hidex86 = True if self.vc_ver <= 12.0 else False + hidex86 = True if self.vs_ver <= 12.0 else False arch = self.pi.current_dir(x64=True, hidex86=hidex86) fx = 'WinSDK-NetFx%dTools%s' % (netfxver, arch.replace('\\', '-')) - # liste all possibles registry paths + # list all possibles registry paths regpaths = [] - if self.vc_ver >= 14.0: + if self.vs_ver >= 14.0: for ver in self.NetFxSdkVersion: - regpaths += [os.path.join(self.ri.netfx_sdk, ver, fx)] + regpaths += [join(self.ri.netfx_sdk, ver, fx)] for ver in self.WindowsSdkVersion: - regpaths += [os.path.join(self.ri.windows_sdk, 'v%sA' % ver, fx)] + regpaths += [join(self.ri.windows_sdk, 'v%sA' % ver, fx)] # Return installation folder from the more recent path for path in regpaths: execpath = self.ri.lookup(path, 'installationfolder') if execpath: - break - return execpath + return execpath @property def FSharpInstallDir(self): """ Microsoft Visual F# directory. + + Return + ------ + str + path """ - path = r'%0.1f\Setup\F#' % self.vc_ver - path = os.path.join(self.ri.visualstudio, path) + path = join(self.ri.visualstudio, r'%0.1f\Setup\F#' % self.vs_ver) return self.ri.lookup(path, 'productdir') or '' @property def UniversalCRTSdkDir(self): """ Microsoft Universal CRT SDK directory. + + Return + ------ + str + path """ # Set Kit Roots versions for specified MSVC++ version - if self.vc_ver >= 14.0: - vers = ('10', '81') - else: - vers = () + vers = ('10', '81') if self.vs_ver >= 14.0 else () # Find path of the more recent Kit for ver in vers: sdkdir = self.ri.lookup(self.ri.windows_kits_roots, 'kitsroot%s' % ver) if sdkdir: - break - return sdkdir or '' + return sdkdir or '' @property def UniversalCRTSdkLastVersion(self): """ - Microsoft Universal C Runtime SDK last version + Microsoft Universal C Runtime SDK last version. + + Return + ------ + str + version """ - return self._use_last_dir_name(os.path.join( - self.UniversalCRTSdkDir, 'lib')) + return self._use_last_dir_name(join(self.UniversalCRTSdkDir, 'lib')) @property def NetFxSdkVersion(self): """ Microsoft .NET Framework SDK versions. + + Return + ------ + tuple of str + versions """ - # Set FxSdk versions for specified MSVC++ version - if self.vc_ver >= 14.0: - return ('4.6.1', '4.6') - else: - return () + # Set FxSdk versions for specified VS version + return (('4.7.2', '4.7.1', '4.7', + '4.6.2', '4.6.1', '4.6', + '4.5.2', '4.5.1', '4.5') + if self.vs_ver >= 14.0 else ()) @property def NetFxSdkDir(self): """ Microsoft .NET Framework SDK directory. + + Return + ------ + str + path """ + sdkdir = '' for ver in self.NetFxSdkVersion: - loc = os.path.join(self.ri.netfx_sdk, ver) + loc = join(self.ri.netfx_sdk, ver) sdkdir = self.ri.lookup(loc, 'kitsinstallationfolder') if sdkdir: break - return sdkdir or '' + return sdkdir @property def FrameworkDir32(self): """ Microsoft .NET Framework 32bit directory. + + Return + ------ + str + path """ # Default path - guess_fw = os.path.join(self.WinDir, r'Microsoft.NET\Framework') + guess_fw = join(self.WinDir, r'Microsoft.NET\Framework') # Try to get path from registry, if fail use default path return self.ri.lookup(self.ri.vc, 'frameworkdir32') or guess_fw @@ -746,9 +953,14 @@ class SystemInfo: def FrameworkDir64(self): """ Microsoft .NET Framework 64bit directory. + + Return + ------ + str + path """ # Default path - guess_fw = os.path.join(self.WinDir, r'Microsoft.NET\Framework64') + guess_fw = join(self.WinDir, r'Microsoft.NET\Framework64') # Try to get path from registry, if fail use default path return self.ri.lookup(self.ri.vc, 'frameworkdir64') or guess_fw @@ -757,6 +969,11 @@ class SystemInfo: def FrameworkVersion32(self): """ Microsoft .NET Framework 32bit versions. + + Return + ------ + tuple of str + versions """ return self._find_dot_net_versions(32) @@ -764,6 +981,11 @@ class SystemInfo: def FrameworkVersion64(self): """ Microsoft .NET Framework 64bit versions. + + Return + ------ + tuple of str + versions """ return self._find_dot_net_versions(64) @@ -775,6 +997,11 @@ class SystemInfo: ---------- bits: int Platform number of bits: 32 or 64. + + Return + ------ + tuple of str + versions """ # Find actual .NET version in registry reg_ver = self.ri.lookup(self.ri.vc, 'frameworkver%d' % bits) @@ -782,18 +1009,17 @@ class SystemInfo: ver = reg_ver or self._use_last_dir_name(dot_net_dir, 'v') or '' # Set .NET versions for specified MSVC++ version - if self.vc_ver >= 12.0: - frameworkver = (ver, 'v4.0') - elif self.vc_ver >= 10.0: - frameworkver = ('v4.0.30319' if ver.lower()[:2] != 'v4' else ver, - 'v3.5') - elif self.vc_ver == 9.0: - frameworkver = ('v3.5', 'v2.0.50727') - if self.vc_ver == 8.0: - frameworkver = ('v3.0', 'v2.0.50727') - return frameworkver - - def _use_last_dir_name(self, path, prefix=''): + if self.vs_ver >= 12.0: + return ver, 'v4.0' + elif self.vs_ver >= 10.0: + return 'v4.0.30319' if ver.lower()[:2] != 'v4' else ver, 'v3.5' + elif self.vs_ver == 9.0: + return 'v3.5', 'v2.0.50727' + elif self.vs_ver == 8.0: + return 'v3.0', 'v2.0.50727' + + @staticmethod + def _use_last_dir_name(path, prefix=''): """ Return name of the last dir in path or '' if no dir found. @@ -802,12 +1028,17 @@ class SystemInfo: path: str Use dirs in this path prefix: str - Use only dirs startings by this prefix + Use only dirs starting by this prefix + + Return + ------ + str + name """ matching_dirs = ( dir_name - for dir_name in reversed(os.listdir(path)) - if os.path.isdir(os.path.join(path, dir_name)) and + for dir_name in reversed(listdir(path)) + if isdir(join(path, dir_name)) and dir_name.startswith(prefix) ) return next(matching_dirs, None) or '' @@ -818,7 +1049,7 @@ class EnvironmentInfo: Return environment variables for specified Microsoft Visual C++ version and platform : Lib, Include, Path and libpath. - This function is compatible with Microsoft Visual C++ 9.0 to 14.0. + This function is compatible with Microsoft Visual C++ 9.0 to 14.X. Script created by analysing Microsoft environment configuration files like "vcvars[...].bat", "SetEnv.Cmd", "vcbuildtools.bat", ... @@ -835,7 +1066,7 @@ class EnvironmentInfo: """ # Variables and properties in this class use originals CamelCase variables - # names from Microsoft source files for more easy comparaison. + # names from Microsoft source files for more easy comparison. def __init__(self, arch, vc_ver=None, vc_min_ver=0): self.pi = PlatformInfo(arch) @@ -846,205 +1077,255 @@ class EnvironmentInfo: err = 'No suitable Microsoft Visual C++ version found' raise distutils.errors.DistutilsPlatformError(err) + @property + def vs_ver(self): + """ + Microsoft Visual Studio. + + Return + ------ + float + version + """ + return self.si.vs_ver + @property def vc_ver(self): """ Microsoft Visual C++ version. + + Return + ------ + float + version """ return self.si.vc_ver @property def VSTools(self): """ - Microsoft Visual Studio Tools + Microsoft Visual Studio Tools. + + Return + ------ + list of str + paths """ paths = [r'Common7\IDE', r'Common7\Tools'] - if self.vc_ver >= 14.0: + if self.vs_ver >= 14.0: arch_subdir = self.pi.current_dir(hidex86=True, x64=True) paths += [r'Common7\IDE\CommonExtensions\Microsoft\TestWindow'] paths += [r'Team Tools\Performance Tools'] paths += [r'Team Tools\Performance Tools%s' % arch_subdir] - return [os.path.join(self.si.VSInstallDir, path) for path in paths] + return [join(self.si.VSInstallDir, path) for path in paths] @property def VCIncludes(self): """ - Microsoft Visual C++ & Microsoft Foundation Class Includes + Microsoft Visual C++ & Microsoft Foundation Class Includes. + + Return + ------ + list of str + paths """ - return [os.path.join(self.si.VCInstallDir, 'Include'), - os.path.join(self.si.VCInstallDir, r'ATLMFC\Include')] + return [join(self.si.VCInstallDir, 'Include'), + join(self.si.VCInstallDir, r'ATLMFC\Include')] @property def VCLibraries(self): """ - Microsoft Visual C++ & Microsoft Foundation Class Libraries + Microsoft Visual C++ & Microsoft Foundation Class Libraries. + + Return + ------ + list of str + paths """ - if self.vc_ver >= 15.0: + if self.vs_ver >= 15.0: arch_subdir = self.pi.target_dir(x64=True) else: arch_subdir = self.pi.target_dir(hidex86=True) paths = ['Lib%s' % arch_subdir, r'ATLMFC\Lib%s' % arch_subdir] - if self.vc_ver >= 14.0: + if self.vs_ver >= 14.0: paths += [r'Lib\store%s' % arch_subdir] - return [os.path.join(self.si.VCInstallDir, path) for path in paths] + return [join(self.si.VCInstallDir, path) for path in paths] @property def VCStoreRefs(self): """ - Microsoft Visual C++ store references Libraries + Microsoft Visual C++ store references Libraries. + + Return + ------ + list of str + paths """ - if self.vc_ver < 14.0: + if self.vs_ver < 14.0: return [] - return [os.path.join(self.si.VCInstallDir, r'Lib\store\references')] + return [join(self.si.VCInstallDir, r'Lib\store\references')] @property def VCTools(self): """ - Microsoft Visual C++ Tools + Microsoft Visual C++ Tools. + + Return + ------ + list of str + paths """ si = self.si - tools = [os.path.join(si.VCInstallDir, 'VCPackages')] + tools = [join(si.VCInstallDir, 'VCPackages')] - forcex86 = True if self.vc_ver <= 10.0 else False + forcex86 = True if self.vs_ver <= 10.0 else False arch_subdir = self.pi.cross_dir(forcex86) if arch_subdir: - tools += [os.path.join(si.VCInstallDir, 'Bin%s' % arch_subdir)] + tools += [join(si.VCInstallDir, 'Bin%s' % arch_subdir)] - if self.vc_ver == 14.0: + if self.vs_ver == 14.0: path = 'Bin%s' % self.pi.current_dir(hidex86=True) - tools += [os.path.join(si.VCInstallDir, path)] + tools += [join(si.VCInstallDir, path)] - elif self.vc_ver >= 15.0: + elif self.vs_ver >= 15.0: host_dir = (r'bin\HostX86%s' if self.pi.current_is_x86() else r'bin\HostX64%s') - tools += [os.path.join( + tools += [join( si.VCInstallDir, host_dir % self.pi.target_dir(x64=True))] if self.pi.current_cpu != self.pi.target_cpu: - tools += [os.path.join( + tools += [join( si.VCInstallDir, host_dir % self.pi.current_dir(x64=True))] else: - tools += [os.path.join(si.VCInstallDir, 'Bin')] + tools += [join(si.VCInstallDir, 'Bin')] return tools @property def OSLibraries(self): """ - Microsoft Windows SDK Libraries + Microsoft Windows SDK Libraries. + + Return + ------ + list of str + paths """ - if self.vc_ver <= 10.0: + if self.vs_ver <= 10.0: arch_subdir = self.pi.target_dir(hidex86=True, x64=True) - return [os.path.join(self.si.WindowsSdkDir, 'Lib%s' % arch_subdir)] + return [join(self.si.WindowsSdkDir, 'Lib%s' % arch_subdir)] else: arch_subdir = self.pi.target_dir(x64=True) - lib = os.path.join(self.si.WindowsSdkDir, 'lib') + lib = join(self.si.WindowsSdkDir, 'lib') libver = self._sdk_subdir - return [os.path.join(lib, '%sum%s' % (libver , arch_subdir))] + return [join(lib, '%sum%s' % (libver , arch_subdir))] @property def OSIncludes(self): """ - Microsoft Windows SDK Include + Microsoft Windows SDK Include. + + Return + ------ + list of str + paths """ - include = os.path.join(self.si.WindowsSdkDir, 'include') + include = join(self.si.WindowsSdkDir, 'include') - if self.vc_ver <= 10.0: - return [include, os.path.join(include, 'gl')] + if self.vs_ver <= 10.0: + return [include, join(include, 'gl')] else: - if self.vc_ver >= 14.0: + if self.vs_ver >= 14.0: sdkver = self._sdk_subdir else: sdkver = '' - return [os.path.join(include, '%sshared' % sdkver), - os.path.join(include, '%sum' % sdkver), - os.path.join(include, '%swinrt' % sdkver)] + return [join(include, '%sshared' % sdkver), + join(include, '%sum' % sdkver), + join(include, '%swinrt' % sdkver)] @property def OSLibpath(self): """ - Microsoft Windows SDK Libraries Paths + Microsoft Windows SDK Libraries Paths. + + Return + ------ + list of str + paths """ - ref = os.path.join(self.si.WindowsSdkDir, 'References') + ref = join(self.si.WindowsSdkDir, 'References') libpath = [] - if self.vc_ver <= 9.0: + if self.vs_ver <= 9.0: libpath += self.OSLibraries - if self.vc_ver >= 11.0: - libpath += [os.path.join(ref, r'CommonConfiguration\Neutral')] + if self.vs_ver >= 11.0: + libpath += [join(ref, r'CommonConfiguration\Neutral')] - if self.vc_ver >= 14.0: + if self.vs_ver >= 14.0: libpath += [ ref, - os.path.join(self.si.WindowsSdkDir, 'UnionMetadata'), - os.path.join( - ref, - 'Windows.Foundation.UniversalApiContract', - '1.0.0.0', - ), - os.path.join( - ref, - 'Windows.Foundation.FoundationContract', - '1.0.0.0', - ), - os.path.join( - ref, - 'Windows.Networking.Connectivity.WwanContract', - '1.0.0.0', - ), - os.path.join( - self.si.WindowsSdkDir, - 'ExtensionSDKs', - 'Microsoft.VCLibs', - '%0.1f' % self.vc_ver, - 'References', - 'CommonConfiguration', - 'neutral', - ), + join(self.si.WindowsSdkDir, 'UnionMetadata'), + join(ref, 'Windows.Foundation.UniversalApiContract', '1.0.0.0'), + join(ref, 'Windows.Foundation.FoundationContract', '1.0.0.0'), + join(ref,'Windows.Networking.Connectivity.WwanContract', + '1.0.0.0'), + join(self.si.WindowsSdkDir, 'ExtensionSDKs', 'Microsoft.VCLibs', + '%0.1f' % self.vs_ver, 'References', 'CommonConfiguration', + 'neutral'), ] return libpath @property def SdkTools(self): """ - Microsoft Windows SDK Tools + Microsoft Windows SDK Tools. + + Return + ------ + list of str + paths """ return list(self._sdk_tools()) def _sdk_tools(self): """ - Microsoft Windows SDK Tools paths generator + Microsoft Windows SDK Tools paths generator. + + Return + ------ + generator of str + paths """ - if self.vc_ver < 15.0: - bin_dir = 'Bin' if self.vc_ver <= 11.0 else r'Bin\x86' - yield os.path.join(self.si.WindowsSdkDir, bin_dir) + if self.vs_ver < 15.0: + bin_dir = 'Bin' if self.vs_ver <= 11.0 else r'Bin\x86' + yield join(self.si.WindowsSdkDir, bin_dir) if not self.pi.current_is_x86(): arch_subdir = self.pi.current_dir(x64=True) path = 'Bin%s' % arch_subdir - yield os.path.join(self.si.WindowsSdkDir, path) + yield join(self.si.WindowsSdkDir, path) - if self.vc_ver == 10.0 or self.vc_ver == 11.0: + if self.vs_ver in (10.0, 11.0): if self.pi.target_is_x86(): arch_subdir = '' else: arch_subdir = self.pi.current_dir(hidex86=True, x64=True) path = r'Bin\NETFX 4.0 Tools%s' % arch_subdir - yield os.path.join(self.si.WindowsSdkDir, path) + yield join(self.si.WindowsSdkDir, path) - elif self.vc_ver >= 15.0: - path = os.path.join(self.si.WindowsSdkDir, 'Bin') + elif self.vs_ver >= 15.0: + path = join(self.si.WindowsSdkDir, 'Bin') arch_subdir = self.pi.current_dir(x64=True) sdkver = self.si.WindowsSdkLastVersion - yield os.path.join(path, '%s%s' % (sdkver, arch_subdir)) + yield join(path, '%s%s' % (sdkver, arch_subdir)) if self.si.WindowsSDKExecutablePath: yield self.si.WindowsSDKExecutablePath @@ -1052,7 +1333,12 @@ class EnvironmentInfo: @property def _sdk_subdir(self): """ - Microsoft Windows SDK version subdir + Microsoft Windows SDK version subdir. + + Return + ------ + str + subdir """ ucrtver = self.si.WindowsSdkLastVersion return ('%s\\' % ucrtver) if ucrtver else '' @@ -1060,22 +1346,32 @@ class EnvironmentInfo: @property def SdkSetup(self): """ - Microsoft Windows SDK Setup + Microsoft Windows SDK Setup. + + Return + ------ + list of str + paths """ - if self.vc_ver > 9.0: + if self.vs_ver > 9.0: return [] - return [os.path.join(self.si.WindowsSdkDir, 'Setup')] + return [join(self.si.WindowsSdkDir, 'Setup')] @property def FxTools(self): """ - Microsoft .NET Framework Tools + Microsoft .NET Framework Tools. + + Return + ------ + list of str + paths """ pi = self.pi si = self.si - if self.vc_ver <= 10.0: + if self.vs_ver <= 10.0: include32 = True include64 = not pi.target_is_x86() and not pi.current_is_x86() else: @@ -1084,102 +1380,142 @@ class EnvironmentInfo: tools = [] if include32: - tools += [os.path.join(si.FrameworkDir32, ver) + tools += [join(si.FrameworkDir32, ver) for ver in si.FrameworkVersion32] if include64: - tools += [os.path.join(si.FrameworkDir64, ver) + tools += [join(si.FrameworkDir64, ver) for ver in si.FrameworkVersion64] return tools @property def NetFxSDKLibraries(self): """ - Microsoft .Net Framework SDK Libraries + Microsoft .Net Framework SDK Libraries. + + Return + ------ + list of str + paths """ - if self.vc_ver < 14.0 or not self.si.NetFxSdkDir: + if self.vs_ver < 14.0 or not self.si.NetFxSdkDir: return [] arch_subdir = self.pi.target_dir(x64=True) - return [os.path.join(self.si.NetFxSdkDir, r'lib\um%s' % arch_subdir)] + return [join(self.si.NetFxSdkDir, r'lib\um%s' % arch_subdir)] @property def NetFxSDKIncludes(self): """ - Microsoft .Net Framework SDK Includes + Microsoft .Net Framework SDK Includes. + + Return + ------ + list of str + paths """ - if self.vc_ver < 14.0 or not self.si.NetFxSdkDir: + if self.vs_ver < 14.0 or not self.si.NetFxSdkDir: return [] - return [os.path.join(self.si.NetFxSdkDir, r'include\um')] + return [join(self.si.NetFxSdkDir, r'include\um')] @property def VsTDb(self): """ - Microsoft Visual Studio Team System Database + Microsoft Visual Studio Team System Database. + + Return + ------ + list of str + paths """ - return [os.path.join(self.si.VSInstallDir, r'VSTSDB\Deploy')] + return [join(self.si.VSInstallDir, r'VSTSDB\Deploy')] @property def MSBuild(self): """ - Microsoft Build Engine + Microsoft Build Engine. + + Return + ------ + list of str + paths """ - if self.vc_ver < 12.0: + if self.vs_ver < 12.0: return [] - elif self.vc_ver < 15.0: + elif self.vs_ver < 15.0: base_path = self.si.ProgramFilesx86 arch_subdir = self.pi.current_dir(hidex86=True) else: base_path = self.si.VSInstallDir arch_subdir = '' - path = r'MSBuild\%0.1f\bin%s' % (self.vc_ver, arch_subdir) - build = [os.path.join(base_path, path)] + path = r'MSBuild\%0.1f\bin%s' % (self.vs_ver, arch_subdir) + build = [join(base_path, path)] - if self.vc_ver >= 15.0: + if self.vs_ver >= 15.0: # Add Roslyn C# & Visual Basic Compiler - build += [os.path.join(base_path, path, 'Roslyn')] + build += [join(base_path, path, 'Roslyn')] return build @property def HTMLHelpWorkshop(self): """ - Microsoft HTML Help Workshop + Microsoft HTML Help Workshop. + + Return + ------ + list of str + paths """ - if self.vc_ver < 11.0: + if self.vs_ver < 11.0: return [] - return [os.path.join(self.si.ProgramFilesx86, 'HTML Help Workshop')] + return [join(self.si.ProgramFilesx86, 'HTML Help Workshop')] @property def UCRTLibraries(self): """ - Microsoft Universal C Runtime SDK Libraries + Microsoft Universal C Runtime SDK Libraries. + + Return + ------ + list of str + paths """ - if self.vc_ver < 14.0: + if self.vs_ver < 14.0: return [] arch_subdir = self.pi.target_dir(x64=True) - lib = os.path.join(self.si.UniversalCRTSdkDir, 'lib') + lib = join(self.si.UniversalCRTSdkDir, 'lib') ucrtver = self._ucrt_subdir - return [os.path.join(lib, '%sucrt%s' % (ucrtver, arch_subdir))] + return [join(lib, '%sucrt%s' % (ucrtver, arch_subdir))] @property def UCRTIncludes(self): """ - Microsoft Universal C Runtime SDK Include + Microsoft Universal C Runtime SDK Include. + + Return + ------ + list of str + paths """ - if self.vc_ver < 14.0: + if self.vs_ver < 14.0: return [] - include = os.path.join(self.si.UniversalCRTSdkDir, 'include') - return [os.path.join(include, '%sucrt' % self._ucrt_subdir)] + include = join(self.si.UniversalCRTSdkDir, 'include') + return [join(include, '%sucrt' % self._ucrt_subdir)] @property def _ucrt_subdir(self): """ - Microsoft Universal C Runtime SDK version subdir + Microsoft Universal C Runtime SDK version subdir. + + Return + ------ + str + subdir """ ucrtver = self.si.UniversalCRTSdkLastVersion return ('%s\\' % ucrtver) if ucrtver else '' @@ -1187,31 +1523,52 @@ class EnvironmentInfo: @property def FSharp(self): """ - Microsoft Visual F# + Microsoft Visual F#. + + Return + ------ + list of str + paths """ - if self.vc_ver < 11.0 and self.vc_ver > 12.0: + if 11.0 > self.vs_ver > 12.0: return [] - return self.si.FSharpInstallDir + return [self.si.FSharpInstallDir] @property def VCRuntimeRedist(self): """ - Microsoft Visual C++ runtime redistribuable dll - """ - arch_subdir = self.pi.target_dir(x64=True) - if self.vc_ver < 15: - redist_path = self.si.VCInstallDir - vcruntime = 'redist%s\\Microsoft.VC%d0.CRT\\vcruntime%d0.dll' - else: - redist_path = self.si.VCInstallDir.replace('\\Tools', '\\Redist') - vcruntime = 'onecore%s\\Microsoft.VC%d0.CRT\\vcruntime%d0.dll' - - # Visual Studio 2017 is still Visual C++ 14.0 - dll_ver = 14.0 if self.vc_ver == 15 else self.vc_ver + Microsoft Visual C++ runtime redistributable dll. - vcruntime = vcruntime % (arch_subdir, self.vc_ver, dll_ver) - return os.path.join(redist_path, vcruntime) + Return + ------ + str + path + """ + vcruntime = 'vcruntime%d0.dll' % self.vc_ver + arch_subdir = self.pi.target_dir(x64=True).strip('\\') + + # Installation prefixes candidates + prefixes = [] + tools_path = self.si.VCInstallDir + redist_path = dirname(tools_path.replace(r'\Tools', r'\Redist')) + if isdir(redist_path): + # Redist version may not be exactly the same as tools + redist_path = join(redist_path, listdir(redist_path)[-1]) + prefixes += [redist_path, join(redist_path, 'onecore')] + + prefixes += [join(tools_path, 'redist')] # VS14 legacy path + + # CRT directory + crt_dirs = ('Microsoft.VC%d.CRT' % (self.vc_ver * 10), + # Sometime store in directory with VS version instead of VC + 'Microsoft.VC%d.CRT' % (int(self.vs_ver) * 10)) + + # vcruntime path + for prefix, crt_dir in itertools.product(prefixes, crt_dirs): + path = join(prefix, arch_subdir, crt_dir, vcruntime) + if isfile(path): + return path def return_env(self, exists=True): """ @@ -1221,6 +1578,11 @@ class EnvironmentInfo: ---------- exists: bool It True, only return existing paths. + + Return + ------ + dict + environment """ env = dict( include=self._build_paths('include', @@ -1254,7 +1616,7 @@ class EnvironmentInfo: self.FSharp], exists), ) - if self.vc_ver >= 14 and os.path.isfile(self.VCRuntimeRedist): + if self.vs_ver >= 14 and isfile(self.VCRuntimeRedist): env['py_vcruntime_redist'] = self.VCRuntimeRedist return env @@ -1265,20 +1627,35 @@ class EnvironmentInfo: unique, extant, directories from those paths and from the environment variable. Raise an error if no paths are resolved. + + Parameters + ---------- + name: str + Environment variable name + spec_path_lists: list of str + Paths + exists: bool + It True, only return existing paths. + + Return + ------ + str + Pathsep-separated paths """ # flatten spec_path_lists spec_paths = itertools.chain.from_iterable(spec_path_lists) - env_paths = safe_env.get(name, '').split(os.pathsep) + env_paths = environ.get(name, '').split(pathsep) paths = itertools.chain(spec_paths, env_paths) - extant_paths = list(filter(os.path.isdir, paths)) if exists else paths + extant_paths = list(filter(isdir, paths)) if exists else paths if not extant_paths: msg = "%s environment variable is empty" % name.upper() raise distutils.errors.DistutilsPlatformError(msg) unique_paths = self._unique_everseen(extant_paths) - return os.pathsep.join(unique_paths) + return pathsep.join(unique_paths) # from Python docs - def _unique_everseen(self, iterable, key=None): + @staticmethod + def _unique_everseen(iterable, key=None): """ List unique elements, preserving order. Remember all elements ever seen. -- cgit v1.2.3 From eb7436b36f9967d1becd2b822da548bd59b35d05 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 8 Jul 2019 09:32:21 -0700 Subject: Fix some usage of deprecated `imp` module --- setuptools/command/build_ext.py | 10 ++++++++-- setuptools/command/install_lib.py | 6 +++--- 2 files changed, 11 insertions(+), 5 deletions(-) (limited to 'setuptools') diff --git a/setuptools/command/build_ext.py b/setuptools/command/build_ext.py index 60a8a32f..daa8e4fe 100644 --- a/setuptools/command/build_ext.py +++ b/setuptools/command/build_ext.py @@ -1,7 +1,6 @@ import os import sys import itertools -import imp from distutils.command.build_ext import build_ext as _du_build_ext from distutils.file_util import copy_file from distutils.ccompiler import new_compiler @@ -12,6 +11,13 @@ from distutils import log from setuptools.extension import Library from setuptools.extern import six +if six.PY2: + import imp + + EXTENSION_SUFFIXES = [s for s, _, tp in imp.get_suffixes() if tp == imp.C_EXTENSION] +else: + from importlib.machinery import EXTENSION_SUFFIXES + try: # Attempt to use Cython for building extensions, if available from Cython.Distutils.build_ext import build_ext as _build_ext @@ -64,7 +70,7 @@ if_dl = lambda s: s if have_rtld else '' def get_abi3_suffix(): """Return the file extension for an abi3-compliant Extension()""" - for suffix, _, _ in (s for s in imp.get_suffixes() if s[2] == imp.C_EXTENSION): + for suffix in EXTENSION_SUFFIXES: if '.abi3' in suffix: # Unix return suffix elif suffix == '.pyd': # Windows diff --git a/setuptools/command/install_lib.py b/setuptools/command/install_lib.py index 2b31c3e3..07d65933 100644 --- a/setuptools/command/install_lib.py +++ b/setuptools/command/install_lib.py @@ -1,5 +1,5 @@ import os -import imp +import sys from itertools import product, starmap import distutils.command.install_lib as orig @@ -74,10 +74,10 @@ class install_lib(orig.install_lib): yield '__init__.pyc' yield '__init__.pyo' - if not hasattr(imp, 'get_tag'): + if not hasattr(sys, 'implementation'): return - base = os.path.join('__pycache__', '__init__.' + imp.get_tag()) + base = os.path.join('__pycache__', '__init__.' + sys.implementation.cache_tag) yield base + '.pyc' yield base + '.pyo' yield base + '.opt-1.pyc' -- cgit v1.2.3 From 43add1d3f5138e38adc4940647cc6eae94fb6123 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 17 Aug 2019 19:14:48 -0700 Subject: Fixes for python3.10 --- setuptools/command/bdist_egg.py | 2 +- setuptools/command/easy_install.py | 6 +++--- setuptools/package_index.py | 2 +- setuptools/tests/test_bdist_egg.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) (limited to 'setuptools') diff --git a/setuptools/command/bdist_egg.py b/setuptools/command/bdist_egg.py index 9f8df917..98470f17 100644 --- a/setuptools/command/bdist_egg.py +++ b/setuptools/command/bdist_egg.py @@ -284,7 +284,7 @@ class bdist_egg(Command): "or refer to a module" % (ep,) ) - pyver = sys.version[:3] + pyver = '{}.{}'.format(*sys.version_info) pkg = ep.module_name full = '.'.join(ep.attrs) base = ep.attrs[0] diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index 06c98271..593ed777 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -241,7 +241,7 @@ class easy_install(Command): """ Render the Setuptools version and installation details, then exit. """ - ver = sys.version[:3] + ver = '{}.{}'.format(*sys.version_info) dist = get_distribution('setuptools') tmpl = 'setuptools {dist.version} from {dist.location} (Python {ver})' print(tmpl.format(**locals())) @@ -1412,7 +1412,7 @@ def get_site_dirs(): os.path.join( prefix, "lib", - "python" + sys.version[:3], + "python{}.{}".format(*sys.version_info), "site-packages", ), os.path.join(prefix, "lib", "site-python"), @@ -1433,7 +1433,7 @@ def get_site_dirs(): home, 'Library', 'Python', - sys.version[:3], + '{}.{}'.format(*sys.version_info), 'site-packages', ) sitedirs.append(home_sp) diff --git a/setuptools/package_index.py b/setuptools/package_index.py index 6b06f2ca..f419d471 100644 --- a/setuptools/package_index.py +++ b/setuptools/package_index.py @@ -46,7 +46,7 @@ __all__ = [ _SOCKET_TIMEOUT = 15 _tmpl = "setuptools/{setuptools.__version__} Python-urllib/{py_major}" -user_agent = _tmpl.format(py_major=sys.version[:3], setuptools=setuptools) +user_agent = _tmpl.format(py_major='{}.{}'.format(*sys.version_info), setuptools=setuptools) def parse_requirement_arg(spec): diff --git a/setuptools/tests/test_bdist_egg.py b/setuptools/tests/test_bdist_egg.py index 54742aa6..fb5b90b1 100644 --- a/setuptools/tests/test_bdist_egg.py +++ b/setuptools/tests/test_bdist_egg.py @@ -42,7 +42,7 @@ class Test: # let's see if we got our egg link at the right place [content] = os.listdir('dist') - assert re.match(r'foo-0.0.0-py[23].\d.egg$', content) + assert re.match(r'foo-0.0.0-py[23].\d+.egg$', content) @pytest.mark.xfail( os.environ.get('PYTHONDONTWRITEBYTECODE'), -- cgit v1.2.3 From ca0ee009f81a460b483c7e451e58dfdcd7787045 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2019 16:39:57 +0100 Subject: Add test capturing failure. Ref #1787. --- setuptools/tests/test_config.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'setuptools') diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index bc97664d..42187138 100644 --- a/setuptools/tests/test_config.py +++ b/setuptools/tests/test_config.py @@ -819,6 +819,18 @@ class TestOptions: ] assert sorted(dist.data_files) == sorted(expected) + def test_python_requires_invalid(self, tmpdir): + fake_env( + tmpdir, + DALS(""" + [options] + python_requires=invalid + """), + ) + with pytest.raises(DistutilsOptionError): + with get_dist(tmpdir) as dist: + dist.parse_config_files() + saved_dist_init = _Distribution.__init__ -- cgit v1.2.3 From b31777cd50c7cb59b4ef6c22bd014f0229ef32fa Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2019 16:52:58 +0100 Subject: Add more tests for valid behavior. Expand exception, any should do. --- setuptools/tests/test_config.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) (limited to 'setuptools') diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index 42187138..1b94a586 100644 --- a/setuptools/tests/test_config.py +++ b/setuptools/tests/test_config.py @@ -819,6 +819,28 @@ class TestOptions: ] assert sorted(dist.data_files) == sorted(expected) + def test_python_requires_simple(self, tmpdir): + fake_env( + tmpdir, + DALS(""" + [options] + python_requires=>=2.7 + """), + ) + with get_dist(tmpdir) as dist: + dist.parse_config_files() + + def test_python_requires_compound(self, tmpdir): + fake_env( + tmpdir, + DALS(""" + [options] + python_requires=>=2.7,!=3.0.* + """), + ) + with get_dist(tmpdir) as dist: + dist.parse_config_files() + def test_python_requires_invalid(self, tmpdir): fake_env( tmpdir, @@ -827,7 +849,7 @@ class TestOptions: python_requires=invalid """), ) - with pytest.raises(DistutilsOptionError): + with pytest.raises(Exception): with get_dist(tmpdir) as dist: dist.parse_config_files() -- cgit v1.2.3 From c3df086ed3570e7065e6935a52d95c8cdef2b071 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2019 16:54:07 +0100 Subject: Ensure that python_requires is checked during option processing. Fixes #1787. --- setuptools/config.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'setuptools') diff --git a/setuptools/config.py b/setuptools/config.py index b6626043..2d50e25e 100644 --- a/setuptools/config.py +++ b/setuptools/config.py @@ -12,6 +12,7 @@ from importlib import import_module from distutils.errors import DistutilsOptionError, DistutilsFileError from setuptools.extern.packaging.version import LegacyVersion, parse +from setuptools.extern.packaging.specifiers import SpecifierSet from setuptools.extern.six import string_types, PY3 @@ -554,6 +555,7 @@ class ConfigOptionsHandler(ConfigHandler): 'packages': self._parse_packages, 'entry_points': self._parse_file, 'py_modules': parse_list, + 'python_requires': SpecifierSet, } def _parse_packages(self, value): -- cgit v1.2.3 From 6f962a07f586162d05e087a90ea8f44461772070 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 6 Oct 2019 20:42:14 -0400 Subject: Refresh vendored packages (ordereddict 3.1.1) --- setuptools/_vendor/ordered_set.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'setuptools') diff --git a/setuptools/_vendor/ordered_set.py b/setuptools/_vendor/ordered_set.py index d257470b..14876000 100644 --- a/setuptools/_vendor/ordered_set.py +++ b/setuptools/_vendor/ordered_set.py @@ -87,14 +87,14 @@ class OrderedSet(MutableSet, Sequence): """ if isinstance(index, slice) and index == SLICE_ALL: return self.copy() + elif is_iterable(index): + return [self.items[i] for i in index] elif hasattr(index, "__index__") or isinstance(index, slice): result = self.items[index] if isinstance(result, list): return self.__class__(result) else: return result - elif is_iterable(index): - return [self.items[i] for i in index] else: raise TypeError("Don't know how to index an OrderedSet by %r" % index) -- cgit v1.2.3 From 53d662a9de0b8d449d167bf1c5cf291a2fecb094 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 6 Oct 2019 21:17:53 -0400 Subject: Allow 'long_description_content_type' warnings for new versions of packaging. Fixes #1858. --- setuptools/tests/test_integration.py | 1 + 1 file changed, 1 insertion(+) (limited to 'setuptools') diff --git a/setuptools/tests/test_integration.py b/setuptools/tests/test_integration.py index 1e132188..1c0b2b18 100644 --- a/setuptools/tests/test_integration.py +++ b/setuptools/tests/test_integration.py @@ -143,6 +143,7 @@ def test_build_deps_on_distutils(request, tmpdir_factory, build_dep): 'tests_require', 'python_requires', 'install_requires', + 'long_description_content_type', ] assert not match or match.group(1).strip('"\'') in allowed_unknowns -- cgit v1.2.3 From 734d09c5a3f48338b6a3d7687c5f9d0ed02ab479 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 7 Oct 2019 16:36:54 -0400 Subject: Pin ordered-set to current version for consistency. --- setuptools/_vendor/vendored.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'setuptools') diff --git a/setuptools/_vendor/vendored.txt b/setuptools/_vendor/vendored.txt index 379aae56..5731b424 100644 --- a/setuptools/_vendor/vendored.txt +++ b/setuptools/_vendor/vendored.txt @@ -1,4 +1,4 @@ packaging==16.8 pyparsing==2.2.1 six==1.10.0 -ordered-set +ordered-set==3.1.1 -- cgit v1.2.3 From d7810a901382b827146874704f33bce896e1fb21 Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Sat, 10 Aug 2019 02:18:34 +0200 Subject: wheel: silence info trace when writing `requires.txt` --- setuptools/wheel.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) (limited to 'setuptools') diff --git a/setuptools/wheel.py b/setuptools/wheel.py index e11f0a1d..2982926a 100644 --- a/setuptools/wheel.py +++ b/setuptools/wheel.py @@ -1,6 +1,7 @@ """Wheels support.""" from distutils.util import get_platform +from distutils import log import email import itertools import os @@ -162,11 +163,17 @@ class Wheel: extras_require=extras_require, ), ) - write_requirements( - setup_dist.get_command_obj('egg_info'), - None, - os.path.join(egg_info, 'requires.txt'), - ) + # Temporarily disable info traces. + log_threshold = log._global_log.threshold + log.set_threshold(log.WARN) + try: + write_requirements( + setup_dist.get_command_obj('egg_info'), + None, + os.path.join(egg_info, 'requires.txt'), + ) + finally: + log.set_threshold(log_threshold) @staticmethod def _move_data_entries(destination_eggdir, dist_data): -- cgit v1.2.3 From 16a3ef93fc66373f6c5f4da12303dd111403fcb1 Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Mon, 17 Sep 2018 23:40:12 +0200 Subject: wheel: fix installation of empty namespace package --- setuptools/tests/test_wheel.py | 28 ++++++++++++++++++++++++++++ setuptools/wheel.py | 4 +++- 2 files changed, 31 insertions(+), 1 deletion(-) (limited to 'setuptools') diff --git a/setuptools/tests/test_wheel.py b/setuptools/tests/test_wheel.py index e85a4a7e..d50816c2 100644 --- a/setuptools/tests/test_wheel.py +++ b/setuptools/tests/test_wheel.py @@ -450,6 +450,34 @@ WHEEL_INSTALL_TESTS = ( }), ), + dict( + id='empty_namespace_package', + file_defs={ + 'foobar': { + '__init__.py': "__import__('pkg_resources').declare_namespace(__name__)", + }, + }, + setup_kwargs=dict( + namespace_packages=['foobar'], + packages=['foobar'], + ), + install_tree=flatten_tree({ + 'foo-1.0-py{py_version}.egg': [ + 'foo-1.0-py{py_version}-nspkg.pth', + {'EGG-INFO': [ + 'PKG-INFO', + 'RECORD', + 'WHEEL', + 'namespace_packages.txt', + 'top_level.txt', + ]}, + {'foobar': [ + '__init__.py', + ]}, + ] + }), + ), + dict( id='data_in_package', file_defs={ diff --git a/setuptools/wheel.py b/setuptools/wheel.py index 2982926a..22eec05e 100644 --- a/setuptools/wheel.py +++ b/setuptools/wheel.py @@ -213,6 +213,8 @@ class Wheel: for mod in namespace_packages: mod_dir = os.path.join(destination_eggdir, *mod.split('.')) mod_init = os.path.join(mod_dir, '__init__.py') - if os.path.exists(mod_dir) and not os.path.exists(mod_init): + if not os.path.exists(mod_dir): + os.mkdir(mod_dir) + if not os.path.exists(mod_init): with open(mod_init, 'w') as fp: fp.write(NAMESPACE_PACKAGE_INIT) -- cgit v1.2.3 From cb8769d7d1a694d37194c44b98c543b2d5d38fc5 Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Wed, 26 Jun 2019 22:25:03 +0200 Subject: minor cleanup --- setuptools/command/easy_install.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'setuptools') diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index 06c98271..d7b7566c 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -1180,8 +1180,7 @@ class easy_install(Command): # to the setup.cfg file. ei_opts = self.distribution.get_option_dict('easy_install').copy() fetch_directives = ( - 'find_links', 'site_dirs', 'index_url', 'optimize', - 'site_dirs', 'allow_hosts', + 'find_links', 'site_dirs', 'index_url', 'optimize', 'allow_hosts', ) fetch_options = {} for key, val in ei_opts.items(): -- cgit v1.2.3 From bf069fe9ddcadaa2c029067601d06a07d037d4f7 Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Fri, 23 Aug 2019 23:07:52 +0200 Subject: setuptools: update vendored packaging --- setuptools/_vendor/packaging/__about__.py | 14 +- setuptools/_vendor/packaging/__init__.py | 20 +- setuptools/_vendor/packaging/_compat.py | 7 +- setuptools/_vendor/packaging/_structures.py | 4 +- setuptools/_vendor/packaging/markers.py | 91 +++--- setuptools/_vendor/packaging/requirements.py | 41 ++- setuptools/_vendor/packaging/specifiers.py | 71 ++--- setuptools/_vendor/packaging/tags.py | 404 +++++++++++++++++++++++++++ setuptools/_vendor/packaging/utils.py | 43 +++ setuptools/_vendor/packaging/version.py | 149 ++++++---- setuptools/_vendor/vendored.txt | 2 +- 11 files changed, 660 insertions(+), 186 deletions(-) create mode 100644 setuptools/_vendor/packaging/tags.py (limited to 'setuptools') diff --git a/setuptools/_vendor/packaging/__about__.py b/setuptools/_vendor/packaging/__about__.py index 95d330ef..dc95138d 100644 --- a/setuptools/_vendor/packaging/__about__.py +++ b/setuptools/_vendor/packaging/__about__.py @@ -4,18 +4,24 @@ from __future__ import absolute_import, division, print_function __all__ = [ - "__title__", "__summary__", "__uri__", "__version__", "__author__", - "__email__", "__license__", "__copyright__", + "__title__", + "__summary__", + "__uri__", + "__version__", + "__author__", + "__email__", + "__license__", + "__copyright__", ] __title__ = "packaging" __summary__ = "Core utilities for Python packages" __uri__ = "https://github.com/pypa/packaging" -__version__ = "16.8" +__version__ = "19.2" __author__ = "Donald Stufft and individual contributors" __email__ = "donald@stufft.io" __license__ = "BSD or Apache License, Version 2.0" -__copyright__ = "Copyright 2014-2016 %s" % __author__ +__copyright__ = "Copyright 2014-2019 %s" % __author__ diff --git a/setuptools/_vendor/packaging/__init__.py b/setuptools/_vendor/packaging/__init__.py index 5ee62202..a0cf67df 100644 --- a/setuptools/_vendor/packaging/__init__.py +++ b/setuptools/_vendor/packaging/__init__.py @@ -4,11 +4,23 @@ from __future__ import absolute_import, division, print_function from .__about__ import ( - __author__, __copyright__, __email__, __license__, __summary__, __title__, - __uri__, __version__ + __author__, + __copyright__, + __email__, + __license__, + __summary__, + __title__, + __uri__, + __version__, ) __all__ = [ - "__title__", "__summary__", "__uri__", "__version__", "__author__", - "__email__", "__license__", "__copyright__", + "__title__", + "__summary__", + "__uri__", + "__version__", + "__author__", + "__email__", + "__license__", + "__copyright__", ] diff --git a/setuptools/_vendor/packaging/_compat.py b/setuptools/_vendor/packaging/_compat.py index 210bb80b..25da473c 100644 --- a/setuptools/_vendor/packaging/_compat.py +++ b/setuptools/_vendor/packaging/_compat.py @@ -12,9 +12,9 @@ PY3 = sys.version_info[0] == 3 # flake8: noqa if PY3: - string_types = str, + string_types = (str,) else: - string_types = basestring, + string_types = (basestring,) def with_metaclass(meta, *bases): @@ -27,4 +27,5 @@ def with_metaclass(meta, *bases): class metaclass(meta): def __new__(cls, name, this_bases, d): return meta(name, bases, d) - return type.__new__(metaclass, 'temporary_class', (), {}) + + return type.__new__(metaclass, "temporary_class", (), {}) diff --git a/setuptools/_vendor/packaging/_structures.py b/setuptools/_vendor/packaging/_structures.py index ccc27861..68dcca63 100644 --- a/setuptools/_vendor/packaging/_structures.py +++ b/setuptools/_vendor/packaging/_structures.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function class Infinity(object): - def __repr__(self): return "Infinity" @@ -33,11 +32,11 @@ class Infinity(object): def __neg__(self): return NegativeInfinity + Infinity = Infinity() class NegativeInfinity(object): - def __repr__(self): return "-Infinity" @@ -65,4 +64,5 @@ class NegativeInfinity(object): def __neg__(self): return Infinity + NegativeInfinity = NegativeInfinity() diff --git a/setuptools/_vendor/packaging/markers.py b/setuptools/_vendor/packaging/markers.py index 031332a3..4bdfdb24 100644 --- a/setuptools/_vendor/packaging/markers.py +++ b/setuptools/_vendor/packaging/markers.py @@ -17,8 +17,11 @@ from .specifiers import Specifier, InvalidSpecifier __all__ = [ - "InvalidMarker", "UndefinedComparison", "UndefinedEnvironmentName", - "Marker", "default_environment", + "InvalidMarker", + "UndefinedComparison", + "UndefinedEnvironmentName", + "Marker", + "default_environment", ] @@ -42,7 +45,6 @@ class UndefinedEnvironmentName(ValueError): class Node(object): - def __init__(self, value): self.value = value @@ -57,62 +59,52 @@ class Node(object): class Variable(Node): - def serialize(self): return str(self) class Value(Node): - def serialize(self): return '"{0}"'.format(self) class Op(Node): - def serialize(self): return str(self) VARIABLE = ( - L("implementation_version") | - L("platform_python_implementation") | - L("implementation_name") | - L("python_full_version") | - L("platform_release") | - L("platform_version") | - L("platform_machine") | - L("platform_system") | - L("python_version") | - L("sys_platform") | - L("os_name") | - L("os.name") | # PEP-345 - L("sys.platform") | # PEP-345 - L("platform.version") | # PEP-345 - L("platform.machine") | # PEP-345 - L("platform.python_implementation") | # PEP-345 - L("python_implementation") | # undocumented setuptools legacy - L("extra") + L("implementation_version") + | L("platform_python_implementation") + | L("implementation_name") + | L("python_full_version") + | L("platform_release") + | L("platform_version") + | L("platform_machine") + | L("platform_system") + | L("python_version") + | L("sys_platform") + | L("os_name") + | L("os.name") + | L("sys.platform") # PEP-345 + | L("platform.version") # PEP-345 + | L("platform.machine") # PEP-345 + | L("platform.python_implementation") # PEP-345 + | L("python_implementation") # PEP-345 + | L("extra") # undocumented setuptools legacy ) ALIASES = { - 'os.name': 'os_name', - 'sys.platform': 'sys_platform', - 'platform.version': 'platform_version', - 'platform.machine': 'platform_machine', - 'platform.python_implementation': 'platform_python_implementation', - 'python_implementation': 'platform_python_implementation' + "os.name": "os_name", + "sys.platform": "sys_platform", + "platform.version": "platform_version", + "platform.machine": "platform_machine", + "platform.python_implementation": "platform_python_implementation", + "python_implementation": "platform_python_implementation", } VARIABLE.setParseAction(lambda s, l, t: Variable(ALIASES.get(t[0], t[0]))) VERSION_CMP = ( - L("===") | - L("==") | - L(">=") | - L("<=") | - L("!=") | - L("~=") | - L(">") | - L("<") + L("===") | L("==") | L(">=") | L("<=") | L("!=") | L("~=") | L(">") | L("<") ) MARKER_OP = VERSION_CMP | L("not in") | L("in") @@ -152,8 +144,11 @@ def _format_marker(marker, first=True): # where the single item is itself it's own list. In that case we want skip # the rest of this function so that we don't get extraneous () on the # outside. - if (isinstance(marker, list) and len(marker) == 1 and - isinstance(marker[0], (list, tuple))): + if ( + isinstance(marker, list) + and len(marker) == 1 + and isinstance(marker[0], (list, tuple)) + ): return _format_marker(marker[0]) if isinstance(marker, list): @@ -239,20 +234,20 @@ def _evaluate_markers(markers, environment): def format_full_version(info): - version = '{0.major}.{0.minor}.{0.micro}'.format(info) + version = "{0.major}.{0.minor}.{0.micro}".format(info) kind = info.releaselevel - if kind != 'final': + if kind != "final": version += kind[0] + str(info.serial) return version def default_environment(): - if hasattr(sys, 'implementation'): + if hasattr(sys, "implementation"): iver = format_full_version(sys.implementation.version) implementation_name = sys.implementation.name else: - iver = '0' - implementation_name = '' + iver = "0" + implementation_name = "" return { "implementation_name": implementation_name, @@ -264,19 +259,19 @@ def default_environment(): "platform_version": platform.version(), "python_full_version": platform.python_version(), "platform_python_implementation": platform.python_implementation(), - "python_version": platform.python_version()[:3], + "python_version": ".".join(platform.python_version_tuple()[:2]), "sys_platform": sys.platform, } class Marker(object): - def __init__(self, marker): try: self._markers = _coerce_parse_result(MARKER.parseString(marker)) except ParseException as e: err_str = "Invalid marker: {0!r}, parse error at {1!r}".format( - marker, marker[e.loc:e.loc + 8]) + marker, marker[e.loc : e.loc + 8] + ) raise InvalidMarker(err_str) def __str__(self): diff --git a/setuptools/_vendor/packaging/requirements.py b/setuptools/_vendor/packaging/requirements.py index 5b493416..8a0c2cb9 100644 --- a/setuptools/_vendor/packaging/requirements.py +++ b/setuptools/_vendor/packaging/requirements.py @@ -38,8 +38,8 @@ IDENTIFIER = Combine(ALPHANUM + ZeroOrMore(IDENTIFIER_END)) NAME = IDENTIFIER("name") EXTRA = IDENTIFIER -URI = Regex(r'[^ ]+')("url") -URL = (AT + URI) +URI = Regex(r"[^ ]+")("url") +URL = AT + URI EXTRAS_LIST = EXTRA + ZeroOrMore(COMMA + EXTRA) EXTRAS = (LBRACKET + Optional(EXTRAS_LIST) + RBRACKET)("extras") @@ -48,28 +48,31 @@ VERSION_PEP440 = Regex(Specifier._regex_str, re.VERBOSE | re.IGNORECASE) VERSION_LEGACY = Regex(LegacySpecifier._regex_str, re.VERBOSE | re.IGNORECASE) VERSION_ONE = VERSION_PEP440 ^ VERSION_LEGACY -VERSION_MANY = Combine(VERSION_ONE + ZeroOrMore(COMMA + VERSION_ONE), - joinString=",", adjacent=False)("_raw_spec") +VERSION_MANY = Combine( + VERSION_ONE + ZeroOrMore(COMMA + VERSION_ONE), joinString=",", adjacent=False +)("_raw_spec") _VERSION_SPEC = Optional(((LPAREN + VERSION_MANY + RPAREN) | VERSION_MANY)) -_VERSION_SPEC.setParseAction(lambda s, l, t: t._raw_spec or '') +_VERSION_SPEC.setParseAction(lambda s, l, t: t._raw_spec or "") VERSION_SPEC = originalTextFor(_VERSION_SPEC)("specifier") VERSION_SPEC.setParseAction(lambda s, l, t: t[1]) MARKER_EXPR = originalTextFor(MARKER_EXPR())("marker") MARKER_EXPR.setParseAction( - lambda s, l, t: Marker(s[t._original_start:t._original_end]) + lambda s, l, t: Marker(s[t._original_start : t._original_end]) ) -MARKER_SEPERATOR = SEMICOLON -MARKER = MARKER_SEPERATOR + MARKER_EXPR +MARKER_SEPARATOR = SEMICOLON +MARKER = MARKER_SEPARATOR + MARKER_EXPR VERSION_AND_MARKER = VERSION_SPEC + Optional(MARKER) URL_AND_MARKER = URL + Optional(MARKER) -NAMED_REQUIREMENT = \ - NAME + Optional(EXTRAS) + (URL_AND_MARKER | VERSION_AND_MARKER) +NAMED_REQUIREMENT = NAME + Optional(EXTRAS) + (URL_AND_MARKER | VERSION_AND_MARKER) REQUIREMENT = stringStart + NAMED_REQUIREMENT + stringEnd +# setuptools.extern.pyparsing isn't thread safe during initialization, so we do it eagerly, see +# issue #104 +REQUIREMENT.parseString("x[]") class Requirement(object): @@ -90,15 +93,21 @@ class Requirement(object): req = REQUIREMENT.parseString(requirement_string) except ParseException as e: raise InvalidRequirement( - "Invalid requirement, parse error at \"{0!r}\"".format( - requirement_string[e.loc:e.loc + 8])) + 'Parse error at "{0!r}": {1}'.format( + requirement_string[e.loc : e.loc + 8], e.msg + ) + ) self.name = req.name if req.url: parsed_url = urlparse.urlparse(req.url) - if not (parsed_url.scheme and parsed_url.netloc) or ( - not parsed_url.scheme and not parsed_url.netloc): - raise InvalidRequirement("Invalid URL given") + if parsed_url.scheme == "file": + if urlparse.urlunparse(parsed_url) != req.url: + raise InvalidRequirement("Invalid URL given") + elif not (parsed_url.scheme and parsed_url.netloc) or ( + not parsed_url.scheme and not parsed_url.netloc + ): + raise InvalidRequirement("Invalid URL: {0}".format(req.url)) self.url = req.url else: self.url = None @@ -117,6 +126,8 @@ class Requirement(object): if self.url: parts.append("@ {0}".format(self.url)) + if self.marker: + parts.append(" ") if self.marker: parts.append("; {0}".format(self.marker)) diff --git a/setuptools/_vendor/packaging/specifiers.py b/setuptools/_vendor/packaging/specifiers.py index 7f5a76cf..743576a0 100644 --- a/setuptools/_vendor/packaging/specifiers.py +++ b/setuptools/_vendor/packaging/specifiers.py @@ -19,7 +19,6 @@ class InvalidSpecifier(ValueError): class BaseSpecifier(with_metaclass(abc.ABCMeta, object)): - @abc.abstractmethod def __str__(self): """ @@ -84,10 +83,7 @@ class _IndividualSpecifier(BaseSpecifier): if not match: raise InvalidSpecifier("Invalid specifier: '{0}'".format(spec)) - self._spec = ( - match.group("operator").strip(), - match.group("version").strip(), - ) + self._spec = (match.group("operator").strip(), match.group("version").strip()) # Store whether or not this Specifier should accept prereleases self._prereleases = prereleases @@ -99,11 +95,7 @@ class _IndividualSpecifier(BaseSpecifier): else "" ) - return "<{0}({1!r}{2})>".format( - self.__class__.__name__, - str(self), - pre, - ) + return "<{0}({1!r}{2})>".format(self.__class__.__name__, str(self), pre) def __str__(self): return "{0}{1}".format(*self._spec) @@ -194,11 +186,12 @@ class _IndividualSpecifier(BaseSpecifier): # If our version is a prerelease, and we were not set to allow # prereleases, then we'll store it for later incase nothing # else matches this specifier. - if (parsed_version.is_prerelease and not - (prereleases or self.prereleases)): + if parsed_version.is_prerelease and not ( + prereleases or self.prereleases + ): found_prereleases.append(version) # Either this is not a prerelease, or we should have been - # accepting prereleases from the begining. + # accepting prereleases from the beginning. else: yielded = True yield version @@ -213,8 +206,7 @@ class _IndividualSpecifier(BaseSpecifier): class LegacySpecifier(_IndividualSpecifier): - _regex_str = ( - r""" + _regex_str = r""" (?P(==|!=|<=|>=|<|>)) \s* (?P @@ -225,10 +217,8 @@ class LegacySpecifier(_IndividualSpecifier): # them, and a comma since it's a version separator. ) """ - ) - _regex = re.compile( - r"^\s*" + _regex_str + r"\s*$", re.VERBOSE | re.IGNORECASE) + _regex = re.compile(r"^\s*" + _regex_str + r"\s*$", re.VERBOSE | re.IGNORECASE) _operators = { "==": "equal", @@ -269,13 +259,13 @@ def _require_version_compare(fn): if not isinstance(prospective, Version): return False return fn(self, prospective, spec) + return wrapped class Specifier(_IndividualSpecifier): - _regex_str = ( - r""" + _regex_str = r""" (?P(~=|==|!=|<=|>=|<|>|===)) (?P (?: @@ -367,10 +357,8 @@ class Specifier(_IndividualSpecifier): ) ) """ - ) - _regex = re.compile( - r"^\s*" + _regex_str + r"\s*$", re.VERBOSE | re.IGNORECASE) + _regex = re.compile(r"^\s*" + _regex_str + r"\s*$", re.VERBOSE | re.IGNORECASE) _operators = { "~=": "compatible", @@ -397,8 +385,7 @@ class Specifier(_IndividualSpecifier): prefix = ".".join( list( itertools.takewhile( - lambda x: (not x.startswith("post") and not - x.startswith("dev")), + lambda x: (not x.startswith("post") and not x.startswith("dev")), _version_split(spec), ) )[:-1] @@ -407,8 +394,9 @@ class Specifier(_IndividualSpecifier): # Add the prefix notation to the end of our string prefix += ".*" - return (self._get_operator(">=")(prospective, spec) and - self._get_operator("==")(prospective, prefix)) + return self._get_operator(">=")(prospective, spec) and self._get_operator("==")( + prospective, prefix + ) @_require_version_compare def _compare_equal(self, prospective, spec): @@ -428,7 +416,7 @@ class Specifier(_IndividualSpecifier): # Shorten the prospective version to be the same length as the spec # so that we can determine if the specifier is a prefix of the # prospective version or not. - prospective = prospective[:len(spec)] + prospective = prospective[: len(spec)] # Pad out our two sides with zeros so that they both equal the same # length. @@ -503,7 +491,7 @@ class Specifier(_IndividualSpecifier): return False # Ensure that we do not allow a local version of the version mentioned - # in the specifier, which is techincally greater than, to match. + # in the specifier, which is technically greater than, to match. if prospective.local is not None: if Version(prospective.base_version) == Version(spec.base_version): return False @@ -567,27 +555,17 @@ def _pad_version(left, right): right_split.append(list(itertools.takewhile(lambda x: x.isdigit(), right))) # Get the rest of our versions - left_split.append(left[len(left_split[0]):]) - right_split.append(right[len(right_split[0]):]) + left_split.append(left[len(left_split[0]) :]) + right_split.append(right[len(right_split[0]) :]) # Insert our padding - left_split.insert( - 1, - ["0"] * max(0, len(right_split[0]) - len(left_split[0])), - ) - right_split.insert( - 1, - ["0"] * max(0, len(left_split[0]) - len(right_split[0])), - ) + left_split.insert(1, ["0"] * max(0, len(right_split[0]) - len(left_split[0]))) + right_split.insert(1, ["0"] * max(0, len(left_split[0]) - len(right_split[0]))) - return ( - list(itertools.chain(*left_split)), - list(itertools.chain(*right_split)), - ) + return (list(itertools.chain(*left_split)), list(itertools.chain(*right_split))) class SpecifierSet(BaseSpecifier): - def __init__(self, specifiers="", prereleases=None): # Split on , to break each indidivual specifier into it's own item, and # strip each item to remove leading/trailing whitespace. @@ -721,10 +699,7 @@ class SpecifierSet(BaseSpecifier): # given version is contained within all of them. # Note: This use of all() here means that an empty set of specifiers # will always return True, this is an explicit design decision. - return all( - s.contains(item, prereleases=prereleases) - for s in self._specs - ) + return all(s.contains(item, prereleases=prereleases) for s in self._specs) def filter(self, iterable, prereleases=None): # Determine if we're forcing a prerelease or not, if we're not forcing diff --git a/setuptools/_vendor/packaging/tags.py b/setuptools/_vendor/packaging/tags.py new file mode 100644 index 00000000..ec9942f0 --- /dev/null +++ b/setuptools/_vendor/packaging/tags.py @@ -0,0 +1,404 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import absolute_import + +import distutils.util + +try: + from importlib.machinery import EXTENSION_SUFFIXES +except ImportError: # pragma: no cover + import imp + + EXTENSION_SUFFIXES = [x[0] for x in imp.get_suffixes()] + del imp +import platform +import re +import sys +import sysconfig +import warnings + + +INTERPRETER_SHORT_NAMES = { + "python": "py", # Generic. + "cpython": "cp", + "pypy": "pp", + "ironpython": "ip", + "jython": "jy", +} + + +_32_BIT_INTERPRETER = sys.maxsize <= 2 ** 32 + + +class Tag(object): + + __slots__ = ["_interpreter", "_abi", "_platform"] + + def __init__(self, interpreter, abi, platform): + self._interpreter = interpreter.lower() + self._abi = abi.lower() + self._platform = platform.lower() + + @property + def interpreter(self): + return self._interpreter + + @property + def abi(self): + return self._abi + + @property + def platform(self): + return self._platform + + def __eq__(self, other): + return ( + (self.platform == other.platform) + and (self.abi == other.abi) + and (self.interpreter == other.interpreter) + ) + + def __hash__(self): + return hash((self._interpreter, self._abi, self._platform)) + + def __str__(self): + return "{}-{}-{}".format(self._interpreter, self._abi, self._platform) + + def __repr__(self): + return "<{self} @ {self_id}>".format(self=self, self_id=id(self)) + + +def parse_tag(tag): + tags = set() + interpreters, abis, platforms = tag.split("-") + for interpreter in interpreters.split("."): + for abi in abis.split("."): + for platform_ in platforms.split("."): + tags.add(Tag(interpreter, abi, platform_)) + return frozenset(tags) + + +def _normalize_string(string): + return string.replace(".", "_").replace("-", "_") + + +def _cpython_interpreter(py_version): + # TODO: Is using py_version_nodot for interpreter version critical? + return "cp{major}{minor}".format(major=py_version[0], minor=py_version[1]) + + +def _cpython_abis(py_version): + abis = [] + version = "{}{}".format(*py_version[:2]) + debug = pymalloc = ucs4 = "" + with_debug = sysconfig.get_config_var("Py_DEBUG") + has_refcount = hasattr(sys, "gettotalrefcount") + # Windows doesn't set Py_DEBUG, so checking for support of debug-compiled + # extension modules is the best option. + # https://github.com/pypa/pip/issues/3383#issuecomment-173267692 + has_ext = "_d.pyd" in EXTENSION_SUFFIXES + if with_debug or (with_debug is None and (has_refcount or has_ext)): + debug = "d" + if py_version < (3, 8): + with_pymalloc = sysconfig.get_config_var("WITH_PYMALLOC") + if with_pymalloc or with_pymalloc is None: + pymalloc = "m" + if py_version < (3, 3): + unicode_size = sysconfig.get_config_var("Py_UNICODE_SIZE") + if unicode_size == 4 or ( + unicode_size is None and sys.maxunicode == 0x10FFFF + ): + ucs4 = "u" + elif debug: + # Debug builds can also load "normal" extension modules. + # We can also assume no UCS-4 or pymalloc requirement. + abis.append("cp{version}".format(version=version)) + abis.insert( + 0, + "cp{version}{debug}{pymalloc}{ucs4}".format( + version=version, debug=debug, pymalloc=pymalloc, ucs4=ucs4 + ), + ) + return abis + + +def _cpython_tags(py_version, interpreter, abis, platforms): + for abi in abis: + for platform_ in platforms: + yield Tag(interpreter, abi, platform_) + for tag in (Tag(interpreter, "abi3", platform_) for platform_ in platforms): + yield tag + for tag in (Tag(interpreter, "none", platform_) for platform_ in platforms): + yield tag + # PEP 384 was first implemented in Python 3.2. + for minor_version in range(py_version[1] - 1, 1, -1): + for platform_ in platforms: + interpreter = "cp{major}{minor}".format( + major=py_version[0], minor=minor_version + ) + yield Tag(interpreter, "abi3", platform_) + + +def _pypy_interpreter(): + return "pp{py_major}{pypy_major}{pypy_minor}".format( + py_major=sys.version_info[0], + pypy_major=sys.pypy_version_info.major, + pypy_minor=sys.pypy_version_info.minor, + ) + + +def _generic_abi(): + abi = sysconfig.get_config_var("SOABI") + if abi: + return _normalize_string(abi) + else: + return "none" + + +def _pypy_tags(py_version, interpreter, abi, platforms): + for tag in (Tag(interpreter, abi, platform) for platform in platforms): + yield tag + for tag in (Tag(interpreter, "none", platform) for platform in platforms): + yield tag + + +def _generic_tags(interpreter, py_version, abi, platforms): + for tag in (Tag(interpreter, abi, platform) for platform in platforms): + yield tag + if abi != "none": + tags = (Tag(interpreter, "none", platform_) for platform_ in platforms) + for tag in tags: + yield tag + + +def _py_interpreter_range(py_version): + """ + Yield Python versions in descending order. + + After the latest version, the major-only version will be yielded, and then + all following versions up to 'end'. + """ + yield "py{major}{minor}".format(major=py_version[0], minor=py_version[1]) + yield "py{major}".format(major=py_version[0]) + for minor in range(py_version[1] - 1, -1, -1): + yield "py{major}{minor}".format(major=py_version[0], minor=minor) + + +def _independent_tags(interpreter, py_version, platforms): + """ + Return the sequence of tags that are consistent across implementations. + + The tags consist of: + - py*-none- + - -none-any + - py*-none-any + """ + for version in _py_interpreter_range(py_version): + for platform_ in platforms: + yield Tag(version, "none", platform_) + yield Tag(interpreter, "none", "any") + for version in _py_interpreter_range(py_version): + yield Tag(version, "none", "any") + + +def _mac_arch(arch, is_32bit=_32_BIT_INTERPRETER): + if not is_32bit: + return arch + + if arch.startswith("ppc"): + return "ppc" + + return "i386" + + +def _mac_binary_formats(version, cpu_arch): + formats = [cpu_arch] + if cpu_arch == "x86_64": + if version < (10, 4): + return [] + formats.extend(["intel", "fat64", "fat32"]) + + elif cpu_arch == "i386": + if version < (10, 4): + return [] + formats.extend(["intel", "fat32", "fat"]) + + elif cpu_arch == "ppc64": + # TODO: Need to care about 32-bit PPC for ppc64 through 10.2? + if version > (10, 5) or version < (10, 4): + return [] + formats.append("fat64") + + elif cpu_arch == "ppc": + if version > (10, 6): + return [] + formats.extend(["fat32", "fat"]) + + formats.append("universal") + return formats + + +def _mac_platforms(version=None, arch=None): + version_str, _, cpu_arch = platform.mac_ver() + if version is None: + version = tuple(map(int, version_str.split(".")[:2])) + if arch is None: + arch = _mac_arch(cpu_arch) + platforms = [] + for minor_version in range(version[1], -1, -1): + compat_version = version[0], minor_version + binary_formats = _mac_binary_formats(compat_version, arch) + for binary_format in binary_formats: + platforms.append( + "macosx_{major}_{minor}_{binary_format}".format( + major=compat_version[0], + minor=compat_version[1], + binary_format=binary_format, + ) + ) + return platforms + + +# From PEP 513. +def _is_manylinux_compatible(name, glibc_version): + # Check for presence of _manylinux module. + try: + import _manylinux + + return bool(getattr(_manylinux, name + "_compatible")) + except (ImportError, AttributeError): + # Fall through to heuristic check below. + pass + + return _have_compatible_glibc(*glibc_version) + + +def _glibc_version_string(): + # Returns glibc version string, or None if not using glibc. + import ctypes + + # ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen + # manpage says, "If filename is NULL, then the returned handle is for the + # main program". This way we can let the linker do the work to figure out + # which libc our process is actually using. + process_namespace = ctypes.CDLL(None) + try: + gnu_get_libc_version = process_namespace.gnu_get_libc_version + except AttributeError: + # Symbol doesn't exist -> therefore, we are not linked to + # glibc. + return None + + # Call gnu_get_libc_version, which returns a string like "2.5" + gnu_get_libc_version.restype = ctypes.c_char_p + version_str = gnu_get_libc_version() + # py2 / py3 compatibility: + if not isinstance(version_str, str): + version_str = version_str.decode("ascii") + + return version_str + + +# Separated out from have_compatible_glibc for easier unit testing. +def _check_glibc_version(version_str, required_major, minimum_minor): + # Parse string and check against requested version. + # + # We use a regexp instead of str.split because we want to discard any + # random junk that might come after the minor version -- this might happen + # in patched/forked versions of glibc (e.g. Linaro's version of glibc + # uses version strings like "2.20-2014.11"). See gh-3588. + m = re.match(r"(?P[0-9]+)\.(?P[0-9]+)", version_str) + if not m: + warnings.warn( + "Expected glibc version with 2 components major.minor," + " got: %s" % version_str, + RuntimeWarning, + ) + return False + return ( + int(m.group("major")) == required_major + and int(m.group("minor")) >= minimum_minor + ) + + +def _have_compatible_glibc(required_major, minimum_minor): + version_str = _glibc_version_string() + if version_str is None: + return False + return _check_glibc_version(version_str, required_major, minimum_minor) + + +def _linux_platforms(is_32bit=_32_BIT_INTERPRETER): + linux = _normalize_string(distutils.util.get_platform()) + if linux == "linux_x86_64" and is_32bit: + linux = "linux_i686" + manylinux_support = ( + ("manylinux2014", (2, 17)), # CentOS 7 w/ glibc 2.17 (PEP 599) + ("manylinux2010", (2, 12)), # CentOS 6 w/ glibc 2.12 (PEP 571) + ("manylinux1", (2, 5)), # CentOS 5 w/ glibc 2.5 (PEP 513) + ) + manylinux_support_iter = iter(manylinux_support) + for name, glibc_version in manylinux_support_iter: + if _is_manylinux_compatible(name, glibc_version): + platforms = [linux.replace("linux", name)] + break + else: + platforms = [] + # Support for a later manylinux implies support for an earlier version. + platforms += [linux.replace("linux", name) for name, _ in manylinux_support_iter] + platforms.append(linux) + return platforms + + +def _generic_platforms(): + platform = _normalize_string(distutils.util.get_platform()) + return [platform] + + +def _interpreter_name(): + name = platform.python_implementation().lower() + return INTERPRETER_SHORT_NAMES.get(name) or name + + +def _generic_interpreter(name, py_version): + version = sysconfig.get_config_var("py_version_nodot") + if not version: + version = "".join(map(str, py_version[:2])) + return "{name}{version}".format(name=name, version=version) + + +def sys_tags(): + """ + Returns the sequence of tag triples for the running interpreter. + + The order of the sequence corresponds to priority order for the + interpreter, from most to least important. + """ + py_version = sys.version_info[:2] + interpreter_name = _interpreter_name() + if platform.system() == "Darwin": + platforms = _mac_platforms() + elif platform.system() == "Linux": + platforms = _linux_platforms() + else: + platforms = _generic_platforms() + + if interpreter_name == "cp": + interpreter = _cpython_interpreter(py_version) + abis = _cpython_abis(py_version) + for tag in _cpython_tags(py_version, interpreter, abis, platforms): + yield tag + elif interpreter_name == "pp": + interpreter = _pypy_interpreter() + abi = _generic_abi() + for tag in _pypy_tags(py_version, interpreter, abi, platforms): + yield tag + else: + interpreter = _generic_interpreter(interpreter_name, py_version) + abi = _generic_abi() + for tag in _generic_tags(interpreter, py_version, abi, platforms): + yield tag + for tag in _independent_tags(interpreter, py_version, platforms): + yield tag diff --git a/setuptools/_vendor/packaging/utils.py b/setuptools/_vendor/packaging/utils.py index 942387ce..88418786 100644 --- a/setuptools/_vendor/packaging/utils.py +++ b/setuptools/_vendor/packaging/utils.py @@ -5,6 +5,8 @@ from __future__ import absolute_import, division, print_function import re +from .version import InvalidVersion, Version + _canonicalize_regex = re.compile(r"[-_.]+") @@ -12,3 +14,44 @@ _canonicalize_regex = re.compile(r"[-_.]+") def canonicalize_name(name): # This is taken from PEP 503. return _canonicalize_regex.sub("-", name).lower() + + +def canonicalize_version(version): + """ + This is very similar to Version.__str__, but has one subtle differences + with the way it handles the release segment. + """ + + try: + version = Version(version) + except InvalidVersion: + # Legacy versions cannot be normalized + return version + + parts = [] + + # Epoch + if version.epoch != 0: + parts.append("{0}!".format(version.epoch)) + + # Release segment + # NB: This strips trailing '.0's to normalize + parts.append(re.sub(r"(\.0)+$", "", ".".join(str(x) for x in version.release))) + + # Pre-release + if version.pre is not None: + parts.append("".join(str(x) for x in version.pre)) + + # Post-release + if version.post is not None: + parts.append(".post{0}".format(version.post)) + + # Development release + if version.dev is not None: + parts.append(".dev{0}".format(version.dev)) + + # Local version segment + if version.local is not None: + parts.append("+{0}".format(version.local)) + + return "".join(parts) diff --git a/setuptools/_vendor/packaging/version.py b/setuptools/_vendor/packaging/version.py index 83b5ee8c..95157a1f 100644 --- a/setuptools/_vendor/packaging/version.py +++ b/setuptools/_vendor/packaging/version.py @@ -10,14 +10,11 @@ import re from ._structures import Infinity -__all__ = [ - "parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN" -] +__all__ = ["parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN"] _Version = collections.namedtuple( - "_Version", - ["epoch", "release", "dev", "pre", "post", "local"], + "_Version", ["epoch", "release", "dev", "pre", "post", "local"] ) @@ -40,7 +37,6 @@ class InvalidVersion(ValueError): class _BaseVersion(object): - def __hash__(self): return hash(self._key) @@ -70,7 +66,6 @@ class _BaseVersion(object): class LegacyVersion(_BaseVersion): - def __init__(self, version): self._version = str(version) self._key = _legacy_cmpkey(self._version) @@ -89,6 +84,26 @@ class LegacyVersion(_BaseVersion): def base_version(self): return self._version + @property + def epoch(self): + return -1 + + @property + def release(self): + return None + + @property + def pre(self): + return None + + @property + def post(self): + return None + + @property + def dev(self): + return None + @property def local(self): return None @@ -101,13 +116,19 @@ class LegacyVersion(_BaseVersion): def is_postrelease(self): return False + @property + def is_devrelease(self): + return False -_legacy_version_component_re = re.compile( - r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE, -) + +_legacy_version_component_re = re.compile(r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE) _legacy_version_replacement_map = { - "pre": "c", "preview": "c", "-": "final-", "rc": "c", "dev": "@", + "pre": "c", + "preview": "c", + "-": "final-", + "rc": "c", + "dev": "@", } @@ -154,6 +175,7 @@ def _legacy_cmpkey(version): return epoch, parts + # Deliberately not anchored to the start and end of the string, to make it # easier for 3rd party code to reuse VERSION_PATTERN = r""" @@ -190,10 +212,7 @@ VERSION_PATTERN = r""" class Version(_BaseVersion): - _regex = re.compile( - r"^\s*" + VERSION_PATTERN + r"\s*$", - re.VERBOSE | re.IGNORECASE, - ) + _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE) def __init__(self, version): # Validate the version and parse it into pieces @@ -205,18 +224,11 @@ class Version(_BaseVersion): self._version = _Version( epoch=int(match.group("epoch")) if match.group("epoch") else 0, release=tuple(int(i) for i in match.group("release").split(".")), - pre=_parse_letter_version( - match.group("pre_l"), - match.group("pre_n"), - ), + pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")), post=_parse_letter_version( - match.group("post_l"), - match.group("post_n1") or match.group("post_n2"), - ), - dev=_parse_letter_version( - match.group("dev_l"), - match.group("dev_n"), + match.group("post_l"), match.group("post_n1") or match.group("post_n2") ), + dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")), local=_parse_local_version(match.group("local")), ) @@ -237,32 +249,57 @@ class Version(_BaseVersion): parts = [] # Epoch - if self._version.epoch != 0: - parts.append("{0}!".format(self._version.epoch)) + if self.epoch != 0: + parts.append("{0}!".format(self.epoch)) # Release segment - parts.append(".".join(str(x) for x in self._version.release)) + parts.append(".".join(str(x) for x in self.release)) # Pre-release - if self._version.pre is not None: - parts.append("".join(str(x) for x in self._version.pre)) + if self.pre is not None: + parts.append("".join(str(x) for x in self.pre)) # Post-release - if self._version.post is not None: - parts.append(".post{0}".format(self._version.post[1])) + if self.post is not None: + parts.append(".post{0}".format(self.post)) # Development release - if self._version.dev is not None: - parts.append(".dev{0}".format(self._version.dev[1])) + if self.dev is not None: + parts.append(".dev{0}".format(self.dev)) # Local version segment - if self._version.local is not None: - parts.append( - "+{0}".format(".".join(str(x) for x in self._version.local)) - ) + if self.local is not None: + parts.append("+{0}".format(self.local)) return "".join(parts) + @property + def epoch(self): + return self._version.epoch + + @property + def release(self): + return self._version.release + + @property + def pre(self): + return self._version.pre + + @property + def post(self): + return self._version.post[1] if self._version.post else None + + @property + def dev(self): + return self._version.dev[1] if self._version.dev else None + + @property + def local(self): + if self._version.local: + return ".".join(str(x) for x in self._version.local) + else: + return None + @property def public(self): return str(self).split("+", 1)[0] @@ -272,27 +309,25 @@ class Version(_BaseVersion): parts = [] # Epoch - if self._version.epoch != 0: - parts.append("{0}!".format(self._version.epoch)) + if self.epoch != 0: + parts.append("{0}!".format(self.epoch)) # Release segment - parts.append(".".join(str(x) for x in self._version.release)) + parts.append(".".join(str(x) for x in self.release)) return "".join(parts) - @property - def local(self): - version_string = str(self) - if "+" in version_string: - return version_string.split("+", 1)[1] - @property def is_prerelease(self): - return bool(self._version.dev or self._version.pre) + return self.dev is not None or self.pre is not None @property def is_postrelease(self): - return bool(self._version.post) + return self.post is not None + + @property + def is_devrelease(self): + return self.dev is not None def _parse_letter_version(letter, number): @@ -326,7 +361,7 @@ def _parse_letter_version(letter, number): return letter, int(number) -_local_version_seperators = re.compile(r"[\._-]") +_local_version_separators = re.compile(r"[\._-]") def _parse_local_version(local): @@ -336,7 +371,7 @@ def _parse_local_version(local): if local is not None: return tuple( part.lower() if not part.isdigit() else int(part) - for part in _local_version_seperators.split(local) + for part in _local_version_separators.split(local) ) @@ -347,12 +382,7 @@ def _cmpkey(epoch, release, pre, post, dev, local): # re-reverse it back into the correct order and make it a tuple and use # that for our sorting key. release = tuple( - reversed(list( - itertools.dropwhile( - lambda x: x == 0, - reversed(release), - ) - )) + reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release)))) ) # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0. @@ -385,9 +415,6 @@ def _cmpkey(epoch, release, pre, post, dev, local): # - Numeric segments sort numerically # - Shorter versions sort before longer versions when the prefixes # match exactly - local = tuple( - (i, "") if isinstance(i, int) else (-Infinity, i) - for i in local - ) + local = tuple((i, "") if isinstance(i, int) else (-Infinity, i) for i in local) return epoch, release, pre, post, dev, local diff --git a/setuptools/_vendor/vendored.txt b/setuptools/_vendor/vendored.txt index 5731b424..65183d9a 100644 --- a/setuptools/_vendor/vendored.txt +++ b/setuptools/_vendor/vendored.txt @@ -1,4 +1,4 @@ -packaging==16.8 +packaging==19.2 pyparsing==2.2.1 six==1.10.0 ordered-set==3.1.1 -- cgit v1.2.3 From 3d811b93a83d5931b821916c6ca172a69c403a97 Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Fri, 23 Aug 2019 23:18:02 +0200 Subject: wheel: switch to `packaging.tags` for checking PEP 425 tags --- setuptools/glibc.py | 86 ---------- setuptools/pep425tags.py | 319 ------------------------------------ setuptools/tests/test_glibc.py | 52 ------ setuptools/tests/test_pep425tags.py | 170 ------------------- setuptools/wheel.py | 4 +- 5 files changed, 2 insertions(+), 629 deletions(-) delete mode 100644 setuptools/glibc.py delete mode 100644 setuptools/pep425tags.py delete mode 100644 setuptools/tests/test_glibc.py delete mode 100644 setuptools/tests/test_pep425tags.py (limited to 'setuptools') diff --git a/setuptools/glibc.py b/setuptools/glibc.py deleted file mode 100644 index a134591c..00000000 --- a/setuptools/glibc.py +++ /dev/null @@ -1,86 +0,0 @@ -# This file originally from pip: -# https://github.com/pypa/pip/blob/8f4f15a5a95d7d5b511ceaee9ed261176c181970/src/pip/_internal/utils/glibc.py -from __future__ import absolute_import - -import ctypes -import re -import warnings - - -def glibc_version_string(): - "Returns glibc version string, or None if not using glibc." - - # ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen - # manpage says, "If filename is NULL, then the returned handle is for the - # main program". This way we can let the linker do the work to figure out - # which libc our process is actually using. - process_namespace = ctypes.CDLL(None) - try: - gnu_get_libc_version = process_namespace.gnu_get_libc_version - except AttributeError: - # Symbol doesn't exist -> therefore, we are not linked to - # glibc. - return None - - # Call gnu_get_libc_version, which returns a string like "2.5" - gnu_get_libc_version.restype = ctypes.c_char_p - version_str = gnu_get_libc_version() - # py2 / py3 compatibility: - if not isinstance(version_str, str): - version_str = version_str.decode("ascii") - - return version_str - - -# Separated out from have_compatible_glibc for easier unit testing -def check_glibc_version(version_str, required_major, minimum_minor): - # Parse string and check against requested version. - # - # We use a regexp instead of str.split because we want to discard any - # random junk that might come after the minor version -- this might happen - # in patched/forked versions of glibc (e.g. Linaro's version of glibc - # uses version strings like "2.20-2014.11"). See gh-3588. - m = re.match(r"(?P[0-9]+)\.(?P[0-9]+)", version_str) - if not m: - warnings.warn("Expected glibc version with 2 components major.minor," - " got: %s" % version_str, RuntimeWarning) - return False - return (int(m.group("major")) == required_major and - int(m.group("minor")) >= minimum_minor) - - -def have_compatible_glibc(required_major, minimum_minor): - version_str = glibc_version_string() - if version_str is None: - return False - return check_glibc_version(version_str, required_major, minimum_minor) - - -# platform.libc_ver regularly returns completely nonsensical glibc -# versions. E.g. on my computer, platform says: -# -# ~$ python2.7 -c 'import platform; print(platform.libc_ver())' -# ('glibc', '2.7') -# ~$ python3.5 -c 'import platform; print(platform.libc_ver())' -# ('glibc', '2.9') -# -# But the truth is: -# -# ~$ ldd --version -# ldd (Debian GLIBC 2.22-11) 2.22 -# -# This is unfortunate, because it means that the linehaul data on libc -# versions that was generated by pip 8.1.2 and earlier is useless and -# misleading. Solution: instead of using platform, use our code that actually -# works. -def libc_ver(): - """Try to determine the glibc version - - Returns a tuple of strings (lib, version) which default to empty strings - in case the lookup fails. - """ - glibc_version = glibc_version_string() - if glibc_version is None: - return ("", "") - else: - return ("glibc", glibc_version) diff --git a/setuptools/pep425tags.py b/setuptools/pep425tags.py deleted file mode 100644 index 48745a29..00000000 --- a/setuptools/pep425tags.py +++ /dev/null @@ -1,319 +0,0 @@ -# This file originally from pip: -# https://github.com/pypa/pip/blob/8f4f15a5a95d7d5b511ceaee9ed261176c181970/src/pip/_internal/pep425tags.py -"""Generate and work with PEP 425 Compatibility Tags.""" -from __future__ import absolute_import - -import distutils.util -from distutils import log -import platform -import re -import sys -import sysconfig -import warnings -from collections import OrderedDict - -from .extern import six - -from . import glibc - -_osx_arch_pat = re.compile(r'(.+)_(\d+)_(\d+)_(.+)') - - -def get_config_var(var): - try: - return sysconfig.get_config_var(var) - except IOError as e: # Issue #1074 - warnings.warn("{}".format(e), RuntimeWarning) - return None - - -def get_abbr_impl(): - """Return abbreviated implementation name.""" - if hasattr(sys, 'pypy_version_info'): - pyimpl = 'pp' - elif sys.platform.startswith('java'): - pyimpl = 'jy' - elif sys.platform == 'cli': - pyimpl = 'ip' - else: - pyimpl = 'cp' - return pyimpl - - -def get_impl_ver(): - """Return implementation version.""" - impl_ver = get_config_var("py_version_nodot") - if not impl_ver or get_abbr_impl() == 'pp': - impl_ver = ''.join(map(str, get_impl_version_info())) - return impl_ver - - -def get_impl_version_info(): - """Return sys.version_info-like tuple for use in decrementing the minor - version.""" - if get_abbr_impl() == 'pp': - # as per https://github.com/pypa/pip/issues/2882 - return (sys.version_info[0], sys.pypy_version_info.major, - sys.pypy_version_info.minor) - else: - return sys.version_info[0], sys.version_info[1] - - -def get_impl_tag(): - """ - Returns the Tag for this specific implementation. - """ - return "{}{}".format(get_abbr_impl(), get_impl_ver()) - - -def get_flag(var, fallback, expected=True, warn=True): - """Use a fallback method for determining SOABI flags if the needed config - var is unset or unavailable.""" - val = get_config_var(var) - if val is None: - if warn: - log.debug("Config variable '%s' is unset, Python ABI tag may " - "be incorrect", var) - return fallback() - return val == expected - - -def get_abi_tag(): - """Return the ABI tag based on SOABI (if available) or emulate SOABI - (CPython 2, PyPy).""" - soabi = get_config_var('SOABI') - impl = get_abbr_impl() - if not soabi and impl in {'cp', 'pp'} and hasattr(sys, 'maxunicode'): - d = '' - m = '' - u = '' - if get_flag('Py_DEBUG', - lambda: hasattr(sys, 'gettotalrefcount'), - warn=(impl == 'cp')): - d = 'd' - if get_flag('WITH_PYMALLOC', - lambda: impl == 'cp', - warn=(impl == 'cp')): - m = 'm' - if get_flag('Py_UNICODE_SIZE', - lambda: sys.maxunicode == 0x10ffff, - expected=4, - warn=(impl == 'cp' and - six.PY2)) \ - and six.PY2: - u = 'u' - abi = '%s%s%s%s%s' % (impl, get_impl_ver(), d, m, u) - elif soabi and soabi.startswith('cpython-'): - abi = 'cp' + soabi.split('-')[1] - elif soabi: - abi = soabi.replace('.', '_').replace('-', '_') - else: - abi = None - return abi - - -def _is_running_32bit(): - return sys.maxsize == 2147483647 - - -def get_platform(): - """Return our platform name 'win32', 'linux_x86_64'""" - if sys.platform == 'darwin': - # distutils.util.get_platform() returns the release based on the value - # of MACOSX_DEPLOYMENT_TARGET on which Python was built, which may - # be significantly older than the user's current machine. - release, _, machine = platform.mac_ver() - split_ver = release.split('.') - - if machine == "x86_64" and _is_running_32bit(): - machine = "i386" - elif machine == "ppc64" and _is_running_32bit(): - machine = "ppc" - - return 'macosx_{}_{}_{}'.format(split_ver[0], split_ver[1], machine) - - # XXX remove distutils dependency - result = distutils.util.get_platform().replace('.', '_').replace('-', '_') - if result == "linux_x86_64" and _is_running_32bit(): - # 32 bit Python program (running on a 64 bit Linux): pip should only - # install and run 32 bit compiled extensions in that case. - result = "linux_i686" - - return result - - -def is_manylinux1_compatible(): - # Only Linux, and only x86-64 / i686 - if get_platform() not in {"linux_x86_64", "linux_i686"}: - return False - - # Check for presence of _manylinux module - try: - import _manylinux - return bool(_manylinux.manylinux1_compatible) - except (ImportError, AttributeError): - # Fall through to heuristic check below - pass - - # Check glibc version. CentOS 5 uses glibc 2.5. - return glibc.have_compatible_glibc(2, 5) - - -def get_darwin_arches(major, minor, machine): - """Return a list of supported arches (including group arches) for - the given major, minor and machine architecture of a macOS machine. - """ - arches = [] - - def _supports_arch(major, minor, arch): - # Looking at the application support for macOS versions in the chart - # provided by https://en.wikipedia.org/wiki/OS_X#Versions it appears - # our timeline looks roughly like: - # - # 10.0 - Introduces ppc support. - # 10.4 - Introduces ppc64, i386, and x86_64 support, however the ppc64 - # and x86_64 support is CLI only, and cannot be used for GUI - # applications. - # 10.5 - Extends ppc64 and x86_64 support to cover GUI applications. - # 10.6 - Drops support for ppc64 - # 10.7 - Drops support for ppc - # - # Given that we do not know if we're installing a CLI or a GUI - # application, we must be conservative and assume it might be a GUI - # application and behave as if ppc64 and x86_64 support did not occur - # until 10.5. - # - # Note: The above information is taken from the "Application support" - # column in the chart not the "Processor support" since I believe - # that we care about what instruction sets an application can use - # not which processors the OS supports. - if arch == 'ppc': - return (major, minor) <= (10, 5) - if arch == 'ppc64': - return (major, minor) == (10, 5) - if arch == 'i386': - return (major, minor) >= (10, 4) - if arch == 'x86_64': - return (major, minor) >= (10, 5) - if arch in groups: - for garch in groups[arch]: - if _supports_arch(major, minor, garch): - return True - return False - - groups = OrderedDict([ - ("fat", ("i386", "ppc")), - ("intel", ("x86_64", "i386")), - ("fat64", ("x86_64", "ppc64")), - ("fat32", ("x86_64", "i386", "ppc")), - ]) - - if _supports_arch(major, minor, machine): - arches.append(machine) - - for garch in groups: - if machine in groups[garch] and _supports_arch(major, minor, garch): - arches.append(garch) - - arches.append('universal') - - return arches - - -def get_supported(versions=None, noarch=False, platform=None, - impl=None, abi=None): - """Return a list of supported tags for each version specified in - `versions`. - - :param versions: a list of string versions, of the form ["33", "32"], - or None. The first version will be assumed to support our ABI. - :param platform: specify the exact platform you want valid - tags for, or None. If None, use the local system platform. - :param impl: specify the exact implementation you want valid - tags for, or None. If None, use the local interpreter impl. - :param abi: specify the exact abi you want valid - tags for, or None. If None, use the local interpreter abi. - """ - supported = [] - - # Versions must be given with respect to the preference - if versions is None: - versions = [] - version_info = get_impl_version_info() - major = version_info[:-1] - # Support all previous minor Python versions. - for minor in range(version_info[-1], -1, -1): - versions.append(''.join(map(str, major + (minor,)))) - - impl = impl or get_abbr_impl() - - abis = [] - - abi = abi or get_abi_tag() - if abi: - abis[0:0] = [abi] - - abi3s = set() - import imp - for suffix in imp.get_suffixes(): - if suffix[0].startswith('.abi'): - abi3s.add(suffix[0].split('.', 2)[1]) - - abis.extend(sorted(list(abi3s))) - - abis.append('none') - - if not noarch: - arch = platform or get_platform() - if arch.startswith('macosx'): - # support macosx-10.6-intel on macosx-10.9-x86_64 - match = _osx_arch_pat.match(arch) - if match: - name, major, minor, actual_arch = match.groups() - tpl = '{}_{}_%i_%s'.format(name, major) - arches = [] - for m in reversed(range(int(minor) + 1)): - for a in get_darwin_arches(int(major), m, actual_arch): - arches.append(tpl % (m, a)) - else: - # arch pattern didn't match (?!) - arches = [arch] - elif platform is None and is_manylinux1_compatible(): - arches = [arch.replace('linux', 'manylinux1'), arch] - else: - arches = [arch] - - # Current version, current API (built specifically for our Python): - for abi in abis: - for arch in arches: - supported.append(('%s%s' % (impl, versions[0]), abi, arch)) - - # abi3 modules compatible with older version of Python - for version in versions[1:]: - # abi3 was introduced in Python 3.2 - if version in {'31', '30'}: - break - for abi in abi3s: # empty set if not Python 3 - for arch in arches: - supported.append(("%s%s" % (impl, version), abi, arch)) - - # Has binaries, does not use the Python API: - for arch in arches: - supported.append(('py%s' % (versions[0][0]), 'none', arch)) - - # No abi / arch, but requires our implementation: - supported.append(('%s%s' % (impl, versions[0]), 'none', 'any')) - # Tagged specifically as being cross-version compatible - # (with just the major version specified) - supported.append(('%s%s' % (impl, versions[0][0]), 'none', 'any')) - - # No abi / arch, generic Python - for i, version in enumerate(versions): - supported.append(('py%s' % (version,), 'none', 'any')) - if i == 0: - supported.append(('py%s' % (version[0]), 'none', 'any')) - - return supported - - -implementation_tag = get_impl_tag() diff --git a/setuptools/tests/test_glibc.py b/setuptools/tests/test_glibc.py deleted file mode 100644 index 795fdc56..00000000 --- a/setuptools/tests/test_glibc.py +++ /dev/null @@ -1,52 +0,0 @@ -import warnings - -import pytest - -from setuptools.glibc import check_glibc_version - -__metaclass__ = type - - -@pytest.fixture(params=[ - "2.20", - # used by "linaro glibc", see gh-3588 - "2.20-2014.11", - # weird possibilities that I just made up - "2.20+dev", - "2.20-custom", - "2.20.1", - ]) -def two_twenty(request): - return request.param - - -@pytest.fixture(params=["asdf", "", "foo.bar"]) -def bad_string(request): - return request.param - - -class TestGlibc: - def test_manylinux1_check_glibc_version(self, two_twenty): - """ - Test that the check_glibc_version function is robust against weird - glibc version strings. - """ - assert check_glibc_version(two_twenty, 2, 15) - assert check_glibc_version(two_twenty, 2, 20) - assert not check_glibc_version(two_twenty, 2, 21) - assert not check_glibc_version(two_twenty, 3, 15) - assert not check_glibc_version(two_twenty, 1, 15) - - def test_bad_versions(self, bad_string): - """ - For unparseable strings, warn and return False - """ - with warnings.catch_warnings(record=True) as ws: - warnings.filterwarnings("always") - assert not check_glibc_version(bad_string, 2, 5) - for w in ws: - if "Expected glibc version with" in str(w.message): - break - else: - # Didn't find the warning we were expecting - assert False diff --git a/setuptools/tests/test_pep425tags.py b/setuptools/tests/test_pep425tags.py deleted file mode 100644 index 30afdec7..00000000 --- a/setuptools/tests/test_pep425tags.py +++ /dev/null @@ -1,170 +0,0 @@ -import sys - -import pytest -from mock import patch - -from setuptools import pep425tags - -__metaclass__ = type - - -class TestPEP425Tags: - - def mock_get_config_var(self, **kwd): - """ - Patch sysconfig.get_config_var for arbitrary keys. - """ - get_config_var = pep425tags.sysconfig.get_config_var - - def _mock_get_config_var(var): - if var in kwd: - return kwd[var] - return get_config_var(var) - return _mock_get_config_var - - def abi_tag_unicode(self, flags, config_vars): - """ - Used to test ABI tags, verify correct use of the `u` flag - """ - config_vars.update({'SOABI': None}) - base = pep425tags.get_abbr_impl() + pep425tags.get_impl_ver() - - if sys.version_info < (3, 3): - config_vars.update({'Py_UNICODE_SIZE': 2}) - mock_gcf = self.mock_get_config_var(**config_vars) - with patch( - 'setuptools.pep425tags.sysconfig.get_config_var', - mock_gcf): - abi_tag = pep425tags.get_abi_tag() - assert abi_tag == base + flags - - config_vars.update({'Py_UNICODE_SIZE': 4}) - mock_gcf = self.mock_get_config_var(**config_vars) - with patch('setuptools.pep425tags.sysconfig.get_config_var', - mock_gcf): - abi_tag = pep425tags.get_abi_tag() - assert abi_tag == base + flags + 'u' - - else: - # On Python >= 3.3, UCS-4 is essentially permanently enabled, and - # Py_UNICODE_SIZE is None. SOABI on these builds does not include - # the 'u' so manual SOABI detection should not do so either. - config_vars.update({'Py_UNICODE_SIZE': None}) - mock_gcf = self.mock_get_config_var(**config_vars) - with patch('setuptools.pep425tags.sysconfig.get_config_var', - mock_gcf): - abi_tag = pep425tags.get_abi_tag() - assert abi_tag == base + flags - - def test_broken_sysconfig(self): - """ - Test that pep425tags still works when sysconfig is broken. - Can be a problem on Python 2.7 - Issue #1074. - """ - def raises_ioerror(var): - raise IOError("I have the wrong path!") - - with patch('setuptools.pep425tags.sysconfig.get_config_var', - raises_ioerror): - with pytest.warns(RuntimeWarning): - assert len(pep425tags.get_supported()) - - def test_no_hyphen_tag(self): - """ - Test that no tag contains a hyphen. - """ - mock_gcf = self.mock_get_config_var(SOABI='cpython-35m-darwin') - - with patch('setuptools.pep425tags.sysconfig.get_config_var', - mock_gcf): - supported = pep425tags.get_supported() - - for (py, abi, plat) in supported: - assert '-' not in py - assert '-' not in abi - assert '-' not in plat - - def test_manual_abi_noflags(self): - """ - Test that no flags are set on a non-PyDebug, non-Pymalloc ABI tag. - """ - self.abi_tag_unicode('', {'Py_DEBUG': False, 'WITH_PYMALLOC': False}) - - def test_manual_abi_d_flag(self): - """ - Test that the `d` flag is set on a PyDebug, non-Pymalloc ABI tag. - """ - self.abi_tag_unicode('d', {'Py_DEBUG': True, 'WITH_PYMALLOC': False}) - - def test_manual_abi_m_flag(self): - """ - Test that the `m` flag is set on a non-PyDebug, Pymalloc ABI tag. - """ - self.abi_tag_unicode('m', {'Py_DEBUG': False, 'WITH_PYMALLOC': True}) - - def test_manual_abi_dm_flags(self): - """ - Test that the `dm` flags are set on a PyDebug, Pymalloc ABI tag. - """ - self.abi_tag_unicode('dm', {'Py_DEBUG': True, 'WITH_PYMALLOC': True}) - - -class TestManylinux1Tags: - - @patch('setuptools.pep425tags.get_platform', lambda: 'linux_x86_64') - @patch('setuptools.glibc.have_compatible_glibc', - lambda major, minor: True) - def test_manylinux1_compatible_on_linux_x86_64(self): - """ - Test that manylinux1 is enabled on linux_x86_64 - """ - assert pep425tags.is_manylinux1_compatible() - - @patch('setuptools.pep425tags.get_platform', lambda: 'linux_i686') - @patch('setuptools.glibc.have_compatible_glibc', - lambda major, minor: True) - def test_manylinux1_compatible_on_linux_i686(self): - """ - Test that manylinux1 is enabled on linux_i686 - """ - assert pep425tags.is_manylinux1_compatible() - - @patch('setuptools.pep425tags.get_platform', lambda: 'linux_x86_64') - @patch('setuptools.glibc.have_compatible_glibc', - lambda major, minor: False) - def test_manylinux1_2(self): - """ - Test that manylinux1 is disabled with incompatible glibc - """ - assert not pep425tags.is_manylinux1_compatible() - - @patch('setuptools.pep425tags.get_platform', lambda: 'arm6vl') - @patch('setuptools.glibc.have_compatible_glibc', - lambda major, minor: True) - def test_manylinux1_3(self): - """ - Test that manylinux1 is disabled on arm6vl - """ - assert not pep425tags.is_manylinux1_compatible() - - @patch('setuptools.pep425tags.get_platform', lambda: 'linux_x86_64') - @patch('setuptools.glibc.have_compatible_glibc', - lambda major, minor: True) - @patch('sys.platform', 'linux2') - def test_manylinux1_tag_is_first(self): - """ - Test that the more specific tag manylinux1 comes first. - """ - groups = {} - for pyimpl, abi, arch in pep425tags.get_supported(): - groups.setdefault((pyimpl, abi), []).append(arch) - - for arches in groups.values(): - if arches == ['any']: - continue - # Expect the most specific arch first: - if len(arches) == 3: - assert arches == ['manylinux1_x86_64', 'linux_x86_64', 'any'] - else: - assert arches == ['manylinux1_x86_64', 'linux_x86_64'] diff --git a/setuptools/wheel.py b/setuptools/wheel.py index e11f0a1d..502f8410 100644 --- a/setuptools/wheel.py +++ b/setuptools/wheel.py @@ -11,9 +11,9 @@ import zipfile import pkg_resources import setuptools from pkg_resources import parse_version +from setuptools.extern.packaging.tags import sys_tags from setuptools.extern.packaging.utils import canonicalize_name from setuptools.extern.six import PY3 -from setuptools import pep425tags from setuptools.command.egg_info import write_requirements @@ -76,7 +76,7 @@ class Wheel: def is_compatible(self): '''Is the wheel is compatible with the current platform?''' - supported_tags = pep425tags.get_supported() + supported_tags = set(map(str, sys_tags())) return next((True for t in self.tags() if t in supported_tags), False) def egg_name(self): -- cgit v1.2.3 From 1410d87f8abb5bb28bf97f53219ee0db7b6340a3 Mon Sep 17 00:00:00 2001 From: isidentical Date: Fri, 4 Oct 2019 19:18:54 +0300 Subject: Upgrade setuptools.depends to importlib from depracated imp --- setuptools/depends.py | 89 ++++++++++++++++++++++++++++++------ setuptools/tests/test_integration.py | 1 + 2 files changed, 76 insertions(+), 14 deletions(-) (limited to 'setuptools') diff --git a/setuptools/depends.py b/setuptools/depends.py index 45e7052d..97f0ed9d 100644 --- a/setuptools/depends.py +++ b/setuptools/depends.py @@ -1,11 +1,23 @@ import sys -import imp import marshal from distutils.version import StrictVersion -from imp import PKG_DIRECTORY, PY_COMPILED, PY_SOURCE, PY_FROZEN +from setuptools.extern import six from .py33compat import Bytecode +if six.PY2: + import imp + from imp import PKG_DIRECTORY, PY_COMPILED, PY_SOURCE, PY_FROZEN +else: + import os.path + from importlib.util import find_spec, spec_from_loader + from importlib.machinery import SOURCE_SUFFIXES, BYTECODE_SUFFIXES, EXTENSION_SUFFIXES, BuiltinImporter, FrozenImporter + PY_SOURCE = 1 + PY_COMPILED = 2 + C_EXTENSION = 3 + C_BUILTIN = 6 + PY_FROZEN = 7 + __all__ = [ 'Require', 'find_module', 'get_module_constant', 'extract_constant' @@ -81,21 +93,59 @@ class Require: def find_module(module, paths=None): """Just like 'imp.find_module()', but with package support""" + if six.PY3: + spec = find_spec(module, paths) + if spec is None: + raise ImportError("Can't find %s" % module) + if not spec.has_location and hasattr(spec, 'submodule_search_locations'): + spec = spec_from_loader('__init__.py', spec.loader) + + kind = -1 + file = None + static = isinstance(spec.loader, type) + if spec.origin == 'frozen' or static and issubclass(spec.loader, FrozenImporter): + kind = PY_FROZEN + path = None # imp compabilty + suffix = mode = '' # imp compability + elif spec.origin == 'built-in' or static and issubclass(spec.loader, BuiltinImporter): + kind = C_BUILTIN + path = None # imp compabilty + suffix = mode = '' # imp compability + elif spec.has_location: + frozen = False + path = spec.origin + suffix = os.path.splitext(path)[1] + mode = 'r' if suffix in SOURCE_SUFFIXES else 'rb' + + if suffix in SOURCE_SUFFIXES: + kind = PY_SOURCE + elif suffix in BYTECODE_SUFFIXES: + kind = PY_COMPILED + elif suffix in EXTENSION_SUFFIXES: + kind = C_EXTENSION + + if kind in {PY_SOURCE, PY_COMPILED}: + file = open(path, mode) + else: + path = None + suffix = mode= '' - parts = module.split('.') + return file, path, (suffix, mode, kind) - while parts: - part = parts.pop(0) - f, path, (suffix, mode, kind) = info = imp.find_module(part, paths) + else: + parts = module.split('.') + while parts: + part = parts.pop(0) + f, path, (suffix, mode, kind) = info = imp.find_module(part, paths) - if kind == PKG_DIRECTORY: - parts = parts or ['__init__'] - paths = [path] + if kind == PKG_DIRECTORY: + parts = parts or ['__init__'] + paths = [path] - elif parts: - raise ImportError("Can't find %r in %s" % (parts, module)) + elif parts: + raise ImportError("Can't find %r in %s" % (parts, module)) - return info + return info def get_module_constant(module, symbol, default=-1, paths=None): @@ -111,18 +161,29 @@ def get_module_constant(module, symbol, default=-1, paths=None): # Module doesn't exist return None + if six.PY3: + spec = find_spec(module, paths) + if hasattr(spec, 'submodule_search_locations'): + spec = spec_from_loader('__init__.py', spec.loader) + try: if kind == PY_COMPILED: f.read(8) # skip magic & date code = marshal.load(f) elif kind == PY_FROZEN: - code = imp.get_frozen_object(module) + if six.PY2: + code = imp.get_frozen_object(module) + else: + code = spec.loader.get_code(module) elif kind == PY_SOURCE: code = compile(f.read(), path, 'exec') else: # Not something we can parse; we'll have to import it. :( if module not in sys.modules: - imp.load_module(module, f, path, (suffix, mode, kind)) + if six.PY2: + imp.load_module(module, f, path, (suffix, mode, kind)) + else: + sys.modules[module] = module_from_spec(spec) return getattr(sys.modules[module], symbol, None) finally: diff --git a/setuptools/tests/test_integration.py b/setuptools/tests/test_integration.py index 1e132188..1c0b2b18 100644 --- a/setuptools/tests/test_integration.py +++ b/setuptools/tests/test_integration.py @@ -143,6 +143,7 @@ def test_build_deps_on_distutils(request, tmpdir_factory, build_dep): 'tests_require', 'python_requires', 'install_requires', + 'long_description_content_type', ] assert not match or match.group(1).strip('"\'') in allowed_unknowns -- cgit v1.2.3 From 4c22a6ca57753d3b5604a90b61a0c6c5efe53a1d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 18 Oct 2019 20:37:42 -0400 Subject: Add new hook 'setuptools.finalize_distribution_options' for plugins like 'setuptools_scm' to alter distribution options. --- setuptools/dist.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'setuptools') diff --git a/setuptools/dist.py b/setuptools/dist.py index 2e5ad4bd..987d684e 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -728,6 +728,10 @@ class Distribution(_Distribution): if self.features: self._set_global_opts_from_features() + hook_key = 'setuptools.finalize_distribution_options' + for ep in pkg_resources.iter_entry_points(hook_key): + ep.load()(self) + for ep in pkg_resources.iter_entry_points('distutils.setup_keywords'): value = getattr(self, ep.name, None) if value is not None: -- cgit v1.2.3 From d89682fcba90595d5d6aaf071d6efcc815bceba8 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Mon, 21 Oct 2019 16:49:13 -0700 Subject: Change coding cookie to use utf-8 (lowercase) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit While perfectly valid, the encoding 'UTF-8' (uppercase) is not recognized by the Emacs MULE system. As such, it displays the following warning when opening a file with it used as an encoding cookie: Warning (mule): Invalid coding system ‘UTF-8’ is specified for the current buffer/file by the :coding tag. It is highly recommended to fix it before writing to a file. Some discussion of this can be found at: https://stackoverflow.com/questions/14031724/how-to-make-emacs-accept-utf-8-uppercase-encoding While the post does offer a workaround for Emacs users, rather than ask all to implement it, use the more typical utf-8 (lowercase). --- setuptools/tests/test_config.py | 2 +- setuptools/tests/test_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'setuptools') diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index 1b94a586..69d8d00d 100644 --- a/setuptools/tests/test_config.py +++ b/setuptools/tests/test_config.py @@ -1,4 +1,4 @@ -# -*- coding: UTF-8 -*- +# -*- coding: utf-8 -*- from __future__ import unicode_literals import contextlib diff --git a/setuptools/tests/test_test.py b/setuptools/tests/test_test.py index faaa6ba9..3415913b 100644 --- a/setuptools/tests/test_test.py +++ b/setuptools/tests/test_test.py @@ -1,4 +1,4 @@ -# -*- coding: UTF-8 -*- +# -*- coding: utf-8 -*- from __future__ import unicode_literals -- cgit v1.2.3 From cd84510713ada48bf33d4efa749c2952e3fc1a49 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 19 Oct 2019 08:39:30 -0700 Subject: Deprecate the test command Provide a warning to users. Suggest using tox as an alternative generic entry point. Refs #1684 --- setuptools/command/test.py | 10 ++++++++- setuptools/tests/test_test.py | 50 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) (limited to 'setuptools') diff --git a/setuptools/command/test.py b/setuptools/command/test.py index 973e4eb2..c148b38d 100644 --- a/setuptools/command/test.py +++ b/setuptools/command/test.py @@ -74,7 +74,7 @@ class NonDataProperty: class test(Command): """Command to run unit tests after in-place build""" - description = "run unit tests after in-place build" + description = "run unit tests after in-place build (deprecated)" user_options = [ ('test-module=', 'm', "Run 'test_suite' in specified module"), @@ -214,6 +214,14 @@ class test(Command): return itertools.chain(ir_d, tr_d, er_d) def run(self): + self.announce( + "WARNING: Testing via this command is deprecated and will be " + "removed in a future version. Users looking for a generic test " + "entry point independent of test runner are encouraged to use " + "tox.", + log.WARN, + ) + installed_dists = self.install_dists(self.distribution) cmd = ' '.join(self._argv) diff --git a/setuptools/tests/test_test.py b/setuptools/tests/test_test.py index faaa6ba9..382bd640 100644 --- a/setuptools/tests/test_test.py +++ b/setuptools/tests/test_test.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals +import mock from distutils import log import os @@ -124,3 +125,52 @@ def test_tests_are_run_once(capfd): cmd.run() out, err = capfd.readouterr() assert out == 'Foo\n' + + +@pytest.mark.usefixtures('sample_test') +def test_warns_deprecation(capfd): + params = dict( + name='foo', + packages=['name', 'name.space', 'name.space.tests'], + namespace_packages=['name'], + test_suite='name.space.tests.test_suite', + use_2to3=True + ) + dist = Distribution(params) + dist.script_name = 'setup.py' + cmd = test(dist) + cmd.ensure_finalized() + cmd.announce = mock.Mock() + cmd.run() + capfd.readouterr() + msg = ( + "WARNING: Testing via this command is deprecated and will be " + "removed in a future version. Users looking for a generic test " + "entry point independent of test runner are encouraged to use " + "tox." + ) + cmd.announce.assert_any_call(msg, log.WARN) + + +@pytest.mark.usefixtures('sample_test') +def test_deprecation_stderr(capfd): + params = dict( + name='foo', + packages=['name', 'name.space', 'name.space.tests'], + namespace_packages=['name'], + test_suite='name.space.tests.test_suite', + use_2to3=True + ) + dist = Distribution(params) + dist.script_name = 'setup.py' + cmd = test(dist) + cmd.ensure_finalized() + cmd.run() + out, err = capfd.readouterr() + msg = ( + "WARNING: Testing via this command is deprecated and will be " + "removed in a future version. Users looking for a generic test " + "entry point independent of test runner are encouraged to use " + "tox.\n" + ) + assert msg in err -- cgit v1.2.3 From 4069e0b536802a47c8c15ce839fd98a5f4c84620 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Mon, 21 Oct 2019 17:01:18 -0700 Subject: Remove outdated comment and suppressed exception from test_test.py The test command has not called sys.exit since commit 2c4fd43277fc477d85b50e15c37b176136676270. --- setuptools/tests/test_test.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) (limited to 'setuptools') diff --git a/setuptools/tests/test_test.py b/setuptools/tests/test_test.py index 280c837b..6242a018 100644 --- a/setuptools/tests/test_test.py +++ b/setuptools/tests/test_test.py @@ -86,9 +86,7 @@ def test_test(capfd): dist.script_name = 'setup.py' cmd = test(dist) cmd.ensure_finalized() - # The test runner calls sys.exit - with contexts.suppress_exceptions(SystemExit): - cmd.run() + cmd.run() out, err = capfd.readouterr() assert out == 'Foo\n' @@ -120,9 +118,7 @@ def test_tests_are_run_once(capfd): dist.script_name = 'setup.py' cmd = test(dist) cmd.ensure_finalized() - # The test runner calls sys.exit - with contexts.suppress_exceptions(SystemExit): - cmd.run() + cmd.run() out, err = capfd.readouterr() assert out == 'Foo\n' -- cgit v1.2.3 From f430e585d84a5c63bb3b52e17af2f1b40fec8b71 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Oct 2019 19:16:36 -0400 Subject: Remove apparently unrelated change to test --- setuptools/tests/test_integration.py | 1 - 1 file changed, 1 deletion(-) (limited to 'setuptools') diff --git a/setuptools/tests/test_integration.py b/setuptools/tests/test_integration.py index 1c0b2b18..1e132188 100644 --- a/setuptools/tests/test_integration.py +++ b/setuptools/tests/test_integration.py @@ -143,7 +143,6 @@ def test_build_deps_on_distutils(request, tmpdir_factory, build_dep): 'tests_require', 'python_requires', 'install_requires', - 'long_description_content_type', ] assert not match or match.group(1).strip('"\'') in allowed_unknowns -- cgit v1.2.3 From 85a9ca5e75abf00e0dde55dde4e2b0a11f93c04a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Oct 2019 19:50:53 -0400 Subject: Extract 'imp' re-implementation to setuptools._imp and wrap it in py27compat for compatibility. --- setuptools/_imp.py | 72 ++++++++++++++++++++++++++++++++++++ setuptools/depends.py | 95 ++++-------------------------------------------- setuptools/py27compat.py | 32 ++++++++++++++++ 3 files changed, 112 insertions(+), 87 deletions(-) create mode 100644 setuptools/_imp.py (limited to 'setuptools') diff --git a/setuptools/_imp.py b/setuptools/_imp.py new file mode 100644 index 00000000..6bc90243 --- /dev/null +++ b/setuptools/_imp.py @@ -0,0 +1,72 @@ +""" +Re-implementation of find_module and get_frozen_object +from the deprecated imp module. +""" + +import os +import importlib.util +import importlib.machinery + + +PY_SOURCE = 1 +PY_COMPILED = 2 +C_EXTENSION = 3 +C_BUILTIN = 6 +PY_FROZEN = 7 + + +def find_module(module, paths=None): + """ + """ + spec = importlib.util.find_spec(module, paths) + if spec is None: + raise ImportError("Can't find %s" % module) + if not spec.has_location and hasattr(spec, 'submodule_search_locations'): + spec = importlib.util.spec_from_loader('__init__.py', spec.loader) + + kind = -1 + file = None + static = isinstance(spec.loader, type) + if spec.origin == 'frozen' or static and issubclass( + spec.loader, importlib.machinery.FrozenImporter): + kind = PY_FROZEN + path = None # imp compabilty + suffix = mode = '' # imp compability + elif spec.origin == 'built-in' or static and issubclass( + spec.loader, importlib.machinery.BuiltinImporter): + kind = C_BUILTIN + path = None # imp compabilty + suffix = mode = '' # imp compability + elif spec.has_location: + path = spec.origin + suffix = os.path.splitext(path)[1] + mode = 'r' if suffix in importlib.machinery.SOURCE_SUFFIXES else 'rb' + + if suffix in importlib.machinery.SOURCE_SUFFIXES: + kind = PY_SOURCE + elif suffix in importlib.machinery.BYTECODE_SUFFIXES: + kind = PY_COMPILED + elif suffix in importlib.machinery.EXTENSION_SUFFIXES: + kind = C_EXTENSION + + if kind in {PY_SOURCE, PY_COMPILED}: + file = open(path, mode) + else: + path = None + suffix = mode = '' + + return file, path, (suffix, mode, kind) + + +def get_frozen_object(module, paths): + spec = importlib.util.find_spec(module, paths) + if hasattr(spec, 'submodule_search_locations'): + spec = importlib.util.spec_from_loader('__init__.py', spec.loader) + return spec.loader.get_code(module) + + +def get_module(module, paths, info): + spec = importlib.util.find_spec(module, paths) + if hasattr(spec, 'submodule_search_locations'): + spec = importlib.util.spec_from_loader('__init__.py', spec.loader) + return importlib.util.module_from_spec(spec) diff --git a/setuptools/depends.py b/setuptools/depends.py index 97f0ed9d..eed4913a 100644 --- a/setuptools/depends.py +++ b/setuptools/depends.py @@ -1,22 +1,11 @@ import sys import marshal from distutils.version import StrictVersion -from setuptools.extern import six from .py33compat import Bytecode -if six.PY2: - import imp - from imp import PKG_DIRECTORY, PY_COMPILED, PY_SOURCE, PY_FROZEN -else: - import os.path - from importlib.util import find_spec, spec_from_loader - from importlib.machinery import SOURCE_SUFFIXES, BYTECODE_SUFFIXES, EXTENSION_SUFFIXES, BuiltinImporter, FrozenImporter - PY_SOURCE = 1 - PY_COMPILED = 2 - C_EXTENSION = 3 - C_BUILTIN = 6 - PY_FROZEN = 7 +from .py27compat import find_module, PY_COMPILED, PY_FROZEN, PY_SOURCE +from . import py27compat __all__ = [ @@ -27,7 +16,8 @@ __all__ = [ class Require: """A prerequisite to building or installing a distribution""" - def __init__(self, name, requested_version, module, homepage='', + def __init__( + self, name, requested_version, module, homepage='', attribute=None, format=None): if format is None and requested_version is not None: @@ -91,63 +81,6 @@ class Require: return self.version_ok(version) -def find_module(module, paths=None): - """Just like 'imp.find_module()', but with package support""" - if six.PY3: - spec = find_spec(module, paths) - if spec is None: - raise ImportError("Can't find %s" % module) - if not spec.has_location and hasattr(spec, 'submodule_search_locations'): - spec = spec_from_loader('__init__.py', spec.loader) - - kind = -1 - file = None - static = isinstance(spec.loader, type) - if spec.origin == 'frozen' or static and issubclass(spec.loader, FrozenImporter): - kind = PY_FROZEN - path = None # imp compabilty - suffix = mode = '' # imp compability - elif spec.origin == 'built-in' or static and issubclass(spec.loader, BuiltinImporter): - kind = C_BUILTIN - path = None # imp compabilty - suffix = mode = '' # imp compability - elif spec.has_location: - frozen = False - path = spec.origin - suffix = os.path.splitext(path)[1] - mode = 'r' if suffix in SOURCE_SUFFIXES else 'rb' - - if suffix in SOURCE_SUFFIXES: - kind = PY_SOURCE - elif suffix in BYTECODE_SUFFIXES: - kind = PY_COMPILED - elif suffix in EXTENSION_SUFFIXES: - kind = C_EXTENSION - - if kind in {PY_SOURCE, PY_COMPILED}: - file = open(path, mode) - else: - path = None - suffix = mode= '' - - return file, path, (suffix, mode, kind) - - else: - parts = module.split('.') - while parts: - part = parts.pop(0) - f, path, (suffix, mode, kind) = info = imp.find_module(part, paths) - - if kind == PKG_DIRECTORY: - parts = parts or ['__init__'] - paths = [path] - - elif parts: - raise ImportError("Can't find %r in %s" % (parts, module)) - - return info - - def get_module_constant(module, symbol, default=-1, paths=None): """Find 'module' by searching 'paths', and extract 'symbol' @@ -156,35 +89,23 @@ def get_module_constant(module, symbol, default=-1, paths=None): constant. Otherwise, return 'default'.""" try: - f, path, (suffix, mode, kind) = find_module(module, paths) + f, path, (suffix, mode, kind) = info = find_module(module, paths) except ImportError: # Module doesn't exist return None - if six.PY3: - spec = find_spec(module, paths) - if hasattr(spec, 'submodule_search_locations'): - spec = spec_from_loader('__init__.py', spec.loader) - try: if kind == PY_COMPILED: f.read(8) # skip magic & date code = marshal.load(f) elif kind == PY_FROZEN: - if six.PY2: - code = imp.get_frozen_object(module) - else: - code = spec.loader.get_code(module) + code = py27compat.get_frozen_object(module, paths) elif kind == PY_SOURCE: code = compile(f.read(), path, 'exec') else: # Not something we can parse; we'll have to import it. :( - if module not in sys.modules: - if six.PY2: - imp.load_module(module, f, path, (suffix, mode, kind)) - else: - sys.modules[module] = module_from_spec(spec) - return getattr(sys.modules[module], symbol, None) + imported = py27compat.get_module(module, paths, info) + return getattr(imported, symbol, None) finally: if f: diff --git a/setuptools/py27compat.py b/setuptools/py27compat.py index 2985011b..cf5fb33e 100644 --- a/setuptools/py27compat.py +++ b/setuptools/py27compat.py @@ -2,6 +2,7 @@ Compatibility Support for Python 2.7 and earlier """ +import sys import platform from setuptools.extern import six @@ -26,3 +27,34 @@ linux_py2_ascii = ( rmtree_safe = str if linux_py2_ascii else lambda x: x """Workaround for http://bugs.python.org/issue24672""" + + +try: + from ._imp import find_module, PY_COMPILED, PY_FROZEN, PY_SOURCE + from ._imp import get_frozen_object, get_module +except ImportError: + import imp + from imp import PY_COMPILED, PY_FROZEN, PY_SOURCE # noqa + + def find_module(module, paths=None): + """Just like 'imp.find_module()', but with package support""" + parts = module.split('.') + while parts: + part = parts.pop(0) + f, path, (suffix, mode, kind) = info = imp.find_module(part, paths) + + if kind == imp.PKG_DIRECTORY: + parts = parts or ['__init__'] + paths = [path] + + elif parts: + raise ImportError("Can't find %r in %s" % (parts, module)) + + return info + + def get_frozen_object(module, paths): + return imp.get_frozen_object(module) + + def get_module(module, paths, info): + imp.load_module(*info) + return sys.modules[module] -- cgit v1.2.3 From 773f1ec3c2622a78ee0280eb1d2b03c60871c52b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Oct 2019 19:54:42 -0400 Subject: Rely on contextlib.closing for brevity. --- setuptools/depends.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) (limited to 'setuptools') diff --git a/setuptools/depends.py b/setuptools/depends.py index eed4913a..a37675cb 100644 --- a/setuptools/depends.py +++ b/setuptools/depends.py @@ -1,5 +1,6 @@ import sys import marshal +import contextlib from distutils.version import StrictVersion from .py33compat import Bytecode @@ -81,6 +82,17 @@ class Require: return self.version_ok(version) +def maybe_close(f): + @contextlib.contextmanager + def empty(): + yield + return + if not f: + return empty() + + return contextlib.closing(f) + + def get_module_constant(module, symbol, default=-1, paths=None): """Find 'module' by searching 'paths', and extract 'symbol' @@ -94,7 +106,7 @@ def get_module_constant(module, symbol, default=-1, paths=None): # Module doesn't exist return None - try: + with maybe_close(f): if kind == PY_COMPILED: f.read(8) # skip magic & date code = marshal.load(f) @@ -107,10 +119,6 @@ def get_module_constant(module, symbol, default=-1, paths=None): imported = py27compat.get_module(module, paths, info) return getattr(imported, symbol, None) - finally: - if f: - f.close() - return extract_constant(code, symbol, default) -- cgit v1.2.3 From 37d617cd983446dfe8073038d03dd31fc7f66796 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Oct 2019 20:40:07 -0400 Subject: Extract _resolve --- setuptools/_imp.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) (limited to 'setuptools') diff --git a/setuptools/_imp.py b/setuptools/_imp.py index 6bc90243..c400d455 100644 --- a/setuptools/_imp.py +++ b/setuptools/_imp.py @@ -60,13 +60,17 @@ def find_module(module, paths=None): def get_frozen_object(module, paths): spec = importlib.util.find_spec(module, paths) - if hasattr(spec, 'submodule_search_locations'): - spec = importlib.util.spec_from_loader('__init__.py', spec.loader) - return spec.loader.get_code(module) + return spec.loader.get_code(_resolve(module)) + + +def _resolve(spec): + return ( + importlib.util.spec_from_loader('__init__.py', spec.loader) + if hasattr(spec, 'submodule_search_locations') + else spec + ) def get_module(module, paths, info): spec = importlib.util.find_spec(module, paths) - if hasattr(spec, 'submodule_search_locations'): - spec = importlib.util.spec_from_loader('__init__.py', spec.loader) - return importlib.util.module_from_spec(spec) + return importlib.util.module_from_spec(_resolve(spec)) -- cgit v1.2.3 From 4948b14a66f00eba749e52b77281f711413e071b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Oct 2019 20:40:58 -0400 Subject: Avoid _resolve in get_module (causes failures). --- setuptools/_imp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'setuptools') diff --git a/setuptools/_imp.py b/setuptools/_imp.py index c400d455..cee91551 100644 --- a/setuptools/_imp.py +++ b/setuptools/_imp.py @@ -73,4 +73,4 @@ def _resolve(spec): def get_module(module, paths, info): spec = importlib.util.find_spec(module, paths) - return importlib.util.module_from_spec(_resolve(spec)) + return importlib.util.module_from_spec(spec) -- cgit v1.2.3 From 16051d6b2d3641dc8951e90f7f04bcd04b8954d3 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Mon, 28 Oct 2019 10:12:53 +0300 Subject: imp load_module fix --- setuptools/py27compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'setuptools') diff --git a/setuptools/py27compat.py b/setuptools/py27compat.py index cf5fb33e..1d57360f 100644 --- a/setuptools/py27compat.py +++ b/setuptools/py27compat.py @@ -56,5 +56,5 @@ except ImportError: return imp.get_frozen_object(module) def get_module(module, paths, info): - imp.load_module(*info) + imp.load_module(module, *info) return sys.modules[module] -- cgit v1.2.3 From 65fe7abeaba9e82b8f3755054759fed21c0c489b Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Mon, 28 Oct 2019 10:28:55 +0300 Subject: py34 compat --- setuptools/_imp.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'setuptools') diff --git a/setuptools/_imp.py b/setuptools/_imp.py index cee91551..09073d44 100644 --- a/setuptools/_imp.py +++ b/setuptools/_imp.py @@ -4,6 +4,7 @@ from the deprecated imp module. """ import os +import sys import importlib.util import importlib.machinery @@ -70,7 +71,12 @@ def _resolve(spec): else spec ) +def _module_from_spec(spec): + if sys.version_info >= (3, 5): + return importlib.util.module_from_spec(spec) + else: + return spec.loader.load_module(spec.name) def get_module(module, paths, info): spec = importlib.util.find_spec(module, paths) - return importlib.util.module_from_spec(spec) + return _module_from_spec(spec) -- cgit v1.2.3 From ce01c0199f93612848e664bfd920083168eaa293 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Mon, 28 Oct 2019 10:59:21 +0300 Subject: add docstring to find_module --- setuptools/_imp.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'setuptools') diff --git a/setuptools/_imp.py b/setuptools/_imp.py index 09073d44..dc4b1b2c 100644 --- a/setuptools/_imp.py +++ b/setuptools/_imp.py @@ -17,8 +17,7 @@ PY_FROZEN = 7 def find_module(module, paths=None): - """ - """ + """Just like 'imp.find_module()', but with package support""" spec = importlib.util.find_spec(module, paths) if spec is None: raise ImportError("Can't find %s" % module) -- cgit v1.2.3 From 2f4952927e41643100e6e6f58124f34331c14add Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Mon, 28 Oct 2019 11:07:27 +0300 Subject: remove _resolve --- setuptools/_imp.py | 7 ------- 1 file changed, 7 deletions(-) (limited to 'setuptools') diff --git a/setuptools/_imp.py b/setuptools/_imp.py index dc4b1b2c..49ddc852 100644 --- a/setuptools/_imp.py +++ b/setuptools/_imp.py @@ -63,13 +63,6 @@ def get_frozen_object(module, paths): return spec.loader.get_code(_resolve(module)) -def _resolve(spec): - return ( - importlib.util.spec_from_loader('__init__.py', spec.loader) - if hasattr(spec, 'submodule_search_locations') - else spec - ) - def _module_from_spec(spec): if sys.version_info >= (3, 5): return importlib.util.module_from_spec(spec) -- cgit v1.2.3 From 7489ea4047661a7dbd46ff155abe45c548284676 Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Mon, 28 Oct 2019 18:47:31 +0100 Subject: msvc: fix Python 2 support --- setuptools/msvc.py | 1 + 1 file changed, 1 insertion(+) (limited to 'setuptools') diff --git a/setuptools/msvc.py b/setuptools/msvc.py index ffa7053b..2ffe1c81 100644 --- a/setuptools/msvc.py +++ b/setuptools/msvc.py @@ -20,6 +20,7 @@ This may also support compilers shipped with compatible Visual Studio versions. """ import json +from io import open from os import listdir, pathsep from os.path import join, isfile, isdir, dirname import sys -- cgit v1.2.3 From 823ab9d2ec4ab89f90c0a781d872c9071b4afc13 Mon Sep 17 00:00:00 2001 From: Mick Koch Date: Mon, 20 May 2019 18:25:19 -0400 Subject: Add support for `license_files` option in metadata --- setuptools/command/sdist.py | 26 +++-- setuptools/config.py | 1 + setuptools/dist.py | 1 + setuptools/tests/test_egg_info.py | 198 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 218 insertions(+), 8 deletions(-) (limited to 'setuptools') diff --git a/setuptools/command/sdist.py b/setuptools/command/sdist.py index dc253981..24316640 100644 --- a/setuptools/command/sdist.py +++ b/setuptools/command/sdist.py @@ -200,10 +200,12 @@ class sdist(sdist_add_defaults, orig.sdist): manifest.close() def check_license(self): - """Checks if license_file' is configured and adds it to - 'self.filelist' if the value contains a valid path. + """Checks if license_file' or 'license_files' is configured and adds any + valid paths to 'self.filelist'. """ + files = set() + opts = self.distribution.get_option_dict('metadata') # ignore the source of the value @@ -211,11 +213,19 @@ class sdist(sdist_add_defaults, orig.sdist): if license_file is None: log.debug("'license_file' option was not specified") - return + else: + files.add(license_file) - if not os.path.exists(license_file): - log.warn("warning: Failed to find the configured license file '%s'", - license_file) - return + try: + files.update(self.distribution.metadata.license_files) + except TypeError: + log.warn("warning: 'license_files' option is malformed") + + for f in files: + if not os.path.exists(f): + log.warn( + "warning: Failed to find the configured license file '%s'", + f) + continue - self.filelist.append(license_file) + self.filelist.append(f) diff --git a/setuptools/config.py b/setuptools/config.py index 2d50e25e..9b9a0c45 100644 --- a/setuptools/config.py +++ b/setuptools/config.py @@ -483,6 +483,7 @@ class ConfigMetadataHandler(ConfigHandler): 'obsoletes': parse_list, 'classifiers': self._get_parser_compound(parse_file, parse_list), 'license': exclude_files_parser('license'), + 'license_files': parse_list, 'description': parse_file, 'long_description': parse_file, 'version': self._parse_version, diff --git a/setuptools/dist.py b/setuptools/dist.py index 2e5ad4bd..fb379a20 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -409,6 +409,7 @@ class Distribution(_Distribution): 'long_description_content_type': None, 'project_urls': dict, 'provides_extras': ordered_set.OrderedSet, + 'license_files': list, } _patched_dist = None diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index 316eb2ed..61da1bda 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -567,6 +567,204 @@ class TestEggInfo: assert 'LICENSE' not in sources_text assert 'INVALID_LICENSE' not in sources_text # for invalid license test + @pytest.mark.parametrize("files, incl_licenses, excl_licenses", [ + ({ + 'setup.cfg': DALS(""" + [metadata] + license_files = + LICENSE-ABC + LICENSE-XYZ + """), + 'LICENSE-ABC': DALS("ABC license"), + 'LICENSE-XYZ': DALS("XYZ license") + }, ['LICENSE-ABC', 'LICENSE-XYZ'], []), # with licenses + ({ + 'setup.cfg': DALS(""" + [metadata] + license_files = LICENSE-ABC, LICENSE-XYZ + """), + 'LICENSE-ABC': DALS("ABC license"), + 'LICENSE-XYZ': DALS("XYZ license") + }, ['LICENSE-ABC', 'LICENSE-XYZ'], []), # with commas + ({ + 'setup.cfg': DALS(""" + [metadata] + license_files = + LICENSE-ABC + """), + 'LICENSE-ABC': DALS("ABC license"), + 'LICENSE-XYZ': DALS("XYZ license") + }, ['LICENSE-ABC'], ['LICENSE-XYZ']), # with one license + ({ + 'setup.cfg': DALS(""" + [metadata] + license_files = + """), + 'LICENSE-ABC': DALS("ABC license"), + 'LICENSE-XYZ': DALS("XYZ license") + }, [], ['LICENSE-ABC', 'LICENSE-XYZ']), # empty + ({ + 'setup.cfg': DALS(""" + [metadata] + license_files = LICENSE-XYZ + """), + 'LICENSE-ABC': DALS("ABC license"), + 'LICENSE-XYZ': DALS("XYZ license") + }, ['LICENSE-XYZ'], ['LICENSE-ABC']), # on same line + ({ + 'setup.cfg': DALS(""" + [metadata] + license_files = + LICENSE-ABC + INVALID_LICENSE + """), + 'LICENSE-ABC': DALS("Test license") + }, ['LICENSE-ABC'], ['INVALID_LICENSE']), # with an invalid license + ({ + 'setup.cfg': DALS(""" + """), + 'LICENSE': DALS("Test license") + }, [], ['LICENSE']), # no license_files attribute + ({ + 'setup.cfg': DALS(""" + [metadata] + license_files = LICENSE + """), + 'MANIFEST.in': DALS("exclude LICENSE"), + 'LICENSE': DALS("Test license") + }, [], ['LICENSE']), # license file is manually excluded + ({ + 'setup.cfg': DALS(""" + [metadata] + license_files = + LICENSE-ABC + LICENSE-XYZ + """), + 'MANIFEST.in': DALS("exclude LICENSE-XYZ"), + 'LICENSE-ABC': DALS("ABC license"), + 'LICENSE-XYZ': DALS("XYZ license") + }, ['LICENSE-ABC'], ['LICENSE-XYZ']) # subset is manually excluded + ]) + def test_setup_cfg_license_files( + self, tmpdir_cwd, env, files, incl_licenses, excl_licenses): + self._create_project() + build_files(files) + + environment.run_setup_py( + cmd=['egg_info'], + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]) + ) + egg_info_dir = os.path.join('.', 'foo.egg-info') + + with open(os.path.join(egg_info_dir, 'SOURCES.txt')) as sources_file: + sources_lines = list(line.strip() for line in sources_file) + + for lf in incl_licenses: + assert sources_lines.count(lf) == 1 + + for lf in excl_licenses: + assert sources_lines.count(lf) == 0 + + @pytest.mark.parametrize("files, incl_licenses, excl_licenses", [ + ({ + 'setup.cfg': DALS(""" + [metadata] + license_file = + license_files = + """), + 'LICENSE-ABC': DALS("ABC license"), + 'LICENSE-XYZ': DALS("XYZ license") + }, [], ['LICENSE-ABC', 'LICENSE-XYZ']), # both empty + ({ + 'setup.cfg': DALS(""" + [metadata] + license_file = + LICENSE-ABC + LICENSE-XYZ + """), + 'LICENSE-ABC': DALS("ABC license"), + 'LICENSE-XYZ': DALS("XYZ license") + }, [], ['LICENSE-ABC', 'LICENSE-XYZ']), # license_file is still singular + ({ + 'setup.cfg': DALS(""" + [metadata] + license_file = LICENSE-ABC + license_files = + LICENSE-XYZ + LICENSE-PQR + """), + 'LICENSE-ABC': DALS("ABC license"), + 'LICENSE-PQR': DALS("PQR license"), + 'LICENSE-XYZ': DALS("XYZ license") + }, ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'], []), # combined + ({ + 'setup.cfg': DALS(""" + [metadata] + license_file = LICENSE-ABC + license_files = + LICENSE-ABC + LICENSE-XYZ + LICENSE-PQR + """), + 'LICENSE-ABC': DALS("ABC license"), + 'LICENSE-PQR': DALS("PQR license"), + 'LICENSE-XYZ': DALS("XYZ license") + }, ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'], []), # duplicate license + ({ + 'setup.cfg': DALS(""" + [metadata] + license_file = LICENSE-ABC + license_files = + LICENSE-XYZ + """), + 'LICENSE-ABC': DALS("ABC license"), + 'LICENSE-PQR': DALS("PQR license"), + 'LICENSE-XYZ': DALS("XYZ license") + }, ['LICENSE-ABC', 'LICENSE-XYZ'], ['LICENSE-PQR']), # combined subset + ({ + 'setup.cfg': DALS(""" + [metadata] + license_file = LICENSE-ABC + license_files = + LICENSE-XYZ + LICENSE-PQR + """), + 'LICENSE-PQR': DALS("Test license") + }, ['LICENSE-PQR'], ['LICENSE-ABC', 'LICENSE-XYZ']), # with invalid licenses + ({ + 'setup.cfg': DALS(""" + [metadata] + license_file = LICENSE-ABC + license_files = + LICENSE-PQR + LICENSE-XYZ + """), + 'MANIFEST.in': DALS("exclude LICENSE-ABC\nexclude LICENSE-PQR"), + 'LICENSE-ABC': DALS("ABC license"), + 'LICENSE-PQR': DALS("PQR license"), + 'LICENSE-XYZ': DALS("XYZ license") + }, ['LICENSE-XYZ'], ['LICENSE-ABC', 'LICENSE-PQR']) # manually excluded + ]) + def test_setup_cfg_license_file_license_files( + self, tmpdir_cwd, env, files, incl_licenses, excl_licenses): + self._create_project() + build_files(files) + + environment.run_setup_py( + cmd=['egg_info'], + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]) + ) + egg_info_dir = os.path.join('.', 'foo.egg-info') + + with open(os.path.join(egg_info_dir, 'SOURCES.txt')) as sources_file: + sources_lines = list(line.strip() for line in sources_file) + + for lf in incl_licenses: + assert sources_lines.count(lf) == 1 + + for lf in excl_licenses: + assert sources_lines.count(lf) == 0 + def test_long_description_content_type(self, tmpdir_cwd, env): # Test that specifying a `long_description_content_type` keyword arg to # the `setup` function results in writing a `Description-Content-Type` -- cgit v1.2.3 From 648dfe5afea3bf2a690c9267131a503bcd37d289 Mon Sep 17 00:00:00 2001 From: Mick Koch Date: Mon, 28 Oct 2019 18:38:30 -0400 Subject: Remove DALS for single-line strings --- setuptools/tests/test_egg_info.py | 80 +++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 40 deletions(-) (limited to 'setuptools') diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index 61da1bda..0db204ba 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -524,27 +524,27 @@ class TestEggInfo: [metadata] license_file = LICENSE """), - 'LICENSE': DALS("Test license") + 'LICENSE': "Test license" }, True), # with license ({ 'setup.cfg': DALS(""" [metadata] license_file = INVALID_LICENSE """), - 'LICENSE': DALS("Test license") + 'LICENSE': "Test license" }, False), # with an invalid license ({ 'setup.cfg': DALS(""" """), - 'LICENSE': DALS("Test license") + 'LICENSE': "Test license" }, False), # no license_file attribute ({ 'setup.cfg': DALS(""" [metadata] license_file = LICENSE """), - 'MANIFEST.in': DALS("exclude LICENSE"), - 'LICENSE': DALS("Test license") + 'MANIFEST.in': "exclude LICENSE", + 'LICENSE': "Test license" }, False) # license file is manually excluded ]) def test_setup_cfg_license_file( @@ -575,16 +575,16 @@ class TestEggInfo: LICENSE-ABC LICENSE-XYZ """), - 'LICENSE-ABC': DALS("ABC license"), - 'LICENSE-XYZ': DALS("XYZ license") + 'LICENSE-ABC': "ABC license", + 'LICENSE-XYZ': "XYZ license" }, ['LICENSE-ABC', 'LICENSE-XYZ'], []), # with licenses ({ 'setup.cfg': DALS(""" [metadata] license_files = LICENSE-ABC, LICENSE-XYZ """), - 'LICENSE-ABC': DALS("ABC license"), - 'LICENSE-XYZ': DALS("XYZ license") + 'LICENSE-ABC': "ABC license", + 'LICENSE-XYZ': "XYZ license" }, ['LICENSE-ABC', 'LICENSE-XYZ'], []), # with commas ({ 'setup.cfg': DALS(""" @@ -592,24 +592,24 @@ class TestEggInfo: license_files = LICENSE-ABC """), - 'LICENSE-ABC': DALS("ABC license"), - 'LICENSE-XYZ': DALS("XYZ license") + 'LICENSE-ABC': "ABC license", + 'LICENSE-XYZ': "XYZ license" }, ['LICENSE-ABC'], ['LICENSE-XYZ']), # with one license ({ 'setup.cfg': DALS(""" [metadata] license_files = """), - 'LICENSE-ABC': DALS("ABC license"), - 'LICENSE-XYZ': DALS("XYZ license") + 'LICENSE-ABC': "ABC license", + 'LICENSE-XYZ': "XYZ license" }, [], ['LICENSE-ABC', 'LICENSE-XYZ']), # empty ({ 'setup.cfg': DALS(""" [metadata] license_files = LICENSE-XYZ """), - 'LICENSE-ABC': DALS("ABC license"), - 'LICENSE-XYZ': DALS("XYZ license") + 'LICENSE-ABC': "ABC license", + 'LICENSE-XYZ': "XYZ license" }, ['LICENSE-XYZ'], ['LICENSE-ABC']), # on same line ({ 'setup.cfg': DALS(""" @@ -618,20 +618,20 @@ class TestEggInfo: LICENSE-ABC INVALID_LICENSE """), - 'LICENSE-ABC': DALS("Test license") + 'LICENSE-ABC': "Test license" }, ['LICENSE-ABC'], ['INVALID_LICENSE']), # with an invalid license ({ 'setup.cfg': DALS(""" """), - 'LICENSE': DALS("Test license") + 'LICENSE': "Test license" }, [], ['LICENSE']), # no license_files attribute ({ 'setup.cfg': DALS(""" [metadata] license_files = LICENSE """), - 'MANIFEST.in': DALS("exclude LICENSE"), - 'LICENSE': DALS("Test license") + 'MANIFEST.in': "exclude LICENSE", + 'LICENSE': "Test license" }, [], ['LICENSE']), # license file is manually excluded ({ 'setup.cfg': DALS(""" @@ -640,9 +640,9 @@ class TestEggInfo: LICENSE-ABC LICENSE-XYZ """), - 'MANIFEST.in': DALS("exclude LICENSE-XYZ"), - 'LICENSE-ABC': DALS("ABC license"), - 'LICENSE-XYZ': DALS("XYZ license") + 'MANIFEST.in': "exclude LICENSE-XYZ", + 'LICENSE-ABC': "ABC license", + 'LICENSE-XYZ': "XYZ license" }, ['LICENSE-ABC'], ['LICENSE-XYZ']) # subset is manually excluded ]) def test_setup_cfg_license_files( @@ -672,8 +672,8 @@ class TestEggInfo: license_file = license_files = """), - 'LICENSE-ABC': DALS("ABC license"), - 'LICENSE-XYZ': DALS("XYZ license") + 'LICENSE-ABC': "ABC license", + 'LICENSE-XYZ': "XYZ license" }, [], ['LICENSE-ABC', 'LICENSE-XYZ']), # both empty ({ 'setup.cfg': DALS(""" @@ -682,8 +682,8 @@ class TestEggInfo: LICENSE-ABC LICENSE-XYZ """), - 'LICENSE-ABC': DALS("ABC license"), - 'LICENSE-XYZ': DALS("XYZ license") + 'LICENSE-ABC': "ABC license", + 'LICENSE-XYZ': "XYZ license" }, [], ['LICENSE-ABC', 'LICENSE-XYZ']), # license_file is still singular ({ 'setup.cfg': DALS(""" @@ -693,9 +693,9 @@ class TestEggInfo: LICENSE-XYZ LICENSE-PQR """), - 'LICENSE-ABC': DALS("ABC license"), - 'LICENSE-PQR': DALS("PQR license"), - 'LICENSE-XYZ': DALS("XYZ license") + 'LICENSE-ABC': "ABC license", + 'LICENSE-PQR': "PQR license", + 'LICENSE-XYZ': "XYZ license" }, ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'], []), # combined ({ 'setup.cfg': DALS(""" @@ -706,9 +706,9 @@ class TestEggInfo: LICENSE-XYZ LICENSE-PQR """), - 'LICENSE-ABC': DALS("ABC license"), - 'LICENSE-PQR': DALS("PQR license"), - 'LICENSE-XYZ': DALS("XYZ license") + 'LICENSE-ABC': "ABC license", + 'LICENSE-PQR': "PQR license", + 'LICENSE-XYZ': "XYZ license" }, ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'], []), # duplicate license ({ 'setup.cfg': DALS(""" @@ -717,9 +717,9 @@ class TestEggInfo: license_files = LICENSE-XYZ """), - 'LICENSE-ABC': DALS("ABC license"), - 'LICENSE-PQR': DALS("PQR license"), - 'LICENSE-XYZ': DALS("XYZ license") + 'LICENSE-ABC': "ABC license", + 'LICENSE-PQR': "PQR license", + 'LICENSE-XYZ': "XYZ license" }, ['LICENSE-ABC', 'LICENSE-XYZ'], ['LICENSE-PQR']), # combined subset ({ 'setup.cfg': DALS(""" @@ -729,7 +729,7 @@ class TestEggInfo: LICENSE-XYZ LICENSE-PQR """), - 'LICENSE-PQR': DALS("Test license") + 'LICENSE-PQR': "Test license" }, ['LICENSE-PQR'], ['LICENSE-ABC', 'LICENSE-XYZ']), # with invalid licenses ({ 'setup.cfg': DALS(""" @@ -739,10 +739,10 @@ class TestEggInfo: LICENSE-PQR LICENSE-XYZ """), - 'MANIFEST.in': DALS("exclude LICENSE-ABC\nexclude LICENSE-PQR"), - 'LICENSE-ABC': DALS("ABC license"), - 'LICENSE-PQR': DALS("PQR license"), - 'LICENSE-XYZ': DALS("XYZ license") + 'MANIFEST.in': "exclude LICENSE-ABC\nexclude LICENSE-PQR", + 'LICENSE-ABC': "ABC license", + 'LICENSE-PQR': "PQR license", + 'LICENSE-XYZ': "XYZ license" }, ['LICENSE-XYZ'], ['LICENSE-ABC', 'LICENSE-PQR']) # manually excluded ]) def test_setup_cfg_license_file_license_files( -- cgit v1.2.3 From 4a31168e517134529c229b310e89039323fdb02f Mon Sep 17 00:00:00 2001 From: Mick Koch Date: Mon, 28 Oct 2019 18:45:42 -0400 Subject: Use an OrderedSet for accumulating license files --- setuptools/command/sdist.py | 4 ++-- setuptools/dist.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'setuptools') diff --git a/setuptools/command/sdist.py b/setuptools/command/sdist.py index 24316640..6043e0b9 100644 --- a/setuptools/command/sdist.py +++ b/setuptools/command/sdist.py @@ -5,7 +5,7 @@ import sys import io import contextlib -from setuptools.extern import six +from setuptools.extern import six, ordered_set from .py36compat import sdist_add_defaults @@ -204,7 +204,7 @@ class sdist(sdist_add_defaults, orig.sdist): valid paths to 'self.filelist'. """ - files = set() + files = ordered_set.OrderedSet() opts = self.distribution.get_option_dict('metadata') diff --git a/setuptools/dist.py b/setuptools/dist.py index fb379a20..0f3f7322 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -409,7 +409,7 @@ class Distribution(_Distribution): 'long_description_content_type': None, 'project_urls': dict, 'provides_extras': ordered_set.OrderedSet, - 'license_files': list, + 'license_files': ordered_set.OrderedSet, } _patched_dist = None -- cgit v1.2.3 From e08ec2b640f6b2bf943fea70e2a7f9881bbe6e91 Mon Sep 17 00:00:00 2001 From: Mick Koch Date: Mon, 28 Oct 2019 19:16:13 -0400 Subject: Filter out missing files and use extend() --- setuptools/command/sdist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'setuptools') diff --git a/setuptools/command/sdist.py b/setuptools/command/sdist.py index 6043e0b9..55ecdd97 100644 --- a/setuptools/command/sdist.py +++ b/setuptools/command/sdist.py @@ -226,6 +226,6 @@ class sdist(sdist_add_defaults, orig.sdist): log.warn( "warning: Failed to find the configured license file '%s'", f) - continue + files.remove(f) - self.filelist.append(f) + self.filelist.extend(files) -- cgit v1.2.3 From 3a0520b43dfac9f6ba507c6d09a60290219a0802 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 28 Oct 2019 21:52:40 -0400 Subject: Extract compatibility function into compatibility module. --- setuptools/_imp.py | 10 +++------- setuptools/py34compat.py | 8 ++++++++ 2 files changed, 11 insertions(+), 7 deletions(-) create mode 100644 setuptools/py34compat.py (limited to 'setuptools') diff --git a/setuptools/_imp.py b/setuptools/_imp.py index 49ddc852..ee719c9a 100644 --- a/setuptools/_imp.py +++ b/setuptools/_imp.py @@ -8,6 +8,8 @@ import sys import importlib.util import importlib.machinery +from .py34compat import module_from_spec + PY_SOURCE = 1 PY_COMPILED = 2 @@ -63,12 +65,6 @@ def get_frozen_object(module, paths): return spec.loader.get_code(_resolve(module)) -def _module_from_spec(spec): - if sys.version_info >= (3, 5): - return importlib.util.module_from_spec(spec) - else: - return spec.loader.load_module(spec.name) - def get_module(module, paths, info): spec = importlib.util.find_spec(module, paths) - return _module_from_spec(spec) + return module_from_spec(spec) diff --git a/setuptools/py34compat.py b/setuptools/py34compat.py new file mode 100644 index 00000000..bc7eefa9 --- /dev/null +++ b/setuptools/py34compat.py @@ -0,0 +1,8 @@ +import importlib.util + + +try: + module_from_spec = importlib.util.module_from_spec +except AttributeError: + def module_from_spec(spec): + return spec.loader.load_module(spec.name) -- cgit v1.2.3 From e1f340b53f0088993b16e19999a4d6b0e86a9991 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 28 Oct 2019 22:01:34 -0400 Subject: Avoid importerror on older Pythons --- setuptools/py34compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'setuptools') diff --git a/setuptools/py34compat.py b/setuptools/py34compat.py index bc7eefa9..54157a63 100644 --- a/setuptools/py34compat.py +++ b/setuptools/py34compat.py @@ -1,4 +1,4 @@ -import importlib.util +import importlib try: -- cgit v1.2.3 From 82689e1aa8e6548f26f2ce3bcd069411cb39bfcf Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 28 Oct 2019 22:06:44 -0400 Subject: Ensure importlib.util is imported on Python 3.5 --- setuptools/py34compat.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'setuptools') diff --git a/setuptools/py34compat.py b/setuptools/py34compat.py index 54157a63..3ad91722 100644 --- a/setuptools/py34compat.py +++ b/setuptools/py34compat.py @@ -1,5 +1,10 @@ import importlib +try: + import importlib.util +except ImportError: + pass + try: module_from_spec = importlib.util.module_from_spec -- cgit v1.2.3 From 20d6407aa5f68dbeba61e8967290f2fbde4f85ab Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Tue, 29 Oct 2019 10:51:54 +0300 Subject: Allow calling get_frozen_object without paths, raise ImportError when it cant find module --- setuptools/_imp.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'setuptools') diff --git a/setuptools/_imp.py b/setuptools/_imp.py index ee719c9a..ab29ef21 100644 --- a/setuptools/_imp.py +++ b/setuptools/_imp.py @@ -60,11 +60,15 @@ def find_module(module, paths=None): return file, path, (suffix, mode, kind) -def get_frozen_object(module, paths): +def get_frozen_object(module, paths=None): spec = importlib.util.find_spec(module, paths) - return spec.loader.get_code(_resolve(module)) + if not spec: + raise ImportError("Can't find %s" % module) + return spec.loader.get_code(module) def get_module(module, paths, info): spec = importlib.util.find_spec(module, paths) + if not spec: + raise ImportError("Can't find %s" % module) return module_from_spec(spec) -- cgit v1.2.3 From cfa9245a7dea8a35f11580c0bfd27472a3182c7e Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Tue, 29 Oct 2019 10:53:40 +0300 Subject: Remove 'sys' import --- setuptools/_imp.py | 1 - 1 file changed, 1 deletion(-) (limited to 'setuptools') diff --git a/setuptools/_imp.py b/setuptools/_imp.py index ab29ef21..a3cce9b2 100644 --- a/setuptools/_imp.py +++ b/setuptools/_imp.py @@ -4,7 +4,6 @@ from the deprecated imp module. """ import os -import sys import importlib.util import importlib.machinery -- cgit v1.2.3 From f413f95e95b34b26d9ed9d9c43b3e4b3d30caecc Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Thu, 31 Oct 2019 11:25:57 -0400 Subject: Remove "upload" and "register" commands. The upload and register commands were deprecated over a year ago, in July 2018 (PR GH-1410, discussed in issue GH-1381). It is time to actively remove them in favor of twine. --- setuptools/command/__init__.py | 3 +- setuptools/command/register.py | 22 ++-- setuptools/command/upload.py | 195 ++--------------------------------- setuptools/errors.py | 16 +++ setuptools/tests/test_register.py | 43 ++------ setuptools/tests/test_upload.py | 211 ++------------------------------------ 6 files changed, 57 insertions(+), 433 deletions(-) create mode 100644 setuptools/errors.py (limited to 'setuptools') diff --git a/setuptools/command/__init__.py b/setuptools/command/__init__.py index fe619e2e..743f5588 100644 --- a/setuptools/command/__init__.py +++ b/setuptools/command/__init__.py @@ -2,8 +2,7 @@ __all__ = [ 'alias', 'bdist_egg', 'bdist_rpm', 'build_ext', 'build_py', 'develop', 'easy_install', 'egg_info', 'install', 'install_lib', 'rotate', 'saveopts', 'sdist', 'setopt', 'test', 'install_egg_info', 'install_scripts', - 'register', 'bdist_wininst', 'upload_docs', 'upload', 'build_clib', - 'dist_info', + 'bdist_wininst', 'upload_docs', 'build_clib', 'dist_info', ] from distutils.command.bdist import bdist diff --git a/setuptools/command/register.py b/setuptools/command/register.py index 98bc0156..b8266b9a 100644 --- a/setuptools/command/register.py +++ b/setuptools/command/register.py @@ -1,18 +1,18 @@ from distutils import log import distutils.command.register as orig +from setuptools.errors import RemovedCommandError + class register(orig.register): - __doc__ = orig.register.__doc__ + """Formerly used to register packages on PyPI.""" def run(self): - try: - # Make sure that we are using valid current name/version info - self.run_command('egg_info') - orig.register.run(self) - finally: - self.announce( - "WARNING: Registering is deprecated, use twine to " - "upload instead (https://pypi.org/p/twine/)", - log.WARN - ) + msg = ( + "The register command has been removed, use twine to upload " + + "instead (https://pypi.org/p/twine)" + ) + + self.announce("ERROR: " + msg, log.ERROR) + + raise RemovedCommandError(msg) diff --git a/setuptools/command/upload.py b/setuptools/command/upload.py index 6db8888b..ec7f81e2 100644 --- a/setuptools/command/upload.py +++ b/setuptools/command/upload.py @@ -1,196 +1,17 @@ -import io -import os -import hashlib -import getpass - -from base64 import standard_b64encode - from distutils import log from distutils.command import upload as orig -from distutils.spawn import spawn - -from distutils.errors import DistutilsError -from setuptools.extern.six.moves.urllib.request import urlopen, Request -from setuptools.extern.six.moves.urllib.error import HTTPError -from setuptools.extern.six.moves.urllib.parse import urlparse +from setuptools.errors import RemovedCommandError class upload(orig.upload): - """ - Override default upload behavior to obtain password - in a variety of different ways. - """ - def run(self): - try: - orig.upload.run(self) - finally: - self.announce( - "WARNING: Uploading via this command is deprecated, use twine " - "to upload instead (https://pypi.org/p/twine/)", - log.WARN - ) + """Formerly used to upload packages to PyPI.""" - def finalize_options(self): - orig.upload.finalize_options(self) - self.username = ( - self.username or - getpass.getuser() - ) - # Attempt to obtain password. Short circuit evaluation at the first - # sign of success. - self.password = ( - self.password or - self._load_password_from_keyring() or - self._prompt_for_password() + def run(self): + msg = ( + "The upload command has been removed, use twine to upload " + + "instead (https://pypi.org/p/twine)" ) - def upload_file(self, command, pyversion, filename): - # Makes sure the repository URL is compliant - schema, netloc, url, params, query, fragments = \ - urlparse(self.repository) - if params or query or fragments: - raise AssertionError("Incompatible url %s" % self.repository) - - if schema not in ('http', 'https'): - raise AssertionError("unsupported schema " + schema) - - # Sign if requested - if self.sign: - gpg_args = ["gpg", "--detach-sign", "-a", filename] - if self.identity: - gpg_args[2:2] = ["--local-user", self.identity] - spawn(gpg_args, - dry_run=self.dry_run) - - # Fill in the data - send all the meta-data in case we need to - # register a new release - with open(filename, 'rb') as f: - content = f.read() - - meta = self.distribution.metadata - - data = { - # action - ':action': 'file_upload', - 'protocol_version': '1', - - # identify release - 'name': meta.get_name(), - 'version': meta.get_version(), - - # file content - 'content': (os.path.basename(filename), content), - 'filetype': command, - 'pyversion': pyversion, - 'md5_digest': hashlib.md5(content).hexdigest(), - - # additional meta-data - 'metadata_version': str(meta.get_metadata_version()), - 'summary': meta.get_description(), - 'home_page': meta.get_url(), - 'author': meta.get_contact(), - 'author_email': meta.get_contact_email(), - 'license': meta.get_licence(), - 'description': meta.get_long_description(), - 'keywords': meta.get_keywords(), - 'platform': meta.get_platforms(), - 'classifiers': meta.get_classifiers(), - 'download_url': meta.get_download_url(), - # PEP 314 - 'provides': meta.get_provides(), - 'requires': meta.get_requires(), - 'obsoletes': meta.get_obsoletes(), - } - - data['comment'] = '' - - if self.sign: - data['gpg_signature'] = (os.path.basename(filename) + ".asc", - open(filename+".asc", "rb").read()) - - # set up the authentication - user_pass = (self.username + ":" + self.password).encode('ascii') - # The exact encoding of the authentication string is debated. - # Anyway PyPI only accepts ascii for both username or password. - auth = "Basic " + standard_b64encode(user_pass).decode('ascii') - - # Build up the MIME payload for the POST data - boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254' - sep_boundary = b'\r\n--' + boundary.encode('ascii') - end_boundary = sep_boundary + b'--\r\n' - body = io.BytesIO() - for key, value in data.items(): - title = '\r\nContent-Disposition: form-data; name="%s"' % key - # handle multiple entries for the same name - if not isinstance(value, list): - value = [value] - for value in value: - if type(value) is tuple: - title += '; filename="%s"' % value[0] - value = value[1] - else: - value = str(value).encode('utf-8') - body.write(sep_boundary) - body.write(title.encode('utf-8')) - body.write(b"\r\n\r\n") - body.write(value) - body.write(end_boundary) - body = body.getvalue() - - msg = "Submitting %s to %s" % (filename, self.repository) - self.announce(msg, log.INFO) - - # build the Request - headers = { - 'Content-type': 'multipart/form-data; boundary=%s' % boundary, - 'Content-length': str(len(body)), - 'Authorization': auth, - } - - request = Request(self.repository, data=body, - headers=headers) - # send the data - try: - result = urlopen(request) - status = result.getcode() - reason = result.msg - except HTTPError as e: - status = e.code - reason = e.msg - except OSError as e: - self.announce(str(e), log.ERROR) - raise - - if status == 200: - self.announce('Server response (%s): %s' % (status, reason), - log.INFO) - if self.show_response: - text = getattr(self, '_read_pypi_response', - lambda x: None)(result) - if text is not None: - msg = '\n'.join(('-' * 75, text, '-' * 75)) - self.announce(msg, log.INFO) - else: - msg = 'Upload failed (%s): %s' % (status, reason) - self.announce(msg, log.ERROR) - raise DistutilsError(msg) - - def _load_password_from_keyring(self): - """ - Attempt to load password from keyring. Suppress Exceptions. - """ - try: - keyring = __import__('keyring') - return keyring.get_password(self.repository, self.username) - except Exception: - pass - - def _prompt_for_password(self): - """ - Prompt for a password on the tty. Suppress Exceptions. - """ - try: - return getpass.getpass() - except (Exception, KeyboardInterrupt): - pass + self.announce("ERROR: " + msg, log.ERROR) + raise RemovedCommandError(msg) diff --git a/setuptools/errors.py b/setuptools/errors.py new file mode 100644 index 00000000..2701747f --- /dev/null +++ b/setuptools/errors.py @@ -0,0 +1,16 @@ +"""setuptools.errors + +Provides exceptions used by setuptools modules. +""" + +from distutils.errors import DistutilsError + + +class RemovedCommandError(DistutilsError, RuntimeError): + """Error used for commands that have been removed in setuptools. + + Since ``setuptools`` is built on ``distutils``, simply removing a command + from ``setuptools`` will make the behavior fall back to ``distutils``; this + error is raised if a command exists in ``distutils`` but has been actively + removed in ``setuptools``. + """ diff --git a/setuptools/tests/test_register.py b/setuptools/tests/test_register.py index 96114595..98605806 100644 --- a/setuptools/tests/test_register.py +++ b/setuptools/tests/test_register.py @@ -1,43 +1,22 @@ -import mock -from distutils import log - -import pytest - from setuptools.command.register import register from setuptools.dist import Distribution +from setuptools.errors import RemovedCommandError +try: + from unittest import mock +except ImportError: + import mock -class TestRegisterTest: - def test_warns_deprecation(self): - dist = Distribution() - - cmd = register(dist) - cmd.run_command = mock.Mock() - cmd.send_metadata = mock.Mock() - cmd.announce = mock.Mock() - - cmd.run() +import pytest - cmd.announce.assert_called_with( - "WARNING: Registering is deprecated, use twine to upload instead " - "(https://pypi.org/p/twine/)", - log.WARN - ) - def test_warns_deprecation_when_raising(self): +class TestRegister: + def test_register_exception(self): + """Ensure that the register command has been properly removed.""" dist = Distribution() + dist.dist_files = [(mock.Mock(), mock.Mock(), mock.Mock())] cmd = register(dist) - cmd.run_command = mock.Mock() - cmd.send_metadata = mock.Mock() - cmd.send_metadata.side_effect = Exception - cmd.announce = mock.Mock() - with pytest.raises(Exception): + with pytest.raises(RemovedCommandError): cmd.run() - - cmd.announce.assert_called_with( - "WARNING: Registering is deprecated, use twine to upload instead " - "(https://pypi.org/p/twine/)", - log.WARN - ) diff --git a/setuptools/tests/test_upload.py b/setuptools/tests/test_upload.py index 320c6959..7586cb26 100644 --- a/setuptools/tests/test_upload.py +++ b/setuptools/tests/test_upload.py @@ -1,213 +1,22 @@ -import mock -import os -import re - -from distutils import log -from distutils.errors import DistutilsError - -import pytest - from setuptools.command.upload import upload from setuptools.dist import Distribution -from setuptools.extern import six - - -def _parse_upload_body(body): - boundary = u'\r\n----------------GHSKFJDLGDS7543FJKLFHRE75642756743254' - entries = [] - name_re = re.compile(u'^Content-Disposition: form-data; name="([^\"]+)"') - - for entry in body.split(boundary): - pair = entry.split(u'\r\n\r\n') - if not len(pair) == 2: - continue - - key, value = map(six.text_type.strip, pair) - m = name_re.match(key) - if m is not None: - key = m.group(1) - - entries.append((key, value)) - - return entries - - -@pytest.fixture -def patched_upload(tmpdir): - class Fix: - def __init__(self, cmd, urlopen): - self.cmd = cmd - self.urlopen = urlopen - - def __iter__(self): - return iter((self.cmd, self.urlopen)) - - def get_uploaded_metadata(self): - request = self.urlopen.call_args_list[0][0][0] - body = request.data.decode('utf-8') - entries = dict(_parse_upload_body(body)) - - return entries +from setuptools.errors import RemovedCommandError - class ResponseMock(mock.Mock): - def getheader(self, name, default=None): - """Mocked getheader method for response object""" - return { - 'content-type': 'text/plain; charset=utf-8', - }.get(name.lower(), default) +try: + from unittest import mock +except ImportError: + import mock - with mock.patch('setuptools.command.upload.urlopen') as urlopen: - urlopen.return_value = ResponseMock() - urlopen.return_value.getcode.return_value = 200 - urlopen.return_value.read.return_value = b'' - - content = os.path.join(str(tmpdir), "content_data") - - with open(content, 'w') as f: - f.write("Some content") - - dist = Distribution() - dist.dist_files = [('sdist', '3.7.0', content)] - - cmd = upload(dist) - cmd.announce = mock.Mock() - cmd.username = 'user' - cmd.password = 'hunter2' - - yield Fix(cmd, urlopen) - - -class TestUploadTest: - def test_upload_metadata(self, patched_upload): - cmd, patch = patched_upload - - # Set the metadata version to 2.1 - cmd.distribution.metadata.metadata_version = '2.1' - - # Run the command - cmd.ensure_finalized() - cmd.run() - - # Make sure we did the upload - patch.assert_called_once() - - # Make sure the metadata version is correct in the headers - entries = patched_upload.get_uploaded_metadata() - assert entries['metadata_version'] == '2.1' - - def test_warns_deprecation(self): - dist = Distribution() - dist.dist_files = [(mock.Mock(), mock.Mock(), mock.Mock())] - - cmd = upload(dist) - cmd.upload_file = mock.Mock() - cmd.announce = mock.Mock() - - cmd.run() +import pytest - cmd.announce.assert_called_once_with( - "WARNING: Uploading via this command is deprecated, use twine to " - "upload instead (https://pypi.org/p/twine/)", - log.WARN - ) - def test_warns_deprecation_when_raising(self): +class TestUpload: + def test_upload_exception(self): + """Ensure that the register command has been properly removed.""" dist = Distribution() dist.dist_files = [(mock.Mock(), mock.Mock(), mock.Mock())] cmd = upload(dist) - cmd.upload_file = mock.Mock() - cmd.upload_file.side_effect = Exception - cmd.announce = mock.Mock() - - with pytest.raises(Exception): - cmd.run() - - cmd.announce.assert_called_once_with( - "WARNING: Uploading via this command is deprecated, use twine to " - "upload instead (https://pypi.org/p/twine/)", - log.WARN - ) - - @pytest.mark.parametrize('url', [ - 'https://example.com/a;parameter', # Has parameters - 'https://example.com/a?query', # Has query - 'https://example.com/a#fragment', # Has fragment - 'ftp://example.com', # Invalid scheme - - ]) - def test_upload_file_invalid_url(self, url, patched_upload): - patched_upload.urlopen.side_effect = Exception("Should not be reached") - - cmd = patched_upload.cmd - cmd.repository = url - - cmd.ensure_finalized() - with pytest.raises(AssertionError): - cmd.run() - - def test_upload_file_http_error(self, patched_upload): - patched_upload.urlopen.side_effect = six.moves.urllib.error.HTTPError( - 'https://example.com', - 404, - 'File not found', - None, - None - ) - - cmd = patched_upload.cmd - cmd.ensure_finalized() - with pytest.raises(DistutilsError): + with pytest.raises(RemovedCommandError): cmd.run() - - cmd.announce.assert_any_call( - 'Upload failed (404): File not found', - log.ERROR) - - def test_upload_file_os_error(self, patched_upload): - patched_upload.urlopen.side_effect = OSError("Invalid") - - cmd = patched_upload.cmd - cmd.ensure_finalized() - - with pytest.raises(OSError): - cmd.run() - - cmd.announce.assert_any_call('Invalid', log.ERROR) - - @mock.patch('setuptools.command.upload.spawn') - def test_upload_file_gpg(self, spawn, patched_upload): - cmd, urlopen = patched_upload - - cmd.sign = True - cmd.identity = "Alice" - cmd.dry_run = True - content_fname = cmd.distribution.dist_files[0][2] - signed_file = content_fname + '.asc' - - with open(signed_file, 'wb') as f: - f.write("signed-data".encode('utf-8')) - - cmd.ensure_finalized() - cmd.run() - - # Make sure that GPG was called - spawn.assert_called_once_with([ - "gpg", "--detach-sign", "--local-user", "Alice", "-a", - content_fname - ], dry_run=True) - - # Read the 'signed' data that was transmitted - entries = patched_upload.get_uploaded_metadata() - assert entries['gpg_signature'] == 'signed-data' - - def test_show_response_no_error(self, patched_upload): - # This test is just that show_response doesn't throw an error - # It is not really important what the printed response looks like - # in a deprecated command, but we don't want to introduce new - # errors when importing this function from distutils - - patched_upload.cmd.show_response = True - patched_upload.cmd.ensure_finalized() - patched_upload.cmd.run() -- cgit v1.2.3 From fed59d837495208c13cec64b5394cdd2cc3cb6de Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Thu, 14 Nov 2019 19:09:20 +0100 Subject: tests: fix some pytest warnings under Python 2 --- setuptools/tests/test_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'setuptools') diff --git a/setuptools/tests/test_integration.py b/setuptools/tests/test_integration.py index 1c0b2b18..f1a27f8b 100644 --- a/setuptools/tests/test_integration.py +++ b/setuptools/tests/test_integration.py @@ -64,7 +64,7 @@ def install_context(request, tmpdir, monkeypatch): monkeypatch.setattr('site.USER_BASE', user_base.strpath) monkeypatch.setattr('site.USER_SITE', user_site.strpath) monkeypatch.setattr('sys.path', sys.path + [install_dir.strpath]) - monkeypatch.setenv('PYTHONPATH', os.path.pathsep.join(sys.path)) + monkeypatch.setenv(str('PYTHONPATH'), str(os.path.pathsep.join(sys.path))) # Set up the command for performing the installation. dist = Distribution() -- cgit v1.2.3 From d6948c636f5e657ac56911b71b7a459d326d8389 Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Sun, 29 Apr 2018 19:47:42 +0200 Subject: dist: re-implement `fetch_build_egg` to use pip --- setuptools/dist.py | 28 +---- setuptools/installer.py | 129 ++++++++++++++++++++++ setuptools/tests/server.py | 19 +++- setuptools/tests/test_easy_install.py | 197 ++++++++++++++++++++++++++++------ setuptools/tests/test_virtualenv.py | 18 ++-- 5 files changed, 326 insertions(+), 65 deletions(-) create mode 100644 setuptools/installer.py (limited to 'setuptools') diff --git a/setuptools/dist.py b/setuptools/dist.py index 2e5ad4bd..4a76b52b 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -759,32 +759,8 @@ class Distribution(_Distribution): def fetch_build_egg(self, req): """Fetch an egg needed for building""" - from setuptools.command.easy_install import easy_install - dist = self.__class__({'script_args': ['easy_install']}) - opts = dist.get_option_dict('easy_install') - opts.clear() - opts.update( - (k, v) - for k, v in self.get_option_dict('easy_install').items() - if k in ( - # don't use any other settings - 'find_links', 'site_dirs', 'index_url', - 'optimize', 'site_dirs', 'allow_hosts', - )) - if self.dependency_links: - links = self.dependency_links[:] - if 'find_links' in opts: - links = opts['find_links'][1] + links - opts['find_links'] = ('setup', links) - install_dir = self.get_egg_cache_dir() - cmd = easy_install( - dist, args=["x"], install_dir=install_dir, - exclude_scripts=True, - always_copy=False, build_directory=None, editable=False, - upgrade=False, multi_version=True, no_report=True, user=False - ) - cmd.ensure_finalized() - return cmd.easy_install(req) + from setuptools.installer import fetch_build_egg + return fetch_build_egg(self, req) def _set_global_opts_from_features(self): """Add --with-X/--without-X options based on optional features""" diff --git a/setuptools/installer.py b/setuptools/installer.py new file mode 100644 index 00000000..35bc3cc5 --- /dev/null +++ b/setuptools/installer.py @@ -0,0 +1,129 @@ +import glob +import os +import subprocess +import sys +from distutils import log +from distutils.errors import DistutilsError + +import pkg_resources +from setuptools.command.easy_install import easy_install +from setuptools.wheel import Wheel + +from .py31compat import TemporaryDirectory + + +def _legacy_fetch_build_egg(dist, req): + """Fetch an egg needed for building. + + Legacy path using EasyInstall. + """ + tmp_dist = dist.__class__({'script_args': ['easy_install']}) + opts = tmp_dist.get_option_dict('easy_install') + opts.clear() + opts.update( + (k, v) + for k, v in dist.get_option_dict('easy_install').items() + if k in ( + # don't use any other settings + 'find_links', 'site_dirs', 'index_url', + 'optimize', 'site_dirs', 'allow_hosts', + )) + if dist.dependency_links: + links = dist.dependency_links[:] + if 'find_links' in opts: + links = opts['find_links'][1] + links + opts['find_links'] = ('setup', links) + install_dir = dist.get_egg_cache_dir() + cmd = easy_install( + tmp_dist, args=["x"], install_dir=install_dir, + exclude_scripts=True, + always_copy=False, build_directory=None, editable=False, + upgrade=False, multi_version=True, no_report=True, user=False + ) + cmd.ensure_finalized() + return cmd.easy_install(req) + + +def fetch_build_egg(dist, req): + """Fetch an egg needed for building. + + Use pip/wheel to fetch/build a wheel.""" + # Check pip is available. + try: + pkg_resources.get_distribution('pip') + except pkg_resources.DistributionNotFound: + dist.announce( + 'WARNING: The pip package is not available, falling back ' + 'to EasyInstall for handling setup_requires/test_requires; ' + 'this is deprecated and will be removed in a future version.' + , log.WARN + ) + return _legacy_fetch_build_egg(dist, req) + # Warn if wheel is not. + try: + pkg_resources.get_distribution('wheel') + except pkg_resources.DistributionNotFound: + dist.announce('WARNING: The wheel package is not available.', log.WARN) + if not isinstance(req, pkg_resources.Requirement): + req = pkg_resources.Requirement.parse(req) + # Take easy_install options into account, but do not override relevant + # pip environment variables (like PIP_INDEX_URL or PIP_QUIET); they'll + # take precedence. + opts = dist.get_option_dict('easy_install') + if 'allow_hosts' in opts: + raise DistutilsError('the `allow-hosts` option is not supported ' + 'when using pip to install requirements.') + if 'PIP_QUIET' in os.environ or 'PIP_VERBOSE' in os.environ: + quiet = False + else: + quiet = True + if 'PIP_INDEX_URL' in os.environ: + index_url = None + elif 'index_url' in opts: + index_url = opts['index_url'][1] + else: + index_url = None + if 'find_links' in opts: + find_links = opts['find_links'][1][:] + else: + find_links = [] + if dist.dependency_links: + find_links.extend(dist.dependency_links) + eggs_dir = os.path.realpath(dist.get_egg_cache_dir()) + environment = pkg_resources.Environment() + for egg_dist in pkg_resources.find_distributions(eggs_dir): + if egg_dist in req and environment.can_add(egg_dist): + return egg_dist + with TemporaryDirectory() as tmpdir: + cmd = [ + sys.executable, '-m', 'pip', + '--disable-pip-version-check', + 'wheel', '--no-deps', + '-w', tmpdir, + ] + if quiet: + cmd.append('--quiet') + if index_url is not None: + cmd.extend(('--index-url', index_url)) + if find_links is not None: + for link in find_links: + cmd.extend(('--find-links', link)) + # If requirement is a PEP 508 direct URL, directly pass + # the URL to pip, as `req @ url` does not work on the + # command line. + if req.url: + cmd.append(req.url) + else: + cmd.append(str(req)) + try: + subprocess.check_call(cmd) + except subprocess.CalledProcessError as e: + raise DistutilsError(str(e)) + wheel = Wheel(glob.glob(os.path.join(tmpdir, '*.whl'))[0]) + dist_location = os.path.join(eggs_dir, wheel.egg_name()) + wheel.install_as_egg(dist_location) + dist_metadata = pkg_resources.PathMetadata( + dist_location, os.path.join(dist_location, 'EGG-INFO')) + dist = pkg_resources.Distribution.from_filename( + dist_location, metadata=dist_metadata) + return dist diff --git a/setuptools/tests/server.py b/setuptools/tests/server.py index fc3a5975..8b17b081 100644 --- a/setuptools/tests/server.py +++ b/setuptools/tests/server.py @@ -1,10 +1,13 @@ """Basic http server for tests to simulate PyPI or custom indexes """ +import os import time import threading from setuptools.extern.six.moves import BaseHTTPServer, SimpleHTTPServer +from setuptools.extern.six.moves.urllib_parse import urljoin +from setuptools.extern.six.moves.urllib.request import pathname2url class IndexServer(BaseHTTPServer.HTTPServer): @@ -69,6 +72,20 @@ class MockServer(BaseHTTPServer.HTTPServer, threading.Thread): def run(self): self.serve_forever() + @property + def netloc(self): + return 'localhost:%s' % self.server_port + @property def url(self): - return 'http://localhost:%(server_port)s/' % vars(self) + return 'http://%s/' % self.netloc + + +def path_to_url(path, authority=None): + """ Convert a path to a file: URL. """ + path = os.path.normpath(os.path.abspath(path)) + base = 'file:' + if authority is not None: + base += '//' + authority + url = urljoin(base, pathname2url(path)) + return url diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py index c3fd1c6e..aa75899a 100644 --- a/setuptools/tests/test_easy_install.py +++ b/setuptools/tests/test_easy_install.py @@ -15,24 +15,24 @@ import distutils.errors import io import zipfile import mock -from setuptools.command.easy_install import ( - EasyInstallDeprecationWarning, ScriptWriter, WindowsScriptWriter, -) import time + from setuptools.extern import six -from setuptools.extern.six.moves import urllib import pytest from setuptools import sandbox from setuptools.sandbox import run_setup import setuptools.command.easy_install as ei -from setuptools.command.easy_install import PthDistributions +from setuptools.command.easy_install import ( + EasyInstallDeprecationWarning, ScriptWriter, PthDistributions, + WindowsScriptWriter, +) from setuptools.command import easy_install as easy_install_pkg from setuptools.dist import Distribution from pkg_resources import normalize_path, working_set from pkg_resources import Distribution as PRDistribution -import setuptools.tests.server +from setuptools.tests.server import MockServer, path_to_url from setuptools.tests import fail_on_ascii import pkg_resources @@ -440,35 +440,40 @@ def distutils_package(): yield +@pytest.fixture +def mock_index(): + # set up a server which will simulate an alternate package index. + p_index = MockServer() + if p_index.server_port == 0: + # Some platforms (Jython) don't find a port to which to bind, + # so skip test for them. + pytest.skip("could not find a valid port") + p_index.start() + return p_index + + class TestDistutilsPackage: def test_bdist_egg_available_on_distutils_pkg(self, distutils_package): run_setup('setup.py', ['bdist_egg']) class TestSetupRequires: - def test_setup_requires_honors_fetch_params(self): + + def test_setup_requires_honors_fetch_params(self, mock_index, monkeypatch): """ When easy_install installs a source distribution which specifies setup_requires, it should honor the fetch parameters (such as - allow-hosts, index-url, and find-links). + index-url, and find-links). """ - # set up a server which will simulate an alternate package index. - p_index = setuptools.tests.server.MockServer() - p_index.start() - netloc = 1 - p_index_loc = urllib.parse.urlparse(p_index.url)[netloc] - if p_index_loc.endswith(':0'): - # Some platforms (Jython) don't find a port to which to bind, - # so skip this test for them. - return + monkeypatch.setenv(str('PIP_RETRIES'), str('0')) + monkeypatch.setenv(str('PIP_TIMEOUT'), str('0')) with contexts.quiet(): # create an sdist that has a build-time dependency. with TestSetupRequires.create_sdist() as dist_file: with contexts.tempdir() as temp_install_dir: with contexts.environment(PYTHONPATH=temp_install_dir): ei_params = [ - '--index-url', p_index.url, - '--allow-hosts', p_index_loc, + '--index-url', mock_index.url, '--exclude-scripts', '--install-dir', temp_install_dir, dist_file, @@ -478,10 +483,8 @@ class TestSetupRequires: # fail because it doesn't exist. with pytest.raises(SystemExit): easy_install_pkg.main(ei_params) - # there should have been two or three requests to the server - # (three happens on Python 3.3a) - assert 2 <= len(p_index.requests) <= 3 - assert p_index.requests[0].path == '/does-not-exist/' + # there should have been one requests to the server + assert [r.path for r in mock_index.requests] == ['/does-not-exist/'] @staticmethod @contextlib.contextmanager @@ -500,7 +503,9 @@ class TestSetupRequires: version="1.0", setup_requires = ['does-not-exist'], ) - """))]) + """)), + ('setup.cfg', ''), + ]) yield dist_path use_setup_cfg = ( @@ -632,6 +637,113 @@ class TestSetupRequires: assert len(lines) > 0 assert lines[-1].strip() == '42' + def test_setup_requires_honors_pip_env(self, mock_index, monkeypatch): + monkeypatch.setenv(str('PIP_RETRIES'), str('0')) + monkeypatch.setenv(str('PIP_TIMEOUT'), str('0')) + monkeypatch.setenv(str('PIP_INDEX_URL'), mock_index.url) + with contexts.save_pkg_resources_state(): + with contexts.tempdir() as temp_dir: + test_pkg = create_setup_requires_package( + temp_dir, 'python-xlib', '0.19', + setup_attrs=dict(dependency_links=[])) + test_setup_cfg = os.path.join(test_pkg, 'setup.cfg') + with open(test_setup_cfg, 'w') as fp: + fp.write(DALS( + ''' + [easy_install] + index_url = https://pypi.org/legacy/ + ''')) + test_setup_py = os.path.join(test_pkg, 'setup.py') + with pytest.raises(distutils.errors.DistutilsError): + run_setup(test_setup_py, [str('--version')]) + assert len(mock_index.requests) == 1 + assert mock_index.requests[0].path == '/python-xlib/' + + def test_setup_requires_with_pep508_url(self, mock_index, monkeypatch): + monkeypatch.setenv(str('PIP_RETRIES'), str('0')) + monkeypatch.setenv(str('PIP_TIMEOUT'), str('0')) + monkeypatch.setenv(str('PIP_INDEX_URL'), mock_index.url) + with contexts.save_pkg_resources_state(): + with contexts.tempdir() as temp_dir: + dep_sdist = os.path.join(temp_dir, 'dep.tar.gz') + make_trivial_sdist(dep_sdist, 'dependency', '42') + dep_url = path_to_url(dep_sdist, authority='localhost') + test_pkg = create_setup_requires_package( + temp_dir, + 'python-xlib', '0.19', # Ignored (overriden by setup_attrs). + setup_attrs=dict(setup_requires='dependency @ %s' % dep_url)) + test_setup_py = os.path.join(test_pkg, 'setup.py') + run_setup(test_setup_py, [str('--version')]) + assert len(mock_index.requests) == 0 + + def test_setup_requires_with_allow_hosts(self, mock_index): + ''' The `allow-hosts` option in not supported anymore. ''' + with contexts.save_pkg_resources_state(): + with contexts.tempdir() as temp_dir: + test_pkg = os.path.join(temp_dir, 'test_pkg') + test_setup_py = os.path.join(test_pkg, 'setup.py') + test_setup_cfg = os.path.join(test_pkg, 'setup.cfg') + os.mkdir(test_pkg) + with open(test_setup_py, 'w') as fp: + fp.write(DALS( + ''' + from setuptools import setup + setup(setup_requires='python-xlib') + ''')) + with open(test_setup_cfg, 'w') as fp: + fp.write(DALS( + ''' + [easy_install] + allow_hosts = * + ''')) + with pytest.raises(distutils.errors.DistutilsError): + run_setup(test_setup_py, [str('--version')]) + assert len(mock_index.requests) == 0 + + def test_setup_requires_with_python_requires(self, monkeypatch, tmpdir): + ''' Check `python_requires` is honored. ''' + monkeypatch.setenv(str('PIP_RETRIES'), str('0')) + monkeypatch.setenv(str('PIP_TIMEOUT'), str('0')) + monkeypatch.setenv(str('PIP_NO_INDEX'), str('1')) + monkeypatch.setenv(str('PIP_VERBOSE'), str('1')) + dep_1_0_sdist = 'dep-1.0.tar.gz' + dep_1_0_url = path_to_url(str(tmpdir / dep_1_0_sdist)) + dep_1_0_python_requires = '>=2.7' + make_python_requires_sdist(str(tmpdir / dep_1_0_sdist), 'dep', '1.0', dep_1_0_python_requires) + dep_2_0_sdist = 'dep-2.0.tar.gz' + dep_2_0_url = path_to_url(str(tmpdir / dep_2_0_sdist)) + dep_2_0_python_requires = '!=' + '.'.join(map(str, sys.version_info[:2])) + '.*' + make_python_requires_sdist(str(tmpdir / dep_2_0_sdist), 'dep', '2.0', dep_2_0_python_requires) + index = tmpdir / 'index.html' + index.write_text(DALS( + ''' + + Links for dep + +

Links for dep

+ {dep_1_0_sdist}
+ {dep_2_0_sdist}
+ + + ''').format( + dep_1_0_url=dep_1_0_url, + dep_1_0_sdist=dep_1_0_sdist, + dep_1_0_python_requires=dep_1_0_python_requires, + dep_2_0_url=dep_2_0_url, + dep_2_0_sdist=dep_2_0_sdist, + dep_2_0_python_requires=dep_2_0_python_requires, + ), 'utf-8') + index_url = path_to_url(str(index)) + with contexts.save_pkg_resources_state(): + test_pkg = create_setup_requires_package( + str(tmpdir), + 'python-xlib', '0.19', # Ignored (overriden by setup_attrs). + setup_attrs=dict(setup_requires='dep', dependency_links=[index_url])) + test_setup_py = os.path.join(test_pkg, 'setup.py') + run_setup(test_setup_py, [str('--version')]) + eggs = list(map(str, pkg_resources.find_distributions(os.path.join(test_pkg, '.eggs')))) + assert eggs == ['dep 1.0'] + def make_trivial_sdist(dist_path, distname, version): """ @@ -647,7 +759,9 @@ def make_trivial_sdist(dist_path, distname, version): name=%r, version=%r ) - """ % (distname, version)))]) + """ % (distname, version))), + ('setup.cfg', ''), + ]) def make_nspkg_sdist(dist_path, distname, version): @@ -683,12 +797,29 @@ def make_nspkg_sdist(dist_path, distname, version): make_sdist(dist_path, files) +def make_python_requires_sdist(dist_path, distname, version, python_requires): + make_sdist(dist_path, [ + ('setup.py', DALS("""\ + import setuptools + setuptools.setup( + name={name!r}, + version={version!r}, + python_requires={python_requires!r}, + ) + """).format(name=distname, version=version, + python_requires=python_requires)), + ('setup.cfg', ''), + ]) + + def make_sdist(dist_path, files): """ Create a simple sdist tarball at dist_path, containing the files listed in ``files`` as ``(filename, content)`` tuples. """ + # Distributions with only one file don't play well with pip. + assert len(files) > 1 with tarfile.open(dist_path, 'w:gz') as dist: for filename, content in files: file_bytes = io.BytesIO(content.encode('utf-8')) @@ -721,8 +852,8 @@ def create_setup_requires_package(path, distname='foobar', version='0.1', test_pkg = os.path.join(path, 'test_pkg') os.mkdir(test_pkg) + # setup.cfg if use_setup_cfg: - test_setup_cfg = os.path.join(test_pkg, 'setup.cfg') options = [] metadata = [] for name in use_setup_cfg: @@ -734,8 +865,7 @@ def create_setup_requires_package(path, distname='foobar', version='0.1', if isinstance(value, (tuple, list)): value = ';'.join(value) section.append('%s: %s' % (name, value)) - with open(test_setup_cfg, 'w') as f: - f.write(DALS( + test_setup_cfg_contents = DALS( """ [metadata] {metadata} @@ -745,16 +875,19 @@ def create_setup_requires_package(path, distname='foobar', version='0.1', ).format( options='\n'.join(options), metadata='\n'.join(metadata), - )) - - test_setup_py = os.path.join(test_pkg, 'setup.py') + ) + else: + test_setup_cfg_contents = '' + with open(os.path.join(test_pkg, 'setup.cfg'), 'w') as f: + f.write(test_setup_cfg_contents) + # setup.py if setup_py_template is None: setup_py_template = DALS("""\ import setuptools setuptools.setup(**%r) """) - with open(test_setup_py, 'w') as f: + with open(os.path.join(test_pkg, 'setup.py'), 'w') as f: f.write(setup_py_template % test_setup_attrs) foobar_path = os.path.join(path, '%s-%s.tar.gz' % (distname, version)) diff --git a/setuptools/tests/test_virtualenv.py b/setuptools/tests/test_virtualenv.py index 74a1284c..cd3d9313 100644 --- a/setuptools/tests/test_virtualenv.py +++ b/setuptools/tests/test_virtualenv.py @@ -121,14 +121,12 @@ def test_pip_upgrade_from_source(pip_version, virtualenv): virtualenv.run('pip install --no-cache-dir --upgrade ' + sdist) -def test_test_command_install_requirements(bare_virtualenv, tmpdir): +def _check_test_command_install_requirements(virtualenv, tmpdir): """ Check the test command will install all required dependencies. """ - bare_virtualenv.run(' && '.join(( - 'cd {source}', - 'python setup.py develop', - )).format(source=SOURCE_DIR)) + # Install setuptools. + virtualenv.run('python setup.py develop', cd=SOURCE_DIR) def sdist(distname, version): dist_path = tmpdir.join('%s-%s.tar.gz' % (distname, version)) @@ -179,12 +177,20 @@ def test_test_command_install_requirements(bare_virtualenv, tmpdir): open('success', 'w').close() ''')) # Run test command for test package. - bare_virtualenv.run(' && '.join(( + virtualenv.run(' && '.join(( 'cd {tmpdir}', 'python setup.py test -s test', )).format(tmpdir=tmpdir)) assert tmpdir.join('success').check() +def test_test_command_install_requirements(virtualenv, tmpdir): + # Ensure pip/wheel packages are installed. + virtualenv.run("python -c \"__import__('pkg_resources').require(['pip', 'wheel'])\"") + _check_test_command_install_requirements(virtualenv, tmpdir) + +def test_test_command_install_requirements_when_using_easy_install(bare_virtualenv, tmpdir): + _check_test_command_install_requirements(bare_virtualenv, tmpdir) + def test_no_missing_dependencies(bare_virtualenv): """ -- cgit v1.2.3 From 6e1838a9fb5feb000ba9b6a3c37c8b39d7e872b3 Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Thu, 14 Nov 2019 21:51:33 +0100 Subject: drop easy_install script and associated documentation --- setuptools/command/easy_install.py | 55 +---------------------------------- setuptools/tests/test_easy_install.py | 34 ++++++++++++---------- setuptools/tests/test_namespaces.py | 5 ++-- 3 files changed, 21 insertions(+), 73 deletions(-) (limited to 'setuptools') diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index 545c3c44..9d350ac0 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -73,7 +73,7 @@ warnings.filterwarnings("default", category=pkg_resources.PEP440Warning) __all__ = [ 'samefile', 'easy_install', 'PthDistributions', 'extract_wininst_cfg', - 'main', 'get_exe_prefixes', + 'get_exe_prefixes', ] @@ -2283,59 +2283,6 @@ def current_umask(): return tmp -def bootstrap(): - # This function is called when setuptools*.egg is run using /bin/sh - import setuptools - - argv0 = os.path.dirname(setuptools.__path__[0]) - sys.argv[0] = argv0 - sys.argv.append(argv0) - main() - - -def main(argv=None, **kw): - from setuptools import setup - from setuptools.dist import Distribution - - class DistributionWithoutHelpCommands(Distribution): - common_usage = "" - - def _show_help(self, *args, **kw): - with _patch_usage(): - Distribution._show_help(self, *args, **kw) - - if argv is None: - argv = sys.argv[1:] - - with _patch_usage(): - setup( - script_args=['-q', 'easy_install', '-v'] + argv, - script_name=sys.argv[0] or 'easy_install', - distclass=DistributionWithoutHelpCommands, - **kw - ) - - -@contextlib.contextmanager -def _patch_usage(): - import distutils.core - USAGE = textwrap.dedent(""" - usage: %(script)s [options] requirement_or_url ... - or: %(script)s --help - """).lstrip() - - def gen_usage(script_name): - return USAGE % dict( - script=os.path.basename(script_name), - ) - - saved = distutils.core.gen_usage - distutils.core.gen_usage = gen_usage - try: - yield - finally: - distutils.core.gen_usage = saved - class EasyInstallDeprecationWarning(SetuptoolsDeprecationWarning): """Class for warning about deprecations in EasyInstall in SetupTools. Not ignored by default, unlike DeprecationWarning.""" diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py index aa75899a..68319c2f 100644 --- a/setuptools/tests/test_easy_install.py +++ b/setuptools/tests/test_easy_install.py @@ -467,22 +467,24 @@ class TestSetupRequires: """ monkeypatch.setenv(str('PIP_RETRIES'), str('0')) monkeypatch.setenv(str('PIP_TIMEOUT'), str('0')) - with contexts.quiet(): - # create an sdist that has a build-time dependency. - with TestSetupRequires.create_sdist() as dist_file: - with contexts.tempdir() as temp_install_dir: - with contexts.environment(PYTHONPATH=temp_install_dir): - ei_params = [ - '--index-url', mock_index.url, - '--exclude-scripts', - '--install-dir', temp_install_dir, - dist_file, - ] - with sandbox.save_argv(['easy_install']): - # attempt to install the dist. It should - # fail because it doesn't exist. - with pytest.raises(SystemExit): - easy_install_pkg.main(ei_params) + monkeypatch.setenv(str('PIP_VERBOSE'), str('1')) + # create an sdist that has a build-time dependency. + with TestSetupRequires.create_sdist() as dist_file: + with contexts.tempdir() as temp_dir: + setup_py = os.path.join(temp_dir, 'setup.py') + with open(setup_py, 'w') as fp: + fp.write('__import__("setuptools").setup()') + temp_install_dir = os.path.join(temp_dir, 'target') + os.mkdir(temp_install_dir) + with contexts.environment(PYTHONPATH=temp_install_dir): + # attempt to install the dist. It should + # fail because it doesn't exist. + with pytest.raises(SystemExit): + run_setup(setup_py, ['easy_install', + '--exclude-scripts', + '--index-url', mock_index.url, + '--install-dir', temp_install_dir, + dist_file]) # there should have been one requests to the server assert [r.path for r in mock_index.requests] == ['/does-not-exist/'] diff --git a/setuptools/tests/test_namespaces.py b/setuptools/tests/test_namespaces.py index f937d981..3c5df68a 100644 --- a/setuptools/tests/test_namespaces.py +++ b/setuptools/tests/test_namespaces.py @@ -64,9 +64,8 @@ class TestNamespaces: target.mkdir() install_cmd = [ sys.executable, - '-m', 'easy_install', - '-d', str(target), - str(pkg), + '-m', 'pip.__main__', 'install', + '-t', str(target), str(pkg), ] with test.test.paths_on_pythonpath([str(target)]): subprocess.check_call(install_cmd) -- cgit v1.2.3 From b8101f06532b1deab448e6e23d0a61eb125c62df Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Thu, 14 Nov 2019 22:01:09 +0100 Subject: deprecate easy_install command --- setuptools/command/easy_install.py | 8 +++++++- setuptools/command/install.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) (limited to 'setuptools') diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index 9d350ac0..d273bc10 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -410,7 +410,13 @@ class easy_install(Command): ] self._expand_attrs(dirs) - def run(self): + def run(self, show_deprecation=True): + if show_deprecation: + self.announce( + "WARNING: The easy_install command is deprecated " + "and will be removed in a future version." + , log.WARN, + ) if self.verbose != self.distribution.verbose: log.set_verbosity(self.verbose) try: diff --git a/setuptools/command/install.py b/setuptools/command/install.py index 31a5ddb5..72b9a3e4 100644 --- a/setuptools/command/install.py +++ b/setuptools/command/install.py @@ -114,7 +114,7 @@ class install(orig.install): args.insert(0, setuptools.bootstrap_install_from) cmd.args = args - cmd.run() + cmd.run(show_deprecation=False) setuptools.bootstrap_install_from = None -- cgit v1.2.3 From 6b210c65938527a4bbcea34942fe43971be3c014 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 16 Nov 2019 12:06:47 -0500 Subject: Move all finalization of distribution options into hooks. Allow hooks to specify an index for ordering. --- setuptools/dist.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) (limited to 'setuptools') diff --git a/setuptools/dist.py b/setuptools/dist.py index 987d684e..44990431 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -724,19 +724,28 @@ class Distribution(_Distribution): return resolved_dists def finalize_options(self): - _Distribution.finalize_options(self) - if self.features: - self._set_global_opts_from_features() - + """ + Allow plugins to apply arbitrary operations to the + distribution. Each hook may optionally define a 'order' + to influence the order of execution. Smaller numbers + go first and the default is 0. + """ hook_key = 'setuptools.finalize_distribution_options' - for ep in pkg_resources.iter_entry_points(hook_key): + + def by_order(hook): + return getattr(hook, 'order', 0) + eps = pkg_resources.iter_entry_points(hook_key) + for ep in sorted(eps, key=by_order): ep.load()(self) + def _finalize_setup_keywords(self): for ep in pkg_resources.iter_entry_points('distutils.setup_keywords'): value = getattr(self, ep.name, None) if value is not None: ep.require(installer=self.fetch_build_egg) ep.load()(self, ep.name, value) + + def _finalize_2to3_doctests(self): if getattr(self, 'convert_2to3_doctests', None): # XXX may convert to set here when we can rely on set being builtin self.convert_2to3_doctests = [ @@ -790,9 +799,12 @@ class Distribution(_Distribution): cmd.ensure_finalized() return cmd.easy_install(req) - def _set_global_opts_from_features(self): + def _finalize_feature_opts(self): """Add --with-X/--without-X options based on optional features""" + if not self.features: + return + go = [] no = self.negative_opt.copy() -- cgit v1.2.3 From a1e956b20f11f2d02f5a9855bda37660080184c9 Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Sat, 16 Nov 2019 23:30:10 +0100 Subject: Revert "drop easy_install script and associated documentation" This reverts commit 6e1838a9fb5feb000ba9b6a3c37c8b39d7e872b3. --- setuptools/command/easy_install.py | 55 ++++++++++++++++++++++++++++++++++- setuptools/tests/test_easy_install.py | 34 ++++++++++------------ setuptools/tests/test_namespaces.py | 5 ++-- 3 files changed, 73 insertions(+), 21 deletions(-) (limited to 'setuptools') diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index d273bc10..09066f8c 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -73,7 +73,7 @@ warnings.filterwarnings("default", category=pkg_resources.PEP440Warning) __all__ = [ 'samefile', 'easy_install', 'PthDistributions', 'extract_wininst_cfg', - 'get_exe_prefixes', + 'main', 'get_exe_prefixes', ] @@ -2289,6 +2289,59 @@ def current_umask(): return tmp +def bootstrap(): + # This function is called when setuptools*.egg is run using /bin/sh + import setuptools + + argv0 = os.path.dirname(setuptools.__path__[0]) + sys.argv[0] = argv0 + sys.argv.append(argv0) + main() + + +def main(argv=None, **kw): + from setuptools import setup + from setuptools.dist import Distribution + + class DistributionWithoutHelpCommands(Distribution): + common_usage = "" + + def _show_help(self, *args, **kw): + with _patch_usage(): + Distribution._show_help(self, *args, **kw) + + if argv is None: + argv = sys.argv[1:] + + with _patch_usage(): + setup( + script_args=['-q', 'easy_install', '-v'] + argv, + script_name=sys.argv[0] or 'easy_install', + distclass=DistributionWithoutHelpCommands, + **kw + ) + + +@contextlib.contextmanager +def _patch_usage(): + import distutils.core + USAGE = textwrap.dedent(""" + usage: %(script)s [options] requirement_or_url ... + or: %(script)s --help + """).lstrip() + + def gen_usage(script_name): + return USAGE % dict( + script=os.path.basename(script_name), + ) + + saved = distutils.core.gen_usage + distutils.core.gen_usage = gen_usage + try: + yield + finally: + distutils.core.gen_usage = saved + class EasyInstallDeprecationWarning(SetuptoolsDeprecationWarning): """Class for warning about deprecations in EasyInstall in SetupTools. Not ignored by default, unlike DeprecationWarning.""" diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py index 68319c2f..aa75899a 100644 --- a/setuptools/tests/test_easy_install.py +++ b/setuptools/tests/test_easy_install.py @@ -467,24 +467,22 @@ class TestSetupRequires: """ monkeypatch.setenv(str('PIP_RETRIES'), str('0')) monkeypatch.setenv(str('PIP_TIMEOUT'), str('0')) - monkeypatch.setenv(str('PIP_VERBOSE'), str('1')) - # create an sdist that has a build-time dependency. - with TestSetupRequires.create_sdist() as dist_file: - with contexts.tempdir() as temp_dir: - setup_py = os.path.join(temp_dir, 'setup.py') - with open(setup_py, 'w') as fp: - fp.write('__import__("setuptools").setup()') - temp_install_dir = os.path.join(temp_dir, 'target') - os.mkdir(temp_install_dir) - with contexts.environment(PYTHONPATH=temp_install_dir): - # attempt to install the dist. It should - # fail because it doesn't exist. - with pytest.raises(SystemExit): - run_setup(setup_py, ['easy_install', - '--exclude-scripts', - '--index-url', mock_index.url, - '--install-dir', temp_install_dir, - dist_file]) + with contexts.quiet(): + # create an sdist that has a build-time dependency. + with TestSetupRequires.create_sdist() as dist_file: + with contexts.tempdir() as temp_install_dir: + with contexts.environment(PYTHONPATH=temp_install_dir): + ei_params = [ + '--index-url', mock_index.url, + '--exclude-scripts', + '--install-dir', temp_install_dir, + dist_file, + ] + with sandbox.save_argv(['easy_install']): + # attempt to install the dist. It should + # fail because it doesn't exist. + with pytest.raises(SystemExit): + easy_install_pkg.main(ei_params) # there should have been one requests to the server assert [r.path for r in mock_index.requests] == ['/does-not-exist/'] diff --git a/setuptools/tests/test_namespaces.py b/setuptools/tests/test_namespaces.py index 3c5df68a..f937d981 100644 --- a/setuptools/tests/test_namespaces.py +++ b/setuptools/tests/test_namespaces.py @@ -64,8 +64,9 @@ class TestNamespaces: target.mkdir() install_cmd = [ sys.executable, - '-m', 'pip.__main__', 'install', - '-t', str(target), str(pkg), + '-m', 'easy_install', + '-d', str(target), + str(pkg), ] with test.test.paths_on_pythonpath([str(target)]): subprocess.check_call(install_cmd) -- cgit v1.2.3 From 926c80f5e84823f48103f3695f55f23949cc5d37 Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Mon, 25 Nov 2019 11:24:10 +0100 Subject: wheel: fix `is_compatible` implementation --- setuptools/tests/test_wheel.py | 9 +++++++++ setuptools/wheel.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) (limited to 'setuptools') diff --git a/setuptools/tests/test_wheel.py b/setuptools/tests/test_wheel.py index d50816c2..55d346c6 100644 --- a/setuptools/tests/test_wheel.py +++ b/setuptools/tests/test_wheel.py @@ -18,6 +18,7 @@ import pytest from pkg_resources import Distribution, PathMetadata, PY_MAJOR from setuptools.extern.packaging.utils import canonicalize_name +from setuptools.extern.packaging.tags import parse_tag from setuptools.wheel import Wheel from .contexts import tempdir @@ -571,3 +572,11 @@ def test_wheel_no_dist_dir(): _check_wheel_install(wheel_path, install_dir, None, project_name, version, None) + + +def test_wheel_is_compatible(monkeypatch): + def sys_tags(): + for t in parse_tag('cp36-cp36m-manylinux1_x86_64'): + yield t + monkeypatch.setattr('setuptools.wheel.sys_tags', sys_tags) + assert Wheel('onnxruntime-0.1.2-cp36-cp36m-manylinux1_x86_64.whl').is_compatible() diff --git a/setuptools/wheel.py b/setuptools/wheel.py index 3effd79b..025aaa82 100644 --- a/setuptools/wheel.py +++ b/setuptools/wheel.py @@ -77,7 +77,7 @@ class Wheel: def is_compatible(self): '''Is the wheel is compatible with the current platform?''' - supported_tags = set(map(str, sys_tags())) + supported_tags = set((t.interpreter, t.abi, t.platform) for t in sys_tags()) return next((True for t in self.tags() if t in supported_tags), False) def egg_name(self): -- cgit v1.2.3 From 6f46a4b703d4db225e96bb871e1bf6a7c3597329 Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Tue, 26 Nov 2019 18:46:34 +0100 Subject: fix support for easy_install's find-links option in setup.cfg --- setuptools/installer.py | 13 ++++++++++-- setuptools/tests/test_easy_install.py | 38 +++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) (limited to 'setuptools') diff --git a/setuptools/installer.py b/setuptools/installer.py index 35bc3cc5..a5816608 100644 --- a/setuptools/installer.py +++ b/setuptools/installer.py @@ -7,11 +7,20 @@ from distutils.errors import DistutilsError import pkg_resources from setuptools.command.easy_install import easy_install +from setuptools.extern import six from setuptools.wheel import Wheel from .py31compat import TemporaryDirectory +def _fixup_find_links(find_links): + """Ensure find-links option end-up being a list of strings.""" + if isinstance(find_links, six.string_types): + return find_links.split() + assert isinstance(find_links, (tuple, list)) + return find_links + + def _legacy_fetch_build_egg(dist, req): """Fetch an egg needed for building. @@ -31,7 +40,7 @@ def _legacy_fetch_build_egg(dist, req): if dist.dependency_links: links = dist.dependency_links[:] if 'find_links' in opts: - links = opts['find_links'][1] + links + links = _fixup_find_links(opts['find_links'][1]) + links opts['find_links'] = ('setup', links) install_dir = dist.get_egg_cache_dir() cmd = easy_install( @@ -84,7 +93,7 @@ def fetch_build_egg(dist, req): else: index_url = None if 'find_links' in opts: - find_links = opts['find_links'][1][:] + find_links = _fixup_find_links(opts['find_links'][1])[:] else: find_links = [] if dist.dependency_links: diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py index aa75899a..a21651ec 100644 --- a/setuptools/tests/test_easy_install.py +++ b/setuptools/tests/test_easy_install.py @@ -744,6 +744,44 @@ class TestSetupRequires: eggs = list(map(str, pkg_resources.find_distributions(os.path.join(test_pkg, '.eggs')))) assert eggs == ['dep 1.0'] + @pytest.mark.parametrize('use_legacy_installer,with_dependency_links_in_setup_py', + itertools.product((False, True), (False, True))) + def test_setup_requires_with_find_links_in_setup_cfg(self, monkeypatch, + use_legacy_installer, + with_dependency_links_in_setup_py): + monkeypatch.setenv(str('PIP_RETRIES'), str('0')) + monkeypatch.setenv(str('PIP_TIMEOUT'), str('0')) + with contexts.save_pkg_resources_state(): + with contexts.tempdir() as temp_dir: + make_trivial_sdist(os.path.join(temp_dir, 'python-xlib-42.tar.gz'), 'python-xlib', '42') + test_pkg = os.path.join(temp_dir, 'test_pkg') + test_setup_py = os.path.join(test_pkg, 'setup.py') + test_setup_cfg = os.path.join(test_pkg, 'setup.cfg') + os.mkdir(test_pkg) + with open(test_setup_py, 'w') as fp: + if with_dependency_links_in_setup_py: + dependency_links = [os.path.join(temp_dir, 'links')] + else: + dependency_links = [] + fp.write(DALS( + ''' + from setuptools import installer, setup + if {use_legacy_installer}: + installer.fetch_build_egg = installer._legacy_fetch_build_egg + setup(setup_requires='python-xlib==42', + dependency_links={dependency_links!r}) + ''').format(use_legacy_installer=use_legacy_installer, + dependency_links=dependency_links)) + with open(test_setup_cfg, 'w') as fp: + fp.write(DALS( + ''' + [easy_install] + index_url = {index_url} + find_links = {find_links} + ''').format(index_url=os.path.join(temp_dir, 'index'), + find_links=temp_dir)) + run_setup(test_setup_py, [str('--version')]) + def make_trivial_sdist(dist_path, distname, version): """ -- cgit v1.2.3 From 7502dc9ca767927db9599f93cd48851ca59f7a62 Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Tue, 26 Nov 2019 20:56:57 +0100 Subject: fix possible issue with transitive build dependencies Handle the case where a missing transitive build dependency is required by an extra for an already installed build dependency. --- setuptools/installer.py | 7 ++++-- setuptools/tests/test_easy_install.py | 44 +++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) (limited to 'setuptools') diff --git a/setuptools/installer.py b/setuptools/installer.py index 35bc3cc5..ba9cfce9 100644 --- a/setuptools/installer.py +++ b/setuptools/installer.py @@ -64,8 +64,11 @@ def fetch_build_egg(dist, req): pkg_resources.get_distribution('wheel') except pkg_resources.DistributionNotFound: dist.announce('WARNING: The wheel package is not available.', log.WARN) - if not isinstance(req, pkg_resources.Requirement): - req = pkg_resources.Requirement.parse(req) + # Ignore environment markers: if we're here, it's needed. This ensure + # we don't try to ask pip for something like `babel; extra == "i18n"`, + # which would always be ignored. + req = pkg_resources.Requirement.parse(str(req)) + req.marker = None # Take easy_install options into account, but do not override relevant # pip environment variables (like PIP_INDEX_URL or PIP_QUIET); they'll # take precedence. diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py index aa75899a..f6da1b16 100644 --- a/setuptools/tests/test_easy_install.py +++ b/setuptools/tests/test_easy_install.py @@ -37,6 +37,7 @@ from setuptools.tests import fail_on_ascii import pkg_resources from . import contexts +from .files import build_files from .textwrap import DALS __metaclass__ = type @@ -744,6 +745,49 @@ class TestSetupRequires: eggs = list(map(str, pkg_resources.find_distributions(os.path.join(test_pkg, '.eggs')))) assert eggs == ['dep 1.0'] + def test_setup_requires_with_transitive_extra_dependency(self, monkeypatch): + # Use case: installing a package with a build dependency on + # an already installed `dep[extra]`, which in turn depends + # on `extra_dep` (whose is not already installed). + with contexts.save_pkg_resources_state(): + with contexts.tempdir() as temp_dir: + # Create source distribution for `extra_dep`. + make_trivial_sdist(os.path.join(temp_dir, 'extra_dep-1.0.tar.gz'), 'extra_dep', '1.0') + # Create source tree for `dep`. + dep_pkg = os.path.join(temp_dir, 'dep') + os.mkdir(dep_pkg) + build_files({ + 'setup.py': + DALS(""" + import setuptools + setuptools.setup( + name='dep', version='2.0', + extras_require={'extra': ['extra_dep']}, + ) + """), + 'setup.cfg': '', + }, prefix=dep_pkg) + # "Install" dep. + run_setup(os.path.join(dep_pkg, 'setup.py'), [str('dist_info')]) + working_set.add_entry(dep_pkg) + # Create source tree for test package. + test_pkg = os.path.join(temp_dir, 'test_pkg') + test_setup_py = os.path.join(test_pkg, 'setup.py') + test_setup_cfg = os.path.join(test_pkg, 'setup.cfg') + os.mkdir(test_pkg) + with open(test_setup_py, 'w') as fp: + fp.write(DALS( + ''' + from setuptools import installer, setup + setup(setup_requires='dep[extra]') + ''')) + # Check... + monkeypatch.setenv(str('PIP_FIND_LINKS'), str(temp_dir)) + monkeypatch.setenv(str('PIP_NO_INDEX'), str('1')) + monkeypatch.setenv(str('PIP_RETRIES'), str('0')) + monkeypatch.setenv(str('PIP_TIMEOUT'), str('0')) + run_setup(test_setup_py, [str('--version')]) + def make_trivial_sdist(dist_path, distname, version): """ -- cgit v1.2.3 From a2e883e1b838db529d992d4c6c8ab73c16f48591 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 1 Dec 2019 09:38:13 -0500 Subject: Extract function to strip the marker for concise code in the long function. --- setuptools/installer.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) (limited to 'setuptools') diff --git a/setuptools/installer.py b/setuptools/installer.py index ba9cfce9..527b95de 100644 --- a/setuptools/installer.py +++ b/setuptools/installer.py @@ -64,11 +64,8 @@ def fetch_build_egg(dist, req): pkg_resources.get_distribution('wheel') except pkg_resources.DistributionNotFound: dist.announce('WARNING: The wheel package is not available.', log.WARN) - # Ignore environment markers: if we're here, it's needed. This ensure - # we don't try to ask pip for something like `babel; extra == "i18n"`, - # which would always be ignored. - req = pkg_resources.Requirement.parse(str(req)) - req.marker = None + # Ignore environment markers; if supplied, it is required. + req = strip_marker(req) # Take easy_install options into account, but do not override relevant # pip environment variables (like PIP_INDEX_URL or PIP_QUIET); they'll # take precedence. @@ -130,3 +127,15 @@ def fetch_build_egg(dist, req): dist = pkg_resources.Distribution.from_filename( dist_location, metadata=dist_metadata) return dist + + +def strip_marker(req): + """ + Return a new requirement without the environment marker to avoid + calling pip with something like `babel; extra == "i18n"`, which + would always be ignored. + """ + # create a copy to avoid mutating the input + req = pkg_resources.Requirement.parse(str(req)) + req.marker = None + return req -- cgit v1.2.3 From 3910bbb8d57a8f811ce863e9e1d09ae631cfe353 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 21 Dec 2019 22:04:09 -0500 Subject: Extract methods to separate _safe_data_files behavior and _add_data_files. --- setuptools/command/sdist.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) (limited to 'setuptools') diff --git a/setuptools/command/sdist.py b/setuptools/command/sdist.py index 55ecdd97..eebdfd19 100644 --- a/setuptools/command/sdist.py +++ b/setuptools/command/sdist.py @@ -126,14 +126,27 @@ class sdist(sdist_add_defaults, orig.sdist): if self.distribution.has_pure_modules(): build_py = self.get_finalized_command('build_py') self.filelist.extend(build_py.get_source_files()) - # This functionality is incompatible with include_package_data, and - # will in fact create an infinite recursion if include_package_data - # is True. Use of include_package_data will imply that - # distutils-style automatic handling of package_data is disabled - if not self.distribution.include_package_data: - for _, src_dir, _, filenames in build_py.data_files: - self.filelist.extend([os.path.join(src_dir, filename) - for filename in filenames]) + self._add_data_files(self._safe_data_files(build_py)) + + def _safe_data_files(self, build_py): + """ + Extracting data_files from build_py is known to cause + infinite recursion errors when `include_package_data` + is enabled, so suppress it in that case. + """ + if self.distribution.include_package_data: + return () + return build_py.data_files + + def _add_data_files(self, data_files): + """ + Add data files as found in build_py.data_files. + """ + self.filelist.extend( + os.path.join(src_dir, name) + for _, src_dir, _, filenames in data_files + for name in filenames + ) def _add_defaults_data_files(self): try: -- cgit v1.2.3