Imported Upstream version 62.2.0 upstream/62.2.0
authorJinWang An <jinwang.an@samsung.com>
Mon, 27 Mar 2023 08:02:49 +0000 (17:02 +0900)
committerJinWang An <jinwang.an@samsung.com>
Mon, 27 Mar 2023 08:02:49 +0000 (17:02 +0900)
25 files changed:
.bumpversion.cfg
.github/workflows/main.yml
CHANGES.rst
docs/build_meta.rst
docs/deprecated/distutils/examples.rst
docs/deprecated/distutils/setupscript.rst
docs/userguide/package_discovery.rst
docs/userguide/quickstart.rst
setup.cfg
setuptools/_distutils/_functools.py [new file with mode: 0644]
setuptools/_distutils/command/bdist_msi.py
setuptools/_distutils/command/bdist_rpm.py
setuptools/_distutils/command/check.py
setuptools/_distutils/dist.py
setuptools/_distutils/sysconfig.py
setuptools/_distutils/tests/test_check.py
setuptools/_distutils/tests/test_dist.py
setuptools/_distutils/tests/test_register.py
setuptools/_distutils/tests/test_sdist.py
setuptools/_distutils/tests/test_sysconfig.py
setuptools/config/_apply_pyprojecttoml.py
setuptools/dist.py
setuptools/tests/config/downloads/__init__.py
setuptools/tests/config/test_apply_pyprojecttoml.py
tox.ini

index 1125d38d3638ff2ab14046d2f4cbc37055413f4e..7fb9cd18c2dd4226020d15b1c08037bb96228489 100644 (file)
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 62.1.0
+current_version = 62.2.0
 commit = True
 tag = True
 
index c680fb36362847535eaeff841641152d3f87e9f9..092c0dccf8afce3e5e497011deb10c3f418c1e6f 100644 (file)
@@ -39,6 +39,17 @@ jobs:
         uses: actions/setup-python@v2
         with:
           python-version: ${{ matrix.python }}
