From b7ade65fb82fc35a70287c17b7f01e552b0fba28 Mon Sep 17 00:00:00 2001 From: JinWang An Date: Mon, 27 Mar 2023 17:02:57 +0900 Subject: [PATCH] Imported Upstream version 65.5.1 --- .bumpversion.cfg | 2 +- .github/workflows/main.yml | 2 +- CHANGES.rst | 10 ++ docs/userguide/quickstart.rst | 2 +- pkg_resources/tests/test_markers.py | 2 +- pkg_resources/tests/test_pkg_resources.py | 5 +- pytest.ini | 3 + setup.cfg | 4 +- setuptools/command/build.py | 4 +- setuptools/package_index.py | 172 ++++++++++++-------- setuptools/tests/test_bdist_deprecations.py | 2 +- setuptools/tests/test_build_clib.py | 3 +- setuptools/tests/test_easy_install.py | 2 +- setuptools/tests/test_packageindex.py | 87 +++++----- setuptools/tests/test_register.py | 5 +- setuptools/tests/test_upload.py | 5 +- 16 files changed, 180 insertions(+), 130 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 26370e6..316cd4c 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 65.5.0 +current_version = 65.5.1 commit = True tag = True diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7b4669a..8892a6c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -64,7 +64,7 @@ jobs: run: pipx run coverage xml --ignore-errors - name: Publish coverage if: hashFiles('coverage.xml') != '' # Rudimentary `file.exists()` - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v3 with: flags: >- # Mark which lines are covered by which envs ${{ runner.os }}, diff --git a/CHANGES.rst b/CHANGES.rst index ac84ffa..e2b8dbd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,13 @@ +v65.5.1 +------- + + +Misc +^^^^ +* #3638: Drop a test dependency on the ``mock`` package, always use :external+python:py:mod:`unittest.mock` -- by :user:`hroncok` +* #3659: Fixed REDoS vector in package_index. + + v65.5.0 ------- diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst index 13846e2..fe5c5bc 100644 --- a/docs/userguide/quickstart.rst +++ b/docs/userguide/quickstart.rst @@ -299,7 +299,7 @@ Dependency management --------------------- Packages built with ``setuptools`` can specify dependencies to be automatically installed when the package itself is installed. -The example below show how to configure this kind of dependencies: +The example below shows how to configure this kind of dependencies: .. tab:: pyproject.toml diff --git a/pkg_resources/tests/test_markers.py b/pkg_resources/tests/test_markers.py index 15a3b49..9306d5b 100644 --- a/pkg_resources/tests/test_markers.py +++ b/pkg_resources/tests/test_markers.py @@ -1,4 +1,4 @@ -import mock +from unittest import mock from pkg_resources import evaluate_marker diff --git a/pkg_resources/tests/test_pkg_resources.py b/pkg_resources/tests/test_pkg_resources.py index 6518820..684c977 100644 --- a/pkg_resources/tests/test_pkg_resources.py +++ b/pkg_resources/tests/test_pkg_resources.py @@ -9,10 +9,7 @@ import stat import distutils.dist import distutils.command.install_egg_info -try: - from unittest import mock -except ImportError: - import mock +from unittest import mock from pkg_resources import ( DistInfoDistribution, Distribution, EggInfoDistribution, diff --git a/pytest.ini b/pytest.ini index 50a7971..9d326e5 100644 --- a/pytest.ini +++ b/pytest.ini @@ -64,3 +64,6 @@ filterwarnings= ignore:Support for .* in .pyproject.toml. is still .beta. ignore::setuptools.command.editable_wheel.InformationOnly + + # https://github.com/pypa/setuptools/issues/3655 + ignore:The --rsyncdir command line argument and rsyncdirs config variable are deprecated.:DeprecationWarning diff --git a/setup.cfg b/setup.cfg index 201c260..054fd04 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 65.5.0 +version = 65.5.1 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages @@ -59,7 +59,6 @@ testing = pytest-perf # local - mock flake8-2020 virtualenv>=13.0.0 wheel @@ -72,6 +71,7 @@ testing = pip_run>=8.8 ini2toml[lite]>=0.9 tomli-w>=1.0.0 + pytest-timeout testing-integration = pytest diff --git a/setuptools/command/build.py b/setuptools/command/build.py index c0676d8..fa3c99e 100644 --- a/setuptools/command/build.py +++ b/setuptools/command/build.py @@ -48,8 +48,8 @@ class SubCommand(Protocol): Subcommands **SHOULD** take advantage of ``editable_mode=True`` to adequate its behaviour or perform optimisations. - For example, if a subcommand don't need to generate any extra file and - everything it does is to copy a source file into the build directory, + For example, if a subcommand doesn't need to generate an extra file and + all it does is to copy a source file into the build directory, ``run()`` **SHOULD** simply "early return". Similarly, if the subcommand creates files that would be placed alongside diff --git a/setuptools/package_index.py b/setuptools/package_index.py index 14881d2..362e26f 100644 --- a/setuptools/package_index.py +++ b/setuptools/package_index.py @@ -1,4 +1,5 @@ -"""PyPI and direct package downloading""" +"""PyPI and direct package downloading.""" + import sys import os import re @@ -19,9 +20,20 @@ from functools import wraps import setuptools from pkg_resources import ( - CHECKOUT_DIST, Distribution, BINARY_DIST, normalize_path, SOURCE_DIST, - Environment, find_distributions, safe_name, safe_version, - to_filename, Requirement, DEVELOP_DIST, EGG_DIST, parse_version, + CHECKOUT_DIST, + Distribution, + BINARY_DIST, + normalize_path, + SOURCE_DIST, + Environment, + find_distributions, + safe_name, + safe_version, + to_filename, + Requirement, + DEVELOP_DIST, + EGG_DIST, + parse_version, ) from distutils import log from distutils.errors import DistutilsError @@ -40,7 +52,9 @@ URL_SCHEME = re.compile('([-+.a-z0-9]{2,}):', re.I).match EXTENSIONS = ".tar.gz .tar.bz2 .tar .zip .tgz".split() __all__ = [ - 'PackageIndex', 'distros_for_url', 'parse_bdist_wininst', + 'PackageIndex', + 'distros_for_url', + 'parse_bdist_wininst', 'interpret_distro_name', ] @@ -48,7 +62,8 @@ _SOCKET_TIMEOUT = 15 _tmpl = "setuptools/{setuptools.__version__} Python-urllib/{py_major}" user_agent = _tmpl.format( - py_major='{}.{}'.format(*sys.version_info), setuptools=setuptools) + py_major='{}.{}'.format(*sys.version_info), setuptools=setuptools +) def parse_requirement_arg(spec): @@ -120,13 +135,15 @@ def distros_for_location(location, basename, metadata=None): wheel = Wheel(basename) if not wheel.is_compatible(): return [] - return [Distribution( - location=location, - project_name=wheel.project_name, - version=wheel.version, - # Increase priority over eggs. - precedence=EGG_DIST + 1, - )] + return [ + Distribution( + location=location, + project_name=wheel.project_name, + version=wheel.version, + # Increase priority over eggs. + precedence=EGG_DIST + 1, + ) + ] if basename.endswith('.exe'): win_base, py_ver, platform = parse_bdist_wininst(basename) if win_base is not None: @@ -137,7 +154,7 @@ def distros_for_location(location, basename, metadata=None): # for ext in EXTENSIONS: if basename.endswith(ext): - basename = basename[:-len(ext)] + basename = basename[: -len(ext)] return interpret_distro_name(location, basename, metadata) return [] # no extension matched @@ -150,8 +167,7 @@ def distros_for_filename(filename, metadata=None): def interpret_distro_name( - location, basename, metadata, py_version=None, precedence=SOURCE_DIST, - platform=None + location, basename, metadata, py_version=None, precedence=SOURCE_DIST, platform=None ): """Generate alternative interpretations of a source distro name @@ -178,9 +194,13 @@ def interpret_distro_name( for p in range(1, len(parts) + 1): yield Distribution( - location, metadata, '-'.join(parts[:p]), '-'.join(parts[p:]), - py_version=py_version, precedence=precedence, - platform=platform + location, + metadata, + '-'.join(parts[:p]), + '-'.join(parts[p:]), + py_version=py_version, + precedence=precedence, + platform=platform, ) @@ -197,8 +217,10 @@ def unique_values(func): return wrapper -REL = re.compile(r"""<([^>]*\srel\s*=\s*['"]?([^'">]+)[^>]*)>""", re.I) -# this line is here to fix emacs' cruddy broken syntax highlighting +REL = re.compile(r"""<([^>]*\srel\s{0,10}=\s{0,10}['"]?([^'" >]+)[^>]*)>""", re.I) +""" +Regex for an HTML tag with 'rel="val"' attributes. +""" @unique_values @@ -282,11 +304,16 @@ class PackageIndex(Environment): """A distribution index that scans web pages for download URLs""" def __init__( - self, index_url="https://pypi.org/simple/", hosts=('*',), - ca_bundle=None, verify_ssl=True, *args, **kw + self, + index_url="https://pypi.org/simple/", + hosts=('*',), + ca_bundle=None, + verify_ssl=True, + *args, + **kw ): super().__init__(*args, **kw) - self.index_url = index_url + "/" [:not index_url.endswith('/')] + self.index_url = index_url + "/"[: not index_url.endswith('/')] self.scanned_urls = {} self.fetched_urls = {} self.package_pages = {} @@ -379,7 +406,8 @@ class PackageIndex(Environment): return True msg = ( "\nNote: Bypassing %s (disallowed host; see " - "http://bit.ly/2hrImnY for details).\n") + "http://bit.ly/2hrImnY for details).\n" + ) if fatal: raise DistutilsError(msg % url) else: @@ -417,9 +445,7 @@ class PackageIndex(Environment): if not link.startswith(self.index_url): return NO_MATCH_SENTINEL - parts = list(map( - urllib.parse.unquote, link[len(self.index_url):].split('/') - )) + parts = list(map(urllib.parse.unquote, link[len(self.index_url) :].split('/'))) if len(parts) != 2 or '#' in parts[1]: return NO_MATCH_SENTINEL @@ -461,16 +487,15 @@ class PackageIndex(Environment): def need_version_info(self, url): self.scan_all( "Page at %s links to .py file(s) without version info; an index " - "scan is required.", url + "scan is required.", + url, ) def scan_all(self, msg=None, *args): if self.index_url not in self.fetched_urls: if msg: self.warn(msg, *args) - self.info( - "Scanning index of all packages (this may take a while)" - ) + self.info("Scanning index of all packages (this may take a while)") self.scan_url(self.index_url) def find_packages(self, requirement): @@ -501,9 +526,7 @@ class PackageIndex(Environment): """ checker is a ContentChecker """ - checker.report( - self.debug, - "Validating %%s checksum for %s" % filename) + checker.report(self.debug, "Validating %%s checksum for %s" % filename) if not checker.is_valid(): tfp.close() os.unlink(filename) @@ -540,7 +563,8 @@ class PackageIndex(Environment): else: # no distros seen for this name, might be misspelled meth, msg = ( self.warn, - "Couldn't find index page for %r (maybe misspelled?)") + "Couldn't find index page for %r (maybe misspelled?)", + ) meth(msg, requirement.unsafe_name) self.scan_all() @@ -579,8 +603,14 @@ class PackageIndex(Environment): return getattr(self.fetch_distribution(spec, tmpdir), 'location', None) def fetch_distribution( # noqa: C901 # is too complex (14) # FIXME - self, requirement, tmpdir, force_scan=False, source=False, - develop_ok=False, local_index=None): + self, + requirement, + tmpdir, + force_scan=False, + source=False, + develop_ok=False, + local_index=None, + ): """Obtain a distribution suitable for fulfilling `requirement` `requirement` must be a ``pkg_resources.Requirement`` instance. @@ -612,15 +642,13 @@ class PackageIndex(Environment): if dist.precedence == DEVELOP_DIST and not develop_ok: if dist not in skipped: self.warn( - "Skipping development or system egg: %s", dist, + "Skipping development or system egg: %s", + dist, ) skipped[dist] = 1 continue - test = ( - dist in req - and (dist.precedence <= SOURCE_DIST or not source) - ) + test = dist in req and (dist.precedence <= SOURCE_DIST or not source) if test: loc = self.download(dist.location, tmpdir) dist.download_location = loc @@ -669,10 +697,15 @@ class PackageIndex(Environment): def gen_setup(self, filename, fragment, tmpdir): match = EGG_FRAGMENT.match(fragment) - dists = match and [ - d for d in - interpret_distro_name(filename, match.group(1), None) if d.version - ] or [] + dists = ( + match + and [ + d + for d in interpret_distro_name(filename, match.group(1), None) + if d.version + ] + or [] + ) if len(dists) == 1: # unambiguous ``#egg`` fragment basename = os.path.basename(filename) @@ -689,8 +722,9 @@ class PackageIndex(Environment): "from setuptools import setup\n" "setup(name=%r, version=%r, py_modules=[%r])\n" % ( - dists[0].project_name, dists[0].version, - os.path.splitext(basename)[0] + dists[0].project_name, + dists[0].version, + os.path.splitext(basename)[0], ) ) return filename @@ -766,23 +800,22 @@ class PackageIndex(Environment): if warning: self.warn(warning, v.reason) else: - raise DistutilsError("Download error for %s: %s" - % (url, v.reason)) from v + raise DistutilsError( + "Download error for %s: %s" % (url, v.reason) + ) from v except http.client.BadStatusLine as v: if warning: self.warn(warning, v.line) else: raise DistutilsError( '%s returned a bad status line. The server might be ' - 'down, %s' % - (url, v.line) + 'down, %s' % (url, v.line) ) from v except (http.client.HTTPException, socket.error) as v: if warning: self.warn(warning, v) else: - raise DistutilsError("Download error for %s: %s" - % (url, v)) from v + raise DistutilsError("Download error for %s: %s" % (url, v)) from v def _download_url(self, scheme, url, tmpdir): # Determine download filename @@ -887,10 +920,13 @@ class PackageIndex(Environment): if rev is not None: self.info("Checking out %s", rev) - os.system("git -C %s checkout --quiet %s" % ( - filename, - rev, - )) + os.system( + "git -C %s checkout --quiet %s" + % ( + filename, + rev, + ) + ) return filename @@ -903,10 +939,13 @@ class PackageIndex(Environment): if rev is not None: self.info("Updating to %s", rev) - os.system("hg --cwd %s up -C -r %s -q" % ( - filename, - rev, - )) + os.system( + "hg --cwd %s up -C -r %s -q" + % ( + filename, + rev, + ) + ) return filename @@ -1010,7 +1049,8 @@ class PyPIConfig(configparser.RawConfigParser): @property def creds_by_repository(self): sections_with_repositories = [ - section for section in self.sections() + section + for section in self.sections() if self.get(section, 'repository').strip() ] @@ -1114,8 +1154,8 @@ def local_open(url): files.append('{name}'.format(name=f)) else: tmpl = ( - "{url}" - "{files}") + "{url}" "{files}" + ) body = tmpl.format(url=url, files='\n'.join(files)) status, message = 200, "OK" else: diff --git a/setuptools/tests/test_bdist_deprecations.py b/setuptools/tests/test_bdist_deprecations.py index 1a900c6..1b69c41 100644 --- a/setuptools/tests/test_bdist_deprecations.py +++ b/setuptools/tests/test_bdist_deprecations.py @@ -1,7 +1,7 @@ """develop tests """ -import mock import sys +from unittest import mock import pytest diff --git a/setuptools/tests/test_build_clib.py b/setuptools/tests/test_build_clib.py index 48bea2b..af9e7c6 100644 --- a/setuptools/tests/test_build_clib.py +++ b/setuptools/tests/test_build_clib.py @@ -1,6 +1,7 @@ +from unittest import mock + import pytest -import mock from distutils.errors import DistutilsSetupError from setuptools.command.build_clib import build_clib from setuptools.dist import Distribution diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py index d102e58..bca8606 100644 --- a/setuptools/tests/test_easy_install.py +++ b/setuptools/tests/test_easy_install.py @@ -12,7 +12,6 @@ import itertools import distutils.errors import io import zipfile -import mock import time import re import subprocess @@ -20,6 +19,7 @@ import pathlib import warnings from collections import namedtuple from pathlib import Path +from unittest import mock import pytest from jaraco import path diff --git a/setuptools/tests/test_packageindex.py b/setuptools/tests/test_packageindex.py index 8e9435e..7b0bf11 100644 --- a/setuptools/tests/test_packageindex.py +++ b/setuptools/tests/test_packageindex.py @@ -5,8 +5,8 @@ import platform import urllib.request import urllib.error import http.client +from unittest import mock -import mock import pytest import setuptools.package_index @@ -21,7 +21,9 @@ class TestPackageIndex: Name (md5) - """.lstrip().format(**locals()) + """.lstrip().format( + **locals() + ) assert setuptools.package_index.PYPI_MD5.match(doc) def test_bad_url_bad_port(self): @@ -38,9 +40,7 @@ class TestPackageIndex: # issue 16 # easy_install inquant.contentmirror.plone breaks because of a typo # in its home URL - index = setuptools.package_index.PackageIndex( - hosts=('www.example.com',) - ) + index = setuptools.package_index.PackageIndex(hosts=('www.example.com',)) url = ( 'url:%20https://svn.plone.org/svn' @@ -54,9 +54,7 @@ class TestPackageIndex: assert isinstance(v, urllib.error.HTTPError) def test_bad_url_bad_status_line(self): - index = setuptools.package_index.PackageIndex( - hosts=('www.example.com',) - ) + index = setuptools.package_index.PackageIndex(hosts=('www.example.com',)) def _urlopen(*args): raise http.client.BadStatusLine('line') @@ -74,9 +72,7 @@ class TestPackageIndex: """ A bad URL with a double scheme should raise a DistutilsError. """ - index = setuptools.package_index.PackageIndex( - hosts=('www.example.com',) - ) + index = setuptools.package_index.PackageIndex(hosts=('www.example.com',)) # issue 20 url = 'http://http://svn.pythonpaste.org/Paste/wphp/trunk' @@ -93,22 +89,17 @@ class TestPackageIndex: raise RuntimeError("Did not raise") def test_bad_url_screwy_href(self): - index = setuptools.package_index.PackageIndex( - hosts=('www.example.com',) - ) + index = setuptools.package_index.PackageIndex(hosts=('www.example.com',)) # issue #160 if sys.version_info[0] == 2 and sys.version_info[1] == 7: # this should not fail url = 'http://example.com' - page = ('') + page = '' index.process_index(url, page) def test_url_ok(self): - index = setuptools.package_index.PackageIndex( - hosts=('www.example.com',) - ) + index = setuptools.package_index.PackageIndex(hosts=('www.example.com',)) url = 'file:///tmp/test_package_index' assert index.url_ok(url, True) @@ -169,9 +160,7 @@ class TestPackageIndex: 'b0', 'rc0', ] - post = [ - '.post0' - ] + post = ['.post0'] dev = [ '.dev0', ] @@ -186,10 +175,14 @@ class TestPackageIndex: for e in epoch for r in releases for p in sum([pre, post, dev], ['']) - for locs in local] + for locs in local + ] for v, vc in versions: - dists = list(setuptools.package_index.distros_for_url( - 'http://example.com/example.zip#egg=example-' + v)) + dists = list( + setuptools.package_index.distros_for_url( + 'http://example.com/example.zip#egg=example-' + v + ) + ) assert dists[0].version == '' assert dists[1].version == vc @@ -204,8 +197,7 @@ class TestPackageIndex: expected_dir = str(tmpdir / 'project@master') expected = ( - 'git clone --quiet ' - 'https://github.example/group/project {expected_dir}' + 'git clone --quiet ' 'https://github.example/group/project {expected_dir}' ).format(**locals()) first_call_args = os_system_mock.call_args_list[0][0] assert first_call_args == (expected,) @@ -226,8 +218,7 @@ class TestPackageIndex: expected_dir = str(tmpdir / 'project') expected = ( - 'git clone --quiet ' - 'https://github.example/group/project {expected_dir}' + 'git clone --quiet ' 'https://github.example/group/project {expected_dir}' ).format(**locals()) os_system_mock.assert_called_once_with(expected) @@ -243,8 +234,7 @@ class TestPackageIndex: expected_dir = str(tmpdir / 'project') expected = ( - 'svn checkout -q ' - 'svn+https://svn.example/project {expected_dir}' + 'svn checkout -q ' 'svn+https://svn.example/project {expected_dir}' ).format(**locals()) os_system_mock.assert_called_once_with(expected) @@ -252,7 +242,8 @@ class TestPackageIndex: class TestContentCheckers: def test_md5(self): checker = setuptools.package_index.HashChecker.from_url( - 'http://foo/bar#md5=f12895fdffbd45007040d2e44df98478') + 'http://foo/bar#md5=f12895fdffbd45007040d2e44df98478' + ) checker.feed('You should probably not be using MD5'.encode('ascii')) assert checker.hash.hexdigest() == 'f12895fdffbd45007040d2e44df98478' assert checker.is_valid() @@ -260,25 +251,27 @@ class TestContentCheckers: def test_other_fragment(self): "Content checks should succeed silently if no hash is present" checker = setuptools.package_index.HashChecker.from_url( - 'http://foo/bar#something%20completely%20different') + 'http://foo/bar#something%20completely%20different' + ) checker.feed('anything'.encode('ascii')) assert checker.is_valid() def test_blank_md5(self): "Content checks should succeed if a hash is empty" - checker = setuptools.package_index.HashChecker.from_url( - 'http://foo/bar#md5=') + checker = setuptools.package_index.HashChecker.from_url('http://foo/bar#md5=') checker.feed('anything'.encode('ascii')) assert checker.is_valid() def test_get_hash_name_md5(self): checker = setuptools.package_index.HashChecker.from_url( - 'http://foo/bar#md5=f12895fdffbd45007040d2e44df98478') + 'http://foo/bar#md5=f12895fdffbd45007040d2e44df98478' + ) assert checker.hash_name == 'md5' def test_report(self): checker = setuptools.package_index.HashChecker.from_url( - 'http://foo/bar#md5=f12895fdffbd45007040d2e44df98478') + 'http://foo/bar#md5=f12895fdffbd45007040d2e44df98478' + ) rep = checker.report(lambda x: x, 'My message about %s') assert rep == 'My message about md5' @@ -287,8 +280,8 @@ class TestContentCheckers: def temp_home(tmpdir, monkeypatch): key = ( 'USERPROFILE' - if platform.system() == 'Windows' and sys.version_info > (3, 8) else - 'HOME' + if platform.system() == 'Windows' and sys.version_info > (3, 8) + else 'HOME' ) monkeypatch.setitem(os.environ, key, str(tmpdir)) @@ -298,13 +291,25 @@ def temp_home(tmpdir, monkeypatch): class TestPyPIConfig: def test_percent_in_password(self, temp_home): pypirc = temp_home / '.pypirc' - pypirc.write(DALS(""" + pypirc.write( + DALS( + """ [pypi] repository=https://pypi.org username=jaraco password=pity% - """)) + """ + ) + ) cfg = setuptools.package_index.PyPIConfig() cred = cfg.creds_by_repository['https://pypi.org'] assert cred.username == 'jaraco' assert cred.password == 'pity%' + + +@pytest.mark.timeout(1) +def test_REL_DoS(): + """ + REL should not hang on a contrived attack string. + """ + setuptools.package_index.REL.search('< rel=' + ' ' * 2**12) diff --git a/setuptools/tests/test_register.py b/setuptools/tests/test_register.py index 9860580..ed85e9b 100644 --- a/setuptools/tests/test_register.py +++ b/setuptools/tests/test_register.py @@ -2,10 +2,7 @@ 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 +from unittest import mock import pytest diff --git a/setuptools/tests/test_upload.py b/setuptools/tests/test_upload.py index 7586cb2..4ed59bc 100644 --- a/setuptools/tests/test_upload.py +++ b/setuptools/tests/test_upload.py @@ -2,10 +2,7 @@ from setuptools.command.upload import upload from setuptools.dist import Distribution from setuptools.errors import RemovedCommandError -try: - from unittest import mock -except ImportError: - import mock +from unittest import mock import pytest -- 2.34.1