From 6c0ed90ac52aed0fdc5bb85d621146a8539e43cb Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 3 Aug 2022 20:37:17 -0400 Subject: [PATCH 01/89] Convert test_cmd to pytest --- distutils/tests/test_cmd.py | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/distutils/tests/test_cmd.py b/distutils/tests/test_cmd.py index f5104e1db7..e4d5bf3c01 100644 --- a/distutils/tests/test_cmd.py +++ b/distutils/tests/test_cmd.py @@ -1,5 +1,4 @@ """Tests for distutils.cmd.""" -import unittest import os from test.support import captured_stdout @@ -15,14 +14,13 @@ def initialize_options(self): pass -class TestCommand(unittest.TestCase): - def setUp(self): - dist = Distribution() - self.cmd = MyCmd(dist) +@pytest.fixture +def cmd(request): + return MyCmd(Distribution()) - def test_ensure_string_list(self): - cmd = self.cmd +class TestCommand: + def test_ensure_string_list(self, cmd): cmd.not_string_list = ['one', 2, 'three'] cmd.yes_string_list = ['one', 'two', 'three'] cmd.not_string_list2 = object() @@ -47,10 +45,7 @@ def test_ensure_string_list(self): with pytest.raises(DistutilsOptionError): cmd.ensure_string_list('option3') - def test_make_file(self): - - cmd = self.cmd - + def test_make_file(self, cmd): # making sure it raises when infiles is not a string or a list/tuple with pytest.raises(TypeError): cmd.make_file(infiles=1, outfile='', func='func', args=()) @@ -63,14 +58,13 @@ def _execute(func, args, exec_msg, level): cmd.execute = _execute cmd.make_file(infiles='in', outfile='out', func='func', args=()) - def test_dump_options(self): + def test_dump_options(self, cmd): msgs = [] def _announce(msg, level): msgs.append(msg) - cmd = self.cmd cmd.announce = _announce cmd.option1 = 1 cmd.option2 = 1 @@ -80,8 +74,7 @@ def _announce(msg, level): wanted = ["command options for 'MyCmd':", ' option1 = 1', ' option2 = 1'] assert msgs == wanted - def test_ensure_string(self): - cmd = self.cmd + def test_ensure_string(self, cmd): cmd.option1 = 'ok' cmd.ensure_string('option1') @@ -93,24 +86,21 @@ def test_ensure_string(self): with pytest.raises(DistutilsOptionError): cmd.ensure_string('option3') - def test_ensure_filename(self): - cmd = self.cmd + def test_ensure_filename(self, cmd): cmd.option1 = __file__ cmd.ensure_filename('option1') cmd.option2 = 'xxx' with pytest.raises(DistutilsOptionError): cmd.ensure_filename('option2') - def test_ensure_dirname(self): - cmd = self.cmd + def test_ensure_dirname(self, cmd): cmd.option1 = os.path.dirname(__file__) or os.curdir cmd.ensure_dirname('option1') cmd.option2 = 'xxx' with pytest.raises(DistutilsOptionError): cmd.ensure_dirname('option2') - def test_debug_print(self): - cmd = self.cmd + def test_debug_print(self, cmd): with captured_stdout() as stdout: cmd.debug_print('xxx') stdout.seek(0) From c24974b9bc41c13b85233abe36ec8bdddad6517a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 3 Aug 2022 20:51:53 -0400 Subject: [PATCH 02/89] Convert BasePyPIRCCommandTestCase to pytest --- conftest.py | 32 ++++++++++++++++++++++++++++++++ distutils/tests/test_config.py | 34 +++------------------------------- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/conftest.py b/conftest.py index 90efd3230f..0938c1e913 100644 --- a/conftest.py +++ b/conftest.py @@ -89,3 +89,35 @@ def save_cwd(): yield finally: os.chdir(orig) + + +@pytest.fixture +def threshold_warn(): + from distutils.log import set_threshold, WARN + orig = set_threshold(WARN) + yield + set_threshold(orig) + + +@pytest.fixture +def pypirc(request, save_env): + from distutils.core import PyPIRCCommand + from distutils.core import Distribution + + self = request.instance + self.tmp_dir = self.mkdtemp() + os.environ['HOME'] = self.tmp_dir + os.environ['USERPROFILE'] = self.tmp_dir + self.rc = os.path.join(self.tmp_dir, '.pypirc') + self.dist = Distribution() + + class command(PyPIRCCommand): + def __init__(self, dist): + super().__init__(dist) + + def initialize_options(self): + pass + + finalize_options = initialize_options + + self._cmd = command diff --git a/distutils/tests/test_config.py b/distutils/tests/test_config.py index b088d6007f..8fbfef4830 100644 --- a/distutils/tests/test_config.py +++ b/distutils/tests/test_config.py @@ -4,11 +4,6 @@ import pytest -from distutils.core import PyPIRCCommand -from distutils.core import Distribution -from distutils.log import set_threshold -from distutils.log import WARN - from distutils.tests import support PYPIRC = """\ @@ -52,37 +47,14 @@ @support.combine_markers -@pytest.mark.usefixtures('save_env') +@pytest.mark.usefixtures('threshold_warn') +@pytest.mark.usefixtures('pypirc') class BasePyPIRCCommandTestCase( support.TempdirManager, support.LoggingSilencer, unittest.TestCase, ): - def setUp(self): - """Patches the environment.""" - super().setUp() - self.tmp_dir = self.mkdtemp() - os.environ['HOME'] = self.tmp_dir - os.environ['USERPROFILE'] = self.tmp_dir - self.rc = os.path.join(self.tmp_dir, '.pypirc') - self.dist = Distribution() - - class command(PyPIRCCommand): - def __init__(self, dist): - super().__init__(dist) - - def initialize_options(self): - pass - - finalize_options = initialize_options - - self._cmd = command - self.old_threshold = set_threshold(WARN) - - def tearDown(self): - """Removes the patch.""" - set_threshold(self.old_threshold) - super().tearDown() + pass class PyPIRCCommandTestCase(BasePyPIRCCommandTestCase): From ca9ed4949d73d7972aec4af5290ff0469976bf07 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 3 Aug 2022 20:59:21 -0400 Subject: [PATCH 03/89] Convert RegisterTestCase to pytest --- distutils/tests/test_register.py | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/distutils/tests/test_register.py b/distutils/tests/test_register.py index 7657f3914f..dd59ecd316 100644 --- a/distutils/tests/test_register.py +++ b/distutils/tests/test_register.py @@ -78,26 +78,20 @@ def getheader(self, name, default=None): }.get(name.lower(), default) -class RegisterTestCase(BasePyPIRCCommandTestCase): - def setUp(self): - super().setUp() - # patching the password prompt - self._old_getpass = getpass.getpass - - def _getpass(prompt): - return 'password' - - getpass.getpass = _getpass - urllib.request._opener = None - self.old_opener = urllib.request.build_opener - self.conn = urllib.request.build_opener = FakeOpener() - - def tearDown(self): - getpass.getpass = self._old_getpass - urllib.request._opener = None - urllib.request.build_opener = self.old_opener - super().tearDown() +@pytest.fixture(autouse=True) +def autopass(monkeypatch): + monkeypatch.setattr(getpass, 'getpass', lambda prompt: 'password') + +@pytest.fixture(autouse=True) +def fake_opener(monkeypatch, request): + opener = FakeOpener() + monkeypatch.setattr(urllib.request, 'build_opener', opener) + monkeypatch.setattr(urllib.request, '_opener', None) + request.instance.conn = opener + + +class TestRegister(BasePyPIRCCommandTestCase): def _get_cmd(self, metadata=None): if metadata is None: metadata = { From 885a877d3b0fc6bb0eb489ca0407a765f918fa00 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 3 Aug 2022 21:00:53 -0400 Subject: [PATCH 04/89] =?UTF-8?q?=E2=9A=AB=20Fade=20to=20black.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/conftest.py b/conftest.py index 0938c1e913..d970d7217d 100644 --- a/conftest.py +++ b/conftest.py @@ -94,6 +94,7 @@ def save_cwd(): @pytest.fixture def threshold_warn(): from distutils.log import set_threshold, WARN + orig = set_threshold(WARN) yield set_threshold(orig) From 461590cb6c7927814e6c2726ba8cf9d22f5a4dbf Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 3 Aug 2022 21:08:03 -0400 Subject: [PATCH 05/89] Use jaraco.path to generate a tree. --- distutils/tests/test_sdist.py | 14 ++++++++------ tox.ini | 1 + 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/distutils/tests/test_sdist.py b/distutils/tests/test_sdist.py index 24ec9eb608..702d51916f 100644 --- a/distutils/tests/test_sdist.py +++ b/distutils/tests/test_sdist.py @@ -10,6 +10,7 @@ from .unix_compat import require_unix_id, require_uid_0, pwd, grp import pytest +from jaraco import path from .py38compat import check_warnings @@ -52,12 +53,13 @@ def setUp(self): super().setUp() # setting up an environment self.old_path = os.getcwd() - os.mkdir(join(self.tmp_dir, 'somecode')) - os.mkdir(join(self.tmp_dir, 'dist')) - # a package, and a README - self.write_file((self.tmp_dir, 'README'), 'xxx') - self.write_file((self.tmp_dir, 'somecode', '__init__.py'), '#') - self.write_file((self.tmp_dir, 'setup.py'), SETUP_PY) + path.build({ + 'somecode': { + '__init__.py': '#', + }, + 'README': 'xxx', + 'setup.py': SETUP_PY, + }, self.tmp_dir) os.chdir(self.tmp_dir) def tearDown(self): diff --git a/tox.ini b/tox.ini index ef3efecf52..738b5f0e81 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ minversion = 3.25 deps = pytest jaraco.envs>=2.4 + jaraco.path commands = pytest {posargs} setenv = From 12fe2acb178887b008f76c2fd3920c45a232588b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 3 Aug 2022 21:27:58 -0400 Subject: [PATCH 06/89] Port sdist tests to pytest --- conftest.py | 2 +- distutils/tests/test_sdist.py | 35 +++++++++++++++-------------------- tox.ini | 1 + 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/conftest.py b/conftest.py index d970d7217d..85d350d48d 100644 --- a/conftest.py +++ b/conftest.py @@ -101,7 +101,7 @@ def threshold_warn(): @pytest.fixture -def pypirc(request, save_env): +def pypirc(request, save_env, distutils_managed_tempdir): from distutils.core import PyPIRCCommand from distutils.core import Distribution diff --git a/distutils/tests/test_sdist.py b/distutils/tests/test_sdist.py index 702d51916f..ab14ab06ca 100644 --- a/distutils/tests/test_sdist.py +++ b/distutils/tests/test_sdist.py @@ -10,7 +10,8 @@ from .unix_compat import require_unix_id, require_uid_0, pwd, grp import pytest -from jaraco import path +import path +import jaraco.path from .py38compat import check_warnings @@ -46,27 +47,21 @@ """ -class SDistTestCase(BasePyPIRCCommandTestCase): - def setUp(self): - # PyPIRCCommandTestCase creates a temp dir already - # and put it in self.tmp_dir - super().setUp() - # setting up an environment - self.old_path = os.getcwd() - path.build({ - 'somecode': { - '__init__.py': '#', - }, - 'README': 'xxx', - 'setup.py': SETUP_PY, - }, self.tmp_dir) - os.chdir(self.tmp_dir) +@pytest.fixture(autouse=True) +def project_dir(request, pypirc): + self = request.instance + jaraco.path.build({ + 'somecode': { + '__init__.py': '#', + }, + 'README': 'xxx', + 'setup.py': SETUP_PY, + }, self.tmp_dir) + with path.Path(self.tmp_dir): + yield - def tearDown(self): - # back to normal - os.chdir(self.old_path) - super().tearDown() +class TestSDist(BasePyPIRCCommandTestCase): def get_cmd(self, metadata=None): """Returns a cmd""" if metadata is None: diff --git a/tox.ini b/tox.ini index 738b5f0e81..0fe47fff41 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,7 @@ deps = pytest jaraco.envs>=2.4 jaraco.path + path commands = pytest {posargs} setenv = From 0802d588481526b875c7e77f4da587c4bbde72fe Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 3 Aug 2022 21:44:57 -0400 Subject: [PATCH 07/89] Convert TestUpload to pytest --- distutils/tests/test_upload.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/distutils/tests/test_upload.py b/distutils/tests/test_upload.py index e5c6649694..48bd6753a7 100644 --- a/distutils/tests/test_upload.py +++ b/distutils/tests/test_upload.py @@ -65,19 +65,14 @@ def getcode(self): return self.code -class uploadTestCase(BasePyPIRCCommandTestCase): - def setUp(self): - super().setUp() - self.old_open = upload_mod.urlopen - upload_mod.urlopen = self._urlopen - self.last_open = None - self.next_msg = None - self.next_code = None - - def tearDown(self): - upload_mod.urlopen = self.old_open - super().tearDown() +@pytest.fixture(autouse=True) +def urlopen(request, monkeypatch): + self = request.instance + monkeypatch.setattr(upload_mod, 'urlopen', self._urlopen) + self.next_msg = self.next_code = None + +class TestUpload(BasePyPIRCCommandTestCase): def _urlopen(self, url): self.last_open = FakeOpen(url, msg=self.next_msg, code=self.next_code) return self.last_open From 79681e2f5ced3991fd6eddd078864f6941cabcb0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 3 Aug 2022 22:08:53 -0400 Subject: [PATCH 08/89] Convert TestUpload to parametrized test. Remove dependence on unittest for PyPIRC tests. --- distutils/tests/test_build_ext.py | 1 - distutils/tests/test_config.py | 2 -- distutils/tests/test_upload.py | 49 ++++++++++++++++--------------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index 16d4873886..6379510892 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -12,7 +12,6 @@ LoggingSilencer, copy_xxmodule_c, fixup_build_ext, - combine_markers, ) from distutils.extension import Extension from distutils.errors import ( diff --git a/distutils/tests/test_config.py b/distutils/tests/test_config.py index 8fbfef4830..43ba6766ae 100644 --- a/distutils/tests/test_config.py +++ b/distutils/tests/test_config.py @@ -1,6 +1,5 @@ """Tests for distutils.pypirc.pypirc.""" import os -import unittest import pytest @@ -52,7 +51,6 @@ class BasePyPIRCCommandTestCase( support.TempdirManager, support.LoggingSilencer, - unittest.TestCase, ): pass diff --git a/distutils/tests/test_upload.py b/distutils/tests/test_upload.py index 48bd6753a7..a9355ed9c6 100644 --- a/distutils/tests/test_upload.py +++ b/distutils/tests/test_upload.py @@ -1,7 +1,7 @@ """Tests for distutils.command.upload.""" import os import unittest.mock as mock -from urllib.request import HTTPError +from urllib.request import HTTPError # noqa from distutils.command import upload as upload_mod @@ -184,7 +184,19 @@ def test_upload_fails(self): with pytest.raises(DistutilsError): self.test_upload() - def test_wrong_exception_order(self): + @pytest.mark.parametrize( + 'exception,expected,raised_exception', + [ + ("OSError('oserror')", 'oserror', OSError), + ( + "HTTPError('url', 400, 'httperror', {}, None)", + 'Upload failed (400): httperror', + DistutilsError, + ), + ] + ) + def test_wrong_exception_order(self, exception, expected, raised_exception): + exception = eval(exception) tmp = self.mkdtemp() path = os.path.join(tmp, 'xxx') self.write_file(path) @@ -192,24 +204,15 @@ def test_wrong_exception_order(self): self.write_file(self.rc, PYPIRC_LONG_PASSWORD) pkg_dir, dist = self.create_dist(dist_files=dist_files) - tests = [ - (OSError('oserror'), 'oserror', OSError), - ( - HTTPError('url', 400, 'httperror', {}, None), - 'Upload failed (400): httperror', - DistutilsError, - ), - ] - for exception, expected, raised_exception in tests: - with self.subTest(exception=type(exception).__name__): - with mock.patch( - 'distutils.command.upload.urlopen', - new=mock.Mock(side_effect=exception), - ): - with pytest.raises(raised_exception): - cmd = upload(dist) - cmd.ensure_finalized() - cmd.run() - results = self.get_logs(ERROR) - assert expected in results[-1] - self.clear_logs() + + with mock.patch( + 'distutils.command.upload.urlopen', + new=mock.Mock(side_effect=exception), + ): + with pytest.raises(raised_exception): + cmd = upload(dist) + cmd.ensure_finalized() + cmd.run() + results = self.get_logs(ERROR) + assert expected in results[-1] + self.clear_logs() From 4fada1c3e8c9896b99e44af8b0f4d46c466b68fd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 4 Aug 2022 16:44:47 -0400 Subject: [PATCH 09/89] Implement HTTP 400 error as a pytest.param to avoid collection error. Ref pytest-dev/pytest#10184. --- distutils/tests/test_upload.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/distutils/tests/test_upload.py b/distutils/tests/test_upload.py index a9355ed9c6..3a91395b3f 100644 --- a/distutils/tests/test_upload.py +++ b/distutils/tests/test_upload.py @@ -1,7 +1,7 @@ """Tests for distutils.command.upload.""" import os import unittest.mock as mock -from urllib.request import HTTPError # noqa +from urllib.request import HTTPError from distutils.command import upload as upload_mod @@ -187,16 +187,16 @@ def test_upload_fails(self): @pytest.mark.parametrize( 'exception,expected,raised_exception', [ - ("OSError('oserror')", 'oserror', OSError), - ( - "HTTPError('url', 400, 'httperror', {}, None)", + (OSError('oserror'), 'oserror', OSError), + pytest.param( + HTTPError('url', 400, 'httperror', {}, None), 'Upload failed (400): httperror', DistutilsError, + id="HTTP 400", ), ] ) def test_wrong_exception_order(self, exception, expected, raised_exception): - exception = eval(exception) tmp = self.mkdtemp() path = os.path.join(tmp, 'xxx') self.write_file(path) From 51d3ec15640f0388950fde472143cae474e8a0e5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 4 Aug 2022 16:56:55 -0400 Subject: [PATCH 10/89] Convert core tests to pytest --- conftest.py | 13 +++++++++++++ distutils/tests/test_core.py | 29 +++++++---------------------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/conftest.py b/conftest.py index 85d350d48d..14afaea1bc 100644 --- a/conftest.py +++ b/conftest.py @@ -1,6 +1,7 @@ import os import sys import platform +import shutil import pytest @@ -122,3 +123,15 @@ def initialize_options(self): finalize_options = initialize_options self._cmd = command + + +@pytest.fixture +def cleanup_testfn(): + from distutils.tests import py38compat as os_helper + + yield + path = os_helper.TESTFN + if os.path.isfile(path): + os.remove(path) + elif os.path.isdir(path): + shutil.rmtree(path) diff --git a/distutils/tests/test_core.py b/distutils/tests/test_core.py index ef085a8530..86b0040f60 100644 --- a/distutils/tests/test_core.py +++ b/distutils/tests/test_core.py @@ -3,15 +3,12 @@ import io import distutils.core import os -import shutil import sys from test.support import captured_stdout import pytest from . import py38compat as os_helper -import unittest -from distutils import log from distutils.dist import Distribution # setup script that uses __file__ @@ -59,27 +56,15 @@ def main(): """ +@pytest.fixture(autouse=True) +def save_stdout(monkeypatch): + monkeypatch.setattr(sys, 'stdout', sys.stdout) + + @pytest.mark.usefixtures('save_env') @pytest.mark.usefixtures('save_argv') -class CoreTestCase(unittest.TestCase): - def setUp(self): - super().setUp() - self.old_stdout = sys.stdout - self.cleanup_testfn() - self.addCleanup(log.set_threshold, log._global_log.threshold) - - def tearDown(self): - sys.stdout = self.old_stdout - self.cleanup_testfn() - super().tearDown() - - def cleanup_testfn(self): - path = os_helper.TESTFN - if os.path.isfile(path): - os.remove(path) - elif os.path.isdir(path): - shutil.rmtree(path) - +@pytest.mark.usefixtures('cleanup_testfn') +class TestCore: def write_setup(self, text, path=os_helper.TESTFN): f = open(path, "w") try: From e46bbf910ea8560646f20e19338c4ef25d6d88a1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 4 Aug 2022 16:59:45 -0400 Subject: [PATCH 11/89] =?UTF-8?q?=E2=9A=AB=20Fade=20to=20black.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- distutils/tests/test_sdist.py | 15 +++++++++------ distutils/tests/test_upload.py | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/distutils/tests/test_sdist.py b/distutils/tests/test_sdist.py index ab14ab06ca..fa4dfa24eb 100644 --- a/distutils/tests/test_sdist.py +++ b/distutils/tests/test_sdist.py @@ -50,13 +50,16 @@ @pytest.fixture(autouse=True) def project_dir(request, pypirc): self = request.instance - jaraco.path.build({ - 'somecode': { - '__init__.py': '#', + jaraco.path.build( + { + 'somecode': { + '__init__.py': '#', + }, + 'README': 'xxx', + 'setup.py': SETUP_PY, }, - 'README': 'xxx', - 'setup.py': SETUP_PY, - }, self.tmp_dir) + self.tmp_dir, + ) with path.Path(self.tmp_dir): yield diff --git a/distutils/tests/test_upload.py b/distutils/tests/test_upload.py index 3a91395b3f..fb905b641a 100644 --- a/distutils/tests/test_upload.py +++ b/distutils/tests/test_upload.py @@ -194,7 +194,7 @@ def test_upload_fails(self): DistutilsError, id="HTTP 400", ), - ] + ], ) def test_wrong_exception_order(self, exception, expected, raised_exception): tmp = self.mkdtemp() From 1f3e6af3575c28a4c01f348370b624467a596c5c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 4 Aug 2022 17:00:02 -0400 Subject: [PATCH 12/89] Convert TestVersion to pytest. --- distutils/tests/test_version.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/distutils/tests/test_version.py b/distutils/tests/test_version.py index 8115faea3b..ff52ea4683 100644 --- a/distutils/tests/test_version.py +++ b/distutils/tests/test_version.py @@ -1,18 +1,18 @@ """Tests for distutils.version.""" -import unittest +import pytest + import distutils from distutils.version import LooseVersion from distutils.version import StrictVersion -class VersionTestCase(unittest.TestCase): - def setUp(self): - self.ctx = distutils.version.suppress_known_deprecation() - self.ctx.__enter__() +@pytest.fixture(autouse=True) +def suppress_deprecation(): + with distutils.version.suppress_known_deprecation(): + yield - def tearDown(self): - self.ctx.__exit__(None, None, None) +class TestVersion: def test_prerelease(self): version = StrictVersion('1.2.3a1') assert version.version == (1, 2, 3) From 5f5addf958075daa5a0ad97205c8f1d7735a3e0d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 5 Aug 2022 21:58:11 -0400 Subject: [PATCH 13/89] Convert TestConfig to pytest --- distutils/tests/test_config_cmd.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/distutils/tests/test_config_cmd.py b/distutils/tests/test_config_cmd.py index 425cc1ba48..24e9e0d536 100644 --- a/distutils/tests/test_config_cmd.py +++ b/distutils/tests/test_config_cmd.py @@ -4,29 +4,28 @@ import sys from test.support import missing_compiler_executable +import pytest + from distutils.command.config import dump_file, config from distutils.tests import support from distutils import log +@pytest.fixture(autouse=True) +def info_log(request, monkeypatch): + self = request.instance + self._logs = [] + monkeypatch.setattr(log, 'info', self._info) + + @support.combine_markers -class ConfigTestCase( - support.LoggingSilencer, support.TempdirManager, unittest.TestCase +class TestConfig( + support.LoggingSilencer, support.TempdirManager ): def _info(self, msg, *args): for line in msg.splitlines(): self._logs.append(line) - def setUp(self): - super().setUp() - self._logs = [] - self.old_log = log.info - log.info = self._info - - def tearDown(self): - log.info = self.old_log - super().tearDown() - def test_dump_file(self): this_file = os.path.splitext(__file__)[0] + '.py' f = open(this_file) From 0ebb6f0fa3af4aa70d5efb7948e8a010acc0d834 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 5 Aug 2022 22:03:08 -0400 Subject: [PATCH 14/89] Convert TestCygwinCCompiler to pytest --- distutils/tests/test_cygwinccompiler.py | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/distutils/tests/test_cygwinccompiler.py b/distutils/tests/test_cygwinccompiler.py index b14ddb40c3..1825bb202f 100644 --- a/distutils/tests/test_cygwinccompiler.py +++ b/distutils/tests/test_cygwinccompiler.py @@ -3,6 +3,8 @@ import sys import os +import pytest + from distutils.cygwinccompiler import ( check_config_h, CONFIG_H_OK, @@ -11,26 +13,17 @@ get_msvcr, ) from distutils.tests import support -import pytest - - -class CygwinCCompilerTestCase(support.TempdirManager, unittest.TestCase): - def setUp(self): - super().setUp() - self.version = sys.version - self.python_h = os.path.join(self.mkdtemp(), 'python.h') - from distutils import sysconfig +from distutils import sysconfig - self.old_get_config_h_filename = sysconfig.get_config_h_filename - sysconfig.get_config_h_filename = self._get_config_h_filename - def tearDown(self): - sys.version = self.version - from distutils import sysconfig +@pytest.fixture(autouse=True) +def stuff(request, monkeypatch, distutils_managed_tempdir): + self = request.instance + self.python_h = os.path.join(self.mkdtemp(), 'python.h') + monkeypatch.setattr(sysconfig, 'get_config_h_filename', self._get_config_h_filename) - sysconfig.get_config_h_filename = self.old_get_config_h_filename - super().tearDown() +class TestCygwinCCompiler(support.TempdirManager): def _get_config_h_filename(self): return self.python_h From 6b2f75a17ad88438a6afdc1baf17a283f7b24258 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 5 Aug 2022 22:05:48 -0400 Subject: [PATCH 15/89] Convert TestDirUtil to pytest --- distutils/tests/test_dir_util.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/distutils/tests/test_dir_util.py b/distutils/tests/test_dir_util.py index fc32c7fe74..715fdb96ba 100644 --- a/distutils/tests/test_dir_util.py +++ b/distutils/tests/test_dir_util.py @@ -19,27 +19,24 @@ import pytest -class DirUtilTestCase(support.TempdirManager, unittest.TestCase): +@pytest.fixture(autouse=True) +def stuff(request, monkeypatch, distutils_managed_tempdir): + self = request.instance + self._logs = [] + tmp_dir = self.mkdtemp() + self.root_target = os.path.join(tmp_dir, 'deep') + self.target = os.path.join(self.root_target, 'here') + self.target2 = os.path.join(tmp_dir, 'deep2') + monkeypatch.setattr(log, 'info', self._log) + + +class TestDirUtil(support.TempdirManager): def _log(self, msg, *args): if len(args) > 0: self._logs.append(msg % args) else: self._logs.append(msg) - def setUp(self): - super().setUp() - self._logs = [] - tmp_dir = self.mkdtemp() - self.root_target = os.path.join(tmp_dir, 'deep') - self.target = os.path.join(self.root_target, 'here') - self.target2 = os.path.join(tmp_dir, 'deep2') - self.old_log = log.info - log.info = self._log - - def tearDown(self): - log.info = self.old_log - super().tearDown() - def test_mkpath_remove_tree_verbosity(self): mkpath(self.target, verbose=0) From 65fe02443d8cecd405d9d2b376c7895938ebac23 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 5 Aug 2022 22:07:32 -0400 Subject: [PATCH 16/89] Convert TestFileUtil to pytest --- distutils/tests/test_file_util.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/distutils/tests/test_file_util.py b/distutils/tests/test_file_util.py index e95535df05..00b4aa6484 100644 --- a/distutils/tests/test_file_util.py +++ b/distutils/tests/test_file_util.py @@ -12,27 +12,24 @@ import pytest -class FileUtilTestCase(support.TempdirManager, unittest.TestCase): +@pytest.fixture(autouse=True) +def stuff(request, monkeypatch, distutils_managed_tempdir): + self = request.instance + self._logs = [] + tmp_dir = self.mkdtemp() + self.source = os.path.join(tmp_dir, 'f1') + self.target = os.path.join(tmp_dir, 'f2') + self.target_dir = os.path.join(tmp_dir, 'd1') + monkeypatch.setattr(log, 'info', self._log) + + +class TestFileUtil(support.TempdirManager): def _log(self, msg, *args): if len(args) > 0: self._logs.append(msg % args) else: self._logs.append(msg) - def setUp(self): - super().setUp() - self._logs = [] - self.old_log = log.info - log.info = self._log - tmp_dir = self.mkdtemp() - self.source = os.path.join(tmp_dir, 'f1') - self.target = os.path.join(tmp_dir, 'f2') - self.target_dir = os.path.join(tmp_dir, 'd1') - - def tearDown(self): - log.info = self.old_log - super().tearDown() - def test_move_file_verbosity(self): f = open(self.source, 'w') try: From 26a3295031cc80cd6d9a6fb1f5c86ca290af3667 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 5 Aug 2022 22:10:01 -0400 Subject: [PATCH 17/89] Convert TestSysconfig to pytest --- distutils/tests/test_sysconfig.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py index 39e81f1778..3746676289 100644 --- a/distutils/tests/test_sysconfig.py +++ b/distutils/tests/test_sysconfig.py @@ -20,17 +20,8 @@ @pytest.mark.usefixtures('save_env') -class SysconfigTestCase(unittest.TestCase): - def setUp(self): - super().setUp() - self.makefile = None - - def tearDown(self): - if self.makefile is not None: - os.unlink(self.makefile) - self.cleanup_testfn() - super().tearDown() - +@pytest.mark.usefixtures('cleanup_testfn') +class TestSysconfig: def cleanup_testfn(self): if os.path.isfile(TESTFN): os.remove(TESTFN) @@ -237,7 +228,7 @@ def test_sysconfig_compiler_vars(self): import sysconfig as global_sysconfig if sysconfig.get_config_var('CUSTOMIZED_OSX_COMPILER'): - self.skipTest('compiler flags customized') + pytest.skip('compiler flags customized') assert global_sysconfig.get_config_var('LDSHARED') == sysconfig.get_config_var( 'LDSHARED' ) From 2bfa85206ca0f27a9dc611a2883cce3f7094a5d1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 5 Aug 2022 22:10:10 -0400 Subject: [PATCH 18/89] =?UTF-8?q?=E2=9A=AB=20Fade=20to=20black.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- distutils/tests/test_config_cmd.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/distutils/tests/test_config_cmd.py b/distutils/tests/test_config_cmd.py index 24e9e0d536..14e6e03252 100644 --- a/distutils/tests/test_config_cmd.py +++ b/distutils/tests/test_config_cmd.py @@ -19,9 +19,7 @@ def info_log(request, monkeypatch): @support.combine_markers -class TestConfig( - support.LoggingSilencer, support.TempdirManager -): +class TestConfig(support.LoggingSilencer, support.TempdirManager): def _info(self, msg, *args): for line in msg.splitlines(): self._logs.append(line) From 538c72877bbcb82e7799cb5ed50c3452a53e7b7f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 5 Aug 2022 22:10:27 -0400 Subject: [PATCH 19/89] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins=20?= =?UTF-8?q?(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- distutils/tests/test_file_util.py | 1 - 1 file changed, 1 deletion(-) diff --git a/distutils/tests/test_file_util.py b/distutils/tests/test_file_util.py index 00b4aa6484..3ad4bdd103 100644 --- a/distutils/tests/test_file_util.py +++ b/distutils/tests/test_file_util.py @@ -1,5 +1,4 @@ """Tests for distutils.file_util.""" -import unittest import os import errno from unittest.mock import patch From 0bddfa3e081be38515199e43b59f5584dca0aaeb Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 5 Aug 2022 22:15:52 -0400 Subject: [PATCH 20/89] Remove patching of uname. --- distutils/tests/test_util.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/distutils/tests/test_util.py b/distutils/tests/test_util.py index ac0feead55..5625dd4afd 100644 --- a/distutils/tests/test_util.py +++ b/distutils/tests/test_util.py @@ -39,16 +39,6 @@ def setUp(self): self.splitdrive = os.path.splitdrive self._config_vars = copy(sysconfig._config_vars) - # patching os.uname - if hasattr(os, 'uname'): - self.uname = os.uname - self._uname = os.uname() - else: - self.uname = None - self._uname = None - - os.uname = self._get_uname - def tearDown(self): # getting back the environment os.name = self.name @@ -58,19 +48,9 @@ def tearDown(self): os.path.join = self.join os.path.isabs = self.isabs os.path.splitdrive = self.splitdrive - if self.uname is not None: - os.uname = self.uname - else: - del os.uname sysconfig._config_vars = copy(self._config_vars) super().tearDown() - def _set_uname(self, uname): - self._uname = uname - - def _get_uname(self): - return self._uname - def test_get_host_platform(self): with unittest.mock.patch('os.name', 'nt'): with unittest.mock.patch('sys.version', '... [... (ARM64)]'): From c146a9dba5f934c5700f15cd31e631f9e1addbcb Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 5 Aug 2022 22:20:26 -0400 Subject: [PATCH 21/89] Convert TestUtil to pytest --- distutils/tests/test_util.py | 38 ++++++++++++------------------------ 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/distutils/tests/test_util.py b/distutils/tests/test_util.py index 5625dd4afd..06c835e280 100644 --- a/distutils/tests/test_util.py +++ b/distutils/tests/test_util.py @@ -25,32 +25,20 @@ from distutils.errors import DistutilsPlatformError, DistutilsByteCompileError -@pytest.mark.usefixtures('save_env') -class UtilTestCase(unittest.TestCase): - def setUp(self): - super().setUp() - # saving the environment - self.name = os.name - self.platform = sys.platform - self.version = sys.version - self.sep = os.sep - self.join = os.path.join - self.isabs = os.path.isabs - self.splitdrive = os.path.splitdrive - self._config_vars = copy(sysconfig._config_vars) - - def tearDown(self): - # getting back the environment - os.name = self.name - sys.platform = self.platform - sys.version = self.version - os.sep = self.sep - os.path.join = self.join - os.path.isabs = self.isabs - os.path.splitdrive = self.splitdrive - sysconfig._config_vars = copy(self._config_vars) - super().tearDown() +@pytest.fixture(autouse=True) +def environment(monkeypatch): + monkeypatch.setattr(os, 'name', os.name) + monkeypatch.setattr(sys, 'platform', sys.platform) + monkeypatch.setattr(sys, 'version', sys.version) + monkeypatch.setattr(os, 'sep', os.sep) + monkeypatch.setattr(os.path, 'join', os.path.join) + monkeypatch.setattr(os.path, 'isabs', os.path.isabs) + monkeypatch.setattr(os.path, 'splitdrive', os.path.splitdrive) + monkeypatch.setattr(sysconfig, '_config_vars', copy(sysconfig._config_vars)) + +@pytest.mark.usefixtures('save_env') +class TestUtil: def test_get_host_platform(self): with unittest.mock.patch('os.name', 'nt'): with unittest.mock.patch('sys.version', '... [... (ARM64)]'): From 81a7c894f9c4110cb608f99ec1ffcb6d2cbaf300 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 02:31:24 -0400 Subject: [PATCH 22/89] Convert TestUnixCCompiler to pytest --- distutils/tests/test_unixccompiler.py | 28 +++++++++++++-------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/distutils/tests/test_unixccompiler.py b/distutils/tests/test_unixccompiler.py index 4be4ff2753..20d50694d6 100644 --- a/distutils/tests/test_unixccompiler.py +++ b/distutils/tests/test_unixccompiler.py @@ -15,25 +15,23 @@ import pytest -class UnixCCompilerTestCase(support.TempdirManager, unittest.TestCase): - def setUp(self): - super().setUp() - self._backup_platform = sys.platform - self._backup_get_config_var = sysconfig.get_config_var - self._backup_get_config_vars = sysconfig.get_config_vars +@pytest.fixture(autouse=True) +def save_values(monkeypatch): + monkeypatch.setattr(sys, 'platform', sys.platform) + monkeypatch.setattr(sysconfig, 'get_config_var', sysconfig.get_config_var) + monkeypatch.setattr(sysconfig, 'get_config_vars', sysconfig.get_config_vars) - class CompilerWrapper(UnixCCompiler): - def rpath_foo(self): - return self.runtime_library_dir_option('/foo') - self.cc = CompilerWrapper() +@pytest.fixture(autouse=True) +def compiler_wrapper(request): + class CompilerWrapper(UnixCCompiler): + def rpath_foo(self): + return self.runtime_library_dir_option('/foo') - def tearDown(self): - super().tearDown() - sys.platform = self._backup_platform - sysconfig.get_config_var = self._backup_get_config_var - sysconfig.get_config_vars = self._backup_get_config_vars + request.instance.cc = CompilerWrapper() + +class TestUnixCCompiler(support.TempdirManager): @unittest.skipIf(sys.platform == 'win32', "can't test on Windows") def test_runtime_libdir_option(self): # noqa: C901 # Issue #5900; GitHub Issue #37 From e7750dcc6bf71d9dee68a9dfb02fb46248bb054e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 02:36:07 -0400 Subject: [PATCH 23/89] Prefer pytest for skip --- distutils/tests/test_unixccompiler.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/distutils/tests/test_unixccompiler.py b/distutils/tests/test_unixccompiler.py index 20d50694d6..424a9267c5 100644 --- a/distutils/tests/test_unixccompiler.py +++ b/distutils/tests/test_unixccompiler.py @@ -1,7 +1,6 @@ """Tests for distutils.unixccompiler.""" import os import sys -import unittest from unittest.mock import patch from .py38compat import EnvironmentVarGuard @@ -32,7 +31,7 @@ def rpath_foo(self): class TestUnixCCompiler(support.TempdirManager): - @unittest.skipIf(sys.platform == 'win32', "can't test on Windows") + @pytest.mark.skipif('platform.system == "Windows"') def test_runtime_libdir_option(self): # noqa: C901 # Issue #5900; GitHub Issue #37 # @@ -213,7 +212,7 @@ def gcv(v): sysconfig.get_config_var = gcv assert self.cc.rpath_foo() == '-Wl,-R/foo' - @unittest.skipIf(sys.platform == 'win32', "can't test on Windows") + @pytest.mark.skipif('platform.system == "Windows"') def test_cc_overrides_ldshared(self): # Issue #18080: # ensure that setting CC env variable also changes default linker @@ -235,7 +234,7 @@ def gcvs(*args, _orig=sysconfig.get_config_vars): sysconfig.customize_compiler(self.cc) assert self.cc.linker_so[0] == 'my_cc' - @unittest.skipIf(sys.platform == 'win32', "can't test on Windows") + @pytest.mark.skipif('platform.system == "Windows"') def test_cc_overrides_ldshared_for_cxx_correctly(self): """ Ensure that setting CC env variable also changes default linker @@ -275,7 +274,7 @@ def gcvs(*args, _orig=sysconfig.get_config_vars): expected = ['my_cxx', '-bundle', '-undefined', 'dynamic_lookup'] assert call_args[:4] == expected - @unittest.skipIf(sys.platform == 'win32', "can't test on Windows") + @pytest.mark.skipif('platform.system == "Windows"') def test_explicit_ldshared(self): # Issue #18080: # ensure that setting CC env variable does not change From dadeb7ed4b8eb70a49ba44744773ee633b9312bd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 02:58:57 -0400 Subject: [PATCH 24/89] Convert more tests to pytest --- distutils/tests/support.py | 10 ++--- distutils/tests/test_archive_util.py | 67 ++++++++++------------------ 2 files changed, 27 insertions(+), 50 deletions(-) diff --git a/distutils/tests/support.py b/distutils/tests/support.py index 1ff4a1268f..f07daff502 100644 --- a/distutils/tests/support.py +++ b/distutils/tests/support.py @@ -3,7 +3,6 @@ import sys import shutil import tempfile -import unittest import sysconfig import itertools @@ -31,9 +30,8 @@ def clear_logs(self): @pytest.mark.usefixtures('distutils_managed_tempdir') class TempdirManager: - """Mix-in class that handles temporary directories for test cases. - - This is intended to be used with unittest.TestCase. + """ + Mix-in class that handles temporary directories for test cases. """ def mkdtemp(self): @@ -101,9 +99,7 @@ def test_compile(self): """ filename = _get_xxmodule_path() if filename is None: - raise unittest.SkipTest( - 'cannot find xxmodule.c (test must run in ' 'the python build dir)' - ) + pytest.skip('cannot find xxmodule.c (test must run in ' 'the python build dir)') shutil.copy(filename, directory) diff --git a/distutils/tests/test_archive_util.py b/distutils/tests/test_archive_util.py index c8c74032ae..17a528bcf3 100644 --- a/distutils/tests/test_archive_util.py +++ b/distutils/tests/test_archive_util.py @@ -1,10 +1,12 @@ """Tests for distutils.archive_util.""" -import unittest import os import sys import tarfile from os.path import splitdrive import warnings +import functools +import operator +import pathlib import pytest @@ -16,7 +18,7 @@ make_archive, ARCHIVE_FORMATS, ) -from distutils.spawn import find_executable, spawn +from distutils.spawn import spawn from distutils.tests import support from test.support import patch from .unix_compat import require_unix_id, require_uid_0, grp, pwd, UID_0_SUPPORT @@ -25,24 +27,6 @@ from .py38compat import check_warnings -try: - import zipfile - - ZIP_SUPPORT = True -except ImportError: - ZIP_SUPPORT = find_executable('zip') - -try: - import bz2 -except ImportError: - bz2 = None - -try: - import lzma -except ImportError: - lzma = None - - def can_fs_encode(filename): """ Return True if the filename can be saved in the file system. @@ -56,6 +40,14 @@ def can_fs_encode(filename): return True +def all_equal(values): + return functools.reduce(operator.eq, values) + + +def same_drive(*paths): + return all_equal(pathlib.Path(path).drive for path in paths) + + class ArchiveUtilTestCase(support.TempdirManager, support.LoggingSilencer): @pytest.mark.usefixtures('needs_zlib') def test_make_tarball(self, name='archive'): @@ -70,28 +62,24 @@ def test_make_tarball_gzip(self): tmpdir = self._create_files() self._make_tarball(tmpdir, 'archive', '.tar.gz', compress='gzip') - @unittest.skipUnless(bz2, 'Need bz2 support to run') def test_make_tarball_bzip2(self): + pytest.importorskip('bz2') tmpdir = self._create_files() self._make_tarball(tmpdir, 'archive', '.tar.bz2', compress='bzip2') - @unittest.skipUnless(lzma, 'Need lzma support to run') def test_make_tarball_xz(self): + pytest.importorskip('lzma') tmpdir = self._create_files() self._make_tarball(tmpdir, 'archive', '.tar.xz', compress='xz') - @unittest.skipUnless( - can_fs_encode('årchiv'), 'File system cannot handle this filename' - ) + @pytest.mark.skipif("not can_fs_encode('årchiv')") def test_make_tarball_latin1(self): """ Mirror test_make_tarball, except filename contains latin characters. """ self.test_make_tarball('årchiv') # note this isn't a real word - @unittest.skipUnless( - can_fs_encode('のアーカイブ'), 'File system cannot handle this filename' - ) + @pytest.mark.skipif("not can_fs_encode('のアーカイブ')") def test_make_tarball_extended(self): """ Mirror test_make_tarball, except filename contains extended @@ -101,10 +89,8 @@ def test_make_tarball_extended(self): def _make_tarball(self, tmpdir, target_name, suffix, **kwargs): tmpdir2 = self.mkdtemp() - unittest.skipUnless( - splitdrive(tmpdir)[0] == splitdrive(tmpdir2)[0], - "source and target should be on same drive", - ) + if same_drive(tmpdir, tmpdir2): + pytest.skip("source and target should be on same drive") base_name = os.path.join(tmpdir2, target_name) @@ -149,10 +135,7 @@ def _create_files(self): return tmpdir @pytest.mark.usefixtures('needs_zlib') - @unittest.skipUnless( - find_executable('tar') and find_executable('gzip'), - 'Need the tar and gzip commands to run', - ) + @pytest.mark.skipif("not (find_executable('tar') and find_executable('gzip'))") def test_tarfile_vs_tar(self): tmpdir = self._create_files() tmpdir2 = self.mkdtemp() @@ -207,9 +190,7 @@ def test_tarfile_vs_tar(self): tarball = base_name + '.tar' assert os.path.exists(tarball) - @unittest.skipUnless( - find_executable('compress'), 'The compress program is required' - ) + @pytest.mark.skipif("not find_executable('compress')") def test_compress_deprecated(self): tmpdir = self._create_files() base_name = os.path.join(self.mkdtemp(), 'archive') @@ -241,8 +222,8 @@ def test_compress_deprecated(self): assert len(w.warnings) == 1 @pytest.mark.usefixtures('needs_zlib') - @unittest.skipUnless(ZIP_SUPPORT, 'Need zip support to run') def test_make_zipfile(self): + zipfile = pytest.importorskip('zipfile') # creating something to tar tmpdir = self._create_files() base_name = os.path.join(self.mkdtemp(), 'archive') @@ -255,8 +236,8 @@ def test_make_zipfile(self): with zipfile.ZipFile(tarball) as zf: assert sorted(zf.namelist()) == self._zip_created_files - @unittest.skipUnless(ZIP_SUPPORT, 'Need zip support to run') def test_make_zipfile_no_zlib(self): + zipfile = pytest.importorskip('zipfile') patch(self, archive_util.zipfile, 'zlib', None) # force zlib ImportError called = [] @@ -327,8 +308,8 @@ def test_make_archive_gztar(self): assert os.path.basename(res) == 'archive.tar.gz' assert self._tarinfo(res) == self._created_files - @unittest.skipUnless(bz2, 'Need bz2 support to run') def test_make_archive_bztar(self): + pytest.importorskip('bz2') base_dir = self._create_files() base_name = os.path.join(self.mkdtemp(), 'archive') res = make_archive(base_name, 'bztar', base_dir, 'dist') @@ -336,8 +317,8 @@ def test_make_archive_bztar(self): assert os.path.basename(res) == 'archive.tar.bz2' assert self._tarinfo(res) == self._created_files - @unittest.skipUnless(lzma, 'Need xz support to run') def test_make_archive_xztar(self): + pytest.importorskip('lzma') base_dir = self._create_files() base_name = os.path.join(self.mkdtemp(), 'archive') res = make_archive(base_name, 'xztar', base_dir, 'dist') From a84053c91d0bb3b59faeae77a8de40b7f0e34a63 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 03:05:27 -0400 Subject: [PATCH 25/89] Prefer pytest for skip --- distutils/tests/test_bdist_rpm.py | 34 ++++++++++++------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/distutils/tests/test_bdist_rpm.py b/distutils/tests/test_bdist_rpm.py index 411d09ebea..2d14bafc98 100644 --- a/distutils/tests/test_bdist_rpm.py +++ b/distutils/tests/test_bdist_rpm.py @@ -1,6 +1,5 @@ """Tests for distutils.command.bdist_rpm.""" -import unittest import sys import os @@ -9,7 +8,7 @@ from distutils.core import Distribution from distutils.command.bdist_rpm import bdist_rpm from distutils.tests import support -from distutils.spawn import find_executable +from distutils.spawn import find_executable # noqa: F401 from .py38compat import requires_zlib @@ -32,6 +31,12 @@ def sys_executable_encodable(): pytest.skip("sys.executable is not encodable to UTF-8") +mac_woes = pytest.mark.skipif( + "not sys.platform.startswith('linux')", + reason='spurious sdtout/stderr output under macOS', +) + + @pytest.mark.usefixtures('save_env') @pytest.mark.usefixtures('save_argv') @pytest.mark.usefixtures('save_cwd') @@ -39,17 +44,10 @@ class TestBuildRpm( support.TempdirManager, support.LoggingSilencer, ): - - # XXX I am unable yet to make this test work without - # spurious sdtout/stderr output under Mac OS X - @unittest.skipUnless( - sys.platform.startswith('linux'), 'spurious sdtout/stderr output under Mac OS X' - ) + @mac_woes @requires_zlib() - @unittest.skipIf(find_executable('rpm') is None, 'the rpm command is not found') - @unittest.skipIf( - find_executable('rpmbuild') is None, 'the rpmbuild command is not found' - ) + @pytest.mark.skipif("not find_executable('rpm')") + @pytest.mark.skipif("not find_executable('rpmbuild')") def test_quiet(self): # let's create a package tmp_dir = self.mkdtemp() @@ -90,17 +88,11 @@ def test_quiet(self): assert ('bdist_rpm', 'any', 'dist/foo-0.1-1.src.rpm') in dist.dist_files assert ('bdist_rpm', 'any', 'dist/foo-0.1-1.noarch.rpm') in dist.dist_files - # XXX I am unable yet to make this test work without - # spurious sdtout/stderr output under Mac OS X - @unittest.skipUnless( - sys.platform.startswith('linux'), 'spurious sdtout/stderr output under Mac OS X' - ) + @mac_woes @requires_zlib() # http://bugs.python.org/issue1533164 - @unittest.skipIf(find_executable('rpm') is None, 'the rpm command is not found') - @unittest.skipIf( - find_executable('rpmbuild') is None, 'the rpmbuild command is not found' - ) + @pytest.mark.skipif("not find_executable('rpm')") + @pytest.mark.skipif("not find_executable('rpmbuild')") def test_no_optimize_flag(self): # let's create a package that breaks bdist_rpm tmp_dir = self.mkdtemp() From 377425266c7845f64dcbdcb050cf77c09cd3365b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 03:11:07 -0400 Subject: [PATCH 26/89] Prefer pytest for skip --- distutils/tests/test_bdist_wininst.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/distutils/tests/test_bdist_wininst.py b/distutils/tests/test_bdist_wininst.py index 8bc217af68..c432d24be6 100644 --- a/distutils/tests/test_bdist_wininst.py +++ b/distutils/tests/test_bdist_wininst.py @@ -1,7 +1,5 @@ """Tests for distutils.command.bdist_wininst.""" -import sys -import platform -import unittest +import pytest from .py38compat import check_warnings @@ -9,14 +7,8 @@ from distutils.tests import support -@unittest.skipIf( - sys.platform == 'win32' and platform.machine() == 'ARM64', - 'bdist_wininst is not supported in this install', -) -@unittest.skipIf( - getattr(bdist_wininst, '_unsupported', False), - 'bdist_wininst is not supported in this install', -) +@pytest.mark.skipif("platform.machine() == 'ARM64'") +@pytest.mark.skipif("bdist_wininst._unsupported") class TestBuildWinInst(support.TempdirManager, support.LoggingSilencer): def test_get_exe_bytes(self): From 69e46e55d15d8331610785da4b437af59f6f365a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 03:15:45 -0400 Subject: [PATCH 27/89] Convert TestBuild to pytest --- distutils/tests/test_build.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/distutils/tests/test_build.py b/distutils/tests/test_build.py index 45bc22f822..80367607f5 100644 --- a/distutils/tests/test_build.py +++ b/distutils/tests/test_build.py @@ -1,5 +1,4 @@ """Tests for distutils.command.build.""" -import unittest import os import sys @@ -8,7 +7,7 @@ from sysconfig import get_platform -class BuildTestCase(support.TempdirManager, support.LoggingSilencer, unittest.TestCase): +class TestBuild(support.TempdirManager, support.LoggingSilencer): def test_finalize_options(self): pkg_dir, dist = self.create_dist() cmd = build(dist) From 94467c2b05d89c0e55ee30c6f689a12085428b7e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 03:17:02 -0400 Subject: [PATCH 28/89] Prefer pytest for skip --- distutils/tests/test_build_clib.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/distutils/tests/test_build_clib.py b/distutils/tests/test_build_clib.py index 2048e29a52..c931c06ec5 100644 --- a/distutils/tests/test_build_clib.py +++ b/distutils/tests/test_build_clib.py @@ -1,14 +1,13 @@ """Tests for distutils.command.build_clib.""" -import unittest import os -import sys from test.support import missing_compiler_executable +import pytest + from distutils.command.build_clib import build_clib from distutils.errors import DistutilsSetupError from distutils.tests import support -import pytest class TestBuildCLib(support.TempdirManager, support.LoggingSilencer): @@ -111,7 +110,7 @@ def test_finalize_options(self): with pytest.raises(DistutilsSetupError): cmd.finalize_options() - @unittest.skipIf(sys.platform == 'win32', "can't test on Windows") + @pytest.mark.skipif('platform.system() == "Windows"') def test_run(self): pkg_dir, dist = self.create_dist() cmd = build_clib(dist) From b712592d79d580319239067b0e76c0d85349a32a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 03:18:32 -0400 Subject: [PATCH 29/89] Prefer pytest for skip --- distutils/tests/test_build_ext.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index 6379510892..e7ef836c48 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -452,7 +452,7 @@ def test_ext_fullpath(self): wanted = os.path.join(curdir, 'twisted', 'runner', 'portmap' + ext) assert wanted == path - @unittest.skipUnless(sys.platform == 'darwin', 'test only relevant for MacOSX') + @pytest.mark.skipif('platform.system() != "Darwin"') @pytest.mark.usefixtures('save_env') def test_deployment_target_default(self): # Issue 9516: Test that, in the absence of the environment variable, @@ -460,7 +460,7 @@ def test_deployment_target_default(self): # the interpreter. self._try_compile_deployment_target('==', None) - @unittest.skipUnless(sys.platform == 'darwin', 'test only relevant for MacOSX') + @pytest.mark.skipif('platform.system() != "Darwin"') @pytest.mark.usefixtures('save_env') def test_deployment_target_too_low(self): # Issue 9516: Test that an extension module is not allowed to be @@ -468,7 +468,7 @@ def test_deployment_target_too_low(self): with pytest.raises(DistutilsPlatformError): self._try_compile_deployment_target('>', '10.1') - @unittest.skipUnless(sys.platform == 'darwin', 'test only relevant for MacOSX') + @pytest.mark.skipif('platform.system() != "Darwin"') @pytest.mark.usefixtures('save_env') def test_deployment_target_higher_ok(self): # Issue 9516: Test that an extension module can be compiled with a From e43857eeb560a51fbb72dffa8f4294ed76375a50 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 03:26:19 -0400 Subject: [PATCH 30/89] Remove unreachable code --- distutils/tests/test_build_ext.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index e7ef836c48..62f2ffe32e 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -21,17 +21,12 @@ UnknownFileError, ) -import unittest from test import support from . import py38compat as os_helper from test.support.script_helper import assert_python_ok import pytest import re -# http://bugs.python.org/issue4373 -# Don't load the xx module more than once. -ALREADY_TESTED = False - @pytest.fixture() def user_site_dir(request): @@ -61,9 +56,6 @@ def build_ext(self, *args, **kwargs): def test_build_ext(self): cmd = support.missing_compiler_executable() - if cmd is not None: - self.skipTest('The %r command is not found' % cmd) - global ALREADY_TESTED copy_xxmodule_c(self.tmp_dir) xx_c = os.path.join(self.tmp_dir, 'xxmodule.c') xx_ext = Extension('xx', [xx_c]) @@ -84,11 +76,6 @@ def test_build_ext(self): finally: sys.stdout = old_stdout - if ALREADY_TESTED: - self.skipTest('Already tested in %s' % ALREADY_TESTED) - else: - ALREADY_TESTED = type(self).__name__ - code = textwrap.dedent( f""" tmp_dir = {self.tmp_dir!r} @@ -352,8 +339,6 @@ def test_compiler_option(self): def test_get_outputs(self): cmd = support.missing_compiler_executable() - if cmd is not None: - self.skipTest('The %r command is not found' % cmd) tmp_dir = self.mkdtemp() c_file = os.path.join(tmp_dir, 'foo.c') self.write_file(c_file, 'void PyInit_foo(void) {}\n') From 3f16eed0703da60989d695abff667f14a5309767 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 03:46:37 -0400 Subject: [PATCH 31/89] Copy xxmodule.c from Python 3.11 and 3.8, restoring tests for build_ext. --- distutils/tests/support.py | 25 +- distutils/tests/xxmodule-3.8.c | 411 ++++++++++++++++++++++++++++++++ distutils/tests/xxmodule.c | 412 +++++++++++++++++++++++++++++++++ 3 files changed, 830 insertions(+), 18 deletions(-) create mode 100644 distutils/tests/xxmodule-3.8.c create mode 100644 distutils/tests/xxmodule.c diff --git a/distutils/tests/support.py b/distutils/tests/support.py index f07daff502..d9f58b477b 100644 --- a/distutils/tests/support.py +++ b/distutils/tests/support.py @@ -97,27 +97,16 @@ def test_compile(self): If the source file can be found, it will be copied to *directory*. If not, the test will be skipped. Errors during copy are not caught. """ - filename = _get_xxmodule_path() - if filename is None: - pytest.skip('cannot find xxmodule.c (test must run in ' 'the python build dir)') - shutil.copy(filename, directory) + shutil.copy(_get_xxmodule_path(), os.path.join(directory, 'xxmodule.c')) def _get_xxmodule_path(): - srcdir = sysconfig.get_config_var('srcdir') - candidates = [ - # use installed copy if available - os.path.join(os.path.dirname(__file__), 'xxmodule.c'), - # otherwise try using copy from build directory - os.path.join(srcdir, 'Modules', 'xxmodule.c'), - # srcdir mysteriously can be $srcdir/Lib/distutils/tests when - # this file is run from its parent directory, so walk up the - # tree to find the real srcdir - os.path.join(srcdir, '..', '..', '..', 'Modules', 'xxmodule.c'), - ] - for path in candidates: - if os.path.exists(path): - return path + source_name = ( + 'xxmodule.c' + if sys.version_info > (3, 9) + else 'xxmodule-3.8.c' + ) + return os.path.join(os.path.dirname(__file__), source_name) def fixup_build_ext(cmd): diff --git a/distutils/tests/xxmodule-3.8.c b/distutils/tests/xxmodule-3.8.c new file mode 100644 index 0000000000..0250031d72 --- /dev/null +++ b/distutils/tests/xxmodule-3.8.c @@ -0,0 +1,411 @@ + +/* Use this file as a template to start implementing a module that + also declares object types. All occurrences of 'Xxo' should be changed + to something reasonable for your objects. After that, all other + occurrences of 'xx' should be changed to something reasonable for your + module. If your module is named foo your sourcefile should be named + foomodule.c. + + You will probably want to delete all references to 'x_attr' and add + your own types of attributes instead. Maybe you want to name your + local variables other than 'self'. If your object type is needed in + other files, you'll have to create a file "foobarobject.h"; see + floatobject.h for an example. */ + +/* Xxo objects */ + +#include "Python.h" + +static PyObject *ErrorObject; + +typedef struct { + PyObject_HEAD + PyObject *x_attr; /* Attributes dictionary */ +} XxoObject; + +static PyTypeObject Xxo_Type; + +#define XxoObject_Check(v) (Py_TYPE(v) == &Xxo_Type) + +static XxoObject * +newXxoObject(PyObject *arg) +{ + XxoObject *self; + self = PyObject_New(XxoObject, &Xxo_Type); + if (self == NULL) + return NULL; + self->x_attr = NULL; + return self; +} + +/* Xxo methods */ + +static void +Xxo_dealloc(XxoObject *self) +{ + Py_XDECREF(self->x_attr); + PyObject_Del(self); +} + +static PyObject * +Xxo_demo(XxoObject *self, PyObject *args) +{ + if (!PyArg_ParseTuple(args, ":demo")) + return NULL; + Py_INCREF(Py_None); + return Py_None; +} + +static PyMethodDef Xxo_methods[] = { + {"demo", (PyCFunction)Xxo_demo, METH_VARARGS, + PyDoc_STR("demo() -> None")}, + {NULL, NULL} /* sentinel */ +}; + +static PyObject * +Xxo_getattro(XxoObject *self, PyObject *name) +{ + if (self->x_attr != NULL) { + PyObject *v = PyDict_GetItemWithError(self->x_attr, name); + if (v != NULL) { + Py_INCREF(v); + return v; + } + else if (PyErr_Occurred()) { + return NULL; + } + } + return PyObject_GenericGetAttr((PyObject *)self, name); +} + +static int +Xxo_setattr(XxoObject *self, const char *name, PyObject *v) +{ + if (self->x_attr == NULL) { + self->x_attr = PyDict_New(); + if (self->x_attr == NULL) + return -1; + } + if (v == NULL) { + int rv = PyDict_DelItemString(self->x_attr, name); + if (rv < 0 && PyErr_ExceptionMatches(PyExc_KeyError)) + PyErr_SetString(PyExc_AttributeError, + "delete non-existing Xxo attribute"); + return rv; + } + else + return PyDict_SetItemString(self->x_attr, name, v); +} + +static PyTypeObject Xxo_Type = { + /* The ob_type field must be initialized in the module init function + * to be portable to Windows without using C++. */ + PyVarObject_HEAD_INIT(NULL, 0) + "xxmodule.Xxo", /*tp_name*/ + sizeof(XxoObject), /*tp_basicsize*/ + 0, /*tp_itemsize*/ + /* methods */ + (destructor)Xxo_dealloc, /*tp_dealloc*/ + 0, /*tp_vectorcall_offset*/ + (getattrfunc)0, /*tp_getattr*/ + (setattrfunc)Xxo_setattr, /*tp_setattr*/ + 0, /*tp_as_async*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + (getattrofunc)Xxo_getattro, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + Xxo_methods, /*tp_methods*/ + 0, /*tp_members*/ + 0, /*tp_getset*/ + 0, /*tp_base*/ + 0, /*tp_dict*/ + 0, /*tp_descr_get*/ + 0, /*tp_descr_set*/ + 0, /*tp_dictoffset*/ + 0, /*tp_init*/ + 0, /*tp_alloc*/ + 0, /*tp_new*/ + 0, /*tp_free*/ + 0, /*tp_is_gc*/ +}; +/* --------------------------------------------------------------------- */ + +/* Function of two integers returning integer */ + +PyDoc_STRVAR(xx_foo_doc, +"foo(i,j)\n\ +\n\ +Return the sum of i and j."); + +static PyObject * +xx_foo(PyObject *self, PyObject *args) +{ + long i, j; + long res; + if (!PyArg_ParseTuple(args, "ll:foo", &i, &j)) + return NULL; + res = i+j; /* XXX Do something here */ + return PyLong_FromLong(res); +} + + +/* Function of no arguments returning new Xxo object */ + +static PyObject * +xx_new(PyObject *self, PyObject *args) +{ + XxoObject *rv; + + if (!PyArg_ParseTuple(args, ":new")) + return NULL; + rv = newXxoObject(args); + if (rv == NULL) + return NULL; + return (PyObject *)rv; +} + +/* Example with subtle bug from extensions manual ("Thin Ice"). */ + +static PyObject * +xx_bug(PyObject *self, PyObject *args) +{ + PyObject *list, *item; + + if (!PyArg_ParseTuple(args, "O:bug", &list)) + return NULL; + + item = PyList_GetItem(list, 0); + /* Py_INCREF(item); */ + PyList_SetItem(list, 1, PyLong_FromLong(0L)); + PyObject_Print(item, stdout, 0); + printf("\n"); + /* Py_DECREF(item); */ + + Py_INCREF(Py_None); + return Py_None; +} + +/* Test bad format character */ + +static PyObject * +xx_roj(PyObject *self, PyObject *args) +{ + PyObject *a; + long b; + if (!PyArg_ParseTuple(args, "O#:roj", &a, &b)) + return NULL; + Py_INCREF(Py_None); + return Py_None; +} + + +/* ---------- */ + +static PyTypeObject Str_Type = { + /* The ob_type field must be initialized in the module init function + * to be portable to Windows without using C++. */ + PyVarObject_HEAD_INIT(NULL, 0) + "xxmodule.Str", /*tp_name*/ + 0, /*tp_basicsize*/ + 0, /*tp_itemsize*/ + /* methods */ + 0, /*tp_dealloc*/ + 0, /*tp_vectorcall_offset*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_as_async*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + 0, /*tp_methods*/ + 0, /*tp_members*/ + 0, /*tp_getset*/ + 0, /* see PyInit_xx */ /*tp_base*/ + 0, /*tp_dict*/ + 0, /*tp_descr_get*/ + 0, /*tp_descr_set*/ + 0, /*tp_dictoffset*/ + 0, /*tp_init*/ + 0, /*tp_alloc*/ + 0, /*tp_new*/ + 0, /*tp_free*/ + 0, /*tp_is_gc*/ +}; + +/* ---------- */ + +static PyObject * +null_richcompare(PyObject *self, PyObject *other, int op) +{ + Py_INCREF(Py_NotImplemented); + return Py_NotImplemented; +} + +static PyTypeObject Null_Type = { + /* The ob_type field must be initialized in the module init function + * to be portable to Windows without using C++. */ + PyVarObject_HEAD_INIT(NULL, 0) + "xxmodule.Null", /*tp_name*/ + 0, /*tp_basicsize*/ + 0, /*tp_itemsize*/ + /* methods */ + 0, /*tp_dealloc*/ + 0, /*tp_vectorcall_offset*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_as_async*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + null_richcompare, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + 0, /*tp_methods*/ + 0, /*tp_members*/ + 0, /*tp_getset*/ + 0, /* see PyInit_xx */ /*tp_base*/ + 0, /*tp_dict*/ + 0, /*tp_descr_get*/ + 0, /*tp_descr_set*/ + 0, /*tp_dictoffset*/ + 0, /*tp_init*/ + 0, /*tp_alloc*/ + PyType_GenericNew, /*tp_new*/ + 0, /*tp_free*/ + 0, /*tp_is_gc*/ +}; + + +/* ---------- */ + + +/* List of functions defined in the module */ + +static PyMethodDef xx_methods[] = { + {"roj", xx_roj, METH_VARARGS, + PyDoc_STR("roj(a,b) -> None")}, + {"foo", xx_foo, METH_VARARGS, + xx_foo_doc}, + {"new", xx_new, METH_VARARGS, + PyDoc_STR("new() -> new Xx object")}, + {"bug", xx_bug, METH_VARARGS, + PyDoc_STR("bug(o) -> None")}, + {NULL, NULL} /* sentinel */ +}; + +PyDoc_STRVAR(module_doc, +"This is a template module just for instruction."); + + +static int +xx_exec(PyObject *m) +{ + /* Slot initialization is subject to the rules of initializing globals. + C99 requires the initializers to be "address constants". Function + designators like 'PyType_GenericNew', with implicit conversion to + a pointer, are valid C99 address constants. + + However, the unary '&' operator applied to a non-static variable + like 'PyBaseObject_Type' is not required to produce an address + constant. Compilers may support this (gcc does), MSVC does not. + + Both compilers are strictly standard conforming in this particular + behavior. + */ + Null_Type.tp_base = &PyBaseObject_Type; + Str_Type.tp_base = &PyUnicode_Type; + + /* Finalize the type object including setting type of the new type + * object; doing it here is required for portability, too. */ + if (PyType_Ready(&Xxo_Type) < 0) + goto fail; + + /* Add some symbolic constants to the module */ + if (ErrorObject == NULL) { + ErrorObject = PyErr_NewException("xx.error", NULL, NULL); + if (ErrorObject == NULL) + goto fail; + } + Py_INCREF(ErrorObject); + PyModule_AddObject(m, "error", ErrorObject); + + /* Add Str */ + if (PyType_Ready(&Str_Type) < 0) + goto fail; + PyModule_AddObject(m, "Str", (PyObject *)&Str_Type); + + /* Add Null */ + if (PyType_Ready(&Null_Type) < 0) + goto fail; + PyModule_AddObject(m, "Null", (PyObject *)&Null_Type); + return 0; + fail: + Py_XDECREF(m); + return -1; +} + +static struct PyModuleDef_Slot xx_slots[] = { + {Py_mod_exec, xx_exec}, + {0, NULL}, +}; + +static struct PyModuleDef xxmodule = { + PyModuleDef_HEAD_INIT, + "xx", + module_doc, + 0, + xx_methods, + xx_slots, + NULL, + NULL, + NULL +}; + +/* Export function for the module (*must* be called PyInit_xx) */ + +PyMODINIT_FUNC +PyInit_xx(void) +{ + return PyModuleDef_Init(&xxmodule); +} diff --git a/distutils/tests/xxmodule.c b/distutils/tests/xxmodule.c new file mode 100644 index 0000000000..a6e5071d1d --- /dev/null +++ b/distutils/tests/xxmodule.c @@ -0,0 +1,412 @@ + +/* Use this file as a template to start implementing a module that + also declares object types. All occurrences of 'Xxo' should be changed + to something reasonable for your objects. After that, all other + occurrences of 'xx' should be changed to something reasonable for your + module. If your module is named foo your sourcefile should be named + foomodule.c. + + You will probably want to delete all references to 'x_attr' and add + your own types of attributes instead. Maybe you want to name your + local variables other than 'self'. If your object type is needed in + other files, you'll have to create a file "foobarobject.h"; see + floatobject.h for an example. */ + +/* Xxo objects */ + +#include "Python.h" + +static PyObject *ErrorObject; + +typedef struct { + PyObject_HEAD + PyObject *x_attr; /* Attributes dictionary */ +} XxoObject; + +static PyTypeObject Xxo_Type; + +#define XxoObject_Check(v) Py_IS_TYPE(v, &Xxo_Type) + +static XxoObject * +newXxoObject(PyObject *arg) +{ + XxoObject *self; + self = PyObject_New(XxoObject, &Xxo_Type); + if (self == NULL) + return NULL; + self->x_attr = NULL; + return self; +} + +/* Xxo methods */ + +static void +Xxo_dealloc(XxoObject *self) +{ + Py_XDECREF(self->x_attr); + PyObject_Free(self); +} + +static PyObject * +Xxo_demo(XxoObject *self, PyObject *args) +{ + if (!PyArg_ParseTuple(args, ":demo")) + return NULL; + Py_INCREF(Py_None); + return Py_None; +} + +static PyMethodDef Xxo_methods[] = { + {"demo", (PyCFunction)Xxo_demo, METH_VARARGS, + PyDoc_STR("demo() -> None")}, + {NULL, NULL} /* sentinel */ +}; + +static PyObject * +Xxo_getattro(XxoObject *self, PyObject *name) +{ + if (self->x_attr != NULL) { + PyObject *v = PyDict_GetItemWithError(self->x_attr, name); + if (v != NULL) { + Py_INCREF(v); + return v; + } + else if (PyErr_Occurred()) { + return NULL; + } + } + return PyObject_GenericGetAttr((PyObject *)self, name); +} + +static int +Xxo_setattr(XxoObject *self, const char *name, PyObject *v) +{ + if (self->x_attr == NULL) { + self->x_attr = PyDict_New(); + if (self->x_attr == NULL) + return -1; + } + if (v == NULL) { + int rv = PyDict_DelItemString(self->x_attr, name); + if (rv < 0 && PyErr_ExceptionMatches(PyExc_KeyError)) + PyErr_SetString(PyExc_AttributeError, + "delete non-existing Xxo attribute"); + return rv; + } + else + return PyDict_SetItemString(self->x_attr, name, v); +} + +static PyTypeObject Xxo_Type = { + /* The ob_type field must be initialized in the module init function + * to be portable to Windows without using C++. */ + PyVarObject_HEAD_INIT(NULL, 0) + "xxmodule.Xxo", /*tp_name*/ + sizeof(XxoObject), /*tp_basicsize*/ + 0, /*tp_itemsize*/ + /* methods */ + (destructor)Xxo_dealloc, /*tp_dealloc*/ + 0, /*tp_vectorcall_offset*/ + (getattrfunc)0, /*tp_getattr*/ + (setattrfunc)Xxo_setattr, /*tp_setattr*/ + 0, /*tp_as_async*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + (getattrofunc)Xxo_getattro, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + Xxo_methods, /*tp_methods*/ + 0, /*tp_members*/ + 0, /*tp_getset*/ + 0, /*tp_base*/ + 0, /*tp_dict*/ + 0, /*tp_descr_get*/ + 0, /*tp_descr_set*/ + 0, /*tp_dictoffset*/ + 0, /*tp_init*/ + 0, /*tp_alloc*/ + 0, /*tp_new*/ + 0, /*tp_free*/ + 0, /*tp_is_gc*/ +}; +/* --------------------------------------------------------------------- */ + +/* Function of two integers returning integer */ + +PyDoc_STRVAR(xx_foo_doc, +"foo(i,j)\n\ +\n\ +Return the sum of i and j."); + +static PyObject * +xx_foo(PyObject *self, PyObject *args) +{ + long i, j; + long res; + if (!PyArg_ParseTuple(args, "ll:foo", &i, &j)) + return NULL; + res = i+j; /* XXX Do something here */ + return PyLong_FromLong(res); +} + + +/* Function of no arguments returning new Xxo object */ + +static PyObject * +xx_new(PyObject *self, PyObject *args) +{ + XxoObject *rv; + + if (!PyArg_ParseTuple(args, ":new")) + return NULL; + rv = newXxoObject(args); + if (rv == NULL) + return NULL; + return (PyObject *)rv; +} + +/* Example with subtle bug from extensions manual ("Thin Ice"). */ + +static PyObject * +xx_bug(PyObject *self, PyObject *args) +{ + PyObject *list, *item; + + if (!PyArg_ParseTuple(args, "O:bug", &list)) + return NULL; + + item = PyList_GetItem(list, 0); + /* Py_INCREF(item); */ + PyList_SetItem(list, 1, PyLong_FromLong(0L)); + PyObject_Print(item, stdout, 0); + printf("\n"); + /* Py_DECREF(item); */ + + Py_INCREF(Py_None); + return Py_None; +} + +/* Test bad format character */ + +static PyObject * +xx_roj(PyObject *self, PyObject *args) +{ + PyObject *a; + long b; + if (!PyArg_ParseTuple(args, "O#:roj", &a, &b)) + return NULL; + Py_INCREF(Py_None); + return Py_None; +} + + +/* ---------- */ + +static PyTypeObject Str_Type = { + /* The ob_type field must be initialized in the module init function + * to be portable to Windows without using C++. */ + PyVarObject_HEAD_INIT(NULL, 0) + "xxmodule.Str", /*tp_name*/ + 0, /*tp_basicsize*/ + 0, /*tp_itemsize*/ + /* methods */ + 0, /*tp_dealloc*/ + 0, /*tp_vectorcall_offset*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_as_async*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + 0, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + 0, /*tp_methods*/ + 0, /*tp_members*/ + 0, /*tp_getset*/ + 0, /* see PyInit_xx */ /*tp_base*/ + 0, /*tp_dict*/ + 0, /*tp_descr_get*/ + 0, /*tp_descr_set*/ + 0, /*tp_dictoffset*/ + 0, /*tp_init*/ + 0, /*tp_alloc*/ + 0, /*tp_new*/ + 0, /*tp_free*/ + 0, /*tp_is_gc*/ +}; + +/* ---------- */ + +static PyObject * +null_richcompare(PyObject *self, PyObject *other, int op) +{ + Py_INCREF(Py_NotImplemented); + return Py_NotImplemented; +} + +static PyTypeObject Null_Type = { + /* The ob_type field must be initialized in the module init function + * to be portable to Windows without using C++. */ + PyVarObject_HEAD_INIT(NULL, 0) + "xxmodule.Null", /*tp_name*/ + 0, /*tp_basicsize*/ + 0, /*tp_itemsize*/ + /* methods */ + 0, /*tp_dealloc*/ + 0, /*tp_vectorcall_offset*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_as_async*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash*/ + 0, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ + 0, /*tp_doc*/ + 0, /*tp_traverse*/ + 0, /*tp_clear*/ + null_richcompare, /*tp_richcompare*/ + 0, /*tp_weaklistoffset*/ + 0, /*tp_iter*/ + 0, /*tp_iternext*/ + 0, /*tp_methods*/ + 0, /*tp_members*/ + 0, /*tp_getset*/ + 0, /* see PyInit_xx */ /*tp_base*/ + 0, /*tp_dict*/ + 0, /*tp_descr_get*/ + 0, /*tp_descr_set*/ + 0, /*tp_dictoffset*/ + 0, /*tp_init*/ + 0, /*tp_alloc*/ + PyType_GenericNew, /*tp_new*/ + 0, /*tp_free*/ + 0, /*tp_is_gc*/ +}; + + +/* ---------- */ + + +/* List of functions defined in the module */ + +static PyMethodDef xx_methods[] = { + {"roj", xx_roj, METH_VARARGS, + PyDoc_STR("roj(a,b) -> None")}, + {"foo", xx_foo, METH_VARARGS, + xx_foo_doc}, + {"new", xx_new, METH_VARARGS, + PyDoc_STR("new() -> new Xx object")}, + {"bug", xx_bug, METH_VARARGS, + PyDoc_STR("bug(o) -> None")}, + {NULL, NULL} /* sentinel */ +}; + +PyDoc_STRVAR(module_doc, +"This is a template module just for instruction."); + + +static int +xx_exec(PyObject *m) +{ + /* Slot initialization is subject to the rules of initializing globals. + C99 requires the initializers to be "address constants". Function + designators like 'PyType_GenericNew', with implicit conversion to + a pointer, are valid C99 address constants. + + However, the unary '&' operator applied to a non-static variable + like 'PyBaseObject_Type' is not required to produce an address + constant. Compilers may support this (gcc does), MSVC does not. + + Both compilers are strictly standard conforming in this particular + behavior. + */ + Null_Type.tp_base = &PyBaseObject_Type; + Str_Type.tp_base = &PyUnicode_Type; + + /* Finalize the type object including setting type of the new type + * object; doing it here is required for portability, too. */ + if (PyType_Ready(&Xxo_Type) < 0) { + return -1; + } + + /* Add some symbolic constants to the module */ + if (ErrorObject == NULL) { + ErrorObject = PyErr_NewException("xx.error", NULL, NULL); + if (ErrorObject == NULL) { + return -1; + } + } + int rc = PyModule_AddType(m, (PyTypeObject *)ErrorObject); + Py_DECREF(ErrorObject); + if (rc < 0) { + return -1; + } + + /* Add Str and Null types */ + if (PyModule_AddType(m, &Str_Type) < 0) { + return -1; + } + if (PyModule_AddType(m, &Null_Type) < 0) { + return -1; + } + + return 0; +} + +static struct PyModuleDef_Slot xx_slots[] = { + {Py_mod_exec, xx_exec}, + {0, NULL}, +}; + +static struct PyModuleDef xxmodule = { + PyModuleDef_HEAD_INIT, + "xx", + module_doc, + 0, + xx_methods, + xx_slots, + NULL, + NULL, + NULL +}; + +/* Export function for the module (*must* be called PyInit_xx) */ + +PyMODINIT_FUNC +PyInit_xx(void) +{ + return PyModuleDef_Init(&xxmodule); +} From 36f96c147ba9f0d471e48efed12da8f6f0b56d3f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 03:55:41 -0400 Subject: [PATCH 32/89] Exclude Python 3.11 on macOS due to lack of wheels. Ref pypa/distutils#165. --- .github/workflows/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e244014dd4..62f6fcefef 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,6 +20,10 @@ jobs: - ubuntu-latest - macos-latest - windows-latest + exclude: + # macOS is failing to build pyobjc (#165) + - platform: macos-latest + python: ~3.11.0-0 runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v2 From 6fcdbce1acbf4407c3fe55496c77f771899e957c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 04:23:27 -0400 Subject: [PATCH 33/89] Mark test as xfail for now. Ref pypa/distutils#166. --- distutils/tests/test_install.py | 1 + 1 file changed, 1 insertion(+) diff --git a/distutils/tests/test_install.py b/distutils/tests/test_install.py index 519227d25e..4bbe83bff4 100644 --- a/distutils/tests/test_install.py +++ b/distutils/tests/test_install.py @@ -229,6 +229,7 @@ def test_record(self): ] assert found == expected + @pytest.mark.xfail(reason="#166") def test_record_extensions(self): cmd = test_support.missing_compiler_executable() if cmd is not None: From 78a0389a9c077afebc4d9415da5bed23a324e6fe Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 04:27:06 -0400 Subject: [PATCH 34/89] Use pathlib to read the text --- distutils/tests/test_install.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/distutils/tests/test_install.py b/distutils/tests/test_install.py index 4bbe83bff4..b2ee0a3675 100644 --- a/distutils/tests/test_install.py +++ b/distutils/tests/test_install.py @@ -4,6 +4,7 @@ import sys import unittest import site +import pathlib from test.support import captured_stdout @@ -253,11 +254,7 @@ def test_record_extensions(self): cmd.ensure_finalized() cmd.run() - f = open(cmd.record) - try: - content = f.read() - finally: - f.close() + content = pathlib.Path(cmd.record).read_text() found = [os.path.basename(line) for line in content.splitlines()] expected = [ From 3e984a686b998088b1daaf580522914444933a83 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 10:07:21 -0400 Subject: [PATCH 35/89] =?UTF-8?q?=E2=9A=AB=20Fade=20to=20black.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- distutils/tests/support.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/distutils/tests/support.py b/distutils/tests/support.py index d9f58b477b..5203ed19d4 100644 --- a/distutils/tests/support.py +++ b/distutils/tests/support.py @@ -101,11 +101,7 @@ def test_compile(self): def _get_xxmodule_path(): - source_name = ( - 'xxmodule.c' - if sys.version_info > (3, 9) - else 'xxmodule-3.8.c' - ) + source_name = 'xxmodule.c' if sys.version_info > (3, 9) else 'xxmodule-3.8.c' return os.path.join(os.path.dirname(__file__), source_name) From 2bf5e9ee61e35a666a0293d0f0b4ee1d29d9e0de Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 10:08:06 -0400 Subject: [PATCH 36/89] Convert TestInstall to pytest --- distutils/tests/test_install.py | 48 +++++++++++---------------------- 1 file changed, 16 insertions(+), 32 deletions(-) diff --git a/distutils/tests/test_install.py b/distutils/tests/test_install.py index b2ee0a3675..7797cae716 100644 --- a/distutils/tests/test_install.py +++ b/distutils/tests/test_install.py @@ -2,7 +2,6 @@ import os import sys -import unittest import site import pathlib @@ -29,10 +28,9 @@ def _make_ext_name(modname): @support.combine_markers @pytest.mark.usefixtures('save_env') -class InstallTestCase( +class TestInstall( support.TempdirManager, support.LoggingSilencer, - unittest.TestCase, ): @pytest.mark.xfail( 'platform.system() == "Windows" and sys.version_info > (3, 11)', @@ -79,35 +77,23 @@ def check_path(got, expected): check_path(cmd.install_scripts, os.path.join(destination, "bin")) check_path(cmd.install_data, destination) - def test_user_site(self): + def test_user_site(self, monkeypatch): # test install with --user # preparing the environment for the test - self.old_user_base = site.USER_BASE - self.old_user_site = site.USER_SITE self.tmpdir = self.mkdtemp() - self.user_base = os.path.join(self.tmpdir, 'B') - self.user_site = os.path.join(self.tmpdir, 'S') - site.USER_BASE = self.user_base - site.USER_SITE = self.user_site - install_module.USER_BASE = self.user_base - install_module.USER_SITE = self.user_site + orig_site = site.USER_SITE + orig_base = site.USER_BASE + monkeypatch.setattr(site, 'USER_BASE', os.path.join(self.tmpdir, 'B')) + monkeypatch.setattr(site, 'USER_SITE', os.path.join(self.tmpdir, 'S')) + monkeypatch.setattr(install_module, 'USER_BASE', site.USER_BASE) + monkeypatch.setattr(install_module, 'USER_SITE', site.USER_SITE) def _expanduser(path): if path.startswith('~'): return os.path.normpath(self.tmpdir + path[1:]) return path - self.old_expand = os.path.expanduser - os.path.expanduser = _expanduser - - def cleanup(): - site.USER_BASE = self.old_user_base - site.USER_SITE = self.old_user_site - install_module.USER_BASE = self.old_user_base - install_module.USER_SITE = self.old_user_site - os.path.expanduser = self.old_expand - - self.addCleanup(cleanup) + monkeypatch.setattr(os.path, 'expanduser', _expanduser) for key in ('nt_user', 'posix_user'): assert key in INSTALL_SCHEMES @@ -123,24 +109,22 @@ def cleanup(): cmd.user = 1 # user base and site shouldn't be created yet - assert not os.path.exists(self.user_base) - assert not os.path.exists(self.user_site) + assert not os.path.exists(site.USER_BASE) + assert not os.path.exists(site.USER_SITE) # let's run finalize cmd.ensure_finalized() # now they should - assert os.path.exists(self.user_base) - assert os.path.exists(self.user_site) + assert os.path.exists(site.USER_BASE) + assert os.path.exists(site.USER_SITE) assert 'userbase' in cmd.config_vars assert 'usersite' in cmd.config_vars - actual_headers = os.path.relpath(cmd.install_headers, self.user_base) + actual_headers = os.path.relpath(cmd.install_headers, site.USER_BASE) if os.name == 'nt': - site_path = os.path.relpath( - os.path.dirname(self.old_user_site), self.old_user_base - ) + site_path = os.path.relpath(os.path.dirname(orig_site), orig_base) include = os.path.join(site_path, 'Include') else: include = sysconfig.get_python_inc(0, '') @@ -234,7 +218,7 @@ def test_record(self): def test_record_extensions(self): cmd = test_support.missing_compiler_executable() if cmd is not None: - self.skipTest('The %r command is not found' % cmd) + pytest.skip('The %r command is not found' % cmd) install_dir = self.mkdtemp() project_dir, dist = self.create_dist( ext_modules=[Extension('xx', ['xxmodule.c'])] From 3a0b6d64b0a6222df247ac1ad37bc306aef810e8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 10:10:45 -0400 Subject: [PATCH 37/89] Only xfail on Windows --- distutils/tests/test_install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/tests/test_install.py b/distutils/tests/test_install.py index 7797cae716..b13d125d70 100644 --- a/distutils/tests/test_install.py +++ b/distutils/tests/test_install.py @@ -214,7 +214,7 @@ def test_record(self): ] assert found == expected - @pytest.mark.xfail(reason="#166") + @pytest.mark.xfail('platform.system == "Windows"', reason="#166") def test_record_extensions(self): cmd = test_support.missing_compiler_executable() if cmd is not None: From 45cbb60d3ea53a918c0439adb08b131f14bd5f8f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 11:39:24 -0400 Subject: [PATCH 38/89] Allow overriding toxworkdir with an env var. --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index 0fe47fff41..21a97781ba 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,7 @@ [tox] minversion = 3.25 +toxworkdir={env:TOX_WORK_DIR:.tox} + [testenv] deps = From 31f631097de0c0c3e38beb98f2c9f6851732c13a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 11:41:16 -0400 Subject: [PATCH 39/89] Ensure sys.version is restored in test_cygwinccompiler. Fixes #166. --- distutils/tests/test_cygwinccompiler.py | 1 + distutils/tests/test_install.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/tests/test_cygwinccompiler.py b/distutils/tests/test_cygwinccompiler.py index 1825bb202f..c6ec404949 100644 --- a/distutils/tests/test_cygwinccompiler.py +++ b/distutils/tests/test_cygwinccompiler.py @@ -21,6 +21,7 @@ def stuff(request, monkeypatch, distutils_managed_tempdir): self = request.instance self.python_h = os.path.join(self.mkdtemp(), 'python.h') monkeypatch.setattr(sysconfig, 'get_config_h_filename', self._get_config_h_filename) + monkeypatch.setattr(sys, 'version', sys.version) class TestCygwinCCompiler(support.TempdirManager): diff --git a/distutils/tests/test_install.py b/distutils/tests/test_install.py index b13d125d70..32a18b2f2f 100644 --- a/distutils/tests/test_install.py +++ b/distutils/tests/test_install.py @@ -214,7 +214,6 @@ def test_record(self): ] assert found == expected - @pytest.mark.xfail('platform.system == "Windows"', reason="#166") def test_record_extensions(self): cmd = test_support.missing_compiler_executable() if cmd is not None: From 1dafb52f7f41a04e522f4c034103d4d8975dc97b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 13:10:28 -0400 Subject: [PATCH 40/89] Run test_xx in process, utilizing import_helper On Windows, move the extension module to another temporary directory. --- distutils/tests/py38compat.py | 12 +++++ distutils/tests/test_build_ext.py | 75 ++++++++++++++++++------------- 2 files changed, 56 insertions(+), 31 deletions(-) diff --git a/distutils/tests/py38compat.py b/distutils/tests/py38compat.py index 96f93a31c6..35ddbb5bde 100644 --- a/distutils/tests/py38compat.py +++ b/distutils/tests/py38compat.py @@ -42,5 +42,17 @@ ) +try: + from test.support.import_helper import ( + DirsOnSysPath, + CleanImport, + ) +except (ModuleNotFoundError, ImportError): + from test.support import ( + DirsOnSysPath, + CleanImport, + ) + + if sys.version_info < (3, 9): requires_zlib = lambda: test.support.requires_zlib diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index 62f2ffe32e..63f62a84b0 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -3,6 +3,9 @@ from io import StringIO import textwrap import site +import contextlib +import platform +import tempfile from distutils.core import Distribution from distutils.command.build_ext import build_ext @@ -23,7 +26,7 @@ from test import support from . import py38compat as os_helper -from test.support.script_helper import assert_python_ok +from . import py38compat as import_helper import pytest import re @@ -49,6 +52,26 @@ def user_site_dir(request): build_ext.USER_BASE = orig_user_base +@contextlib.contextmanager +def cleanup(mod): + """ + Tests will fail to tear down an extension module if it's been imported. + + Move the file to a temporary directory that won't be cleaned up. + """ + try: + yield + finally: + filename = sys.modules[mod].__file__ + if platform.system() != "Windows": + return + dest = os.path.join( + tempfile.mkdtemp(prefix='deleteme'), os.path.basename(filename) + ) + os.rename(filename, dest) + # TODO: can the file be scheduled for deletion? + + @pytest.mark.usefixtures('user_site_dir') class TestBuildExt(TempdirManager, LoggingSilencer): def build_ext(self, *args, **kwargs): @@ -76,36 +99,26 @@ def test_build_ext(self): finally: sys.stdout = old_stdout - code = textwrap.dedent( - f""" - tmp_dir = {self.tmp_dir!r} - - import sys - import unittest - from test import support - - sys.path.insert(0, tmp_dir) - import xx - - class Tests(unittest.TestCase): - def test_xx(self): - for attr in ('error', 'foo', 'new', 'roj'): - self.assertTrue(hasattr(xx, attr)) - - self.assertEqual(xx.foo(2, 5), 7) - self.assertEqual(xx.foo(13,15), 28) - self.assertEqual(xx.new().demo(), None) - if support.HAVE_DOCSTRINGS: - doc = 'This is a template module just for instruction.' - self.assertEqual(xx.__doc__, doc) - self.assertIsInstance(xx.Null(), xx.Null) - self.assertIsInstance(xx.Str(), xx.Str) - - - unittest.main() - """ - ) - assert_python_ok('-c', code) + with import_helper.CleanImport('xx'): + with import_helper.DirsOnSysPath(self.tmp_dir): + self._test_xx() + + @staticmethod + @cleanup('xx') + def _test_xx(): + import xx + + for attr in ('error', 'foo', 'new', 'roj'): + assert hasattr(xx, attr) + + assert xx.foo(2, 5) == 7 + assert xx.foo(13, 15) == 28 + assert xx.new().demo() is None + if support.HAVE_DOCSTRINGS: + doc = 'This is a template module just for instruction.' + assert xx.__doc__ == doc + assert isinstance(xx.Null(), xx.Null) + assert isinstance(xx.Str(), xx.Str) def test_solaris_enable_shared(self): dist = Distribution({'name': 'xx'}) From 6757e55a06a14de2cb8fc8d5bb1d5cc562637167 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 15:17:52 -0400 Subject: [PATCH 41/89] Redirect extension module to a directory that's not deleted on Windows. --- distutils/tests/test_build_ext.py | 43 +++++++++++++++++++------------ 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index 63f62a84b0..e4cc7dc56b 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -6,6 +6,8 @@ import contextlib import platform import tempfile +import importlib +import shutil from distutils.core import Distribution from distutils.command.build_ext import build_ext @@ -53,23 +55,32 @@ def user_site_dir(request): @contextlib.contextmanager -def cleanup(mod): +def safe_extension_import(name, path): + with import_helper.CleanImport(name): + with extension_redirect(name, path) as new_path: + with import_helper.DirsOnSysPath(new_path): + yield + + +@contextlib.contextmanager +def extension_redirect(mod, path): """ Tests will fail to tear down an extension module if it's been imported. - Move the file to a temporary directory that won't be cleaned up. + Before importing, copy the file to a temporary directory that won't + be cleaned up. Yield the new path. """ - try: - yield - finally: - filename = sys.modules[mod].__file__ - if platform.system() != "Windows": - return - dest = os.path.join( - tempfile.mkdtemp(prefix='deleteme'), os.path.basename(filename) - ) - os.rename(filename, dest) - # TODO: can the file be scheduled for deletion? + if platform.system() != "Windows": + yield path + return + with import_helper.DirsOnSysPath(path): + spec = importlib.util.find_spec(mod) + filename = os.path.basename(spec.origin) + trash_dir = tempfile.mkdtemp(prefix='deleteme') + dest = os.path.join(trash_dir, os.path.basename(filename)) + shutil.copy(spec.origin, dest) + yield trash_dir + # TODO: can the file be scheduled for deletion? @pytest.mark.usefixtures('user_site_dir') @@ -99,12 +110,10 @@ def test_build_ext(self): finally: sys.stdout = old_stdout - with import_helper.CleanImport('xx'): - with import_helper.DirsOnSysPath(self.tmp_dir): - self._test_xx() + with safe_extension_import('xx', self.tmp_dir): + self._test_xx() @staticmethod - @cleanup('xx') def _test_xx(): import xx From 41eeed0a15fe046d6cf579264cfbe646d8772190 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 15:27:39 -0400 Subject: [PATCH 42/89] Include cygwin --- distutils/tests/test_build_ext.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index e4cc7dc56b..e60814ff64 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -70,7 +70,7 @@ def extension_redirect(mod, path): Before importing, copy the file to a temporary directory that won't be cleaned up. Yield the new path. """ - if platform.system() != "Windows": + if platform.system() != "Windows" and sys.platform != "cygwin": yield path return with import_helper.DirsOnSysPath(path): From 43ab8936039b19fbb2b5461e17a2bdb32e5e728d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 19:45:40 -0400 Subject: [PATCH 43/89] Prefer pytest in test_build_py --- distutils/tests/test_build_py.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/distutils/tests/test_build_py.py b/distutils/tests/test_build_py.py index cab5c65b65..68e26bc9d0 100644 --- a/distutils/tests/test_build_py.py +++ b/distutils/tests/test_build_py.py @@ -2,12 +2,13 @@ import os import sys -import unittest +from unittest.mock import patch + +import pytest from distutils.command.build_py import build_py from distutils.core import Distribution from distutils.errors import DistutilsFileError -from unittest.mock import patch from distutils.tests import support @@ -86,7 +87,7 @@ def test_empty_package_dir(self): except DistutilsFileError: self.fail("failed package_data test when package_dir is ''") - @unittest.skipIf(sys.dont_write_bytecode, 'byte-compile disabled') + @pytest.mark.skipif('sys.dont_write_bytecode') def test_byte_compile(self): project_dir, dist = self.create_dist(py_modules=['boiledeggs']) os.chdir(project_dir) @@ -102,7 +103,7 @@ def test_byte_compile(self): found = os.listdir(os.path.join(cmd.build_lib, '__pycache__')) assert found == ['boiledeggs.%s.pyc' % sys.implementation.cache_tag] - @unittest.skipIf(sys.dont_write_bytecode, 'byte-compile disabled') + @pytest.mark.skipif('sys.dont_write_bytecode') def test_byte_compile_optimized(self): project_dir, dist = self.create_dist(py_modules=['boiledeggs']) os.chdir(project_dir) From 02edee3630cc04d9cd443bfcf993dc31b7f89d7f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 20:02:52 -0400 Subject: [PATCH 44/89] Prefer pytest in test_check --- distutils/tests/test_check.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/distutils/tests/test_check.py b/distutils/tests/test_check.py index 7ad3cdfa8c..3e5f6034bf 100644 --- a/distutils/tests/test_check.py +++ b/distutils/tests/test_check.py @@ -1,12 +1,12 @@ """Tests for distutils.command.check.""" import os import textwrap -import unittest -from distutils.command.check import check, HAS_DOCUTILS +import pytest + +from distutils.command.check import check from distutils.tests import support from distutils.errors import DistutilsSetupError -import pytest try: import pygments @@ -102,8 +102,8 @@ def test_check_author_maintainer(self): cmd = self._run(metadata) assert cmd._warnings == 0 - @unittest.skipUnless(HAS_DOCUTILS, "won't test without docutils") def test_check_document(self): + pytest.importorskip('docutils') pkg_info, dist = self.create_dist() cmd = check(dist) @@ -117,8 +117,8 @@ def test_check_document(self): msgs = cmd._check_rst_data(rest) assert len(msgs) == 0 - @unittest.skipUnless(HAS_DOCUTILS, "won't test without docutils") def test_check_restructuredtext(self): + pytest.importorskip('docutils') # let's see if it detects broken rest in long_description broken_rest = 'title\n===\n\ntest' pkg_info, dist = self.create_dist(long_description=broken_rest) @@ -148,8 +148,8 @@ def test_check_restructuredtext(self): cmd = self._run(metadata, cwd=HERE, strict=1, restructuredtext=1) assert cmd._warnings == 0 - @unittest.skipUnless(HAS_DOCUTILS, "won't test without docutils") def test_check_restructuredtext_with_syntax_highlight(self): + pytest.importorskip('docutils') # Don't fail if there is a `code` or `code-block` directive example_rst_docs = [] From 1f95850df66ffd6ac350f6aadba4eb8920e5b183 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 20:03:59 -0400 Subject: [PATCH 45/89] Refactor imports around docutils. --- distutils/command/check.py | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/distutils/command/check.py b/distutils/command/check.py index aaf30713fe..c4e67c2cd1 100644 --- a/distutils/command/check.py +++ b/distutils/command/check.py @@ -2,17 +2,18 @@ Implements the Distutils 'check' command. """ +import contextlib + from distutils.core import Command from distutils.errors import DistutilsSetupError -try: - # docutils is installed - from docutils.utils import Reporter - from docutils.parsers.rst import Parser - from docutils import frontend - from docutils import nodes +with contextlib.suppress(ImportError): + import docutils.utils + import docutils.parsers.rst + import docutils.frontend + import docutils.nodes - class SilentReporter(Reporter): + class SilentReporter(docutils.utils.Reporter): def __init__( self, source, @@ -30,16 +31,10 @@ def __init__( def system_message(self, level, message, *children, **kwargs): self.messages.append((level, message, children, kwargs)) - return nodes.system_message( + return docutils.nodes.system_message( message, level=level, type=self.levels[level], *children, **kwargs ) - HAS_DOCUTILS = True -except Exception: - # Catch all exceptions because exceptions besides ImportError probably - # indicate that docutils is not ported to Py3k. - HAS_DOCUTILS = False - class check(Command): """This command checks the meta-data of the package.""" @@ -81,7 +76,7 @@ def run(self): if self.metadata: self.check_metadata() if self.restructuredtext: - if HAS_DOCUTILS: + if 'docutils' in globals(): self.check_restructuredtext() elif self.strict: raise DistutilsSetupError('The docutils package is needed.') @@ -124,8 +119,10 @@ def _check_rst_data(self, data): """Returns warnings when the provided data doesn't compile.""" # the include and csv_table directives need this to be a path source_path = self.distribution.script_name or 'setup.py' - parser = Parser() - settings = frontend.OptionParser(components=(Parser,)).get_default_values() + parser = docutils.parsers.rst.Parser() + settings = docutils.frontend.OptionParser( + components=(docutils.parsers.rst.Parser,) + ).get_default_values() settings.tab_width = 4 settings.pep_references = None settings.rfc_references = None @@ -139,7 +136,7 @@ def _check_rst_data(self, data): error_handler=settings.error_encoding_error_handler, ) - document = nodes.document(settings, reporter, source=source_path) + document = docutils.nodes.document(settings, reporter, source=source_path) document.note_source(source_path, -1) try: parser.parse(data, document) From ac50c2fdfa2ff66a4b337b11f43ffa0b44683536 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 20:47:42 -0400 Subject: [PATCH 46/89] Replace addCleanup with monkeypatch. --- distutils/tests/test_register.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/distutils/tests/test_register.py b/distutils/tests/test_register.py index dd59ecd316..c1c52f985c 100644 --- a/distutils/tests/test_register.py +++ b/distutils/tests/test_register.py @@ -287,7 +287,7 @@ def test_strict(self): del register_module.input @unittest.skipUnless(docutils is not None, 'needs docutils') - def test_register_invalid_long_description(self): + def test_register_invalid_long_description(self, monkeypatch): description = ':funkie:`str`' # mimic Sphinx-specific markup metadata = { 'url': 'xxx', @@ -301,8 +301,7 @@ def test_register_invalid_long_description(self): cmd.ensure_finalized() cmd.strict = True inputs = Inputs('2', 'tarek', 'tarek@ziade.org') - register_module.input = inputs - self.addCleanup(delattr, register_module, 'input') + monkeypatch.setattr(register_module, 'input', inputs, raising=False) with pytest.raises(DistutilsSetupError): cmd.run() From 327e447f4ce509b57d6f700b0ad5e2f920650d9c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 20:08:37 -0400 Subject: [PATCH 47/89] Enable tests requiring docutils. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 21a97781ba..952c9b58c2 100644 --- a/tox.ini +++ b/tox.ini @@ -9,6 +9,7 @@ deps = jaraco.envs>=2.4 jaraco.path path + docutils commands = pytest {posargs} setenv = From 6a3710c6e124bc035f9e9cf3bdca13a225e0fce9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 21:17:35 -0400 Subject: [PATCH 48/89] Fix broken tests around docutils. --- distutils/command/check.py | 5 ++++- distutils/tests/test_register.py | 25 +++++++------------------ 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/distutils/command/check.py b/distutils/command/check.py index c4e67c2cd1..539481c946 100644 --- a/distutils/command/check.py +++ b/distutils/command/check.py @@ -77,7 +77,10 @@ def run(self): self.check_metadata() if self.restructuredtext: if 'docutils' in globals(): - self.check_restructuredtext() + try: + self.check_restructuredtext() + except TypeError as exc: + raise DistutilsSetupError(str(exc)) elif self.strict: raise DistutilsSetupError('The docutils package is needed.') diff --git a/distutils/tests/test_register.py b/distutils/tests/test_register.py index c1c52f985c..0a5765f1fd 100644 --- a/distutils/tests/test_register.py +++ b/distutils/tests/test_register.py @@ -1,12 +1,7 @@ """Tests for distutils.command.register.""" import os -import unittest import getpass import urllib -import warnings - - -from .py38compat import check_warnings from distutils.command import register as register_module from distutils.command.register import register @@ -100,6 +95,7 @@ def _get_cmd(self, metadata=None): 'author_email': 'xxx', 'name': 'xxx', 'version': 'xxx', + 'long_description': 'xxx', } pkg_info, dist = self.create_dist(**metadata) return register(dist) @@ -158,8 +154,8 @@ def _no_way(prompt=''): req1 = dict(self.conn.reqs[0].headers) req2 = dict(self.conn.reqs[1].headers) - assert req1['Content-length'] == '1359' - assert req2['Content-length'] == '1359' + assert req1['Content-length'] == '1358' + assert req2['Content-length'] == '1358' assert b'xxx' in self.conn.reqs[1].data def test_password_not_in_file(self): @@ -210,13 +206,14 @@ def test_password_reset(self): assert headers['Content-length'] == '290' assert b'tarek' in req.data - @unittest.skipUnless(docutils is not None, 'needs docutils') def test_strict(self): - # testing the script option + # testing the strict option # when on, the register command stops if # the metadata is incomplete or if # long_description is not reSt compliant + pytest.importorskip('docutils') + # empty metadata cmd = self._get_cmd({}) cmd.ensure_finalized() @@ -286,8 +283,8 @@ def test_strict(self): finally: del register_module.input - @unittest.skipUnless(docutils is not None, 'needs docutils') def test_register_invalid_long_description(self, monkeypatch): + pytest.importorskip('docutils') description = ':funkie:`str`' # mimic Sphinx-specific markup metadata = { 'url': 'xxx', @@ -306,14 +303,6 @@ def test_register_invalid_long_description(self, monkeypatch): with pytest.raises(DistutilsSetupError): cmd.run() - def test_check_metadata_deprecated(self): - # makes sure make_metadata is deprecated - cmd = self._get_cmd() - with check_warnings() as w: - warnings.simplefilter("always") - cmd.check_metadata() - assert len(w.warnings) == 1 - def test_list_classifiers(self): cmd = self._get_cmd() cmd.list_classifiers = 1 From c4244291b4ba55f765418386cdac566e9a59633a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 21:21:16 -0400 Subject: [PATCH 49/89] Convert PendingDeprecationWarnings to DeprecationWarnings. --- distutils/archive_util.py | 2 +- distutils/command/register.py | 6 +++--- distutils/tests/test_archive_util.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/distutils/archive_util.py b/distutils/archive_util.py index 4cb9bf3932..5dfe2a16ff 100644 --- a/distutils/archive_util.py +++ b/distutils/archive_util.py @@ -121,7 +121,7 @@ def _set_uid_gid(tarinfo): # compression using `compress` if compress == 'compress': - warn("'compress' will be deprecated.", PendingDeprecationWarning) + warn("'compress' is deprecated.", DeprecationWarning) # the option varies depending on the platform compressed_name = archive_name + compress_ext[compress] if sys.platform == 'win32': diff --git a/distutils/command/register.py b/distutils/command/register.py index 2c6424725d..c1402650d7 100644 --- a/distutils/command/register.py +++ b/distutils/command/register.py @@ -66,9 +66,9 @@ def run(self): def check_metadata(self): """Deprecated API.""" warn( - "distutils.command.register.check_metadata is deprecated, \ - use the check command instead", - PendingDeprecationWarning, + "distutils.command.register.check_metadata is deprecated; " + "use the check command instead", + DeprecationWarning, ) check = self.distribution.get_command_obj('check') check.ensure_finalized() diff --git a/distutils/tests/test_archive_util.py b/distutils/tests/test_archive_util.py index 17a528bcf3..72aa9d7c7b 100644 --- a/distutils/tests/test_archive_util.py +++ b/distutils/tests/test_archive_util.py @@ -195,7 +195,7 @@ def test_compress_deprecated(self): tmpdir = self._create_files() base_name = os.path.join(self.mkdtemp(), 'archive') - # using compress and testing the PendingDeprecationWarning + # using compress and testing the DeprecationWarning old_dir = os.getcwd() os.chdir(tmpdir) try: From e1a17fab1e516d282426e91ff80c318ac7ff1323 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 21:42:34 -0400 Subject: [PATCH 50/89] Ignore unactionable warnings in docutils. --- pytest.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pytest.ini b/pytest.ini index dba42e75a4..0191f5ee83 100644 --- a/pytest.ini +++ b/pytest.ini @@ -4,3 +4,7 @@ filterwarnings= # acknowledge that TestDistribution isn't a test ignore:cannot collect test class 'TestDistribution' ignore:Fallback spawn triggered + + # ignore spurious and unactionable warnings + ignore:The frontend.OptionParser class will be replaced by a subclass of argparse.ArgumentParser in Docutils 0.21 or later.:DeprecationWarning: + ignore: The frontend.Option class will be removed in Docutils 0.21 or later.:DeprecationWarning: From ebed3ab18d5846b25003535217bfe47bbb4e26fa Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 21:43:43 -0400 Subject: [PATCH 51/89] Prefer pytest for skip --- distutils/tests/test_config_cmd.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/distutils/tests/test_config_cmd.py b/distutils/tests/test_config_cmd.py index 14e6e03252..65c60f64dd 100644 --- a/distutils/tests/test_config_cmd.py +++ b/distutils/tests/test_config_cmd.py @@ -1,5 +1,4 @@ """Tests for distutils.command.config.""" -import unittest import os import sys from test.support import missing_compiler_executable @@ -35,7 +34,7 @@ def test_dump_file(self): dump_file(this_file, 'I am the header') assert len(self._logs) == numlines + 1 - @unittest.skipIf(sys.platform == 'win32', "can't test on Windows") + @pytest.mark.skipif('platform.system() == "Windows"') def test_search_cpp(self): cmd = missing_compiler_executable(['preprocessor']) if cmd is not None: From 28a889668246de38e4417ea88a369307e5f26ede Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 21:48:09 -0400 Subject: [PATCH 52/89] Prefer pytest for skip --- distutils/tests/test_cygwinccompiler.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/distutils/tests/test_cygwinccompiler.py b/distutils/tests/test_cygwinccompiler.py index c6ec404949..ef01ae2199 100644 --- a/distutils/tests/test_cygwinccompiler.py +++ b/distutils/tests/test_cygwinccompiler.py @@ -1,5 +1,4 @@ """Tests for distutils.cygwinccompiler.""" -import unittest import sys import os @@ -28,10 +27,8 @@ class TestCygwinCCompiler(support.TempdirManager): def _get_config_h_filename(self): return self.python_h - @unittest.skipIf(sys.platform != "cygwin", "Not running on Cygwin") - @unittest.skipIf( - not os.path.exists("/usr/lib/libbash.dll.a"), "Don't know a linkable library" - ) + @pytest.mark.skipif('sys.platform != "cygwin"') + @pytest.mark.skipif('not os.path.exists("/usr/lib/libbash.dll.a")') def test_find_library_file(self): from distutils.cygwinccompiler import CygwinCCompiler @@ -42,7 +39,7 @@ def test_find_library_file(self): assert os.path.exists(linkable_file) assert linkable_file == f"/usr/lib/lib{link_name:s}.dll.a" - @unittest.skipIf(sys.platform != "cygwin", "Not running on Cygwin") + @pytest.mark.skipif('sys.platform != "cygwin"') def test_runtime_library_dir_option(self): from distutils.cygwinccompiler import CygwinCCompiler From 61ec7e4a2ddf7128fbbe892fe27f435ff664fe47 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 21:56:01 -0400 Subject: [PATCH 53/89] Prefer pytest for skip --- distutils/tests/test_dir_util.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/distutils/tests/test_dir_util.py b/distutils/tests/test_dir_util.py index 715fdb96ba..173a22402d 100644 --- a/distutils/tests/test_dir_util.py +++ b/distutils/tests/test_dir_util.py @@ -1,5 +1,4 @@ """Tests for distutils.dir_util.""" -import unittest import os import stat import sys @@ -53,10 +52,7 @@ def test_mkpath_remove_tree_verbosity(self): wanted = ["removing '%s' (and everything under it)" % self.root_target] assert self._logs == wanted - @unittest.skipIf( - sys.platform.startswith('win'), - "This test is only appropriate for POSIX-like systems.", - ) + @pytest.mark.skipif("platform.system() == 'Windows'") def test_mkpath_with_custom_mode(self): # Get and set the current umask value for testing mode bits. umask = os.umask(0o002) From 34e6c216fb060dc7801fcb09783385fc26a535d8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 21:57:19 -0400 Subject: [PATCH 54/89] Prefer pytest for skip --- distutils/tests/test_dist.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/distutils/tests/test_dist.py b/distutils/tests/test_dist.py index c962d3f3ac..13ab040eb3 100644 --- a/distutils/tests/test_dist.py +++ b/distutils/tests/test_dist.py @@ -2,7 +2,6 @@ import os import io import sys -import unittest import warnings import textwrap import functools @@ -89,9 +88,9 @@ def test_command_packages_cmdline(self, clear_argv): assert isinstance(cmd, test_dist) assert cmd.sample_option == "sometext" - @unittest.skipIf( + @pytest.mark.skipif( 'distutils' not in Distribution.parse_config_files.__module__, - 'Cannot test when virtualenv has monkey-patched Distribution.', + reason='Cannot test when virtualenv has monkey-patched Distribution', ) def test_venv_install_options(self, request): sys.argv.append("install") From be281f52f5dde3fd34def6118cc77ed5a08956da Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 21:57:55 -0400 Subject: [PATCH 55/89] Prefer pytest for skip --- distutils/tests/test_install_lib.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/distutils/tests/test_install_lib.py b/distutils/tests/test_install_lib.py index d8192e06ce..a654a66a79 100644 --- a/distutils/tests/test_install_lib.py +++ b/distutils/tests/test_install_lib.py @@ -2,7 +2,6 @@ import sys import os import importlib.util -import unittest import pytest @@ -38,7 +37,7 @@ def test_finalize_options(self): cmd.finalize_options() assert cmd.optimize == 2 - @unittest.skipIf(sys.dont_write_bytecode, 'byte-compile disabled') + @pytest.mark.skipif('sys.dont_write_bytecode') def test_byte_compile(self): project_dir, dist = self.create_dist() os.chdir(project_dir) From d404b3a61f94da5dd81f04948d6d9be64cd2dba2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 22:00:13 -0400 Subject: [PATCH 56/89] Convert TestLog to pytest. --- distutils/tests/test_log.py | 73 +++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/distutils/tests/test_log.py b/distutils/tests/test_log.py index 614da574ae..7aeee4057f 100644 --- a/distutils/tests/test_log.py +++ b/distutils/tests/test_log.py @@ -2,50 +2,51 @@ import io import sys -import unittest from test.support import swap_attr +import pytest + from distutils import log -class TestLog(unittest.TestCase): - def test_non_ascii(self): - # Issues #8663, #34421: test that non-encodable text is escaped with - # backslashreplace error handler and encodable non-ASCII text is - # output as is. - for errors in ( +class TestLog: + @pytest.mark.parametrize( + 'errors', + ( 'strict', 'backslashreplace', 'surrogateescape', 'replace', 'ignore', - ): - with self.subTest(errors=errors): - stdout = io.TextIOWrapper(io.BytesIO(), encoding='cp437', errors=errors) - stderr = io.TextIOWrapper(io.BytesIO(), encoding='cp437', errors=errors) - old_threshold = log.set_threshold(log.DEBUG) - try: - with swap_attr(sys, 'stdout', stdout), swap_attr( - sys, 'stderr', stderr - ): - log.debug('Dεbug\tMėssãge') - log.fatal('Fαtal\tÈrrōr') - finally: - log.set_threshold(old_threshold) + ), + ) + def test_non_ascii(self, errors): + # Issues #8663, #34421: test that non-encodable text is escaped with + # backslashreplace error handler and encodable non-ASCII text is + # output as is. + stdout = io.TextIOWrapper(io.BytesIO(), encoding='cp437', errors=errors) + stderr = io.TextIOWrapper(io.BytesIO(), encoding='cp437', errors=errors) + old_threshold = log.set_threshold(log.DEBUG) + try: + with swap_attr(sys, 'stdout', stdout), swap_attr(sys, 'stderr', stderr): + log.debug('Dεbug\tMėssãge') + log.fatal('Fαtal\tÈrrōr') + finally: + log.set_threshold(old_threshold) - stdout.seek(0) - assert stdout.read().rstrip() == ( - 'Dεbug\tM?ss?ge' - if errors == 'replace' - else 'Dεbug\tMssge' - if errors == 'ignore' - else 'Dεbug\tM\\u0117ss\\xe3ge' - ) - stderr.seek(0) - assert stderr.read().rstrip() == ( - 'Fαtal\t?rr?r' - if errors == 'replace' - else 'Fαtal\trrr' - if errors == 'ignore' - else 'Fαtal\t\\xc8rr\\u014dr' - ) + stdout.seek(0) + assert stdout.read().rstrip() == ( + 'Dεbug\tM?ss?ge' + if errors == 'replace' + else 'Dεbug\tMssge' + if errors == 'ignore' + else 'Dεbug\tM\\u0117ss\\xe3ge' + ) + stderr.seek(0) + assert stderr.read().rstrip() == ( + 'Fαtal\t?rr?r' + if errors == 'replace' + else 'Fαtal\trrr' + if errors == 'ignore' + else 'Fαtal\t\\xc8rr\\u014dr' + ) From b321a6255dcf935ff30dc8be19ec04cd31c10b24 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 22:01:52 -0400 Subject: [PATCH 57/89] Prefer pytest for skip --- distutils/tests/test_msvccompiler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/distutils/tests/test_msvccompiler.py b/distutils/tests/test_msvccompiler.py index db4694e1b3..3d5fc92792 100644 --- a/distutils/tests/test_msvccompiler.py +++ b/distutils/tests/test_msvccompiler.py @@ -58,7 +58,7 @@ def test_get_vc2017(self): assert version >= 15 assert os.path.isdir(path) else: - raise unittest.SkipTest("VS 2017 is not installed") + pytest.skip("VS 2017 is not installed") @needs_winreg def test_get_vc2015(self): @@ -69,7 +69,7 @@ def test_get_vc2015(self): assert version >= 14 assert os.path.isdir(path) else: - raise unittest.SkipTest("VS 2015 is not installed") + pytest.skip("VS 2015 is not installed") class CheckThread(threading.Thread): From 8f53164050b20c78fe943592924d688d73867eee Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 22:05:13 -0400 Subject: [PATCH 58/89] Consolidate tests --- distutils/tests/test_msvccompiler.py | 31 ++++++++++------------------ 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/distutils/tests/test_msvccompiler.py b/distutils/tests/test_msvccompiler.py index 3d5fc92792..a7229c5054 100644 --- a/distutils/tests/test_msvccompiler.py +++ b/distutils/tests/test_msvccompiler.py @@ -50,26 +50,17 @@ def test_get_vc_env_unicode(self): os.environ['DISTUTILS_USE_SDK'] = old_distutils_use_sdk @needs_winreg - def test_get_vc2017(self): - # This function cannot be mocked, so pass it if we find VS 2017 - # and mark it skipped if we do not. - version, path = _msvccompiler._find_vc2017() - if version: - assert version >= 15 - assert os.path.isdir(path) - else: - pytest.skip("VS 2017 is not installed") - - @needs_winreg - def test_get_vc2015(self): - # This function cannot be mocked, so pass it if we find VS 2015 - # and mark it skipped if we do not. - version, path = _msvccompiler._find_vc2015() - if version: - assert version >= 14 - assert os.path.isdir(path) - else: - pytest.skip("VS 2015 is not installed") + @pytest.mark.parametrize('ver', (2015, 2017)) + def test_get_vc(self, ver): + # This function cannot be mocked, so pass if VC is found + # and skip otherwise. + lookup = getattr(_msvccompiler, f'_find_vc{ver}') + expected_version = {2015: 14, 2017: 15}[ver] + version, path = lookup() + if not version: + pytest.skip(f"VS {ver} is not installed") + assert version >= expected_version + assert os.path.isdir(path) class CheckThread(threading.Thread): From e69b76190619ad35fd1d1047214f3699e65a2524 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 22:08:18 -0400 Subject: [PATCH 59/89] Prefer pytest for skip --- distutils/tests/test_sdist.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/distutils/tests/test_sdist.py b/distutils/tests/test_sdist.py index fa4dfa24eb..b11fe7c41e 100644 --- a/distutils/tests/test_sdist.py +++ b/distutils/tests/test_sdist.py @@ -1,7 +1,6 @@ """Tests for distutils.command.sdist.""" import os import tarfile -import unittest import warnings import zipfile from os.path import join @@ -19,7 +18,7 @@ from distutils.core import Distribution from distutils.tests.test_config import BasePyPIRCCommandTestCase from distutils.errors import DistutilsOptionError -from distutils.spawn import find_executable +from distutils.spawn import find_executable # noqa: F401 from distutils.log import WARN from distutils.filelist import FileList from distutils.archive_util import ARCHIVE_FORMATS @@ -133,8 +132,8 @@ def test_prune_file_list(self): assert sorted(content) == ['fake-1.0/' + x for x in expected] @pytest.mark.usefixtures('needs_zlib') - @unittest.skipIf(find_executable('tar') is None, "The tar command is not found") - @unittest.skipIf(find_executable('gzip') is None, "The gzip command is not found") + @pytest.mark.skipif("not find_executable('tar')") + @pytest.mark.skipif("not find_executable('gzip')") def test_make_distribution(self): # now building a sdist dist, cmd = self.get_cmd() @@ -341,7 +340,7 @@ def test_invalid_template_wrong_arguments(self): # this manifest command takes one argument self._check_template('prune') - @unittest.skipIf(os.name != 'nt', 'test relevant for Windows only') + @pytest.mark.skipif("platform.system() != 'Windows'") def test_invalid_template_wrong_path(self): # on Windows, trailing slashes are not allowed # this used to crash instead of raising a warning: #8286 @@ -466,8 +465,8 @@ def test_manual_manifest(self): @pytest.mark.usefixtures('needs_zlib') @require_unix_id @require_uid_0 - @unittest.skipIf(find_executable('tar') is None, "The tar command is not found") - @unittest.skipIf(find_executable('gzip') is None, "The gzip command is not found") + @pytest.mark.skipif("not find_executable('tar')") + @pytest.mark.skipif("not find_executable('gzip')") def test_make_distribution_owner_group(self): # now building a sdist dist, cmd = self.get_cmd() From 3d5bd7c778596bf594291a4a8db7420d4f754d68 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 22:19:19 -0400 Subject: [PATCH 60/89] Prefer pytest for skip --- distutils/tests/test_sysconfig.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py index 3746676289..25137911a0 100644 --- a/distutils/tests/test_sysconfig.py +++ b/distutils/tests/test_sysconfig.py @@ -32,12 +32,8 @@ def test_get_config_h_filename(self): config_h = sysconfig.get_config_h_filename() assert os.path.isfile(config_h), config_h - @unittest.skipIf( - sys.platform == 'win32', 'Makefile only exists on Unix like systems' - ) - @unittest.skipIf( - sys.implementation.name != 'cpython', 'Makefile only exists in CPython' - ) + @pytest.mark.skipif("platform.system() == 'Windows'") + @pytest.mark.skipif("sys.implementation.name != 'cpython'") def test_get_makefile_filename(self): makefile = sysconfig.get_makefile_filename() assert os.path.isfile(makefile), makefile From 61c65f5beed21f4ee8ea5dfa43313fc45de12589 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 22:19:45 -0400 Subject: [PATCH 61/89] Convert unconditional skip to conditional skip. Mark test as xfail because it's failing. --- distutils/tests/test_sysconfig.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py index 25137911a0..a973b03b0f 100644 --- a/distutils/tests/test_sysconfig.py +++ b/distutils/tests/test_sysconfig.py @@ -49,7 +49,8 @@ def test_get_config_vars(self): assert isinstance(cvars, dict) assert cvars - @unittest.skip('sysconfig.IS_PYPY') + @pytest.mark.skipif('sysconfig.IS_PYPY') + @pytest.mark.xfail(reason="broken") def test_srcdir(self): # See Issues #15322, #15364. srcdir = sysconfig.get_config_var('srcdir') From d444c8c35e4f552636c0dc8888a2066eee825234 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 22:27:43 -0400 Subject: [PATCH 62/89] Prefer pytest for skip --- distutils/tests/test_sysconfig.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py index a973b03b0f..fdc0f3b3fb 100644 --- a/distutils/tests/test_sysconfig.py +++ b/distutils/tests/test_sysconfig.py @@ -231,10 +231,7 @@ def test_sysconfig_compiler_vars(self): ) assert global_sysconfig.get_config_var('CC') == sysconfig.get_config_var('CC') - @unittest.skipIf( - sysconfig.get_config_var('EXT_SUFFIX') is None, - 'EXT_SUFFIX required for this test', - ) + @pytest.mark.skipif("not sysconfig.get_config_var('EXT_SUFFIX')") def test_SO_deprecation(self): with pytest.warns(DeprecationWarning): sysconfig.get_config_var('SO') @@ -274,21 +271,19 @@ def test_parse_config_h(self): result = sysconfig.parse_config_h(f) assert isinstance(result, dict) - @unittest.skipUnless(sys.platform == 'win32', 'Testing windows pyd suffix') - @unittest.skipUnless( - sys.implementation.name == 'cpython', 'Need cpython for this test' - ) + @pytest.mark.skipif("platform.system() != 'Windows'") + @pytest.mark.skipif( + "sys.implementation.name != 'cpython'") def test_win_ext_suffix(self): assert sysconfig.get_config_var("EXT_SUFFIX").endswith(".pyd") assert sysconfig.get_config_var("EXT_SUFFIX") != ".pyd" - @unittest.skipUnless(sys.platform == 'win32', 'Testing Windows build layout') - @unittest.skipUnless( - sys.implementation.name == 'cpython', 'Need cpython for this test' - ) - @unittest.skipUnless( - '\\PCbuild\\'.casefold() in sys.executable.casefold(), - 'Need sys.executable to be in a source tree', + @pytest.mark.skipif("platform.system() != 'Windows'") + @pytest.mark.skipif( + "sys.implementation.name != 'cpython'") + @pytest.mark.skipif( + '\\PCbuild\\'.casefold() not in sys.executable.casefold(), + reason='Need sys.executable to be in a source tree', ) def test_win_build_venv_from_source_tree(self): """Ensure distutils.sysconfig detects venvs from source tree builds.""" From 8e8d371757c49fa932a9e6bf709f68c7f83cf79d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 22:31:48 -0400 Subject: [PATCH 63/89] =?UTF-8?q?=E2=9A=AB=20Fade=20to=20black.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- distutils/tests/test_sysconfig.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py index fdc0f3b3fb..cede53b15c 100644 --- a/distutils/tests/test_sysconfig.py +++ b/distutils/tests/test_sysconfig.py @@ -272,15 +272,13 @@ def test_parse_config_h(self): assert isinstance(result, dict) @pytest.mark.skipif("platform.system() != 'Windows'") - @pytest.mark.skipif( - "sys.implementation.name != 'cpython'") + @pytest.mark.skipif("sys.implementation.name != 'cpython'") def test_win_ext_suffix(self): assert sysconfig.get_config_var("EXT_SUFFIX").endswith(".pyd") assert sysconfig.get_config_var("EXT_SUFFIX") != ".pyd" @pytest.mark.skipif("platform.system() != 'Windows'") - @pytest.mark.skipif( - "sys.implementation.name != 'cpython'") + @pytest.mark.skipif("sys.implementation.name != 'cpython'") @pytest.mark.skipif( '\\PCbuild\\'.casefold() not in sys.executable.casefold(), reason='Need sys.executable to be in a source tree', From 3e2c8ab7386d02b9fc24cb61b1d62a40ce6c2f20 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 22:32:08 -0400 Subject: [PATCH 64/89] Convert unix_compat to pytest skips --- distutils/tests/unix_compat.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/distutils/tests/unix_compat.py b/distutils/tests/unix_compat.py index 8250b36327..95fc8eebe2 100644 --- a/distutils/tests/unix_compat.py +++ b/distutils/tests/unix_compat.py @@ -1,5 +1,4 @@ import sys -import unittest try: import grp @@ -7,9 +6,13 @@ except ImportError: grp = pwd = None +import pytest + UNIX_ID_SUPPORT = grp and pwd UID_0_SUPPORT = UNIX_ID_SUPPORT and sys.platform != "cygwin" -require_unix_id = unittest.skipUnless(UNIX_ID_SUPPORT, "Requires grp and pwd support") -require_uid_0 = unittest.skipUnless(UID_0_SUPPORT, "Requires UID 0 support") +require_unix_id = pytest.mark.skipif( + not UNIX_ID_SUPPORT, reason="Requires grp and pwd support" +) +require_uid_0 = pytest.mark.skipif(not UID_0_SUPPORT, reason="Requires UID 0 support") From 9c8583254cd05c04790e5e01619180aabe73d6e4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 7 Aug 2022 09:02:42 -0400 Subject: [PATCH 65/89] Consistently import unittest.mock. --- distutils/_msvccompiler.py | 4 ++-- distutils/tests/test_build_py.py | 4 ++-- distutils/tests/test_dir_util.py | 5 ++--- distutils/tests/test_dist.py | 3 +-- distutils/tests/test_file_util.py | 14 ++++++++------ distutils/tests/test_msvccompiler.py | 4 ++-- distutils/tests/test_spawn.py | 20 +++++++++++--------- distutils/tests/test_unixccompiler.py | 8 ++++---- distutils/tests/test_util.py | 20 ++++++++++---------- 9 files changed, 42 insertions(+), 40 deletions(-) diff --git a/distutils/_msvccompiler.py b/distutils/_msvccompiler.py index 35c90942d2..516639aa1b 100644 --- a/distutils/_msvccompiler.py +++ b/distutils/_msvccompiler.py @@ -17,7 +17,7 @@ import subprocess import contextlib import warnings -import unittest.mock +import unittest.mock as mock with contextlib.suppress(ImportError): import winreg @@ -554,7 +554,7 @@ def _fallback_spawn(self, cmd, env): else: return warnings.warn("Fallback spawn triggered. Please update distutils monkeypatch.") - with unittest.mock.patch.dict('os.environ', env): + with mock.patch.dict('os.environ', env): bag.value = super().spawn(cmd) # -- Miscellaneous methods ----------------------------------------- diff --git a/distutils/tests/test_build_py.py b/distutils/tests/test_build_py.py index 68e26bc9d0..63543dcaa1 100644 --- a/distutils/tests/test_build_py.py +++ b/distutils/tests/test_build_py.py @@ -2,7 +2,7 @@ import os import sys -from unittest.mock import patch +import unittest.mock as mock import pytest @@ -167,7 +167,7 @@ def test_dont_write_bytecode(self): assert 'byte-compiling is disabled' in self.logs[0][1] % self.logs[0][2] - @patch("distutils.command.build_py.log.warn") + @mock.patch("distutils.command.build_py.log.warn") def test_namespace_package_does_not_warn(self, log_warn): """ Originally distutils implementation did not account for PEP 420 diff --git a/distutils/tests/test_dir_util.py b/distutils/tests/test_dir_util.py index 173a22402d..cd7e018f5e 100644 --- a/distutils/tests/test_dir_util.py +++ b/distutils/tests/test_dir_util.py @@ -1,8 +1,7 @@ """Tests for distutils.dir_util.""" import os import stat -import sys -from unittest.mock import patch +import unittest.mock as mock from distutils import dir_util, errors from distutils.dir_util import ( @@ -122,7 +121,7 @@ def test_copy_tree_exception_in_listdir(self): """ An exception in listdir should raise a DistutilsFileError """ - with patch("os.listdir", side_effect=OSError()), pytest.raises( + with mock.patch("os.listdir", side_effect=OSError()), pytest.raises( errors.DistutilsFileError ): src = self.tempdirs[-1] diff --git a/distutils/tests/test_dist.py b/distutils/tests/test_dist.py index 13ab040eb3..ddfaf92167 100644 --- a/distutils/tests/test_dist.py +++ b/distutils/tests/test_dist.py @@ -5,8 +5,7 @@ import warnings import textwrap import functools - -from unittest import mock +import unittest.mock as mock import pytest diff --git a/distutils/tests/test_file_util.py b/distutils/tests/test_file_util.py index 3ad4bdd103..b2e83c52f2 100644 --- a/distutils/tests/test_file_util.py +++ b/distutils/tests/test_file_util.py @@ -1,7 +1,7 @@ """Tests for distutils.file_util.""" import os import errno -from unittest.mock import patch +import unittest.mock as mock from distutils.file_util import move_file, copy_file from distutils import log @@ -59,7 +59,7 @@ def test_move_file_verbosity(self): def test_move_file_exception_unpacking_rename(self): # see issue 22182 - with patch("os.rename", side_effect=OSError("wrong", 1)), pytest.raises( + with mock.patch("os.rename", side_effect=OSError("wrong", 1)), pytest.raises( DistutilsFileError ): with open(self.source, 'w') as fobj: @@ -68,9 +68,11 @@ def test_move_file_exception_unpacking_rename(self): def test_move_file_exception_unpacking_unlink(self): # see issue 22182 - with patch("os.rename", side_effect=OSError(errno.EXDEV, "wrong")), patch( - "os.unlink", side_effect=OSError("wrong", 1) - ), pytest.raises(DistutilsFileError): + with mock.patch( + "os.rename", side_effect=OSError(errno.EXDEV, "wrong") + ), mock.patch("os.unlink", side_effect=OSError("wrong", 1)), pytest.raises( + DistutilsFileError + ): with open(self.source, 'w') as fobj: fobj.write('spam eggs') move_file(self.source, self.target, verbose=0) @@ -102,7 +104,7 @@ def test_copy_file_hard_link_failure(self): with open(self.source, 'w') as f: f.write('some content') st = os.stat(self.source) - with patch("os.link", side_effect=OSError(0, "linking unsupported")): + with mock.patch("os.link", side_effect=OSError(0, "linking unsupported")): copy_file(self.source, self.target, link='hard') st2 = os.stat(self.source) st3 = os.stat(self.target) diff --git a/distutils/tests/test_msvccompiler.py b/distutils/tests/test_msvccompiler.py index a7229c5054..f63537b8e5 100644 --- a/distutils/tests/test_msvccompiler.py +++ b/distutils/tests/test_msvccompiler.py @@ -1,8 +1,8 @@ """Tests for distutils._msvccompiler.""" import sys -import unittest import os import threading +import unittest.mock as mock import pytest @@ -109,7 +109,7 @@ def CCompiler_spawn(self, cmd): "A spawn without an env argument." assert os.environ["PATH"] == "expected" - with unittest.mock.patch.object(ccompiler.CCompiler, 'spawn', CCompiler_spawn): + with mock.patch.object(ccompiler.CCompiler, 'spawn', CCompiler_spawn): compiler.spawn(["n/a"]) assert os.environ.get("PATH") != "expected" diff --git a/distutils/tests/test_spawn.py b/distutils/tests/test_spawn.py index c28b8ba594..b68c7d571d 100644 --- a/distutils/tests/test_spawn.py +++ b/distutils/tests/test_spawn.py @@ -2,7 +2,9 @@ import os import stat import sys -import unittest.mock +import unittest +import unittest.mock as mock + from test.support import unix_shell from . import py38compat as os_helper @@ -78,9 +80,9 @@ def test_find_executable(self): # PATH='': no match, except in the current directory with os_helper.EnvironmentVarGuard() as env: env['PATH'] = '' - with unittest.mock.patch( + with mock.patch( 'distutils.spawn.os.confstr', return_value=tmp_dir, create=True - ), unittest.mock.patch('distutils.spawn.os.defpath', tmp_dir): + ), mock.patch('distutils.spawn.os.defpath', tmp_dir): rv = find_executable(program) assert rv is None @@ -92,9 +94,9 @@ def test_find_executable(self): # PATH=':': explicitly looks in the current directory with os_helper.EnvironmentVarGuard() as env: env['PATH'] = os.pathsep - with unittest.mock.patch( + with mock.patch( 'distutils.spawn.os.confstr', return_value='', create=True - ), unittest.mock.patch('distutils.spawn.os.defpath', ''): + ), mock.patch('distutils.spawn.os.defpath', ''): rv = find_executable(program) assert rv is None @@ -108,16 +110,16 @@ def test_find_executable(self): env.pop('PATH', None) # without confstr - with unittest.mock.patch( + with mock.patch( 'distutils.spawn.os.confstr', side_effect=ValueError, create=True - ), unittest.mock.patch('distutils.spawn.os.defpath', tmp_dir): + ), mock.patch('distutils.spawn.os.defpath', tmp_dir): rv = find_executable(program) assert rv == filename # with confstr - with unittest.mock.patch( + with mock.patch( 'distutils.spawn.os.confstr', return_value=tmp_dir, create=True - ), unittest.mock.patch('distutils.spawn.os.defpath', ''): + ), mock.patch('distutils.spawn.os.defpath', ''): rv = find_executable(program) assert rv == filename diff --git a/distutils/tests/test_unixccompiler.py b/distutils/tests/test_unixccompiler.py index 424a9267c5..970ca71f90 100644 --- a/distutils/tests/test_unixccompiler.py +++ b/distutils/tests/test_unixccompiler.py @@ -1,7 +1,7 @@ """Tests for distutils.unixccompiler.""" import os import sys -from unittest.mock import patch +import unittest.mock as mock from .py38compat import EnvironmentVarGuard @@ -257,11 +257,11 @@ def gcvs(*args, _orig=sysconfig.get_config_vars): sysconfig.get_config_var = gcv sysconfig.get_config_vars = gcvs - with patch.object( + with mock.patch.object( self.cc, 'spawn', return_value=None - ) as mock_spawn, patch.object( + ) as mock_spawn, mock.patch.object( self.cc, '_need_link', return_value=True - ), patch.object( + ), mock.patch.object( self.cc, 'mkpath', return_value=None ), EnvironmentVarGuard() as env: env['CC'] = 'ccache my_cc' diff --git a/distutils/tests/test_util.py b/distutils/tests/test_util.py index 06c835e280..8a78456470 100644 --- a/distutils/tests/test_util.py +++ b/distutils/tests/test_util.py @@ -3,8 +3,8 @@ import sys import unittest import sysconfig as stdlib_sysconfig +import unittest.mock as mock from copy import copy -from unittest import mock import pytest @@ -40,24 +40,24 @@ def environment(monkeypatch): @pytest.mark.usefixtures('save_env') class TestUtil: def test_get_host_platform(self): - with unittest.mock.patch('os.name', 'nt'): - with unittest.mock.patch('sys.version', '... [... (ARM64)]'): + with mock.patch('os.name', 'nt'): + with mock.patch('sys.version', '... [... (ARM64)]'): assert get_host_platform() == 'win-arm64' - with unittest.mock.patch('sys.version', '... [... (ARM)]'): + with mock.patch('sys.version', '... [... (ARM)]'): assert get_host_platform() == 'win-arm32' - with unittest.mock.patch('sys.version_info', (3, 9, 0, 'final', 0)): + with mock.patch('sys.version_info', (3, 9, 0, 'final', 0)): assert get_host_platform() == stdlib_sysconfig.get_platform() def test_get_platform(self): - with unittest.mock.patch('os.name', 'nt'): - with unittest.mock.patch.dict('os.environ', {'VSCMD_ARG_TGT_ARCH': 'x86'}): + with mock.patch('os.name', 'nt'): + with mock.patch.dict('os.environ', {'VSCMD_ARG_TGT_ARCH': 'x86'}): assert get_platform() == 'win32' - with unittest.mock.patch.dict('os.environ', {'VSCMD_ARG_TGT_ARCH': 'x64'}): + with mock.patch.dict('os.environ', {'VSCMD_ARG_TGT_ARCH': 'x64'}): assert get_platform() == 'win-amd64' - with unittest.mock.patch.dict('os.environ', {'VSCMD_ARG_TGT_ARCH': 'arm'}): + with mock.patch.dict('os.environ', {'VSCMD_ARG_TGT_ARCH': 'arm'}): assert get_platform() == 'win-arm32' - with unittest.mock.patch.dict( + with mock.patch.dict( 'os.environ', {'VSCMD_ARG_TGT_ARCH': 'arm64'} ): assert get_platform() == 'win-arm64' From 73350d29553b954cf68922cb9b06eb4fc3f94074 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 7 Aug 2022 09:05:10 -0400 Subject: [PATCH 66/89] Prefer pytest for skip --- distutils/tests/test_spawn.py | 3 +-- distutils/tests/test_sysconfig.py | 9 ++------- distutils/tests/test_util.py | 3 +-- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/distutils/tests/test_spawn.py b/distutils/tests/test_spawn.py index b68c7d571d..d2a898ed3f 100644 --- a/distutils/tests/test_spawn.py +++ b/distutils/tests/test_spawn.py @@ -2,7 +2,6 @@ import os import stat import sys -import unittest import unittest.mock as mock from test.support import unix_shell @@ -17,7 +16,7 @@ class TestSpawn(support.TempdirManager, support.LoggingSilencer): - @unittest.skipUnless(os.name in ('nt', 'posix'), 'Runs only under posix or nt') + @pytest.mark.skipif("os.name not in ('nt', 'posix')") def test_spawn(self): tmpdir = self.mkdtemp() diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py index cede53b15c..f6d2f10fac 100644 --- a/distutils/tests/test_sysconfig.py +++ b/distutils/tests/test_sysconfig.py @@ -5,7 +5,6 @@ import subprocess import sys import textwrap -import unittest import pytest import jaraco.envs @@ -113,9 +112,7 @@ def set_executables(self, **kw): return comp - @unittest.skipUnless( - get_default_compiler() == 'unix', 'not testing if default compiler is not unix' - ) + @pytest.mark.skipif("get_default_compiler() != 'unix'") def test_customize_compiler(self): # Make sure that sysconfig._config_vars is initialized sysconfig.get_config_vars() @@ -204,9 +201,7 @@ def test_sysconfig_module(self): 'LDFLAGS' ) - @unittest.skipIf( - sysconfig.get_config_var('CUSTOMIZED_OSX_COMPILER'), 'compiler flags customized' - ) + @pytest.mark.skipif("sysconfig.get_config_var('CUSTOMIZED_OSX_COMPILER')") def test_sysconfig_compiler_vars(self): # On OS X, binary installers support extension module building on # various levels of the operating system with differing Xcode diff --git a/distutils/tests/test_util.py b/distutils/tests/test_util.py index 8a78456470..f15fc40d7d 100644 --- a/distutils/tests/test_util.py +++ b/distutils/tests/test_util.py @@ -1,7 +1,6 @@ """Tests for distutils.util.""" import os import sys -import unittest import sysconfig as stdlib_sysconfig import unittest.mock as mock from copy import copy @@ -147,7 +146,7 @@ def test_check_environ(self): assert os.environ['PLAT'] == get_platform() assert util._environ_checked == 1 - @unittest.skipUnless(os.name == 'posix', 'specific to posix') + @pytest.mark.skipif("os.name != 'posix'") def test_check_environ_getpwuid(self): util._environ_checked = 0 os.environ.pop('HOME', None) From 59a108f4618f0e6734cc5e1e680189595413a3ad Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 7 Aug 2022 09:15:26 -0400 Subject: [PATCH 67/89] Remove unnecessary comment. --- distutils/tests/test_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/tests/test_util.py b/distutils/tests/test_util.py index f15fc40d7d..02ca867623 100644 --- a/distutils/tests/test_util.py +++ b/distutils/tests/test_util.py @@ -19,7 +19,7 @@ grok_environment_error, get_host_platform, ) -from distutils import util # used to patch _environ_checked +from distutils import util from distutils import sysconfig from distutils.errors import DistutilsPlatformError, DistutilsByteCompileError From 46c0d768cfa8aa6e49f9e5793698b0b1326e654e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 7 Aug 2022 10:25:28 -0400 Subject: [PATCH 68/89] =?UTF-8?q?=E2=9A=AB=20Fade=20to=20black.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- distutils/tests/test_util.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/distutils/tests/test_util.py b/distutils/tests/test_util.py index 02ca867623..605b0d40b7 100644 --- a/distutils/tests/test_util.py +++ b/distutils/tests/test_util.py @@ -56,9 +56,7 @@ def test_get_platform(self): assert get_platform() == 'win-amd64' with mock.patch.dict('os.environ', {'VSCMD_ARG_TGT_ARCH': 'arm'}): assert get_platform() == 'win-arm32' - with mock.patch.dict( - 'os.environ', {'VSCMD_ARG_TGT_ARCH': 'arm64'} - ): + with mock.patch.dict('os.environ', {'VSCMD_ARG_TGT_ARCH': 'arm64'}): assert get_platform() == 'win-arm64' def test_convert_path(self): From a7c11f8b1bb1e0b701d752ba9db97d211b81f2a3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 7 Aug 2022 10:26:00 -0400 Subject: [PATCH 69/89] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins=20?= =?UTF-8?q?(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- distutils/tests/test_sysconfig.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py index f6d2f10fac..f1759839bb 100644 --- a/distutils/tests/test_sysconfig.py +++ b/distutils/tests/test_sysconfig.py @@ -11,7 +11,7 @@ import distutils from distutils import sysconfig -from distutils.ccompiler import get_default_compiler +from distutils.ccompiler import get_default_compiler # noqa: F401 from distutils.unixccompiler import UnixCCompiler from test.support import swap_item From f7cff188413bcf94961c7cbef3947ca13747ac3b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 7 Aug 2022 10:30:02 -0400 Subject: [PATCH 70/89] Prefer tabs --- pytest.ini | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pytest.ini b/pytest.ini index 0191f5ee83..a388360c15 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,10 +1,10 @@ [pytest] addopts=--doctest-modules filterwarnings= - # acknowledge that TestDistribution isn't a test - ignore:cannot collect test class 'TestDistribution' - ignore:Fallback spawn triggered + # acknowledge that TestDistribution isn't a test + ignore:cannot collect test class 'TestDistribution' + ignore:Fallback spawn triggered - # ignore spurious and unactionable warnings - ignore:The frontend.OptionParser class will be replaced by a subclass of argparse.ArgumentParser in Docutils 0.21 or later.:DeprecationWarning: - ignore: The frontend.Option class will be removed in Docutils 0.21 or later.:DeprecationWarning: + # ignore spurious and unactionable warnings + ignore:The frontend.OptionParser class will be replaced by a subclass of argparse.ArgumentParser in Docutils 0.21 or later.:DeprecationWarning: + ignore: The frontend.Option class will be removed in Docutils 0.21 or later.:DeprecationWarning: From 17fda7ef4e4d4bf22ad2d0ae634defecf8b7e886 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 7 Aug 2022 10:31:33 -0400 Subject: [PATCH 71/89] Add pytest-flake8 and pytest-black and pytest-cov to test lint and style and coverage --- pyproject.toml | 9 +++++++++ pytest.ini | 13 +++++++++++++ tox.ini | 9 +++++++++ 3 files changed, 31 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 0097e9f6e6..e6863cff08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,11 @@ [tool.black] skip-string-normalization = true + +[tool.pytest-enabler.black] +addopts = "--black" + +[tool.pytest-enabler.flake8] +addopts = "--flake8" + +[tool.pytest-enabler.cov] +addopts = "--cov" diff --git a/pytest.ini b/pytest.ini index a388360c15..56dcdec426 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,19 @@ [pytest] addopts=--doctest-modules filterwarnings= + # Suppress deprecation warning in flake8 + ignore:SelectableGroups dict interface is deprecated::flake8 + + # shopkeep/pytest-black#55 + ignore: is not using a cooperative constructor:pytest.PytestDeprecationWarning + ignore:The \(fspath. py.path.local\) argument to BlackItem is deprecated.:pytest.PytestDeprecationWarning + ignore:BlackItem is an Item subclass and should not be a collector:pytest.PytestWarning + + # tholo/pytest-flake8#83 + ignore: is not using a cooperative constructor:pytest.PytestDeprecationWarning + ignore:The \(fspath. py.path.local\) argument to Flake8Item is deprecated.:pytest.PytestDeprecationWarning + ignore:Flake8Item is an Item subclass and should not be a collector:pytest.PytestWarning + # acknowledge that TestDistribution isn't a test ignore:cannot collect test class 'TestDistribution' ignore:Fallback spawn triggered diff --git a/tox.ini b/tox.ini index 952c9b58c2..5facd6b95f 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,15 @@ toxworkdir={env:TOX_WORK_DIR:.tox} [testenv] deps = pytest + + pytest-flake8 + # workaround for tholo/pytest-flake8#87 + flake8 < 5 + + pytest-black + pytest-cov + pytest-enabler >= 1.3 + jaraco.envs>=2.4 jaraco.path path From 8813a17e7e343f22b13f14c953ab1e2ee0fbd46d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 7 Aug 2022 11:12:49 -0400 Subject: [PATCH 72/89] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins=20?= =?UTF-8?q?(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- distutils/tests/test_unixccompiler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/tests/test_unixccompiler.py b/distutils/tests/test_unixccompiler.py index 970ca71f90..3978c23952 100644 --- a/distutils/tests/test_unixccompiler.py +++ b/distutils/tests/test_unixccompiler.py @@ -31,7 +31,7 @@ def rpath_foo(self): class TestUnixCCompiler(support.TempdirManager): - @pytest.mark.skipif('platform.system == "Windows"') + @pytest.mark.skipif('platform.system == "Windows"') # noqa: C901 def test_runtime_libdir_option(self): # noqa: C901 # Issue #5900; GitHub Issue #37 # From 80dd6f7c2b3b16e638ab5d836758d5b60c8a82d5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 12:07:38 -0400 Subject: [PATCH 73/89] Add test capturing failed expectation. --- distutils/tests/test_ccompiler.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 distutils/tests/test_ccompiler.py diff --git a/distutils/tests/test_ccompiler.py b/distutils/tests/test_ccompiler.py new file mode 100644 index 0000000000..9dfa918b37 --- /dev/null +++ b/distutils/tests/test_ccompiler.py @@ -0,0 +1,14 @@ + +from distutils import ccompiler + + +def test_set_include_dirs(tmp_path): + """ + Extensions should build even if set_include_dirs is invoked. + In particular, compiler-specific paths should not be overridden. + """ + c_file = tmp_path / 'foo.c' + c_file.write_text('void PyInit_foo(void) {}\n') + compiler = ccompiler.new_compiler() + compiler.set_include_dirs([]) + compiler.compile([c_file]) From dc1130766d356e1e9a613ba924e4af942631428c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 12:17:13 -0400 Subject: [PATCH 74/89] Add compatibility for Python 3.7 --- distutils/tests/test_ccompiler.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/distutils/tests/test_ccompiler.py b/distutils/tests/test_ccompiler.py index 9dfa918b37..3dff273a21 100644 --- a/distutils/tests/test_ccompiler.py +++ b/distutils/tests/test_ccompiler.py @@ -1,7 +1,18 @@ +import os +import sys from distutils import ccompiler +def _make_strs(paths): + """ + Convert paths to strings for legacy compatibility. + """ + if sys.version_info > (3, 8): + return paths + return list(map(os.fspath, paths)) + + def test_set_include_dirs(tmp_path): """ Extensions should build even if set_include_dirs is invoked. @@ -11,4 +22,4 @@ def test_set_include_dirs(tmp_path): c_file.write_text('void PyInit_foo(void) {}\n') compiler = ccompiler.new_compiler() compiler.set_include_dirs([]) - compiler.compile([c_file]) + compiler.compile(_make_strs([c_file])) From 47c61e1097396a65ff74081930edd35eac90c10b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 12:21:08 -0400 Subject: [PATCH 75/89] Windows is sensitive even on Python 3.10 --- distutils/tests/test_ccompiler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/distutils/tests/test_ccompiler.py b/distutils/tests/test_ccompiler.py index 3dff273a21..a7fc632432 100644 --- a/distutils/tests/test_ccompiler.py +++ b/distutils/tests/test_ccompiler.py @@ -1,5 +1,6 @@ import os import sys +import platform from distutils import ccompiler @@ -8,7 +9,7 @@ def _make_strs(paths): """ Convert paths to strings for legacy compatibility. """ - if sys.version_info > (3, 8): + if sys.version_info > (3, 8) and platform.system() != "Windows": return paths return list(map(os.fspath, paths)) From 561b70519ccc19a47de1e53f65e74287901083fc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 12:21:50 -0400 Subject: [PATCH 76/89] Also test library dirs --- distutils/tests/test_ccompiler.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/distutils/tests/test_ccompiler.py b/distutils/tests/test_ccompiler.py index a7fc632432..0d3692b23e 100644 --- a/distutils/tests/test_ccompiler.py +++ b/distutils/tests/test_ccompiler.py @@ -24,3 +24,15 @@ def test_set_include_dirs(tmp_path): compiler = ccompiler.new_compiler() compiler.set_include_dirs([]) compiler.compile(_make_strs([c_file])) + + +def test_set_library_dirs(tmp_path): + """ + Extensions should build even if set_library_dirs is invoked. + In particular, compiler-specific paths should not be overridden. + """ + c_file = tmp_path / 'foo.c' + c_file.write_text('void PyInit_foo(void) {}\n') + compiler = ccompiler.new_compiler() + compiler.set_library_dirs([]) + compiler.compile(_make_strs([c_file])) From 1afdbe320d99ee4c3001ba7dcc834b101b7f9bef Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Aug 2022 12:27:48 -0400 Subject: [PATCH 77/89] Extract fixture for c_file --- distutils/tests/test_ccompiler.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/distutils/tests/test_ccompiler.py b/distutils/tests/test_ccompiler.py index 0d3692b23e..8b60abf070 100644 --- a/distutils/tests/test_ccompiler.py +++ b/distutils/tests/test_ccompiler.py @@ -2,6 +2,8 @@ import sys import platform +import pytest + from distutils import ccompiler @@ -14,25 +16,28 @@ def _make_strs(paths): return list(map(os.fspath, paths)) -def test_set_include_dirs(tmp_path): +@pytest.fixture +def c_file(tmp_path): + c_file = tmp_path / 'foo.c' + c_file.write_text('void PyInit_foo(void) {}\n') + return c_file + + +def test_set_include_dirs(c_file): """ Extensions should build even if set_include_dirs is invoked. In particular, compiler-specific paths should not be overridden. """ - c_file = tmp_path / 'foo.c' - c_file.write_text('void PyInit_foo(void) {}\n') compiler = ccompiler.new_compiler() compiler.set_include_dirs([]) compiler.compile(_make_strs([c_file])) -def test_set_library_dirs(tmp_path): +def test_set_library_dirs(c_file): """ Extensions should build even if set_library_dirs is invoked. In particular, compiler-specific paths should not be overridden. """ - c_file = tmp_path / 'foo.c' - c_file.write_text('void PyInit_foo(void) {}\n') compiler = ccompiler.new_compiler() compiler.set_library_dirs([]) compiler.compile(_make_strs([c_file])) From 7849c89b8926e52157bc64a41ee3793804764e7f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 8 Aug 2022 16:03:04 -0400 Subject: [PATCH 78/89] Generate a C file that imports Python.h and something platform specific. --- distutils/tests/test_ccompiler.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/distutils/tests/test_ccompiler.py b/distutils/tests/test_ccompiler.py index 8b60abf070..c395f14d56 100644 --- a/distutils/tests/test_ccompiler.py +++ b/distutils/tests/test_ccompiler.py @@ -1,6 +1,7 @@ import os import sys import platform +import textwrap import pytest @@ -19,7 +20,17 @@ def _make_strs(paths): @pytest.fixture def c_file(tmp_path): c_file = tmp_path / 'foo.c' - c_file.write_text('void PyInit_foo(void) {}\n') + gen_headers = ('Python.h',) + is_windows = platform.system() == "Windows" + plat_headers = ('windows.h',) * is_windows + all_headers = gen_headers + plat_headers + headers = '\n'.join(f'#import <{header}>\n' for header in all_headers) + payload = textwrap.dedent( + """ + #headers + void PyInit_foo(void) {} + """).lstrip().replace('#headers', headers) + c_file.write_text(payload) return c_file From 530b119f4eda4b3b73eb17c4b58a0ffa8a5d6f8c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 8 Aug 2022 16:12:31 -0400 Subject: [PATCH 79/89] Ensure Python include directory is configured. --- distutils/tests/test_ccompiler.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/distutils/tests/test_ccompiler.py b/distutils/tests/test_ccompiler.py index c395f14d56..42a62c8661 100644 --- a/distutils/tests/test_ccompiler.py +++ b/distutils/tests/test_ccompiler.py @@ -2,6 +2,7 @@ import sys import platform import textwrap +import sysconfig import pytest @@ -24,12 +25,17 @@ def c_file(tmp_path): is_windows = platform.system() == "Windows" plat_headers = ('windows.h',) * is_windows all_headers = gen_headers + plat_headers - headers = '\n'.join(f'#import <{header}>\n' for header in all_headers) - payload = textwrap.dedent( - """ + headers = '\n'.join(f'#include <{header}>\n' for header in all_headers) + payload = ( + textwrap.dedent( + """ #headers void PyInit_foo(void) {} - """).lstrip().replace('#headers', headers) + """ + ) + .lstrip() + .replace('#headers', headers) + ) c_file.write_text(payload) return c_file @@ -40,15 +46,6 @@ def test_set_include_dirs(c_file): In particular, compiler-specific paths should not be overridden. """ compiler = ccompiler.new_compiler() - compiler.set_include_dirs([]) - compiler.compile(_make_strs([c_file])) - - -def test_set_library_dirs(c_file): - """ - Extensions should build even if set_library_dirs is invoked. - In particular, compiler-specific paths should not be overridden. - """ - compiler = ccompiler.new_compiler() - compiler.set_library_dirs([]) + python = sysconfig.get_paths()['include'] + compiler.set_include_dirs([python]) compiler.compile(_make_strs([c_file])) From 71cffcb8a8ec7e36dc389a5aa6dc2cc9769a9e97 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 9 Aug 2022 10:33:39 -0400 Subject: [PATCH 80/89] Extend the test to compile a second time after setting include dirs again. --- distutils/tests/test_ccompiler.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/distutils/tests/test_ccompiler.py b/distutils/tests/test_ccompiler.py index 42a62c8661..da1879f237 100644 --- a/distutils/tests/test_ccompiler.py +++ b/distutils/tests/test_ccompiler.py @@ -49,3 +49,7 @@ def test_set_include_dirs(c_file): python = sysconfig.get_paths()['include'] compiler.set_include_dirs([python]) compiler.compile(_make_strs([c_file])) + + # do it again, setting include dirs after any initialization + compiler.set_include_dirs([python]) + compiler.compile(_make_strs([c_file])) From c84d3e59161496e19e1dab47abe5b9f8d4b6210b Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Fri, 17 Jun 2022 15:38:44 +0100 Subject: [PATCH 81/89] Ensure Windows SDK directories are not cleared when caller specifies include/library dirs --- distutils/_msvccompiler.py | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/distutils/_msvccompiler.py b/distutils/_msvccompiler.py index 516639aa1b..846e42f853 100644 --- a/distutils/_msvccompiler.py +++ b/distutils/_msvccompiler.py @@ -253,13 +253,17 @@ def initialize(self, plat_name=None): self.mc = _find_exe("mc.exe", paths) # message compiler self.mt = _find_exe("mt.exe", paths) # message compiler - for dir in vc_env.get('include', '').split(os.pathsep): - if dir: - self.add_include_dir(dir.rstrip(os.sep)) + self.__include_dirs = [ + dir.rstrip(os.sep) + for dir in vc_env.get('include', '').split(os.pathsep) + if dir + ] - for dir in vc_env.get('lib', '').split(os.pathsep): - if dir: - self.add_library_dir(dir.rstrip(os.sep)) + self.__library_dirs = [ + dir.rstrip(os.sep) + for dir in vc_env.get('lib', '').split(os.pathsep): + if dir + ] self.preprocess_options = None # bpo-38597: Always compile with dynamic linking @@ -557,6 +561,24 @@ def _fallback_spawn(self, cmd, env): with mock.patch.dict('os.environ', env): bag.value = super().spawn(cmd) + def _fix_compile_args(self, output_dir, macros, include_dirs): + """Corrects arguments to the compile() method and add compiler-specific dirs""" + fixed_args = super()._fix_lib_args(output_dir, macros, include_dirs) + return ( + fixed_args[0], # output_dir + fixed_args[1], # macros + fixed_args[2] + self.__include_dirs, + ) + + def _fix_lib_args(self, libraries, library_dirs, runtime_library_dirs): + """Corrects arguments to the link_*() methods and add linker-specific dirs""" + fixed_args = super()._fix_lib_args(libraries, library_dirs, runtime_library_dirs) + return ( + fixed_args[0], # libraries + fixed_args[1] + self.__library_dirs, + fixed_args[2], # runtime_library_dirs + ) + # -- Miscellaneous methods ----------------------------------------- # These are all used by the 'gen_lib_options() function, in # ccompiler.py. From d7b020b32349c3d93bb95502fa4f5c566fab2269 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Fri, 17 Jun 2022 16:06:39 +0100 Subject: [PATCH 82/89] Remove stray colon --- distutils/_msvccompiler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/_msvccompiler.py b/distutils/_msvccompiler.py index 846e42f853..c9f0bff07f 100644 --- a/distutils/_msvccompiler.py +++ b/distutils/_msvccompiler.py @@ -261,7 +261,7 @@ def initialize(self, plat_name=None): self.__library_dirs = [ dir.rstrip(os.sep) - for dir in vc_env.get('lib', '').split(os.pathsep): + for dir in vc_env.get('lib', '').split(os.pathsep) if dir ] From a223350c9af7f1aba69993020b126f6d0646d4f5 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Fri, 17 Jun 2022 16:11:24 +0100 Subject: [PATCH 83/89] Fixup bad super() call --- distutils/_msvccompiler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/_msvccompiler.py b/distutils/_msvccompiler.py index c9f0bff07f..0a19109fd7 100644 --- a/distutils/_msvccompiler.py +++ b/distutils/_msvccompiler.py @@ -563,7 +563,7 @@ def _fallback_spawn(self, cmd, env): def _fix_compile_args(self, output_dir, macros, include_dirs): """Corrects arguments to the compile() method and add compiler-specific dirs""" - fixed_args = super()._fix_lib_args(output_dir, macros, include_dirs) + fixed_args = super()._fix_compile_args(output_dir, macros, include_dirs) return ( fixed_args[0], # output_dir fixed_args[1], # macros From d1e3b46b380e77fa3cb70a42818a29578069ab40 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Tue, 2 Aug 2022 00:21:57 +0100 Subject: [PATCH 84/89] Use CCompiler._fix_compile_args to fix args to compile() --- distutils/ccompiler.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/distutils/ccompiler.py b/distutils/ccompiler.py index b4d3d0fbe0..47cd5ad40d 100644 --- a/distutils/ccompiler.py +++ b/distutils/ccompiler.py @@ -324,24 +324,7 @@ def set_link_objects(self, objects): def _setup_compile(self, outdir, macros, incdirs, sources, depends, extra): """Process arguments and decide which source files to compile.""" - if outdir is None: - outdir = self.output_dir - elif not isinstance(outdir, str): - raise TypeError("'output_dir' must be a string or None") - - if macros is None: - macros = self.macros - elif isinstance(macros, list): - macros = macros + (self.macros or []) - else: - raise TypeError("'macros' (if supplied) must be a list of tuples") - - if incdirs is None: - incdirs = self.include_dirs - elif isinstance(incdirs, (list, tuple)): - incdirs = list(incdirs) + (self.include_dirs or []) - else: - raise TypeError("'include_dirs' (if supplied) must be a list of strings") + outdir, macros, incdirs = self._fix_compile_args(outdir, macros, incdirs) if extra is None: extra = [] From 7d9c9d46565181770919ad77c133ab16a8721c59 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 9 Aug 2022 19:34:48 -0400 Subject: [PATCH 85/89] =?UTF-8?q?=E2=9A=AB=20Fade=20to=20black.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- distutils/_msvccompiler.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/distutils/_msvccompiler.py b/distutils/_msvccompiler.py index 0a19109fd7..7abd24e371 100644 --- a/distutils/_msvccompiler.py +++ b/distutils/_msvccompiler.py @@ -260,9 +260,7 @@ def initialize(self, plat_name=None): ] self.__library_dirs = [ - dir.rstrip(os.sep) - for dir in vc_env.get('lib', '').split(os.pathsep) - if dir + dir.rstrip(os.sep) for dir in vc_env.get('lib', '').split(os.pathsep) if dir ] self.preprocess_options = None @@ -572,7 +570,9 @@ def _fix_compile_args(self, output_dir, macros, include_dirs): def _fix_lib_args(self, libraries, library_dirs, runtime_library_dirs): """Corrects arguments to the link_*() methods and add linker-specific dirs""" - fixed_args = super()._fix_lib_args(libraries, library_dirs, runtime_library_dirs) + fixed_args = super()._fix_lib_args( + libraries, library_dirs, runtime_library_dirs + ) return ( fixed_args[0], # libraries fixed_args[1] + self.__library_dirs, From 9f9a3e57643cb49796c1b08b5b5afb2826ecd7f6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 9 Aug 2022 21:02:16 -0400 Subject: [PATCH 86/89] Allow compiler classes to supply include and library dirs at the class level. --- distutils/_msvccompiler.py | 43 ++++++++++++-------------------------- distutils/ccompiler.py | 16 ++++++++++++++ 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/distutils/_msvccompiler.py b/distutils/_msvccompiler.py index 7abd24e371..ade80056e9 100644 --- a/distutils/_msvccompiler.py +++ b/distutils/_msvccompiler.py @@ -224,6 +224,18 @@ def __init__(self, verbose=0, dry_run=0, force=0): self.plat_name = None self.initialized = False + @classmethod + def _configure(cls, vc_env): + """ + Set class-level include/lib dirs. + """ + cls.include_dirs = cls._parse_path(vc_env.get('include', '')) + cls.library_dirs = cls._parse_path(vc_env.get('lib', '')) + + @staticmethod + def _parse_path(val): + return [dir.rstrip(os.sep) for dir in val.split(os.pathsep) if dir] + def initialize(self, plat_name=None): # multi-init means we would need to check platform same each time... assert not self.initialized, "don't init multiple times" @@ -243,6 +255,7 @@ def initialize(self, plat_name=None): raise DistutilsPlatformError( "Unable to find a compatible " "Visual Studio installation." ) + self._configure(vc_env) self._paths = vc_env.get('path', '') paths = self._paths.split(os.pathsep) @@ -253,16 +266,6 @@ def initialize(self, plat_name=None): self.mc = _find_exe("mc.exe", paths) # message compiler self.mt = _find_exe("mt.exe", paths) # message compiler - self.__include_dirs = [ - dir.rstrip(os.sep) - for dir in vc_env.get('include', '').split(os.pathsep) - if dir - ] - - self.__library_dirs = [ - dir.rstrip(os.sep) for dir in vc_env.get('lib', '').split(os.pathsep) if dir - ] - self.preprocess_options = None # bpo-38597: Always compile with dynamic linking # Future releases of Python 3.x will include all past @@ -559,26 +562,6 @@ def _fallback_spawn(self, cmd, env): with mock.patch.dict('os.environ', env): bag.value = super().spawn(cmd) - def _fix_compile_args(self, output_dir, macros, include_dirs): - """Corrects arguments to the compile() method and add compiler-specific dirs""" - fixed_args = super()._fix_compile_args(output_dir, macros, include_dirs) - return ( - fixed_args[0], # output_dir - fixed_args[1], # macros - fixed_args[2] + self.__include_dirs, - ) - - def _fix_lib_args(self, libraries, library_dirs, runtime_library_dirs): - """Corrects arguments to the link_*() methods and add linker-specific dirs""" - fixed_args = super()._fix_lib_args( - libraries, library_dirs, runtime_library_dirs - ) - return ( - fixed_args[0], # libraries - fixed_args[1] + self.__library_dirs, - fixed_args[2], # runtime_library_dirs - ) - # -- Miscellaneous methods ----------------------------------------- # These are all used by the 'gen_lib_options() function, in # ccompiler.py. diff --git a/distutils/ccompiler.py b/distutils/ccompiler.py index 47cd5ad40d..3cf5761cf2 100644 --- a/distutils/ccompiler.py +++ b/distutils/ccompiler.py @@ -91,6 +91,16 @@ class CCompiler: } language_order = ["c++", "objc", "c"] + include_dirs = [] + """ + include dirs specific to this compiler class + """ + + library_dirs = [] + """ + library dirs specific to this compiler class + """ + def __init__(self, verbose=0, dry_run=0, force=0): self.dry_run = dry_run self.force = force @@ -383,6 +393,9 @@ def _fix_compile_args(self, output_dir, macros, include_dirs): else: raise TypeError("'include_dirs' (if supplied) must be a list of strings") + # add include dirs for class + include_dirs += self.__class__.include_dirs + return output_dir, macros, include_dirs def _prep_compile(self, sources, output_dir, depends=None): @@ -439,6 +452,9 @@ def _fix_lib_args(self, libraries, library_dirs, runtime_library_dirs): else: raise TypeError("'library_dirs' (if supplied) must be a list of strings") + # add library dirs for class + library_dirs += self.__class__.library_dirs + if runtime_library_dirs is None: runtime_library_dirs = self.runtime_library_dirs elif isinstance(runtime_library_dirs, (list, tuple)): From 3e4c7a78b337b1894daef40c7d2a980dd7ef18ce Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 9 Aug 2022 21:42:34 -0400 Subject: [PATCH 87/89] Disallow repeat calls to .initialize in one place. --- distutils/_msvccompiler.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/distutils/_msvccompiler.py b/distutils/_msvccompiler.py index 516639aa1b..7c51dc2254 100644 --- a/distutils/_msvccompiler.py +++ b/distutils/_msvccompiler.py @@ -222,11 +222,11 @@ def __init__(self, verbose=0, dry_run=0, force=0): super().__init__(verbose, dry_run, force) # target platform (.plat_name is consistent with 'bdist') self.plat_name = None - self.initialized = False def initialize(self, plat_name=None): - # multi-init means we would need to check platform same each time... - assert not self.initialized, "don't init multiple times" + def _repeat_call_error(plat_name=None): + raise AssertionError("repeat initialize not allowed") + self.initialize = _repeat_call_error if plat_name is None: plat_name = get_platform() # sanity check for platforms to prevent obscure errors later. @@ -309,8 +309,6 @@ def initialize(self, plat_name=None): (CCompiler.SHARED_LIBRARY, True): self.ldflags_static_debug, } - self.initialized = True - # -- Worker methods ------------------------------------------------ def object_filenames(self, source_filenames, strip_dir=0, output_dir=''): From 73d8052e68b92ee67e89b09b495134cdb3398003 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 10 Aug 2022 09:43:55 -0400 Subject: [PATCH 88/89] Update changelog --- changelog.d/3496.misc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/3496.misc.rst diff --git a/changelog.d/3496.misc.rst b/changelog.d/3496.misc.rst new file mode 100644 index 0000000000..5d7542febd --- /dev/null +++ b/changelog.d/3496.misc.rst @@ -0,0 +1 @@ +Update to pypa/distutils@b65aa40 including more robust support for library/include dir handling in msvccompiler (pypa/distutils#153) and test suite improvements. From ba9a3eadaff3b96608129380c0874636772da716 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 10 Aug 2022 10:22:09 -0400 Subject: [PATCH 89/89] Revert "Disallow repeat calls to .initialize in one place." This reverts commit 3e4c7a78b337b1894daef40c7d2a980dd7ef18ce. --- distutils/_msvccompiler.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/distutils/_msvccompiler.py b/distutils/_msvccompiler.py index 491fe87c78..ade80056e9 100644 --- a/distutils/_msvccompiler.py +++ b/distutils/_msvccompiler.py @@ -222,6 +222,7 @@ def __init__(self, verbose=0, dry_run=0, force=0): super().__init__(verbose, dry_run, force) # target platform (.plat_name is consistent with 'bdist') self.plat_name = None + self.initialized = False @classmethod def _configure(cls, vc_env): @@ -236,9 +237,8 @@ def _parse_path(val): return [dir.rstrip(os.sep) for dir in val.split(os.pathsep) if dir] def initialize(self, plat_name=None): - def _repeat_call_error(plat_name=None): - raise AssertionError("repeat initialize not allowed") - self.initialize = _repeat_call_error + # multi-init means we would need to check platform same each time... + assert not self.initialized, "don't init multiple times" if plat_name is None: plat_name = get_platform() # sanity check for platforms to prevent obscure errors later. @@ -314,6 +314,8 @@ def _repeat_call_error(plat_name=None): (CCompiler.SHARED_LIBRARY, True): self.ldflags_static_debug, } + self.initialized = True + # -- Worker methods ------------------------------------------------ def object_filenames(self, source_filenames, strip_dir=0, output_dir=''):