+      - uses: actions/cache@v3
+        id: cache
+        with:
+          path: setuptools/tests/config/downloads/*.cfg
+          key: >-
+            ${{ hashFiles('setuptools/tests/config/setupcfg_examples.txt') }}-
+            ${{ hashFiles('setuptools/tests/config/downloads/*.py') }}
+      - name: Populate download cache
+        if: steps.cache.outputs.cache-hit != 'true'
+        working-directory: setuptools/tests/config
+        run: python -m downloads.preload setupcfg_examples.txt
       - name: Install tox
         run: |
           python -m pip install tox
@@ -56,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
@@ -65,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
index 5061ecb999f01693bdf7db932143631b5baf41df..54fc15bf299369a10f00a4a86b962f2149121b54 100644 (file)
@@ -1,3 +1,16 @@
+v62.2.0
+-------
+
+
+Changes
+^^^^^^^
+* #3299: Optional metadata fields are now truly optional. Includes merge with pypa/distutils@a7cfb56 per pypa/distutils#138.
+
+Misc
+^^^^
+* #3282: Added CI cache for ``setup.cfg`` examples used when testing ``setuptools.config``.
+
+
 v62.1.0
 -------
 
index cb372721140e958ea527b7f84ee1274b705ac94f..57aea986fa8a0aa97d7ca0c6cbe2168c56663815 100644 (file)
@@ -114,7 +114,7 @@ specified by :pep:`517`, is to "tweak" ``setuptools.build_meta`` by using a
    with **environment markers** are enough to differentiate operating systems
    and platforms.
 
-If you add the following configuration to your ``pyprojec.toml``:
+If you add the following configuration to your ``pyproject.toml``:
 
 
 .. code-block:: toml
index d0984655df6272d8ae3dfa1c6265e0cf72ac65cd..00eef73fa9af355cba49cbe62b201bc0a0c86115 100644 (file)
@@ -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
index f49c4f893f5ca3e7995e46aea8f2e4f9e899c145..ec9cf34ed70302c6f317060c0ec15d4b3473da22 100644 (file)
@@ -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,
index ee8e98365985bb61d344052ec1c378dfde19508f..38119bc6fa6f6b1ae8546045259180a13f156be3 100644 (file)
@@ -189,7 +189,7 @@ The package folder(s) are placed directly under the project root::
         └── mymodule.py
 
 This layout is very practical for using the REPL, but in some situations
-it can be can be more error-prone (e.g. during tests or if you have a bunch
+it can be more error-prone (e.g. during tests or if you have a bunch
 of folders or Python files hanging around your project root)
 
 To avoid confusion, file and folder names that are used by popular tools (or
index c72db26b0aebfd94c7e9ca84c60a667e152270af..2f77852178ee65f28061d1b10dda5b8e2889f103 100644 (file)
@@ -121,7 +121,7 @@ Automatic package discovery
 For simple projects, it's usually easy enough to manually add packages to
 the ``packages`` keyword in ``setup.cfg``.  However, for very large projects,
 it can be a big burden to keep the package list updated.
-Therefore, ``setuptoops`` provides a convenient way to automatically list all
+Therefore, ``setuptools`` provides a convenient way to automatically list all
 the packages in your project directory:
 
 .. tab:: setup.cfg
index 4b386243a47e3a9d233e7f17091e611beb04dbf0..0dec946bcc4ffe8777b444ab16e2bbd19fce4a5b 100644 (file)
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = setuptools
-version = 62.1.0
+version = 62.2.0
 author = Python Packaging Authority
 author_email = distutils-sig@python.org
 description = Easily download, build, install, upgrade, and uninstall Python packages
diff --git a/setuptools/_distutils/_functools.py b/setuptools/_distutils/_functools.py
new file mode 100644 (file)
index 0000000..e7053ba
--- /dev/null
@@ -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
index 15259532415a5d1c4b18e5e09b775667b28f8b8e..56c4b9883a526bdcd730faac6acb622fb1136954 100644 (file)
@@ -231,11 +231,7 @@ class bdist_msi(Command):
         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
index 550cbfa1e28a2376fe1b4f994bce86b446c0916e..a2a9e8e5888aac29850aa2712025d7427dd18b6c 100644 (file)
@@ -399,7 +399,7 @@ class bdist_rpm(Command):
             '%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 @@ class bdist_rpm(Command):
             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 @@ class bdist_rpm(Command):
                 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 @@ class bdist_rpm(Command):
         spec_file.extend([
             '',
             '%description',
-            self.distribution.get_long_description()
+            self.distribution.get_long_description() or "",
             ])
 
         # put locale descriptions into spec file
index af311ca90e4e54d65121b7bf5a9ceb9d38788312..8a02dbca7d3375001f85d21a75d1756419212201 100644 (file)
@@ -82,54 +82,19 @@ class check(Command):
         """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 <email>`
-            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."""
index 37db4d6cd7539940d5629ae1f426526a4d8d1d6f..45024975b9c736a9ec062600228f2bbc8c6034e3 100644 (file)
@@ -1064,9 +1064,8 @@ class DistributionMetadata:
 
         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 @@ class DistributionMetadata:
                 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 @@ class DistributionMetadata:
         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 @@ class DistributionMetadata:
         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 @@ class DistributionMetadata:
         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 @@ class DistributionMetadata:
         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):
index 55a42e169dc80decca130737ef595fa32abc559e..7543f794cbc2e0a680decbe010134f017e2dc188 100644 (file)
@@ -16,6 +16,7 @@ import sysconfig
 
 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)
 
index b41dba3d0ad3257177e8c8a3d5d193feb55a6124..2414d6eb5ee8e610c54e22d1b60e577f75d4c8ed 100644 (file)
@@ -43,7 +43,7 @@ class CheckTestCase(support.LoggingSilencer,
         # 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 @@ class CheckTestCase(support.LoggingSilencer,
             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):
index 36155be1524e669f366766eb396ac377b2105af9..9132bc040b9f1c9abbe019f5317884e16bfce6ba 100644 (file)
@@ -519,7 +519,7 @@ class MetadataTestCase(support.TempdirManager, support.EnvironGuard,
         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'])
 
index 5770ed58ae937955ebfa5b911e1adef0b4999f55..4556768645d2e815d4bf90dd53f55a871f607308 100644 (file)
@@ -154,8 +154,8 @@ class RegisterTestCase(BasePyPIRCCommandTestCase):
         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):
index 4c51717ce6a75ad2a168203ef1be56c3fa5a8ee5..aa04dd05469a5855f774e9704f2c132291a4cca0 100644 (file)
@@ -251,7 +251,7 @@ class SDistTestCase(BasePyPIRCCommandTestCase):
         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()
index e671f9e09b7ca430e9e01b0fda12a7c93b0a2cc3..1c88cc85f755485be760e4ee3f005ad66a667ddb 100644 (file)
@@ -7,6 +7,9 @@ import sys
 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 @@ class SysconfigTestCase(support.EnvironGuard, unittest.TestCase):
         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))
index fce5c40e346810ccc92ff59b0882d129e2b84652..a580b63f6f04e64b8023a4c501a3e3dabc41fe33 100644 (file)
@@ -171,21 +171,7 @@ def _people(dist: "Distribution", val: List[dict], _root_dir: _Path, kind: str):
 
 
 def _project_urls(dist: "Distribution", val: dict, _root_dir):
-    special = {"downloadurl": "download_url", "homepage": "url"}
-    for key, url in val.items():
-        norm_key = json_compatible_key(key).replace("_", "")
-        _set_config(dist, special.get(norm_key, key), url)
-    # If `homepage` is missing, distutils will warn the following message:
-    #     "warning: check: missing required meta-data: url"
-    # In the context of PEP 621, users might ask themselves: "which url?".
-    # Let's add a warning before distutils check to help users understand the problem:
-    if not dist.metadata.url:
-        msg = (
-            "Missing `Homepage` url.\nIt is advisable to link some kind of reference "
-            "for your project (e.g. source code or documentation).\n"
-        )
-        _logger.warning(msg)
-    _set_config(dist, "project_urls", val.copy())
+    _set_config(dist, "project_urls", val)
 
 
 def _python_requires(dist: "Distribution", val: dict, _root_dir):
index 215c88e3a83cfa45ccbd46e518e172ebeaacde60..5507167d30a1ca06e7ac9bd756cdb2ab9ecf4001 100644 (file)
@@ -102,7 +102,7 @@ def _read_list_from_msg(msg: "Message", field: str) -> Optional[List[str]]:
 
 def _read_payload_from_msg(msg: "Message") -> Optional[str]:
     value = msg.get_payload().strip()
-    if value == 'UNKNOWN':
+    if value == 'UNKNOWN' or not value:
         return None
     return value
 
@@ -174,7 +174,10 @@ def write_pkg_file(self, file):  # noqa: C901  # is too complex (14)  # FIXME
     write_field('Metadata-Version', str(version))
     write_field('Name', self.get_name())
     write_field('Version', self.get_version())
-    write_field('Summary', single_line(self.get_description()))
+
+    summary = self.get_description()
+    if summary:
+        write_field('Summary', single_line(summary))
 
     optional_fields = (
         ('Home-page', 'url'),
@@ -190,8 +193,10 @@ def write_pkg_file(self, file):  # noqa: C901  # is too complex (14)  # FIXME
         if attr_val is not None:
             write_field(field, attr_val)
 
-    license = rfc822_escape(self.get_license())
-    write_field('License', license)
+    license = self.get_license()
+    if license:
+        write_field('License', rfc822_escape(license))
+
     for project_url in self.project_urls.items():
         write_field('Project-URL', '%s, %s' % project_url)
 
@@ -199,7 +204,8 @@ def write_pkg_file(self, file):  # noqa: C901  # is too complex (14)  # FIXME
     if keywords:
         write_field('Keywords', keywords)
 
-    for platform in self.get_platforms():
+    platforms = self.get_platforms() or []
+    for platform in platforms:
         write_field('Platform', platform)
 
     self._write_list(file, 'Classifier', self.get_classifiers())
@@ -222,7 +228,11 @@ def write_pkg_file(self, file):  # noqa: C901  # is too complex (14)  # FIXME
 
     self._write_list(file, 'License-File', self.license_files or [])
 
-    file.write("\n%s\n\n" % self.get_long_description())
+    long_description = self.get_long_description()
+    if long_description:
+        file.write("\n%s" % long_description)
+        if not long_description.endswith("\n"):
+            file.write("\n")
 
 
 sequence = tuple, list
index de43cffb89a5576109bf230b4402a1aa6ec1b03f..9fb9b14b025aae3e640ea005d3f1a6c28b822bc0 100644 (file)
@@ -1,5 +1,7 @@
 import re
+import time
 from pathlib import Path
+from urllib.error import HTTPError
 from urllib.request import urlopen
 
 __all__ = ["DOWNLOAD_DIR", "retrieve_file", "output_file", "urls_from_file"]
@@ -21,14 +23,18 @@ def output_file(url: str, download_dir: Path = DOWNLOAD_DIR):
     return Path(download_dir, re.sub(r"[^\-_\.\w\d]+", "_", file_name))
 
 
-def retrieve_file(url: str, download_dir: Path = DOWNLOAD_DIR):
+def retrieve_file(url: str, download_dir: Path = DOWNLOAD_DIR, wait: float = 5):
     path = output_file(url, download_dir)
     if path.exists():
         print(f"Skipping {url} (already exists: {path})")
     else:
         download_dir.mkdir(exist_ok=True, parents=True)
         print(f"Downloading {url} to {path}")
-        download(url, path)
+        try:
+            download(url, path)
+        except HTTPError:
+            time.sleep(wait)  # wait a few seconds and try again.
+            download(url, path)
     return path
 
 
index 045d7f40b6a39e0b07dd7b3a1f99779874600b25..4f541697aa1c307e599a827b754aac48e1138ab3 100644 (file)
@@ -298,19 +298,26 @@ class TestMeta:
 def core_metadata(dist) -> str:
     with io.StringIO() as buffer:
         dist.metadata.write_pkg_file(buffer)
-        value = "\n".join(buffer.getvalue().strip().splitlines())
+        pkg_file_txt = buffer.getvalue()
 
+    skip_prefixes = ()
+    skip_lines = set()
     # ---- DIFF NORMALISATION ----
     # PEP 621 is very particular about author/maintainer metadata conversion, so skip
-    value = re.sub(r"^(Author|Maintainer)(-email)?:.*$", "", value, flags=re.M)
+    skip_prefixes += ("Author:", "Author-email:", "Maintainer:", "Maintainer-email:")
     # May be redundant with Home-page
-    value = re.sub(r"^Project-URL: Homepage,.*$", "", value, flags=re.M)
+    skip_prefixes += ("Project-URL: Homepage,", "Home-page:")
     # May be missing in original (relying on default) but backfilled in the TOML
-    value = re.sub(r"^Description-Content-Type:.*$", "", value, flags=re.M)
+    skip_prefixes += ("Description-Content-Type:",)
     # ini2toml can automatically convert `tests_require` to `testing` extra
-    value = value.replace("Provides-Extra: testing\n", "")
+    skip_lines.add("Provides-Extra: testing")
     # Remove empty lines
-    value = re.sub(r"^\s*$", "", value, flags=re.M)
-    value = re.sub(r"^\n", "", value, flags=re.M)
+    skip_lines.add("")
 
-    return value
+    result = []
+    for line in pkg_file_txt.splitlines():
+        if line.startswith(skip_prefixes) or line in skip_lines:
+            continue
+        result.append(line + "\n")
+
+    return "".join(result)
diff --git a/tox.ini b/tox.ini
index 973f3763a6fa18c0b7d5dcc64bab1375bd603aac..bb2e7cb17d4e1910c651d9e84cfb2bcfa7635f23 100644 (file)
--- 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 =