From 09622976e02bf14d5c0d49db283608e802dc76a8 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Fri, 22 Apr 2022 00:32:54 +0100 Subject: [PATCH 01/16] Detect when a venv is created from an in-tree build. Fixes #132 --- distutils/sysconfig.py | 5 +++++ distutils/tests/test_sysconfig.py | 27 +++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py index 55a42e169d..3039be7f18 100644 --- a/distutils/sysconfig.py +++ b/distutils/sysconfig.py @@ -56,6 +56,11 @@ def _fix_pcbuild(d): if d and os.path.normcase(d).startswith( os.path.normcase(os.path.join(PREFIX, "PCbuild"))): return PREFIX + # In a venv, we may be passed sys._home which will be inside + # BASE_PREFIX rather than PREFIX. + if d and os.path.normcase(d).startswith( + os.path.normcase(os.path.join(BASE_PREFIX, "PCbuild"))): + return BASE_PREFIX return d project_base = _fix_pcbuild(project_base) _sys_home = _fix_pcbuild(_sys_home) diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py index e671f9e09b..ab9db4d1b7 100644 --- a/distutils/tests/test_sysconfig.py +++ b/distutils/tests/test_sysconfig.py @@ -309,6 +309,33 @@ def test_win_ext_suffix(self): self.assertTrue(sysconfig.get_config_var("EXT_SUFFIX").endswith(".pyd")) self.assertNotEqual(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') + def test_win_build_venv_from_source_tree(self): + '''Ensure distutils.sysconfig detects venvs from source tree builds.''' + subprocess.check_output([ + str(sys.executable), "-m", "venv", + rf".\{TESTFN}", "--without-pip" + ]) + try: + distutils_path = os.path.dirname(os.path.dirname(sysconfig.__file__)) + subprocess.check_output([ + rf".\{TESTFN}\Scripts\python.exe", + "-c", + "import distutils.sysconfig as s, sys; sys.exit(0 if s.python_build else 3456)" + ], env={**os.environ, "PYTHONPATH": distutils_path}) + except subprocess.CalledProcessError as ex: + # Return code doesn't matter, provided it's unlikely to be confused with + # a different kind of error + if ex.returncode != 3456: + raise + self.fail("expected distutils.sysconfig.python_build == True; got False") + + def test_suite(): suite = unittest.TestSuite() suite.addTest(unittest.TestLoader().loadTestsFromTestCase(SysconfigTestCase)) From caae0974044837c6884fe91b9b5413a743fc1b69 Mon Sep 17 00:00:00 2001 From: wim glenn Date: Tue, 26 Apr 2022 23:23:36 -0500 Subject: [PATCH 02/16] make all fields documented as optional in the spec truly optional (dont warn when omitted), and stop coercing missing values to the string "UNKNOWN" --- .github/workflows/main.yml | 6 ++-- distutils/command/bdist_msi.py | 6 +--- distutils/command/bdist_rpm.py | 8 ++--- distutils/command/check.py | 43 +++-------------------- distutils/dist.py | 60 +++++++++++++++++++------------- distutils/tests/test_check.py | 11 +++--- distutils/tests/test_dist.py | 2 +- distutils/tests/test_install.py | 15 +++++--- distutils/tests/test_register.py | 4 +-- distutils/tests/test_sdist.py | 2 +- docs/distutils/examples.rst | 4 +-- docs/distutils/setupscript.rst | 6 ++-- 12 files changed, 71 insertions(+), 96 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 12b049c621..51816629f6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,9 +11,9 @@ jobs: strategy: matrix: python: - - 3.7 - - 3.8 - - 3.9 + - "3.7" + - "3.8" + - "3.9" - "3.10" platform: - ubuntu-latest diff --git a/distutils/command/bdist_msi.py b/distutils/command/bdist_msi.py index 1525953241..56c4b9883a 100644 --- a/distutils/command/bdist_msi.py +++ b/distutils/command/bdist_msi.py @@ -231,11 +231,7 @@ def run(self): if os.path.exists(installer_name): os.unlink(installer_name) metadata = self.distribution.metadata - author = metadata.author - if not author: - author = metadata.maintainer - if not author: - author = "UNKNOWN" + author = metadata.author or metadata.maintainer version = metadata.get_version() # ProductVersion must be strictly numeric # XXX need to deal with prerelease versions diff --git a/distutils/command/bdist_rpm.py b/distutils/command/bdist_rpm.py index 550cbfa1e2..a2a9e8e588 100644 --- a/distutils/command/bdist_rpm.py +++ b/distutils/command/bdist_rpm.py @@ -399,7 +399,7 @@ def _make_spec_file(self): '%define unmangled_version ' + self.distribution.get_version(), '%define release ' + self.release.replace('-','_'), '', - 'Summary: ' + self.distribution.get_description(), + 'Summary: ' + (self.distribution.get_description() or "UNKNOWN"), ] # Workaround for #14443 which affects some RPM based systems such as @@ -438,7 +438,7 @@ def _make_spec_file(self): spec_file.append('Source0: %{name}-%{unmangled_version}.tar.gz') spec_file.extend([ - 'License: ' + self.distribution.get_license(), + 'License: ' + (self.distribution.get_license() or "UNKNOWN"), 'Group: ' + self.group, 'BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot', 'Prefix: %{_prefix}', ]) @@ -464,7 +464,7 @@ def _make_spec_file(self): spec_file.append('%s: %s' % (field, val)) - if self.distribution.get_url() != 'UNKNOWN': + if self.distribution.get_url(): spec_file.append('Url: ' + self.distribution.get_url()) if self.distribution_name: @@ -483,7 +483,7 @@ def _make_spec_file(self): spec_file.extend([ '', '%description', - self.distribution.get_long_description() + self.distribution.get_long_description() or "", ]) # put locale descriptions into spec file diff --git a/distutils/command/check.py b/distutils/command/check.py index af311ca90e..8a02dbca7d 100644 --- a/distutils/command/check.py +++ b/distutils/command/check.py @@ -82,54 +82,19 @@ def check_metadata(self): """Ensures that all required elements of meta-data are supplied. Required fields: - name, version, URL - - Recommended fields: - (author and author_email) or (maintainer and maintainer_email)) + name, version Warns if any are missing. """ metadata = self.distribution.metadata missing = [] - for attr in ('name', 'version', 'url'): - if not (hasattr(metadata, attr) and getattr(metadata, attr)): + for attr in 'name', 'version': + if not getattr(metadata, attr, None): missing.append(attr) if missing: - self.warn("missing required meta-data: %s" % ', '.join(missing)) - if not ( - self._check_contact("author", metadata) or - self._check_contact("maintainer", metadata) - ): - self.warn("missing meta-data: either (author and author_email) " + - "or (maintainer and maintainer_email) " + - "should be supplied") - - def _check_contact(self, kind, metadata): - """ - Returns True if the contact's name is specified and False otherwise. - This function will warn if the contact's email is not specified. - """ - name = getattr(metadata, kind) or '' - email = getattr(metadata, kind + '_email') or '' - - msg = ("missing meta-data: if '{}' supplied, " + - "'{}' should be supplied too") - - if name and email: - return True - - if name: - self.warn(msg.format(kind, kind + '_email')) - return True - - addresses = [(alias, addr) for alias, addr in getaddresses([email])] - if any(alias and addr for alias, addr in addresses): - # The contact's name can be encoded in the email: `Name ` - return True - - return False + self.warn("missing required meta-data: %s" % ', '.join(missing)) def check_restructuredtext(self): """Checks if the long string fields are reST-compliant.""" diff --git a/distutils/dist.py b/distutils/dist.py index 37db4d6cd7..90e6f12f77 100644 --- a/distutils/dist.py +++ b/distutils/dist.py @@ -1064,9 +1064,8 @@ def read_pkg_file(self, file): def _read_field(name): value = msg[name] - if value == 'UNKNOWN': - return None - return value + if value: + return value def _read_list(name): values = msg.get_all(name, None) @@ -1125,20 +1124,32 @@ def write_pkg_file(self, file): self.classifiers or self.download_url): version = '1.1' + # required fields file.write('Metadata-Version: %s\n' % version) file.write('Name: %s\n' % self.get_name()) file.write('Version: %s\n' % self.get_version()) - file.write('Summary: %s\n' % self.get_description()) - file.write('Home-page: %s\n' % self.get_url()) - file.write('Author: %s\n' % self.get_contact()) - file.write('Author-email: %s\n' % self.get_contact_email()) - file.write('License: %s\n' % self.get_license()) + + # optional fields + summary = self.get_description() + if summary: + file.write('Summary: %s\n' % summary) + home_page = self.get_url() + if home_page: + file.write('Home-page: %s\n' % home_page) + author = self.get_contact() + if author: + file.write('Author: %s\n' % author) + author_email = self.get_contact_email() + if author_email: + file.write('Author-email: %s\n' % author_email) + license = self.get_license() + if license: + file.write('License: %s\n' % license) if self.download_url: file.write('Download-URL: %s\n' % self.download_url) - - long_desc = rfc822_escape(self.get_long_description()) - file.write('Description: %s\n' % long_desc) - + long_desc = self.get_long_description() + if long_desc: + file.write('Description: %s\n' % rfc822_escape(long_desc)) keywords = ','.join(self.get_keywords()) if keywords: file.write('Keywords: %s\n' % keywords) @@ -1152,6 +1163,7 @@ def write_pkg_file(self, file): self._write_list(file, 'Obsoletes', self.get_obsoletes()) def _write_list(self, file, name, values): + values = values or [] for value in values: file.write('%s: %s\n' % (name, value)) @@ -1167,35 +1179,35 @@ def get_fullname(self): return "%s-%s" % (self.get_name(), self.get_version()) def get_author(self): - return self.author or "UNKNOWN" + return self.author def get_author_email(self): - return self.author_email or "UNKNOWN" + return self.author_email def get_maintainer(self): - return self.maintainer or "UNKNOWN" + return self.maintainer def get_maintainer_email(self): - return self.maintainer_email or "UNKNOWN" + return self.maintainer_email def get_contact(self): - return self.maintainer or self.author or "UNKNOWN" + return self.maintainer or self.author def get_contact_email(self): - return self.maintainer_email or self.author_email or "UNKNOWN" + return self.maintainer_email or self.author_email def get_url(self): - return self.url or "UNKNOWN" + return self.url def get_license(self): - return self.license or "UNKNOWN" + return self.license get_licence = get_license def get_description(self): - return self.description or "UNKNOWN" + return self.description def get_long_description(self): - return self.long_description or "UNKNOWN" + return self.long_description def get_keywords(self): return self.keywords or [] @@ -1204,7 +1216,7 @@ def set_keywords(self, value): self.keywords = _ensure_list(value, 'keywords') def get_platforms(self): - return self.platforms or ["UNKNOWN"] + return self.platforms def set_platforms(self, value): self.platforms = _ensure_list(value, 'platforms') @@ -1216,7 +1228,7 @@ def set_classifiers(self, value): self.classifiers = _ensure_list(value, 'classifiers') def get_download_url(self): - return self.download_url or "UNKNOWN" + return self.download_url # PEP 314 def get_requires(self): diff --git a/distutils/tests/test_check.py b/distutils/tests/test_check.py index b41dba3d0a..2414d6eb5e 100644 --- a/distutils/tests/test_check.py +++ b/distutils/tests/test_check.py @@ -43,7 +43,7 @@ def test_check_metadata(self): # by default, check is checking the metadata # should have some warnings cmd = self._run() - self.assertEqual(cmd._warnings, 2) + self.assertEqual(cmd._warnings, 1) # now let's add the required fields # and run it again, to make sure we don't get @@ -81,17 +81,16 @@ def test_check_author_maintainer(self): cmd = self._run(metadata) self.assertEqual(cmd._warnings, 0) - # the check should warn if only email is given and it does not - # contain the name + # the check should not warn if only email is given metadata[kind + '_email'] = 'name@email.com' cmd = self._run(metadata) - self.assertEqual(cmd._warnings, 1) + self.assertEqual(cmd._warnings, 0) - # the check should warn if only the name is given + # the check should not warn if only the name is given metadata[kind] = "Name" del metadata[kind + '_email'] cmd = self._run(metadata) - self.assertEqual(cmd._warnings, 1) + self.assertEqual(cmd._warnings, 0) @unittest.skipUnless(HAS_DOCUTILS, "won't test without docutils") def test_check_document(self): diff --git a/distutils/tests/test_dist.py b/distutils/tests/test_dist.py index 36155be152..9132bc040b 100644 --- a/distutils/tests/test_dist.py +++ b/distutils/tests/test_dist.py @@ -519,7 +519,7 @@ def test_read_metadata(self): self.assertEqual(metadata.description, "xxx") self.assertEqual(metadata.download_url, 'http://example.com') self.assertEqual(metadata.keywords, ['one', 'two']) - self.assertEqual(metadata.platforms, ['UNKNOWN']) + self.assertEqual(metadata.platforms, None) self.assertEqual(metadata.obsoletes, None) self.assertEqual(metadata.requires, ['foo']) diff --git a/distutils/tests/test_install.py b/distutils/tests/test_install.py index 3aef9e432e..e01d979393 100644 --- a/distutils/tests/test_install.py +++ b/distutils/tests/test_install.py @@ -187,7 +187,9 @@ def test_finalize_options(self): def test_record(self): install_dir = self.mkdtemp() - project_dir, dist = self.create_dist(py_modules=['hello'], + project_dir, dist = self.create_dist(name="testdist", + version="0.1", + py_modules=['hello'], scripts=['sayhi']) os.chdir(project_dir) self.write_file('hello.py', "def main(): print('o hai')") @@ -209,7 +211,7 @@ def test_record(self): found = [os.path.basename(line) for line in content.splitlines()] expected = ['hello.py', 'hello.%s.pyc' % sys.implementation.cache_tag, 'sayhi', - 'UNKNOWN-0.0.0-py%s.%s.egg-info' % sys.version_info[:2]] + 'testdist-0.1-py%s.%s.egg-info' % sys.version_info[:2]] self.assertEqual(found, expected) def test_record_extensions(self): @@ -217,8 +219,11 @@ def test_record_extensions(self): if cmd is not None: self.skipTest('The %r command is not found' % cmd) install_dir = self.mkdtemp() - project_dir, dist = self.create_dist(ext_modules=[ - Extension('xx', ['xxmodule.c'])]) + project_dir, dist = self.create_dist( + name="testdist", + version="0.1", + ext_modules=[Extension('xx', ['xxmodule.c'])], + ) os.chdir(project_dir) support.copy_xxmodule_c(project_dir) @@ -242,7 +247,7 @@ def test_record_extensions(self): found = [os.path.basename(line) for line in content.splitlines()] expected = [_make_ext_name('xx'), - 'UNKNOWN-0.0.0-py%s.%s.egg-info' % sys.version_info[:2]] + 'testdist-0.1-py%s.%s.egg-info' % sys.version_info[:2]] self.assertEqual(found, expected) def test_debug_mode(self): diff --git a/distutils/tests/test_register.py b/distutils/tests/test_register.py index 5770ed58ae..4556768645 100644 --- a/distutils/tests/test_register.py +++ b/distutils/tests/test_register.py @@ -154,8 +154,8 @@ def _no_way(prompt=''): req1 = dict(self.conn.reqs[0].headers) req2 = dict(self.conn.reqs[1].headers) - self.assertEqual(req1['Content-length'], '1374') - self.assertEqual(req2['Content-length'], '1374') + self.assertEqual(req1['Content-length'], '1359') + self.assertEqual(req2['Content-length'], '1359') self.assertIn(b'xxx', self.conn.reqs[1].data) def test_password_not_in_file(self): diff --git a/distutils/tests/test_sdist.py b/distutils/tests/test_sdist.py index 4c51717ce6..aa04dd0546 100644 --- a/distutils/tests/test_sdist.py +++ b/distutils/tests/test_sdist.py @@ -251,7 +251,7 @@ def test_metadata_check_option(self): cmd.run() warnings = [msg for msg in self.get_logs(WARN) if msg.startswith('warning: check:')] - self.assertEqual(len(warnings), 2) + self.assertEqual(len(warnings), 1) # trying with a complete set of metadata self.clear_logs() diff --git a/docs/distutils/examples.rst b/docs/distutils/examples.rst index e492b7f605..28582bab36 100644 --- a/docs/distutils/examples.rst +++ b/docs/distutils/examples.rst @@ -253,9 +253,7 @@ Running the ``check`` command will display some warnings: $ python setup.py check running check - warning: check: missing required meta-data: version, url - warning: check: missing meta-data: either (author and author_email) or - (maintainer and maintainer_email) should be supplied + warning: check: missing required meta-data: version If you use the reStructuredText syntax in the ``long_description`` field and diff --git a/docs/distutils/setupscript.rst b/docs/distutils/setupscript.rst index 4386a60b66..3c8e1ab1b3 100644 --- a/docs/distutils/setupscript.rst +++ b/docs/distutils/setupscript.rst @@ -580,7 +580,7 @@ This information includes: | ``maintainer_email`` | email address of the | email address | \(3) | | | package maintainer | | | +----------------------+---------------------------+-----------------+--------+ -| ``url`` | home page for the package | URL | \(1) | +| ``url`` | home page for the package | URL | | +----------------------+---------------------------+-----------------+--------+ | ``description`` | short, summary | short string | | | | description of the | | | @@ -610,8 +610,8 @@ Notes: It is recommended that versions take the form *major.minor[.patch[.sub]]*. (3) - Either the author or the maintainer must be identified. If maintainer is - provided, distutils lists it as the author in :file:`PKG-INFO`. + If maintainer is provided and author is not, distutils lists maintainer as + the author in :file:`PKG-INFO`. (4) The ``long_description`` field is used by PyPI when you publish a package, From 92b255a4a4c24c18d15397068442e5de118f302c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Apr 2022 19:16:13 -0400 Subject: [PATCH 03/16] Extract check for 'd is None'. --- distutils/sysconfig.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py index 3039be7f18..d968ef9737 100644 --- a/distutils/sysconfig.py +++ b/distutils/sysconfig.py @@ -53,12 +53,13 @@ def _is_python_source_dir(d): if os.name == 'nt': def _fix_pcbuild(d): - if d and os.path.normcase(d).startswith( + if d is None: + return + if os.path.normcase(d).startswith( os.path.normcase(os.path.join(PREFIX, "PCbuild"))): return PREFIX - # In a venv, we may be passed sys._home which will be inside - # BASE_PREFIX rather than PREFIX. - if d and os.path.normcase(d).startswith( + # In a venv, sys._home will be inside BASE_PREFIX rather than PREFIX. + if os.path.normcase(d).startswith( os.path.normcase(os.path.join(BASE_PREFIX, "PCbuild"))): return BASE_PREFIX return d From fe3fbd35146d89190b4ef8cdbc7c97341fbf9d7e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Apr 2022 19:23:46 -0400 Subject: [PATCH 04/16] Combine logic between prefix and base_prefix. --- distutils/sysconfig.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py index d968ef9737..c2954ae9bd 100644 --- a/distutils/sysconfig.py +++ b/distutils/sysconfig.py @@ -51,18 +51,26 @@ def _is_python_source_dir(d): _sys_home = getattr(sys, '_home', None) + +def _is_parent(dir_a, dir_b): + """ + Return True if a is a parent of b. + """ + return os.path.normcase(dir_a).startswith(os.path.normcase(dir_b)) + + if os.name == 'nt': def _fix_pcbuild(d): if d is None: return - if os.path.normcase(d).startswith( - os.path.normcase(os.path.join(PREFIX, "PCbuild"))): - return PREFIX # In a venv, sys._home will be inside BASE_PREFIX rather than PREFIX. - if os.path.normcase(d).startswith( - os.path.normcase(os.path.join(BASE_PREFIX, "PCbuild"))): - return BASE_PREFIX - return d + prefixes = PREFIX, BASE_PREFIX + matched = ( + prefix + for prefix in prefixes + if _is_parent(d, os.path.join(prefix, "PCbuild")) + ) + return next(matched, d) project_base = _fix_pcbuild(project_base) _sys_home = _fix_pcbuild(_sys_home) From 66667745c67d5d68247cbe17b00620424672e144 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Apr 2022 19:29:59 -0400 Subject: [PATCH 05/16] Adapt style --- distutils/tests/test_sysconfig.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py index ab9db4d1b7..973d457416 100644 --- a/distutils/tests/test_sysconfig.py +++ b/distutils/tests/test_sysconfig.py @@ -309,25 +309,30 @@ def test_win_ext_suffix(self): self.assertTrue(sysconfig.get_config_var("EXT_SUFFIX").endswith(".pyd")) self.assertNotEqual(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') + @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') def test_win_build_venv_from_source_tree(self): - '''Ensure distutils.sysconfig detects venvs from source tree builds.''' - subprocess.check_output([ + """Ensure distutils.sysconfig detects venvs from source tree builds.""" + cmd = [ str(sys.executable), "-m", "venv", - rf".\{TESTFN}", "--without-pip" - ]) + rf".\{TESTFN}", "--without-pip", + ] + subprocess.check_output(cmd) try: distutils_path = os.path.dirname(os.path.dirname(sysconfig.__file__)) - subprocess.check_output([ + cmd = [ rf".\{TESTFN}\Scripts\python.exe", "-c", "import distutils.sysconfig as s, sys; sys.exit(0 if s.python_build else 3456)" - ], env={**os.environ, "PYTHONPATH": distutils_path}) + ] + subprocess.check_output(cmd, env={**os.environ, "PYTHONPATH": distutils_path}) except subprocess.CalledProcessError as ex: # Return code doesn't matter, provided it's unlikely to be confused with # a different kind of error From 57903582af9c1eb7dec150f25fea3bc196932a0a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Apr 2022 19:34:38 -0400 Subject: [PATCH 06/16] Simply emit the expected value instead of converting to an exit code and deciphering. --- distutils/tests/test_sysconfig.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py index 973d457416..70b7ad1945 100644 --- a/distutils/tests/test_sysconfig.py +++ b/distutils/tests/test_sysconfig.py @@ -325,20 +325,14 @@ def test_win_build_venv_from_source_tree(self): rf".\{TESTFN}", "--without-pip", ] subprocess.check_output(cmd) - try: - distutils_path = os.path.dirname(os.path.dirname(sysconfig.__file__)) - cmd = [ - rf".\{TESTFN}\Scripts\python.exe", - "-c", - "import distutils.sysconfig as s, sys; sys.exit(0 if s.python_build else 3456)" - ] - subprocess.check_output(cmd, env={**os.environ, "PYTHONPATH": distutils_path}) - except subprocess.CalledProcessError as ex: - # Return code doesn't matter, provided it's unlikely to be confused with - # a different kind of error - if ex.returncode != 3456: - raise - self.fail("expected distutils.sysconfig.python_build == True; got False") + distutils_path = os.path.dirname(os.path.dirname(sysconfig.__file__)) + cmd = [ + rf".\{TESTFN}\Scripts\python.exe", + "-c", + "import distutils.sysconfig; print(distutils.sysconfig.python_build)" + ] + out = subprocess.check_output(cmd, env={**os.environ, "PYTHONPATH": distutils_path}) + assert out == "True" def test_suite(): From 414ff105b44ffdfd64f98851bd747fd04295a824 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Apr 2022 20:08:55 -0400 Subject: [PATCH 07/16] Rely on jaraco.envs for environment creation. --- distutils/tests/test_sysconfig.py | 16 +++++++++------- tox.ini | 1 + 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py index 70b7ad1945..1c88cc85f7 100644 --- a/distutils/tests/test_sysconfig.py +++ b/distutils/tests/test_sysconfig.py @@ -7,6 +7,9 @@ import textwrap import unittest +import jaraco.envs + +import distutils from distutils import sysconfig from distutils.ccompiler import get_default_compiler from distutils.unixccompiler import UnixCCompiler @@ -320,17 +323,16 @@ def test_win_ext_suffix(self): '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.""" + env = jaraco.envs.VEnv() + env.create_opts = env.clean_opts + env.root = TESTFN + env.ensure_env() cmd = [ - str(sys.executable), "-m", "venv", - rf".\{TESTFN}", "--without-pip", - ] - subprocess.check_output(cmd) - distutils_path = os.path.dirname(os.path.dirname(sysconfig.__file__)) - cmd = [ - rf".\{TESTFN}\Scripts\python.exe", + env.exe(), "-c", "import distutils.sysconfig; print(distutils.sysconfig.python_build)" ] + distutils_path = os.path.dirname(os.path.dirname(distutils.__file__)) out = subprocess.check_output(cmd, env={**os.environ, "PYTHONPATH": distutils_path}) assert out == "True" diff --git a/tox.ini b/tox.ini index 2f28517509..0398f1c068 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,7 @@ [testenv] deps = pytest + jaraco.envs>=2.4 commands = pytest {posargs} setenv = From 1f60ceea2e8c9b2726f1aad47ad593bafd251226 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Apr 2022 20:09:11 -0400 Subject: [PATCH 08/16] Prefer tabs --- tox.ini | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tox.ini b/tox.ini index 0398f1c068..0da856e93c 100644 --- a/tox.ini +++ b/tox.ini @@ -5,12 +5,12 @@ deps = commands = pytest {posargs} setenv = - PYTHONPATH = {toxinidir} - # pypa/distutils#99 - VIRTUALENV_NO_SETUPTOOLS = 1 + PYTHONPATH = {toxinidir} + # pypa/distutils#99 + VIRTUALENV_NO_SETUPTOOLS = 1 passenv = - # workaround for tox-dev/tox#2382 - PROGRAMDATA - PROGRAMFILES - PROGRAMFILES(X86) + # workaround for tox-dev/tox#2382 + PROGRAMDATA + PROGRAMFILES + PROGRAMFILES(X86) skip_install = True From 58bfe168905f48235c6cbd0a5e7ff5aea7986e17 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Apr 2022 20:15:08 -0400 Subject: [PATCH 09/16] Use pass_none decorator. --- distutils/_functools.py | 20 ++++++++++++++++++++ distutils/sysconfig.py | 4 ++-- 2 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 distutils/_functools.py diff --git a/distutils/_functools.py b/distutils/_functools.py new file mode 100644 index 0000000000..e7053bac12 --- /dev/null +++ b/distutils/_functools.py @@ -0,0 +1,20 @@ +import functools + + +# from jaraco.functools 3.5 +def pass_none(func): + """ + Wrap func so it's not called if its first param is None + + >>> print_text = pass_none(print) + >>> print_text('text') + text + >>> print_text(None) + """ + + @functools.wraps(func) + def wrapper(param, *args, **kwargs): + if param is not None: + return func(param, *args, **kwargs) + + return wrapper diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py index c2954ae9bd..7543f794cb 100644 --- a/distutils/sysconfig.py +++ b/distutils/sysconfig.py @@ -16,6 +16,7 @@ from .errors import DistutilsPlatformError from . import py39compat +from ._functools import pass_none IS_PYPY = '__pypy__' in sys.builtin_module_names @@ -60,9 +61,8 @@ def _is_parent(dir_a, dir_b): if os.name == 'nt': + @pass_none def _fix_pcbuild(d): - if d is None: - return # In a venv, sys._home will be inside BASE_PREFIX rather than PREFIX. prefixes = PREFIX, BASE_PREFIX matched = ( From d3e462df4090e6fb1ec8f07dfc8c6b5a28ba5c05 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Apr 2022 20:29:30 -0400 Subject: [PATCH 10/16] Prefer tox 3.25 with built-in support for Windows env vars. --- tox.ini | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tox.ini b/tox.ini index 2f28517509..143f09fe74 100644 --- a/tox.ini +++ b/tox.ini @@ -1,3 +1,6 @@ +[tox] +minversion = 3.25 + [testenv] deps = pytest @@ -7,9 +10,4 @@ setenv = PYTHONPATH = {toxinidir} # pypa/distutils#99 VIRTUALENV_NO_SETUPTOOLS = 1 -passenv = - # workaround for tox-dev/tox#2382 - PROGRAMDATA - PROGRAMFILES - PROGRAMFILES(X86) skip_install = True From 810d5b4924081082707d7ed7ab629214fe40bf6f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Apr 2022 20:33:55 -0400 Subject: [PATCH 11/16] Run tests using tox, even on cygwin. --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 12b049c621..5aa948bfb1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -50,13 +50,13 @@ jobs: python${{ matrix.python }}, python${{ matrix.python }}-devel, python${{ matrix.python }}-pytest, + python${{ matrix.python }}-tox, gcc-core, gcc-g++, ncompress - name: Run tests shell: C:\cygwin\bin\env.exe CYGWIN_NOWINPATH=1 CHERE_INVOKING=1 C:\cygwin\bin\bash.exe -leo pipefail -o igncr {0} - run: | - pytest -rs + run: tox ci_setuptools: # Integration testing with setuptools From 41d5f0614bcd3a32efff737eba6b4c0458fec7af Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Apr 2022 20:42:53 -0400 Subject: [PATCH 12/16] Prefer tabs --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 143f09fe74..8b3761f24c 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,7 @@ deps = commands = pytest {posargs} setenv = - PYTHONPATH = {toxinidir} - # pypa/distutils#99 - VIRTUALENV_NO_SETUPTOOLS = 1 + PYTHONPATH = {toxinidir} + # pypa/distutils#99 + VIRTUALENV_NO_SETUPTOOLS = 1 skip_install = True From 2c34a366b169d6bd4079b2758badde700d6bd119 Mon Sep 17 00:00:00 2001 From: wim glenn Date: Fri, 29 Apr 2022 21:49:51 -0500 Subject: [PATCH 13/16] address review comments --- .github/workflows/main.yml | 6 +++--- distutils/dist.py | 37 ++++++++++++--------------------- distutils/tests/test_install.py | 15 +++++-------- 3 files changed, 21 insertions(+), 37 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 51816629f6..12b049c621 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,9 +11,9 @@ jobs: strategy: matrix: python: - - "3.7" - - "3.8" - - "3.9" + - 3.7 + - 3.8 + - 3.9 - "3.10" platform: - ubuntu-latest diff --git a/distutils/dist.py b/distutils/dist.py index 90e6f12f77..45024975b9 100644 --- a/distutils/dist.py +++ b/distutils/dist.py @@ -1064,7 +1064,7 @@ def read_pkg_file(self, file): def _read_field(name): value = msg[name] - if value: + if value and value != "UNKNOWN": return value def _read_list(name): @@ -1129,30 +1129,19 @@ def write_pkg_file(self, file): file.write('Name: %s\n' % self.get_name()) file.write('Version: %s\n' % self.get_version()) + def maybe_write(header, val): + if val: + file.write("{}: {}\n".format(header, val)) + # optional fields - summary = self.get_description() - if summary: - file.write('Summary: %s\n' % summary) - home_page = self.get_url() - if home_page: - file.write('Home-page: %s\n' % home_page) - author = self.get_contact() - if author: - file.write('Author: %s\n' % author) - author_email = self.get_contact_email() - if author_email: - file.write('Author-email: %s\n' % author_email) - license = self.get_license() - if license: - file.write('License: %s\n' % license) - if self.download_url: - file.write('Download-URL: %s\n' % self.download_url) - long_desc = self.get_long_description() - if long_desc: - file.write('Description: %s\n' % rfc822_escape(long_desc)) - keywords = ','.join(self.get_keywords()) - if keywords: - file.write('Keywords: %s\n' % keywords) + maybe_write("Summary", self.get_description()) + maybe_write("Home-page", self.get_url()) + maybe_write("Author", self.get_contact()) + maybe_write("Author-email", self.get_contact_email()) + maybe_write("License", self.get_license()) + maybe_write("Download-URL", self.download_url) + maybe_write("Description", rfc822_escape(self.get_long_description() or "")) + maybe_write("Keywords", ",".join(self.get_keywords())) self._write_list(file, 'Platform', self.get_platforms()) self._write_list(file, 'Classifier', self.get_classifiers()) diff --git a/distutils/tests/test_install.py b/distutils/tests/test_install.py index e01d979393..3aef9e432e 100644 --- a/distutils/tests/test_install.py +++ b/distutils/tests/test_install.py @@ -187,9 +187,7 @@ def test_finalize_options(self): def test_record(self): install_dir = self.mkdtemp() - project_dir, dist = self.create_dist(name="testdist", - version="0.1", - py_modules=['hello'], + project_dir, dist = self.create_dist(py_modules=['hello'], scripts=['sayhi']) os.chdir(project_dir) self.write_file('hello.py', "def main(): print('o hai')") @@ -211,7 +209,7 @@ def test_record(self): found = [os.path.basename(line) for line in content.splitlines()] expected = ['hello.py', 'hello.%s.pyc' % sys.implementation.cache_tag, 'sayhi', - 'testdist-0.1-py%s.%s.egg-info' % sys.version_info[:2]] + 'UNKNOWN-0.0.0-py%s.%s.egg-info' % sys.version_info[:2]] self.assertEqual(found, expected) def test_record_extensions(self): @@ -219,11 +217,8 @@ def test_record_extensions(self): if cmd is not None: self.skipTest('The %r command is not found' % cmd) install_dir = self.mkdtemp() - project_dir, dist = self.create_dist( - name="testdist", - version="0.1", - ext_modules=[Extension('xx', ['xxmodule.c'])], - ) + project_dir, dist = self.create_dist(ext_modules=[ + Extension('xx', ['xxmodule.c'])]) os.chdir(project_dir) support.copy_xxmodule_c(project_dir) @@ -247,7 +242,7 @@ def test_record_extensions(self): found = [os.path.basename(line) for line in content.splitlines()] expected = [_make_ext_name('xx'), - 'testdist-0.1-py%s.%s.egg-info' % sys.version_info[:2]] + 'UNKNOWN-0.0.0-py%s.%s.egg-info' % sys.version_info[:2]] self.assertEqual(found, expected) def test_debug_mode(self): From 5bac124942b476e1defa6e22baf4705481a45260 Mon Sep 17 00:00:00 2001 From: wim glenn Date: Sat, 30 Apr 2022 12:55:30 -0500 Subject: [PATCH 14/16] fix a typo --- distutils/ccompiler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/ccompiler.py b/distutils/ccompiler.py index c9eb709ba2..777fc661ea 100644 --- a/distutils/ccompiler.py +++ b/distutils/ccompiler.py @@ -392,7 +392,7 @@ def _fix_compile_args(self, output_dir, macros, include_dirs): return output_dir, macros, include_dirs def _prep_compile(self, sources, output_dir, depends=None): - """Decide which souce files must be recompiled. + """Decide which source files must be recompiled. Determine the list of object files corresponding to 'sources', and figure out which ones really need to be recompiled. From 9afdc033e6aadc5960ff3961404bcce41154df4d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 9 May 2022 10:30:59 -0400 Subject: [PATCH 15/16] Update changelog --- changelog.d/3299.change.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/3299.change.rst b/changelog.d/3299.change.rst index c84d7f0fd8..a5b6a8e7be 100644 --- a/changelog.d/3299.change.rst +++ b/changelog.d/3299.change.rst @@ -1 +1 @@ -Optional metadata fields are now truly optional. +Optional metadata fields are now truly optional. Includes merge with pypa/distutils@a7cfb56 per pypa/distutils#138. From f842f59f677363e88d7250c492b7e7f0a14906ec Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 10 May 2022 08:06:45 -0400 Subject: [PATCH 16/16] Try Python 3.9 --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 45af54e5ba..092c0dccf8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -70,7 +70,7 @@ jobs: strategy: matrix: python: - - 38 + - 39 platform: - windows-latest runs-on: ${{ matrix.platform }}