diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4275bbde7a..092c0dccf8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -67,7 +67,13 @@ jobs: ${{ matrix.python }} test_cygwin: - runs-on: windows-latest + strategy: + matrix: + python: + - 39 + platform: + - windows-latest + runs-on: ${{ matrix.platform }} timeout-minutes: 75 steps: - uses: actions/checkout@v2 @@ -76,19 +82,14 @@ jobs: with: platform: x86_64 packages: >- - git, + python${{ matrix.python }}, + python${{ matrix.python }}-devel, + python${{ matrix.python }}-tox, gcc-core, - python38, - python38-devel, - python38-pip - - name: Install tox - shell: C:\cygwin\bin\env.exe CYGWIN_NOWINPATH=1 CHERE_INVOKING=1 C:\cygwin\bin\bash.exe -leo pipefail -o igncr {0} - run: | - python3.8 -m pip install tox + git, - 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: | - tox -- --cov-report xml + run: tox integration-test: needs: test 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. diff --git a/docs/deprecated/distutils/examples.rst b/docs/deprecated/distutils/examples.rst index d0984655df..00eef73fa9 100644 --- a/docs/deprecated/distutils/examples.rst +++ b/docs/deprecated/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/deprecated/distutils/setupscript.rst b/docs/deprecated/distutils/setupscript.rst index f49c4f893f..ec9cf34ed7 100644 --- a/docs/deprecated/distutils/setupscript.rst +++ b/docs/deprecated/distutils/setupscript.rst @@ -582,7 +582,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 | | | @@ -612,8 +612,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, diff --git a/setuptools/_distutils/_functools.py b/setuptools/_distutils/_functools.py new file mode 100644 index 0000000000..e7053bac12 --- /dev/null +++ b/setuptools/_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/setuptools/_distutils/command/bdist_msi.py b/setuptools/_distutils/command/bdist_msi.py index 1525953241..56c4b9883a 100644 --- a/setuptools/_distutils/command/bdist_msi.py +++ b/setuptools/_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/setuptools/_distutils/command/bdist_rpm.py b/setuptools/_distutils/command/bdist_rpm.py index 550cbfa1e2..a2a9e8e588 100644 --- a/setuptools/_distutils/command/bdist_rpm.py +++ b/setuptools/_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/setuptools/_distutils/command/check.py b/setuptools/_distutils/command/check.py index af311ca90e..8a02dbca7d 100644 --- a/setuptools/_distutils/command/check.py +++ b/setuptools/_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/setuptools/_distutils/dist.py b/setuptools/_distutils/dist.py index 37db4d6cd7..45024975b9 100644 --- a/setuptools/_distutils/dist.py +++ b/setuptools/_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 and value != "UNKNOWN": + return value def _read_list(name): values = msg.get_all(name, None) @@ -1125,23 +1124,24 @@ 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()) - 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) + def maybe_write(header, val): + if val: + file.write("{}: {}\n".format(header, val)) - keywords = ','.join(self.get_keywords()) - if keywords: - file.write('Keywords: %s\n' % keywords) + # optional fields + 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()) @@ -1152,6 +1152,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 +1168,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 +1205,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 +1217,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/setuptools/_distutils/sysconfig.py b/setuptools/_distutils/sysconfig.py index 55a42e169d..7543f794cb 100644 --- a/setuptools/_distutils/sysconfig.py +++ b/setuptools/_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 @@ -51,12 +52,25 @@ 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': + @pass_none def _fix_pcbuild(d): - if d and os.path.normcase(d).startswith( - os.path.normcase(os.path.join(PREFIX, "PCbuild"))): - return PREFIX - return d + # In a venv, sys._home will be inside BASE_PREFIX rather than PREFIX. + 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) diff --git a/setuptools/_distutils/tests/test_check.py b/setuptools/_distutils/tests/test_check.py index b41dba3d0a..2414d6eb5e 100644 --- a/setuptools/_distutils/tests/test_check.py +++ b/setuptools/_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/setuptools/_distutils/tests/test_dist.py b/setuptools/_distutils/tests/test_dist.py index 36155be152..9132bc040b 100644 --- a/setuptools/_distutils/tests/test_dist.py +++ b/setuptools/_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/setuptools/_distutils/tests/test_register.py b/setuptools/_distutils/tests/test_register.py index 5770ed58ae..4556768645 100644 --- a/setuptools/_distutils/tests/test_register.py +++ b/setuptools/_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/setuptools/_distutils/tests/test_sdist.py b/setuptools/_distutils/tests/test_sdist.py index 4c51717ce6..aa04dd0546 100644 --- a/setuptools/_distutils/tests/test_sdist.py +++ b/setuptools/_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/setuptools/_distutils/tests/test_sysconfig.py b/setuptools/_distutils/tests/test_sysconfig.py index e671f9e09b..1c88cc85f7 100644 --- a/setuptools/_distutils/tests/test_sysconfig.py +++ b/setuptools/_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 @@ -309,6 +312,31 @@ 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.""" + env = jaraco.envs.VEnv() + env.create_opts = env.clean_opts + env.root = TESTFN + env.ensure_env() + cmd = [ + 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" + + def test_suite(): suite = unittest.TestSuite() suite.addTest(unittest.TestLoader().loadTestsFromTestCase(SysconfigTestCase)) diff --git a/tox.ini b/tox.ini index 973f3763a6..bb2e7cb17d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = python -minversion = 3.2 +minversion = 3.25 # https://github.com/jaraco/skeleton/issues/6 tox_pip_extensions_ext_venv_update = true toxworkdir={env:TOX_WORK_DIR:.tox} @@ -20,10 +20,6 @@ passenv = windir # required for test_pkg_resources # honor git config in pytest-perf HOME - # workaround for tox-dev/tox#2382 - PROGRAMDATA - PROGRAMFILES - PROGRAMFILES(x86) [testenv:integration] deps = {[testenv]deps} @@ -31,10 +27,6 @@ extras = testing-integration passenv = {[testenv]passenv} DOWNLOAD_PATH - # workaround for tox-dev/tox#2382 - PROGRAMDATA - PROGRAMFILES - PROGRAMFILES(x86) setenv = PROJECT_ROOT = {toxinidir} commands =