From: JinWang An Date: Mon, 27 Mar 2023 08:02:48 +0000 (+0900) Subject: Imported Upstream version 62.0.0 X-Git-Tag: upstream/62.0.0^0 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=2f5205fbc502fea2e09c6c7429ece9bd130567d5;p=platform%2Fupstream%2Fpython-setuptools.git Imported Upstream version 62.0.0 --- diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 87fa535..5c2f2e4 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 61.3.1 +current_version = 62.0.0 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index 590f776..126457b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,23 @@ +v62.0.0 +------- + + +Breaking Changes +^^^^^^^^^^^^^^^^ +* #3151: Made ``setup.py develop --user`` install to the user site packages directory even if it is disabled in the current interpreter. + +Changes +^^^^^^^ +* #3153: When resolving requirements use both canonical and normalized names -- by :user:`ldaniluk` +* #3167: Honor unix file mode in ZipFile when installing wheel via ``install_as_egg`` -- by :user:`delijati` + +Misc +^^^^ +* #3088: Fixed duplicated tag with the ``dist-info`` command. +* #3247: Fixed problem preventing ``readme`` specified as dynamic in ``pyproject.toml`` + from being dynamically specified in ``setup.py``. + + v61.3.1 ------- diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index 852476e..d59226a 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -83,6 +83,7 @@ __import__('pkg_resources.extern.packaging.version') __import__('pkg_resources.extern.packaging.specifiers') __import__('pkg_resources.extern.packaging.requirements') __import__('pkg_resources.extern.packaging.markers') +__import__('pkg_resources.extern.packaging.utils') if sys.version_info < (3, 5): raise RuntimeError("Python 3.5 or later is required") @@ -554,6 +555,7 @@ class WorkingSet: self.entries = [] self.entry_keys = {} self.by_key = {} + self.normalized_to_canonical_keys = {} self.callbacks = [] if entries is None: @@ -634,6 +636,14 @@ class WorkingSet: is returned. """ dist = self.by_key.get(req.key) + + if dist is None: + canonical_key = self.normalized_to_canonical_keys.get(req.key) + + if canonical_key is not None: + req.key = canonical_key + dist = self.by_key.get(canonical_key) + if dist is not None and dist not in req: # XXX add more info raise VersionConflict(dist, req) @@ -702,6 +712,8 @@ class WorkingSet: return self.by_key[dist.key] = dist + normalized_name = packaging.utils.canonicalize_name(dist.key) + self.normalized_to_canonical_keys[normalized_name] = dist.key if dist.key not in keys: keys.append(dist.key) if dist.key not in keys2: @@ -922,14 +934,15 @@ class WorkingSet: def __getstate__(self): return ( self.entries[:], self.entry_keys.copy(), self.by_key.copy(), - self.callbacks[:] + self.normalized_to_canonical_keys.copy(), self.callbacks[:] ) - def __setstate__(self, e_k_b_c): - entries, keys, by_key, callbacks = e_k_b_c + def __setstate__(self, e_k_b_n_c): + entries, keys, by_key, normalized_to_canonical_keys, callbacks = e_k_b_n_c self.entries = entries[:] self.entry_keys = keys.copy() self.by_key = by_key.copy() + self.normalized_to_canonical_keys = normalized_to_canonical_keys.copy() self.callbacks = callbacks[:] diff --git a/pkg_resources/tests/test_working_set.py b/pkg_resources/tests/test_working_set.py index db13c71..575656e 100644 --- a/pkg_resources/tests/test_working_set.py +++ b/pkg_resources/tests/test_working_set.py @@ -42,7 +42,7 @@ def parse_distributions(s): continue fields = spec.split('\n', 1) assert 1 <= len(fields) <= 2 - name, version = fields.pop(0).split('-') + name, version = fields.pop(0).rsplit('-', 1) if fields: requires = textwrap.dedent(fields.pop(0)) metadata = Metadata(('requires.txt', requires)) @@ -465,6 +465,25 @@ def parametrize_test_working_set_resolve(*test_list): # resolved [replace conflicting] VersionConflict ''', + + ''' + # id + wanted_normalized_name_installed_canonical + + # installed + foo.bar-3.6 + + # installable + + # wanted + foo-bar==3.6 + + # resolved + foo.bar-3.6 + + # resolved [replace conflicting] + foo.bar-3.6 + ''', ) def test_working_set_resolve(installed_dists, installable_dists, requirements, replace_conflicting, resolved_dists_or_exception): diff --git a/setup.cfg b/setup.cfg index 4f84656..78c088a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 61.3.1 +version = 62.0.0 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages diff --git a/setuptools/archive_util.py b/setuptools/archive_util.py index 73b2db7..d8e10c1 100644 --- a/setuptools/archive_util.py +++ b/setuptools/archive_util.py @@ -100,29 +100,37 @@ def unpack_zipfile(filename, extract_dir, progress_filter=default_filter): raise UnrecognizedFormat("%s is not a zip file" % (filename,)) with zipfile.ZipFile(filename) as z: - for info in z.infolist(): - name = info.filename + _unpack_zipfile_obj(z, extract_dir, progress_filter) - # don't extract absolute paths or ones with .. in them - if name.startswith('/') or '..' in name.split('/'): - continue - target = os.path.join(extract_dir, *name.split('/')) - target = progress_filter(name, target) - if not target: - continue - if name.endswith('/'): - # directory - ensure_directory(target) - else: - # file - ensure_directory(target) - data = z.read(info.filename) - with open(target, 'wb') as f: - f.write(data) - unix_attributes = info.external_attr >> 16 - if unix_attributes: - os.chmod(target, unix_attributes) +def _unpack_zipfile_obj(zipfile_obj, extract_dir, progress_filter=default_filter): + """Internal/private API used by other parts of setuptools. + Similar to ``unpack_zipfile``, but receives an already opened :obj:`zipfile.ZipFile` + object instead of a filename. + """ + for info in zipfile_obj.infolist(): + name = info.filename + + # don't extract absolute paths or ones with .. in them + if name.startswith('/') or '..' in name.split('/'): + continue + + target = os.path.join(extract_dir, *name.split('/')) + target = progress_filter(name, target) + if not target: + continue + if name.endswith('/'): + # directory + ensure_directory(target) + else: + # file + ensure_directory(target) + data = zipfile_obj.read(info.filename) + with open(target, 'wb') as f: + f.write(data) + unix_attributes = info.external_attr >> 16 + if unix_attributes: + os.chmod(target, unix_attributes) def _resolve_tar_file_or_dir(tar_obj, tar_member_obj): diff --git a/setuptools/command/dist_info.py b/setuptools/command/dist_info.py index c45258f..8b8509f 100644 --- a/setuptools/command/dist_info.py +++ b/setuptools/command/dist_info.py @@ -4,9 +4,13 @@ As defined in the wheel specification """ import os +import re +import warnings +from inspect import cleandoc from distutils.core import Command from distutils import log +from setuptools.extern import packaging class dist_info(Command): @@ -29,8 +33,36 @@ class dist_info(Command): egg_info.egg_base = self.egg_base egg_info.finalize_options() egg_info.run() - dist_info_dir = egg_info.egg_info[:-len('.egg-info')] + '.dist-info' + name = _safe(self.distribution.get_name()) + version = _version(self.distribution.get_version()) + base = self.egg_base or os.curdir + dist_info_dir = os.path.join(base, f"{name}-{version}.dist-info") log.info("creating '{}'".format(os.path.abspath(dist_info_dir))) bdist_wheel = self.get_finalized_command('bdist_wheel') bdist_wheel.egg2dist(egg_info.egg_info, dist_info_dir) + + +def _safe(component: str) -> str: + """Escape a component used to form a wheel name according to PEP 491""" + return re.sub(r"[^\w\d.]+", "_", component) + + +def _version(version: str) -> str: + """Convert an arbitrary string to a version string.""" + v = version.replace(' ', '.') + try: + return str(packaging.version.Version(v)).replace("-", "_") + except packaging.version.InvalidVersion: + msg = f"""!!\n\n + ################### + # Invalid version # + ################### + {version!r} is not valid according to PEP 440.\n + Please make sure specify a valid version for your package. + Also note that future releases of setuptools may halt the build process + if an invalid version is given. + \n\n!! + """ + warnings.warn(cleandoc(msg)) + return _safe(v).strip("_") diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index 77dcd25..444d3b3 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -169,12 +169,8 @@ class easy_install(Command): self.install_data = None self.install_base = None self.install_platbase = None - if site.ENABLE_USER_SITE: - self.install_userbase = site.USER_BASE - self.install_usersite = site.USER_SITE - else: - self.install_userbase = None - self.install_usersite = None + self.install_userbase = site.USER_BASE + self.install_usersite = site.USER_SITE self.no_find_links = None # Options not specifiable via command line @@ -253,11 +249,9 @@ class easy_install(Command): getattr(sys, 'windir', '').replace('.', ''), ) - if site.ENABLE_USER_SITE: - self.config_vars['userbase'] = self.install_userbase - self.config_vars['usersite'] = self.install_usersite - - elif self.user: + self.config_vars['userbase'] = self.install_userbase + self.config_vars['usersite'] = self.install_usersite + if self.user and not site.ENABLE_USER_SITE: log.warn("WARNING: The user site-packages directory is disabled.") self._fix_install_dir_for_user_site() @@ -375,7 +369,7 @@ class easy_install(Command): """ Fix the install_dir if "--user" was used. """ - if not self.user or not site.ENABLE_USER_SITE: + if not self.user: return self.create_home_path() diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index 6338965..c37ab81 100644 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -136,11 +136,21 @@ class InfoCommon: in which case the version string already contains all tags. """ return ( - version if self.vtags and version.endswith(self.vtags) + version if self.vtags and self._already_tagged(version) else version + self.vtags ) - def tags(self): + def _already_tagged(self, version: str) -> bool: + # Depending on their format, tags may change with version normalization. + # So in addition the regular tags, we have to search for the normalized ones. + return version.endswith(self.vtags) or version.endswith(self._safe_tags()) + + def _safe_tags(self) -> str: + # To implement this we can rely on `safe_version` pretending to be version 0 + # followed by tags. Then we simply discard the starting 0 (fake version number) + return safe_version(f"0{self.vtags}")[1:] + + def tags(self) -> str: version = '' if self.tag_build: version += self.tag_build diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py index d402495..be81214 100644 --- a/setuptools/config/pyprojecttoml.py +++ b/setuptools/config/pyprojecttoml.py @@ -316,12 +316,17 @@ class _ConfigExpander: return None def _obtain_readme(self, dist: "Distribution") -> Optional[Dict[str, str]]: - if "readme" in self.dynamic: - dynamic_cfg = self.dynamic_cfg + if "readme" not in self.dynamic: + return None + + dynamic_cfg = self.dynamic_cfg + if "readme" in dynamic_cfg: return { "text": self._obtain(dist, "readme", {}), "content-type": dynamic_cfg["readme"].get("content-type", "text/x-rst"), } + + self._ensure_previously_set(dist, "readme") return None def _obtain_entry_points( diff --git a/setuptools/tests/config/test_pyprojecttoml.py b/setuptools/tests/config/test_pyprojecttoml.py index 4c23701..200312b 100644 --- a/setuptools/tests/config/test_pyprojecttoml.py +++ b/setuptools/tests/config/test_pyprojecttoml.py @@ -253,6 +253,20 @@ class TestClassifiers: with pytest.raises(OptionError, match="No configuration .* .classifiers."): read_configuration(pyproject) + def test_dynamic_readme_from_setup_script_args(self, tmp_path): + config = """ + [project] + name = "myproj" + version = '42' + dynamic = ["readme"] + """ + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text(cleandoc(config)) + dist = Distribution(attrs={"long_description": "42"}) + # No error should occur because of missing `readme` + dist = apply_configuration(dist, pyproject) + assert dist.metadata.long_description == "42" + def test_dynamic_without_file(self, tmp_path): config = """ [project] diff --git a/setuptools/tests/test_dist_info.py b/setuptools/tests/test_dist_info.py index 29fbd09..813ef51 100644 --- a/setuptools/tests/test_dist_info.py +++ b/setuptools/tests/test_dist_info.py @@ -1,12 +1,21 @@ """Test .dist-info style distributions. """ +import pathlib +import re +import subprocess +import sys +from functools import partial import pytest import pkg_resources +from setuptools.archive_util import unpack_archive from .textwrap import DALS +read = partial(pathlib.Path.read_text, encoding="utf-8") + + class TestDistInfo: metadata_base = DALS(""" @@ -72,3 +81,78 @@ class TestDistInfo: pkg_resources.Requirement.parse('quux>=1.1;extra=="baz"'), ] assert d.extras == ['baz'] + + def test_invalid_version(self, tmp_path): + config = "[metadata]\nname=proj\nversion=42\n[egg_info]\ntag_build=invalid!!!\n" + (tmp_path / "setup.cfg").write_text(config, encoding="utf-8") + msg = re.compile("invalid version", re.M | re.I) + output = run_command("dist_info", cwd=tmp_path) + assert msg.search(output) + dist_info = next(tmp_path.glob("*.dist-info")) + assert dist_info.name.startswith("proj-42") + + +class TestWheelCompatibility: + """Make sure the .dist-info directory produced with the ``dist_info`` command + is the same as the one produced by ``bdist_wheel``. + """ + SETUPCFG = DALS(""" + [metadata] + name = {name} + version = {version} + + [options] + install_requires = foo>=12; sys_platform != "linux" + + [options.extras_require] + test = pytest + + [options.entry_points] + console_scripts = + executable-name = my_package.module:function + discover = + myproj = my_package.other_module:function + """) + + EGG_INFO_OPTS = [ + # Related: #3088 #2872 + ("", ""), + (".post", "[egg_info]\ntag_build = post\n"), + (".post", "[egg_info]\ntag_build = .post\n"), + (".post", "[egg_info]\ntag_build = post\ntag_date = 1\n"), + (".dev", "[egg_info]\ntag_build = .dev\n"), + (".dev", "[egg_info]\ntag_build = .dev\ntag_date = 1\n"), + ("a1", "[egg_info]\ntag_build = .a1\n"), + ("+local", "[egg_info]\ntag_build = +local\n"), + ] + + @pytest.mark.parametrize("name", "my-proj my_proj my.proj My.Proj".split()) + @pytest.mark.parametrize("version", ["0.42.13"]) + @pytest.mark.parametrize("suffix, cfg", EGG_INFO_OPTS) + def test_dist_info_is_the_same_as_in_wheel( + self, name, version, tmp_path, suffix, cfg + ): + config = self.SETUPCFG.format(name=name, version=version) + cfg + + for i in "dir_wheel", "dir_dist": + (tmp_path / i).mkdir() + (tmp_path / i / "setup.cfg").write_text(config, encoding="utf-8") + + run_command("bdist_wheel", cwd=tmp_path / "dir_wheel") + wheel = next(tmp_path.glob("dir_wheel/dist/*.whl")) + unpack_archive(wheel, tmp_path / "unpack") + wheel_dist_info = next(tmp_path.glob("unpack/*.dist-info")) + + run_command("dist_info", cwd=tmp_path / "dir_dist") + dist_info = next(tmp_path.glob("dir_dist/*.dist-info")) + + assert dist_info.name == wheel_dist_info.name + assert dist_info.name.startswith(f"{name.replace('-', '_')}-{version}{suffix}") + for file in "METADATA", "entry_points.txt": + assert read(dist_info / file) == read(wheel_dist_info / file) + + +def run_command(*cmd, **kwargs): + opts = {"stderr": subprocess.STDOUT, "text": True, **kwargs} + cmd = [sys.executable, "-c", "__import__('setuptools').setup()", *cmd] + return subprocess.check_output(cmd, **opts) diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py index 0d26dc7..726f9fd 100644 --- a/setuptools/tests/test_easy_install.py +++ b/setuptools/tests/test_easy_install.py @@ -19,6 +19,7 @@ import subprocess import pathlib import warnings from collections import namedtuple +from pathlib import Path import pytest from jaraco import path @@ -1166,3 +1167,52 @@ def test_use_correct_python_version_string(tmpdir, tmpdir_cwd, monkeypatch): assert cmd.config_vars['py_version'] == '3.10.1' assert cmd.config_vars['py_version_short'] == '3.10' assert cmd.config_vars['py_version_nodot'] == '310' + + +def test_editable_user_and_build_isolation(setup_context, monkeypatch, tmp_path): + ''' `setup.py develop` should honor `--user` even under build isolation''' + + # == Arrange == + # Pretend that build isolation was enabled + # e.g pip sets the environment varible PYTHONNOUSERSITE=1 + monkeypatch.setattr('site.ENABLE_USER_SITE', False) + + # Patching $HOME for 2 reasons: + # 1. setuptools/command/easy_install.py:create_home_path + # tries creating directories in $HOME + # given `self.config_vars['DESTDIRS'] = "/home/user/.pyenv/versions/3.9.10 /home/user/.pyenv/versions/3.9.10/lib /home/user/.pyenv/versions/3.9.10/lib/python3.9 /home/user/.pyenv/versions/3.9.10/lib/python3.9/lib-dynload"`` # noqa: E501 + # it will `makedirs("/home/user/.pyenv/versions/3.9.10 /home/user/.pyenv/versions/3.9.10/lib /home/user/.pyenv/versions/3.9.10/lib/python3.9 /home/user/.pyenv/versions/3.9.10/lib/python3.9/lib-dynload")`` # noqa: E501 + # 2. We are going to force `site` to update site.USER_BASE and site.USER_SITE + # To point inside our new home + monkeypatch.setenv('HOME', str(tmp_path / '.home')) + monkeypatch.setenv('USERPROFILE', str(tmp_path / '.home')) + monkeypatch.setenv('APPDATA', str(tmp_path / '.home')) + monkeypatch.setattr('site.USER_BASE', None) + monkeypatch.setattr('site.USER_SITE', None) + user_site = Path(site.getusersitepackages()) + user_site.mkdir(parents=True, exist_ok=True) + + sys_prefix = (tmp_path / '.sys_prefix') + sys_prefix.mkdir(parents=True, exist_ok=True) + monkeypatch.setattr('sys.prefix', str(sys_prefix)) + + setup_script = ( + "__import__('setuptools').setup(name='aproj', version=42, packages=[])\n" + ) + (tmp_path / "setup.py").write_text(setup_script, encoding="utf-8") + + # == Sanity check == + assert list(sys_prefix.glob("*")) == [] + assert list(user_site.glob("*")) == [] + + # == Act == + run_setup('setup.py', ['develop', '--user']) + + # == Assert == + # Should not install to sys.prefix + assert list(sys_prefix.glob("*")) == [] + # Should install to user site + installed = {f.name for f in user_site.glob("*")} + # sometimes easy-install.pth is created and sometimes not + installed = installed - {"easy-install.pth"} + assert installed == {'aproj.egg-link'} diff --git a/setuptools/tests/test_wheel.py b/setuptools/tests/test_wheel.py index a15c3a4..89d65d0 100644 --- a/setuptools/tests/test_wheel.py +++ b/setuptools/tests/test_wheel.py @@ -6,6 +6,8 @@ from distutils.sysconfig import get_config_var from distutils.util import get_platform import contextlib +import pathlib +import stat import glob import inspect import os @@ -614,3 +616,88 @@ def test_wheel_is_compatible(monkeypatch): monkeypatch.setattr('setuptools.wheel.sys_tags', sys_tags) assert Wheel( 'onnxruntime-0.1.2-cp36-cp36m-manylinux1_x86_64.whl').is_compatible() + + +def test_wheel_mode(): + @contextlib.contextmanager + def build_wheel(extra_file_defs=None, **kwargs): + file_defs = { + 'setup.py': (DALS( + ''' + # -*- coding: utf-8 -*- + from setuptools import setup + import setuptools + setup(**%r) + ''' + ) % kwargs).encode('utf-8'), + } + if extra_file_defs: + file_defs.update(extra_file_defs) + with tempdir() as source_dir: + path.build(file_defs, source_dir) + runsh = pathlib.Path(source_dir) / "script.sh" + os.chmod(runsh, 0o777) + subprocess.check_call((sys.executable, 'setup.py', + '-q', 'bdist_wheel'), cwd=source_dir) + yield glob.glob(os.path.join(source_dir, 'dist', '*.whl'))[0] + + params = dict( + id='script', + file_defs={ + 'script.py': DALS( + ''' + #/usr/bin/python + print('hello world!') + ''' + ), + 'script.sh': DALS( + ''' + #/bin/sh + echo 'hello world!' + ''' + ), + }, + setup_kwargs=dict( + scripts=['script.py', 'script.sh'], + ), + install_tree=flatten_tree({ + 'foo-1.0-py{py_version}.egg': { + 'EGG-INFO': [ + 'PKG-INFO', + 'RECORD', + 'WHEEL', + 'top_level.txt', + {'scripts': [ + 'script.py', + 'script.sh' + ]} + + ] + } + }) + ) + + project_name = params.get('name', 'foo') + version = params.get('version', '1.0') + install_tree = params.get('install_tree') + file_defs = params.get('file_defs', {}) + setup_kwargs = params.get('setup_kwargs', {}) + + with build_wheel( + name=project_name, + version=version, + install_requires=[], + extras_require={}, + extra_file_defs=file_defs, + **setup_kwargs + ) as filename, tempdir() as install_dir: + _check_wheel_install(filename, install_dir, + install_tree, project_name, + version, None) + w = Wheel(filename) + base = pathlib.Path(install_dir) / w.egg_name() + script_sh = base / "EGG-INFO" / "scripts" / "script.sh" + assert script_sh.exists() + if sys.platform != 'win32': + # Editable file mode has no effect on Windows + assert oct(stat.S_IMODE(script_sh.stat().st_mode)) == "0o777" diff --git a/setuptools/wheel.py b/setuptools/wheel.py index 9819e8b..0ced0ff 100644 --- a/setuptools/wheel.py +++ b/setuptools/wheel.py @@ -15,6 +15,7 @@ from pkg_resources import parse_version from setuptools.extern.packaging.tags import sys_tags from setuptools.extern.packaging.utils import canonicalize_name from setuptools.command.egg_info import write_requirements +from setuptools.archive_util import _unpack_zipfile_obj WHEEL_NAME = re.compile( @@ -121,8 +122,7 @@ class Wheel: raise ValueError( 'unsupported wheel format version: %s' % wheel_version) # Extract to target directory. - os.mkdir(destination_eggdir) - zf.extractall(destination_eggdir) + _unpack_zipfile_obj(zf, destination_eggdir) # Convert metadata. dist_info = os.path.join(destination_eggdir, dist_info) dist = pkg_resources.Distribution.from_location(