From b599af3b64473611288918deb4f362cf16aea608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E4=B9=9D=E9=BC=8E?= <109224573@qq.com> Date: Mon, 17 May 2021 12:15:03 +0800 Subject: sys._home -> sys.base_exec_prefix --- distutils/command/build_ext.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/distutils/command/build_ext.py b/distutils/command/build_ext.py index bbb34833..55ea0cb9 100644 --- a/distutils/command/build_ext.py +++ b/distutils/command/build_ext.py @@ -202,9 +202,7 @@ class build_ext(Command): # Append the source distribution include and library directories, # this allows distutils on windows to work in the source tree self.include_dirs.append(os.path.dirname(get_config_h_filename())) - _sys_home = getattr(sys, '_home', None) - if _sys_home: - self.library_dirs.append(_sys_home) + self.library_dirs.append(sys.base_exec_prefix) # Use the .lib files for the correct architecture if self.plat_name == 'win32': -- cgit v1.2.1 From 816cc9a42a3649387ff3de13b0239ff1680fec5f Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Fri, 15 Oct 2021 14:04:45 -0400 Subject: WIP: Reject packages without required metadata This needs tests and probably formatting stuff. --- setuptools/dist.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/setuptools/dist.py b/setuptools/dist.py index 8e2111a5..84c06f93 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -466,6 +466,29 @@ class Distribution(_Distribution): ) self._finalize_requires() + def _validate_metadata(self): + required = ["name", "version"] + missing = [] + + for req_attr in required: + if getattr(self.metadata, req_attr) is None: + missing.append(req_attr) + + if missing: + if len(missing) == 1: + message = "%s attribute" % missing[0] + else: + message = "%s and %s attributes" % (", ".join(missing[:-1]), + missing[-1]) + raise DistutilsSetupError( + "Required package metadata is missing: please supply the %s." % message + ) + + def run_commands(self): + self._validate_metadata() + super().run_commands() + + def _set_metadata_defaults(self, attrs): """ Fill-in missing metadata fields not supported by distutils. -- cgit v1.2.1 From b42ec23633cf81aa9e50f6e44d6ff33fd796293b Mon Sep 17 00:00:00 2001 From: Matthew Suozzo Date: Sat, 30 Oct 2021 12:06:15 -0400 Subject: Maintain `requires` order in METADATA. It seems that workflows that build -> install -> build would have the order of requires changed by these sorted() calls. From a brief attempt to trace through all the related interfaces, it seems this is the only code that changes the order of the requirements and so would be the only source of the observed inconsistency of Requires-Dist entries. If this sorting is desirable, the other setuptools and wheel interfaces that interact with the requirements should also sort them. Otherwise, it's valuable to retain the order across all parts of the system. --- setuptools/wheel.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setuptools/wheel.py b/setuptools/wheel.py index 0be811af..722264f6 100644 --- a/setuptools/wheel.py +++ b/setuptools/wheel.py @@ -133,16 +133,16 @@ class Wheel: # Note: Evaluate and strip markers now, # as it's difficult to convert back from the syntax: # foobar; "linux" in sys_platform and extra == 'test' - def raw_req(req): + def to_raw(req): req.marker = None return str(req) - install_requires = list(sorted(map(raw_req, dist.requires()))) + install_requires = list(map(raw_req, dist.requires())) extras_require = { - extra: sorted( + extra: [ req for req in map(raw_req, dist.requires((extra,))) if req not in install_requires - ) + ] for extra in dist.extras } os.rename(dist_info, egg_info) -- cgit v1.2.1 From a2f2c988545d9cfbbb8b5e7e7a6a030f3db692ee Mon Sep 17 00:00:00 2001 From: Matthew Suozzo Date: Sat, 30 Oct 2021 12:09:44 -0400 Subject: Revert inner function name change. --- setuptools/wheel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setuptools/wheel.py b/setuptools/wheel.py index 722264f6..9819e8b9 100644 --- a/setuptools/wheel.py +++ b/setuptools/wheel.py @@ -133,7 +133,7 @@ class Wheel: # Note: Evaluate and strip markers now, # as it's difficult to convert back from the syntax: # foobar; "linux" in sys_platform and extra == 'test' - def to_raw(req): + def raw_req(req): req.marker = None return str(req) install_requires = list(map(raw_req, dist.requires())) -- cgit v1.2.1 From d18777775b2fdfab61c9251a1f86b1f75777a444 Mon Sep 17 00:00:00 2001 From: Matthew Suozzo Date: Sat, 30 Oct 2021 12:20:17 -0400 Subject: Add news entry. --- 2839.change.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 2839.change.rst diff --git a/2839.change.rst b/2839.change.rst new file mode 100644 index 00000000..621fa667 --- /dev/null +++ b/2839.change.rst @@ -0,0 +1 @@ +Removed `requires` sorting when installing wheels as an egg dir. -- cgit v1.2.1 From ac0759c2ea176404ca48b8a99738c82c9682eda6 Mon Sep 17 00:00:00 2001 From: gentlegiantJGC Date: Sat, 13 Nov 2021 11:01:14 +0000 Subject: Update build_meta.py --- setuptools/build_meta.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index d0ac613b..da0efc8b 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -131,6 +131,8 @@ class _BuildMetaBackend(object): def _fix_config(self, config_settings): config_settings = config_settings or {} config_settings.setdefault('--global-option', []) + if isinstance(config_settings["--global-option"], str): + config_settings["--global-option"] = [config_settings["--global-option"]] return config_settings def _get_build_requires(self, config_settings, requirements): -- cgit v1.2.1 From b6fcbbd00cb6d5607c9272dec452a50457bdb292 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 18 Nov 2021 21:18:26 -0500 Subject: Restore local distutils as the default. --- _distutils_hack/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/_distutils_hack/__init__.py b/_distutils_hack/__init__.py index 5f40996a..0299a779 100644 --- a/_distutils_hack/__init__.py +++ b/_distutils_hack/__init__.py @@ -42,7 +42,7 @@ def enabled(): """ Allow selection of distutils by environment variable. """ - which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'stdlib') + which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'local') return which == 'local' diff --git a/setup.py b/setup.py index c6affe97..4cda3d38 100755 --- a/setup.py +++ b/setup.py @@ -54,7 +54,7 @@ class install_with_pth(install): _pth_contents = textwrap.dedent(""" import os var = 'SETUPTOOLS_USE_DISTUTILS' - enabled = os.environ.get(var, 'stdlib') == 'local' + enabled = os.environ.get(var, 'local') == 'local' enabled and __import__('_distutils_hack').add_shim() """).lstrip().replace('\n', '; ') -- cgit v1.2.1 From fe19053225a6030087df2b239b667b9617f9fc6c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 18 Nov 2021 21:33:19 -0500 Subject: Update changelog. --- changelog.d/2896.change.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/2896.change.rst diff --git a/changelog.d/2896.change.rst b/changelog.d/2896.change.rst new file mode 100644 index 00000000..e0aebdcb --- /dev/null +++ b/changelog.d/2896.change.rst @@ -0,0 +1 @@ +Setuptools once again makes its local copy of distutils the default. To override, set SETUPTOOLS_USE_DISTUTILS=stdlib. -- cgit v1.2.1 From c8fcf4d2e3aaf543f065971dcf78451be35adcc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Fri, 19 Nov 2021 16:50:31 +0100 Subject: Incorporate Fedora's distutil patch See https://github.com/pypa/setuptools/pull/2896#issuecomment-973983395 --- distutils/command/install.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/distutils/command/install.py b/distutils/command/install.py index c756b6db..dbc83fce 100644 --- a/distutils/command/install.py +++ b/distutils/command/install.py @@ -471,8 +471,24 @@ class install(Command): raise DistutilsOptionError( "must not supply exec-prefix without prefix") - self.prefix = os.path.normpath(sys.prefix) - self.exec_prefix = os.path.normpath(sys.exec_prefix) + # Fedora (and Fedora derived distros) used to patch distutils + # until Fedora 36 and/or Python 3.11. + # Here, we preserve that behavior conditionally on a special + # _distutils_mangle_rpm_prefix attribute of sysconfig + # that Fedora sets on their older Pythons to support this check. + # When it is set and true-ish, + # self.prefix is set to sys.prefix + /local/ + # if neither RPM build nor virtual environment is + # detected to make pip and distutils install packages to /usr/local. + addition = "" + if (getattr(sysconfig, "_distutils_mangle_rpm_prefix", False) and + not (hasattr(sys, 'real_prefix') or + sys.prefix != sys.base_prefix) and + 'RPM_BUILD_ROOT' not in os.environ): + addition = "/local" + + self.prefix = os.path.normpath(sys.prefix) + addition + self.exec_prefix = os.path.normpath(sys.exec_prefix) + addition else: if self.exec_prefix is None: -- cgit v1.2.1 From 52dc3365ebb74e3002faf6ca4cc41037cad4a0f4 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 19 Nov 2021 20:24:09 +0000 Subject: Add documentation about build_meta wrappers --- docs/build_meta.rst | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/conf.py | 1 + 2 files changed, 78 insertions(+) diff --git a/docs/build_meta.rst b/docs/build_meta.rst index 27df70a2..5851d965 100644 --- a/docs/build_meta.rst +++ b/docs/build_meta.rst @@ -89,3 +89,80 @@ and installed:: or:: $ pip install dist/meowpkg-0.0.1.tar.gz + +Dynamic build dependencies and other ``build_meta`` tweaks +---------------------------------------------------------- + +With the changes introduced by :pep:`517` and :pep:`518`, the +``setup_requires`` configuration field was made deprecated in ``setup.cfg`` and +``setup.py``, in favour of directly listing build dependencies in the +``requires`` field of the ``build-system`` table of ``pyproject.toml``. +This approach has a series of advantages and gives package managers and +installers the ability to inspect in advance the build requirements and +perform a series of optimisations. + +However some package authors might still need to dynamically inspect the final +users machine before deciding these requirements. One way of doing that, as +specified by :pep:`517`, is to "tweak" ``setuptools.build_meta`` by using a +:pep:`in-tree backend <517#in-tree-build-backends>`. + +If you add the following configuration to your ``pyprojec.toml``: + + +.. code-block:: toml + + [build-system] + requires = ["setuptools", "wheel"] + build-backend = "backend" + backend-path = ["_custom_build"] + + +then you should be able to implement a thin wrapper around ``build_meta`` in +the ``_custom_build/backend.py`` file, as shown in the following example: + +.. code-block:: python + + from setuptools import build_meta as _orig + + prepare_metadata_for_build_wheel = _orig.prepare_metadata_for_build_wheel + build_wheel = _orig.build_wheel + build_sdist = _orig.build_sdist + + + def get_requires_for_build_wheel(self, config_settings=None): + return _orig.get_requires_for_build_wheel(config_settings) + [...] + + + def get_requires_for_build_sdist(self, config_settings=None): + return _orig.get_requires_for_build_sdist(config_settings) + [...] + + +Note that you can override any of the functions specified in :pep:`PEP 517 +<517#build-backend-interface>`, not only the ones responsible for gathering +requirements. + +.. tip:: Make sure your backend script is included in the :doc:`source + distribution `, otherwise the build will fail. + This can be done by using a SCM_/VCS_ plugin (like :pypi:`setuptools-scm` + and :pypi:`setuptools-svn`), or by correctly setting up :ref:`MANIFEST.in + `. + + If this is the first time you are using a customised backend, please have a + look on the generated ``.tar.gz`` and ``.whl``. + On POSIX systems that can be done with ``tar -tf dist/*.tar.gz`` + and ``unzip -l dist/*.whl``. + On Windows systems you can rename the ``.whl`` to ``.zip`` to be able to + inspect it on the file explorer, and use the same ``tar`` command in a + command prompt (alternativelly there are GUI programs like `7-zip`_ that + handle ``.tar.gz``). + + In general the backend script should be present in the ``.tar.gz`` (so the + project can be build from the source) but not in the ``.whl`` (otherwise the + backend script would end up being distributed alongside your package). + See ":doc:`/userguide/package_discovery`" for more details about package + files. + + +.. _SCM: https://en.wikipedia.org/wiki/Software_configuration_management +.. _VCS: https://en.wikipedia.org/wiki/Version_control +.. _7-zip: https://www.7-zip.org diff --git a/docs/conf.py b/docs/conf.py index 3cc8e35b..38be1de5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -95,6 +95,7 @@ github_url = 'https://github.com' github_sponsors_url = f'{github_url}/sponsors' extlinks = { 'user': (f'{github_sponsors_url}/%s', '@'), # noqa: WPS323 + 'pypi': ('https://pypi.org/project/%s', '%s'), } extensions += ['sphinx.ext.extlinks'] -- cgit v1.2.1 From ae7237e1b44ff98d4a28a85bb0a26f60b5c1cb97 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 19 Nov 2021 20:37:21 +0000 Subject: Add news fragment --- changelog.d/2897.docs.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelog.d/2897.docs.rst diff --git a/changelog.d/2897.docs.rst b/changelog.d/2897.docs.rst new file mode 100644 index 00000000..763a39b8 --- /dev/null +++ b/changelog.d/2897.docs.rst @@ -0,0 +1,4 @@ +Added documentation about wrapping ``setuptools.build_meta`` in a in-tree +custom backend. This is a :pep:`517`-compliant way of dynamically specifying +build dependencies (e.g. when platform, OS and other markers are not enough) +-- by :user:`abravalheri`. -- cgit v1.2.1 From 7d3db3f9e6f1c6b29217a402d0294b423e1ee187 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 19 Nov 2021 20:49:06 +0000 Subject: Mention environment markers as alternative --- docs/build_meta.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/build_meta.rst b/docs/build_meta.rst index 5851d965..3c236d68 100644 --- a/docs/build_meta.rst +++ b/docs/build_meta.rst @@ -106,6 +106,11 @@ users machine before deciding these requirements. One way of doing that, as specified by :pep:`517`, is to "tweak" ``setuptools.build_meta`` by using a :pep:`in-tree backend <517#in-tree-build-backends>`. +.. tip:: Before implementing a *in-tree* backend, have a look on + :pep:`PEP 508 <508#environment-markers>`. Most of the times, dependencies + with **environment markers** are enough to differentiate operating systems + and platforms. + If you add the following configuration to your ``pyprojec.toml``: @@ -141,7 +146,7 @@ Note that you can override any of the functions specified in :pep:`PEP 517 <517#build-backend-interface>`, not only the ones responsible for gathering requirements. -.. tip:: Make sure your backend script is included in the :doc:`source +.. important:: Make sure your backend script is included in the :doc:`source distribution `, otherwise the build will fail. This can be done by using a SCM_/VCS_ plugin (like :pypi:`setuptools-scm` and :pypi:`setuptools-svn`), or by correctly setting up :ref:`MANIFEST.in -- cgit v1.2.1 From 22e1669edc3ed00a87e59a2f6c5918aa67ae731e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 20 Nov 2021 12:18:15 -0500 Subject: Expose _prefix_addition as a class property on install. --- distutils/command/install.py | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/distutils/command/install.py b/distutils/command/install.py index dbc83fce..583b1713 100644 --- a/distutils/command/install.py +++ b/distutils/command/install.py @@ -190,6 +190,8 @@ class install(Command): negative_opt = {'no-compile' : 'compile'} + # Allow Fedora to add components to the prefix + _prefix_addition = "" def initialize_options(self): """Initializes options.""" @@ -471,24 +473,10 @@ class install(Command): raise DistutilsOptionError( "must not supply exec-prefix without prefix") - # Fedora (and Fedora derived distros) used to patch distutils - # until Fedora 36 and/or Python 3.11. - # Here, we preserve that behavior conditionally on a special - # _distutils_mangle_rpm_prefix attribute of sysconfig - # that Fedora sets on their older Pythons to support this check. - # When it is set and true-ish, - # self.prefix is set to sys.prefix + /local/ - # if neither RPM build nor virtual environment is - # detected to make pip and distutils install packages to /usr/local. - addition = "" - if (getattr(sysconfig, "_distutils_mangle_rpm_prefix", False) and - not (hasattr(sys, 'real_prefix') or - sys.prefix != sys.base_prefix) and - 'RPM_BUILD_ROOT' not in os.environ): - addition = "/local" - - self.prefix = os.path.normpath(sys.prefix) + addition - self.exec_prefix = os.path.normpath(sys.exec_prefix) + addition + self.prefix = ( + os.path.normpath(sys.prefix) + self._prefix_addition) + self.exec_prefix = ( + os.path.normpath(sys.exec_prefix) + self._prefix_addition) else: if self.exec_prefix is None: -- cgit v1.2.1 From 8c64fdc8560d9f7b7d3926350bba7702b0906329 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 20 Nov 2021 12:47:02 -0500 Subject: Update comment for _distutils_system_mod. --- distutils/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/distutils/__init__.py b/distutils/__init__.py index 8fd493b4..ba337639 100644 --- a/distutils/__init__.py +++ b/distutils/__init__.py @@ -15,8 +15,10 @@ __version__ = sys.version[:sys.version.index(' ')] try: - # Allow Debian and pkgsrc (only) to customize system - # behavior. Ref pypa/distutils#2 and pypa/distutils#16. + # Allow Debian and pkgsrc and Fedora (only) to customize + # system + # behavior. Ref pypa/distutils#2 and pypa/distutils#16 + # and pypa/distutils#70. # This hook is deprecated and no other environments # should use it. importlib.import_module('_distutils_system_mod') -- cgit v1.2.1 From 81d6300d60c59cea05a6dfe9a513ced61b901f4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Wed, 24 Nov 2021 13:28:58 +0100 Subject: Load _prefix_addition attribute directly from sysconfig Co-authored-by: Jason R. Coombs --- distutils/command/install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/command/install.py b/distutils/command/install.py index 583b1713..d648dd18 100644 --- a/distutils/command/install.py +++ b/distutils/command/install.py @@ -191,7 +191,7 @@ class install(Command): negative_opt = {'no-compile' : 'compile'} # Allow Fedora to add components to the prefix - _prefix_addition = "" + _prefix_addition = getattr(sysconfig, '_prefix_addition', "") def initialize_options(self): """Initializes options.""" -- cgit v1.2.1 From f76901831084c11ec633eb96c310860d15199edd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 24 Nov 2021 11:57:52 -0500 Subject: Revert "Update comment for _distutils_system_mod." as Fedora is not using that hook. This reverts commit 8c64fdc8560d9f7b7d3926350bba7702b0906329. --- distutils/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/distutils/__init__.py b/distutils/__init__.py index ba337639..8fd493b4 100644 --- a/distutils/__init__.py +++ b/distutils/__init__.py @@ -15,10 +15,8 @@ __version__ = sys.version[:sys.version.index(' ')] try: - # Allow Debian and pkgsrc and Fedora (only) to customize - # system - # behavior. Ref pypa/distutils#2 and pypa/distutils#16 - # and pypa/distutils#70. + # Allow Debian and pkgsrc (only) to customize system + # behavior. Ref pypa/distutils#2 and pypa/distutils#16. # This hook is deprecated and no other environments # should use it. importlib.import_module('_distutils_system_mod') -- cgit v1.2.1 From 0019b0af43b9e381e2f0b14753d1bf40ce204490 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 24 Nov 2021 20:08:49 -0500 Subject: Require Python 3.7 or later. --- .github/workflows/main.yml | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6aad7f11..5424298d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,7 +7,7 @@ jobs: strategy: matrix: python: - - 3.6 + - 3.7 - 3.9 - "3.10" platform: diff --git a/setup.cfg b/setup.cfg index 0f7d652d..bd1da7a2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,7 +15,7 @@ classifiers = [options] packages = find_namespace: include_package_data = true -python_requires = >=3.6 +python_requires = >=3.7 install_requires = [options.packages.find] -- cgit v1.2.1 From ac888734c35731e3e3dc888ad231a8d798cd7bff Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 26 Nov 2021 15:24:02 -0500 Subject: =?UTF-8?q?Bump=20version:=2059.2.0=20=E2=86=92=2059.3.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 13 +++++++++++++ changelog.d/2902.change.rst | 1 - changelog.d/2906.misc.rst | 1 - setup.cfg | 2 +- 5 files changed, 15 insertions(+), 4 deletions(-) delete mode 100644 changelog.d/2902.change.rst delete mode 100644 changelog.d/2906.misc.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index a9d31cd2..67dc0851 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 59.2.0 +current_version = 59.3.0 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index 3206647a..8fbae4f0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,16 @@ +v59.3.0 +------- + + +Changes +^^^^^^^ +* #2902: Merge with pypa/distutils@85db7a41242. + +Misc +^^^^ +* #2906: In ensure_local_distutils, re-use DistutilsMetaFinder to load the module. Avoids race conditions when _distutils_system_mod is employed. + + v59.2.0 ------- diff --git a/changelog.d/2902.change.rst b/changelog.d/2902.change.rst deleted file mode 100644 index 37f3daaf..00000000 --- a/changelog.d/2902.change.rst +++ /dev/null @@ -1 +0,0 @@ -Merge with pypa/distutils@85db7a41242. diff --git a/changelog.d/2906.misc.rst b/changelog.d/2906.misc.rst deleted file mode 100644 index 2ec890b4..00000000 --- a/changelog.d/2906.misc.rst +++ /dev/null @@ -1 +0,0 @@ -In ensure_local_distutils, re-use DistutilsMetaFinder to load the module. Avoids race conditions when _distutils_system_mod is employed. diff --git a/setup.cfg b/setup.cfg index 7909ea10..6aabb1a3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 59.2.0 +version = 59.3.0 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From 58ccfa53b00b8de5d56295cc9825c6efd2e3581f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 27 Nov 2021 22:50:34 -0500 Subject: Move _prefix_addition inline, so it's only configurable through sysconfig. --- distutils/command/install.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/distutils/command/install.py b/distutils/command/install.py index d648dd18..18b352fa 100644 --- a/distutils/command/install.py +++ b/distutils/command/install.py @@ -190,8 +190,6 @@ class install(Command): negative_opt = {'no-compile' : 'compile'} - # Allow Fedora to add components to the prefix - _prefix_addition = getattr(sysconfig, '_prefix_addition', "") def initialize_options(self): """Initializes options.""" @@ -473,10 +471,13 @@ class install(Command): raise DistutilsOptionError( "must not supply exec-prefix without prefix") + # Allow Fedora to add components to the prefix + _prefix_addition = getattr(sysconfig, '_prefix_addition', "") + self.prefix = ( - os.path.normpath(sys.prefix) + self._prefix_addition) + os.path.normpath(sys.prefix) + _prefix_addition) self.exec_prefix = ( - os.path.normpath(sys.exec_prefix) + self._prefix_addition) + os.path.normpath(sys.exec_prefix) + _prefix_addition) else: if self.exec_prefix is None: -- cgit v1.2.1 From a8d097aac0a3f5d59f98a407537bc94a90715767 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 27 Nov 2021 23:35:23 -0500 Subject: Employ the TempdirManager to avoid creating 'scratch' in the test directory. Fixes #72. --- distutils/tests/test_unixccompiler.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/distutils/tests/test_unixccompiler.py b/distutils/tests/test_unixccompiler.py index 63c7dd37..2ea93da1 100644 --- a/distutils/tests/test_unixccompiler.py +++ b/distutils/tests/test_unixccompiler.py @@ -11,9 +11,12 @@ from distutils.errors import DistutilsPlatformError from distutils.unixccompiler import UnixCCompiler from distutils.util import _clear_cached_macosx_ver -class UnixCCompilerTestCase(unittest.TestCase): +from . import support + +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 @@ -23,6 +26,7 @@ class UnixCCompilerTestCase(unittest.TestCase): self.cc = CompilerWrapper() 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 @@ -237,6 +241,7 @@ class UnixCCompilerTestCase(unittest.TestCase): # ensure that setting output_dir does not raise # FileNotFoundError: [Errno 2] No such file or directory: 'a.out' self.cc.output_dir = 'scratch' + os.chdir(self.mkdtemp()) self.cc.has_function('abort', includes=['stdlib.h']) -- cgit v1.2.1 From b9a562a9b6c268d0a3f4cc261d1b28ea91b081d7 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Sat, 27 Nov 2021 21:21:31 -0800 Subject: .github/workflows/main.yml: Add test_cygwin --- .github/workflows/main.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 265be849..d326b635 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -40,6 +40,25 @@ jobs: ${{ runner.os }}, ${{ matrix.python }} + test_cygwin: + strategy: + matrix: + distutils: + - stdlib + - local + runs-on: windows-latest + env: + SETUPTOOLS_USE_DISTUTILS: ${{ matrix.distutils }} + steps: + - uses: actions/checkout@v2 + - name: Install Cygwin with Python and tox + run: | + choco install git gcc-core python38-devel python38-pip --source cygwin + C:\\tools\\cygwin\\bin\\bash -l -x -c 'python3.8 -m pip install tox' + - name: Run tests + run: | + C:\\tools\\cygwin\\bin\\bash -l -x -c 'cd $(cygpath -u "$GITHUB_WORKSPACE") && tox -- --cov-report xml' + release: needs: test if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') -- cgit v1.2.1 From 62a8d379c274e2e64c7865cada9023d5d83b14c8 Mon Sep 17 00:00:00 2001 From: Matthew Suozzo Date: Sun, 28 Nov 2021 18:48:10 -0500 Subject: Remove pkg_resources nondeterminism. --- pkg_resources/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index c84f1dd9..f2b7b911 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -3032,12 +3032,12 @@ class DistInfoDistribution(Distribution): if not req.marker or req.marker.evaluate({'extra': extra}): yield req - common = frozenset(reqs_for_extra(None)) + common = dict.fromkeys(reqs_for_extra(None)) dm[None].extend(common) for extra in self._parsed_pkg_info.get_all('Provides-Extra') or []: s_extra = safe_extra(extra.strip()) - dm[s_extra] = list(frozenset(reqs_for_extra(extra)) - common) + dm[s_extra] = [r for r in reqs_for_extra(extra) if r not in common] return dm -- cgit v1.2.1 From 68795af92cff7929d56d6b8753a8621ad12444fb Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 28 Nov 2021 18:56:32 -0500 Subject: Revert "Merge pull request #2870 from webknjaz/maintenance/fail-loudly-on-invalid-summary" This reverts commit 77678abf97b4a8ee5e6e67b14cb21f543cd6bfd9, reversing changes made to f2de34767a7ba6dc79b73e474b3e2ffdbfd6e75b. Fixes #2893. --- changelog.d/2893.change.rst | 1 + setuptools/dist.py | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 changelog.d/2893.change.rst diff --git a/changelog.d/2893.change.rst b/changelog.d/2893.change.rst new file mode 100644 index 00000000..acdf69ca --- /dev/null +++ b/changelog.d/2893.change.rst @@ -0,0 +1 @@ +Restore deprecated support for newlines in the Summary field. diff --git a/setuptools/dist.py b/setuptools/dist.py index 848d6b0f..8e2111a5 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -145,11 +145,11 @@ def read_pkg_file(self, file): def single_line(val): - """Validate that the value does not have line breaks.""" - # Ref: https://github.com/pypa/setuptools/issues/1390 + # quick and dirty validation for description pypa/setuptools#1390 if '\n' in val: - raise ValueError('Newlines are not allowed') - + # TODO after 2021-07-31: Replace with `raise ValueError("newlines not allowed")` + warnings.warn("newlines not allowed and will break in the future") + val = val.replace('\n', ' ') return val -- cgit v1.2.1 From 48efdcf9984af461c40343f670454cac18f51d63 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 28 Nov 2021 19:08:51 -0500 Subject: When repairing bad summaries, use only the first line. --- setuptools/dist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index 8e2111a5..fb168861 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -149,7 +149,7 @@ def single_line(val): if '\n' in val: # TODO after 2021-07-31: Replace with `raise ValueError("newlines not allowed")` warnings.warn("newlines not allowed and will break in the future") - val = val.replace('\n', ' ') + val = val.strip().split('\n')[0] return val -- cgit v1.2.1 From bdba8e773ee2b1557d2d44157e2642f1de8ab139 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 28 Nov 2021 19:08:57 -0500 Subject: =?UTF-8?q?Bump=20version:=2059.3.0=20=E2=86=92=2059.4.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 9 +++++++++ changelog.d/2893.change.rst | 1 - setup.cfg | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/2893.change.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 67dc0851..9dfdfbd6 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 59.3.0 +current_version = 59.4.0 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index 8fbae4f0..58f35ed5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,12 @@ +v59.4.0 +------- + + +Changes +^^^^^^^ +* #2893: Restore deprecated support for newlines in the Summary field. + + v59.3.0 ------- diff --git a/changelog.d/2893.change.rst b/changelog.d/2893.change.rst deleted file mode 100644 index acdf69ca..00000000 --- a/changelog.d/2893.change.rst +++ /dev/null @@ -1 +0,0 @@ -Restore deprecated support for newlines in the Summary field. diff --git a/setup.cfg b/setup.cfg index 6aabb1a3..b96c2f24 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 59.3.0 +version = 59.4.0 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From 3dabc9b0080116f041856c108b0261bf1ae68653 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 29 Nov 2021 08:56:30 -0500 Subject: Update comment and docstring. --- setuptools/dist.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index fb168861..74afa98f 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -145,9 +145,12 @@ def read_pkg_file(self, file): def single_line(val): - # quick and dirty validation for description pypa/setuptools#1390 + """ + Quick and dirty validation for Summary pypa/setuptools#1390. + """ if '\n' in val: - # TODO after 2021-07-31: Replace with `raise ValueError("newlines not allowed")` + # TODO: Replace with `raise ValueError("newlines not allowed")` + # after reviewing #2893. warnings.warn("newlines not allowed and will break in the future") val = val.strip().split('\n')[0] return val -- cgit v1.2.1 From 2989ca44a12a422c8d18d4b9a1ccc996dcb7746c Mon Sep 17 00:00:00 2001 From: Rodrigo Mologni Date: Sat, 4 Dec 2021 00:11:59 -0300 Subject: fix: 'python_requires' must be a string --- docs/userguide/dependency_management.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/userguide/dependency_management.rst b/docs/userguide/dependency_management.rst index 23578a57..9c29dbd5 100644 --- a/docs/userguide/dependency_management.rst +++ b/docs/userguide/dependency_management.rst @@ -360,6 +360,6 @@ or ``setup.py``. setup( name="Project-B", - python_requires=[">=3.6"], + python_requires=">=3.6", ..., ) -- cgit v1.2.1 From 4fd382d23f43bce178d6b2fb497d876200b3920e Mon Sep 17 00:00:00 2001 From: Rodrigo Mologni Date: Sat, 4 Dec 2021 00:13:41 -0300 Subject: fix: typo 'extras_require' --- docs/userguide/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst index 6bf353a0..98e34c19 100644 --- a/docs/userguide/quickstart.rst +++ b/docs/userguide/quickstart.rst @@ -159,7 +159,7 @@ When your project is installed, all of the dependencies not already installed will be located (via PyPI), downloaded, built (if necessary), and installed. This, of course, is a simplified scenarios. ``setuptools`` also provide additional keywords such as ``setup_requires`` that allows you to install -dependencies before running the script, and ``extras_requires`` that take +dependencies before running the script, and ``extras_require`` that take care of those needed by automatically generated scripts. It also provides mechanisms to handle dependencies that are not in PyPI. For more advanced use, see :doc:`dependency_management` -- cgit v1.2.1 From be4466cc15011d337dba3a49867a7d597d2f36e4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 3 Dec 2021 23:02:26 -0500 Subject: Re-implement yield_lines as a singledispatch function. --- pkg_resources/__init__.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index 42129d5b..955fdc48 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -2396,18 +2396,19 @@ def _set_parent_ns(packageName): setattr(sys.modules[parent], name, sys.modules[packageName]) -def yield_lines(strs): - """Yield non-empty/non-comment lines of a string or sequence""" - if isinstance(strs, str): - for s in strs.splitlines(): - s = s.strip() - # skip blank lines/comments - if s and not s.startswith('#'): - yield s - else: - for ss in strs: - for s in yield_lines(ss): - yield s +def _nonblank(str): + return str and not str.startswith('#') + + +@functools.singledispatch +def yield_lines(iterable): + """Yield valid lines of a string or iterable""" + return itertools.chain.from_iterable(map(yield_lines, iterable)) + + +@yield_lines.register(str) +def _(text): + return filter(_nonblank, map(str.strip, text.splitlines())) MODULE = re.compile(r"\w+(\.\w+)*$").match -- cgit v1.2.1 From 3aa9e83db97fd70ee643890c270b895324b049bd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 5 Dec 2021 11:35:21 -0500 Subject: =?UTF-8?q?Bump=20version:=2059.4.0=20=E2=86=92=2059.5.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 9 +++++++++ changelog.d/2914.change.rst | 1 - setup.cfg | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/2914.change.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 9dfdfbd6..c7ced0e3 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 59.4.0 +current_version = 59.5.0 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index 58f35ed5..7d7bfc76 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,12 @@ +v59.5.0 +------- + + +Changes +^^^^^^^ +* #2914: Merge with pypa/distutils@8f2df0bf6. + + v59.4.0 ------- diff --git a/changelog.d/2914.change.rst b/changelog.d/2914.change.rst deleted file mode 100644 index 229f9e25..00000000 --- a/changelog.d/2914.change.rst +++ /dev/null @@ -1 +0,0 @@ -Merge with pypa/distutils@8f2df0bf6. diff --git a/setup.cfg b/setup.cfg index b96c2f24..f1ffad7e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 59.4.0 +version = 59.5.0 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From c9d13ee722b603ea4e7c0892b976464e61a7906b Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Mon, 6 Dec 2021 16:35:37 -0800 Subject: fix failures w/ py3-only loaders --- pkg_resources/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index 955fdc48..850ca4da 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -2205,12 +2205,14 @@ def _handle_ns(packageName, path_item): # use find_spec (PEP 451) and fall-back to find_module (PEP 302) try: - loader = importer.find_spec(packageName).loader + spec = importer.find_spec(packageName) except AttributeError: # capture warnings due to #1111 with warnings.catch_warnings(): warnings.simplefilter("ignore") loader = importer.find_module(packageName) + else: + loader = spec.loader if spec else None if loader is None: return None -- cgit v1.2.1 From 42b51eb3d5945a52256a55417e26807416a86ddd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 10 Dec 2021 17:34:36 -0500 Subject: Consider this a breaking change. --- changelog.d/2896.breaking.rst | 1 + changelog.d/2896.change.rst | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 changelog.d/2896.breaking.rst delete mode 100644 changelog.d/2896.change.rst diff --git a/changelog.d/2896.breaking.rst b/changelog.d/2896.breaking.rst new file mode 100644 index 00000000..e0aebdcb --- /dev/null +++ b/changelog.d/2896.breaking.rst @@ -0,0 +1 @@ +Setuptools once again makes its local copy of distutils the default. To override, set SETUPTOOLS_USE_DISTUTILS=stdlib. diff --git a/changelog.d/2896.change.rst b/changelog.d/2896.change.rst deleted file mode 100644 index e0aebdcb..00000000 --- a/changelog.d/2896.change.rst +++ /dev/null @@ -1 +0,0 @@ -Setuptools once again makes its local copy of distutils the default. To override, set SETUPTOOLS_USE_DISTUTILS=stdlib. -- cgit v1.2.1 From 45a280caafc8e072f6b126c566864bac6c2a3db0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 12 Dec 2021 11:09:26 -0500 Subject: Remove LooseVersion.__init__ as it's identical to super().__init__ --- distutils/version.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/distutils/version.py b/distutils/version.py index c33bebae..f09889ac 100644 --- a/distutils/version.py +++ b/distutils/version.py @@ -301,11 +301,6 @@ class LooseVersion (Version): component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE) - def __init__ (self, vstring=None): - if vstring: - self.parse(vstring) - - def parse (self, vstring): # I've given up on thinking I can reconstruct the version string # from the parsed tuple -- so I just store the string here for -- cgit v1.2.1 From 1701579e0827317d8888c2254a17b5786b6b5246 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 12 Dec 2021 11:20:19 -0500 Subject: Mark Version classes as deprecated. --- distutils/version.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/distutils/version.py b/distutils/version.py index f09889ac..47d88917 100644 --- a/distutils/version.py +++ b/distutils/version.py @@ -27,6 +27,8 @@ Every version number class implements the following interface: """ import re +import warnings + class Version: """Abstract base class for version numbering classes. Just provides @@ -36,6 +38,12 @@ class Version: """ def __init__ (self, vstring=None): + warnings.warn( + "distutils Version classes are deprecated. " + "Use packaging.version instead.", + DeprecationWarning, + stacklevel=2, + ) if vstring: self.parse(vstring) -- cgit v1.2.1 From d8c16238d73ca844dfe902708e73363db186b1f7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 12 Dec 2021 11:27:01 -0500 Subject: Suppress deprecation warnings in distutils test suite. --- distutils/cygwinccompiler.py | 8 +++++--- distutils/tests/test_version.py | 8 ++++++++ distutils/version.py | 15 ++++++++++++++- distutils/versionpredicate.py | 7 +++++-- 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/distutils/cygwinccompiler.py b/distutils/cygwinccompiler.py index f80ca622..ad6cc44b 100644 --- a/distutils/cygwinccompiler.py +++ b/distutils/cygwinccompiler.py @@ -53,6 +53,7 @@ import copy from subprocess import Popen, PIPE, check_output import re +import distutils.version from distutils.unixccompiler import UnixCCompiler from distutils.file_util import write_file from distutils.errors import (DistutilsExecError, CCompilerError, @@ -405,9 +406,10 @@ def _find_exe_version(cmd): result = RE_VERSION.search(out_string) if result is None: return None - # LooseVersion works with strings - # so we need to decode our bytes - return LooseVersion(result.group(1).decode()) + # LooseVersion works with strings; decode + ver_str = result.group(1).decode() + with distutils.version.suppress_known_deprecation(): + return LooseVersion(ver_str) def get_versions(): """ Try to find out the versions of gcc, ld and dllwrap. diff --git a/distutils/tests/test_version.py b/distutils/tests/test_version.py index 8671cd2f..d50cca1f 100644 --- a/distutils/tests/test_version.py +++ b/distutils/tests/test_version.py @@ -1,11 +1,19 @@ """Tests for distutils.version.""" import unittest +import distutils from distutils.version import LooseVersion from distutils.version import StrictVersion from test.support import run_unittest class VersionTestCase(unittest.TestCase): + def setUp(self): + self.ctx = distutils.version.suppress_known_deprecation() + self.ctx.__enter__() + + def tearDown(self): + self.ctx.__exit__(None, None, None) + def test_prerelease(self): version = StrictVersion('1.2.3a1') self.assertEqual(version.version, (1, 2, 3)) diff --git a/distutils/version.py b/distutils/version.py index 47d88917..35e181db 100644 --- a/distutils/version.py +++ b/distutils/version.py @@ -28,6 +28,18 @@ Every version number class implements the following interface: import re import warnings +import contextlib + + +@contextlib.contextmanager +def suppress_known_deprecation(): + with warnings.catch_warnings(record=True) as ctx: + warnings.filterwarnings( + action='default', + category=DeprecationWarning, + message="distutils Version classes are deprecated.", + ) + yield ctx class Version: @@ -173,7 +185,8 @@ class StrictVersion (Version): def _cmp (self, other): if isinstance(other, str): - other = StrictVersion(other) + with suppress_known_deprecation(): + other = StrictVersion(other) elif not isinstance(other, StrictVersion): return NotImplemented diff --git a/distutils/versionpredicate.py b/distutils/versionpredicate.py index 062c98f2..55f25d91 100644 --- a/distutils/versionpredicate.py +++ b/distutils/versionpredicate.py @@ -23,7 +23,9 @@ def splitUp(pred): if not res: raise ValueError("bad package restriction syntax: %r" % pred) comp, verStr = res.groups() - return (comp, distutils.version.StrictVersion(verStr)) + with distutils.version.suppress_known_deprecation(): + other = distutils.version.StrictVersion(verStr) + return (comp, other) compmap = {"<": operator.lt, "<=": operator.le, "==": operator.eq, ">": operator.gt, ">=": operator.ge, "!=": operator.ne} @@ -162,5 +164,6 @@ def split_provision(value): raise ValueError("illegal provides specification: %r" % value) ver = m.group(2) or None if ver: - ver = distutils.version.StrictVersion(ver) + with distutils.version.suppress_known_deprecation(): + ver = distutils.version.StrictVersion(ver) return m.group(1), ver -- cgit v1.2.1 From 9ea9a271e183b2bc9d2da8f0353724d41bf3d421 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 12 Dec 2021 12:06:22 -0500 Subject: In dist, rely on packaging instead of distutils for version management. --- setuptools/dist.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index 74afa98f..37a10d1d 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -25,7 +25,6 @@ from email import message_from_file from distutils.errors import DistutilsOptionError, DistutilsSetupError from distutils.util import rfc822_escape -from distutils.version import StrictVersion from setuptools.extern import packaging from setuptools.extern import ordered_set @@ -39,6 +38,7 @@ from setuptools import windows_support from setuptools.monkey import get_unpatched from setuptools.config import parse_configuration import pkg_resources +from setuptools.extern.packaging import version if TYPE_CHECKING: from email.message import Message @@ -55,7 +55,7 @@ def _get_unpatched(cls): def get_metadata_version(self): mv = getattr(self, 'metadata_version', None) if mv is None: - mv = StrictVersion('2.1') + mv = version.Version('2.1') self.metadata_version = mv return mv @@ -103,7 +103,7 @@ def read_pkg_file(self, file): """Reads the metadata values from a file object.""" msg = message_from_file(file) - self.metadata_version = StrictVersion(msg['metadata-version']) + self.metadata_version = version.Version(msg['metadata-version']) self.name = _read_field_from_msg(msg, 'name') self.version = _read_field_from_msg(msg, 'version') self.description = _read_field_from_msg(msg, 'summary') @@ -121,7 +121,10 @@ def read_pkg_file(self, file): self.download_url = None self.long_description = _read_field_unescaped_from_msg(msg, 'description') - if self.long_description is None and self.metadata_version >= StrictVersion('2.1'): + if ( + self.long_description is None and + self.metadata_version >= version.Version('2.1') + ): self.long_description = _read_payload_from_msg(msg) self.description = _read_field_from_msg(msg, 'summary') @@ -132,7 +135,7 @@ def read_pkg_file(self, file): self.classifiers = _read_list_from_msg(msg, 'classifier') # PEP 314 - these fields only exist in 1.1 - if self.metadata_version == StrictVersion('1.1'): + if self.metadata_version == version.Version('1.1'): self.requires = _read_list_from_msg(msg, 'requires') self.provides = _read_list_from_msg(msg, 'provides') self.obsoletes = _read_list_from_msg(msg, 'obsoletes') -- cgit v1.2.1 From 78fdc162ba1b6feeef712bafc32ac81734b92734 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 12 Dec 2021 12:34:02 -0500 Subject: In depends, rely on packaging instead of distutils for version management. --- setuptools/depends.py | 9 +++++---- setuptools/tests/test_setuptools.py | 12 ++++-------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/setuptools/depends.py b/setuptools/depends.py index 8be6928a..adffd12d 100644 --- a/setuptools/depends.py +++ b/setuptools/depends.py @@ -2,7 +2,8 @@ import sys import marshal import contextlib import dis -from distutils.version import StrictVersion + +from setuptools.extern.packaging import version from ._imp import find_module, PY_COMPILED, PY_FROZEN, PY_SOURCE from . import _imp @@ -21,7 +22,7 @@ class Require: attribute=None, format=None): if format is None and requested_version is not None: - format = StrictVersion + format = version.Version if format is not None: requested_version = format(requested_version) @@ -40,7 +41,7 @@ class Require: def version_ok(self, version): """Is 'version' sufficiently up-to-date?""" return self.attribute is None or self.format is None or \ - str(version) != "unknown" and version >= self.requested_version + str(version) != "unknown" and self.format(version) >= self.requested_version def get_version(self, paths=None, default="unknown"): """Get version number of installed module, 'None', or 'default' @@ -78,7 +79,7 @@ class Require: version = self.get_version(paths) if version is None: return False - return self.version_ok(version) + return self.version_ok(str(version)) def maybe_close(f): diff --git a/setuptools/tests/test_setuptools.py b/setuptools/tests/test_setuptools.py index 42f8e18b..3609ab5e 100644 --- a/setuptools/tests/test_setuptools.py +++ b/setuptools/tests/test_setuptools.py @@ -7,10 +7,11 @@ import distutils.cmd from distutils.errors import DistutilsOptionError from distutils.errors import DistutilsSetupError from distutils.core import Extension -from distutils.version import LooseVersion import pytest +from setuptools.extern.packaging import version + import setuptools import setuptools.dist import setuptools.depends as dep @@ -84,12 +85,12 @@ class TestDepends: assert req.name == 'Json' assert req.module == 'json' - assert req.requested_version == '1.0.3' + assert req.requested_version == version.Version('1.0.3') assert req.attribute == '__version__' assert req.full_name() == 'Json-1.0.3' from json import __version__ - assert req.get_version() == __version__ + assert str(req.get_version()) == __version__ assert req.version_ok('1.0.9') assert not req.version_ok('0.9.1') assert not req.version_ok('unknown') @@ -97,11 +98,6 @@ class TestDepends: assert req.is_present() assert req.is_current() - req = Require('Json 3000', '03000', 'json', format=LooseVersion) - assert req.is_present() - assert not req.is_current() - assert not req.version_ok('unknown') - req = Require('Do-what-I-mean', '1.0', 'd-w-i-m') assert not req.is_present() assert not req.is_current() -- cgit v1.2.1 From 9f1822ee910df3df930a98ab99f66d18bb70659b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 12 Dec 2021 13:33:41 -0500 Subject: =?UTF-8?q?Bump=20version:=2059.5.0=20=E2=86=92=2059.6.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 9 +++++++++ changelog.d/2925.change.rst | 1 - setup.cfg | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/2925.change.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index c7ced0e3..ab0c95cf 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 59.5.0 +current_version = 59.6.0 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index 7d7bfc76..9b38064e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,12 @@ +v59.6.0 +------- + + +Changes +^^^^^^^ +* #2925: Merge with pypa/distutils@92082ee42c including introduction of deprecation warning on Version classes. + + v59.5.0 ------- diff --git a/changelog.d/2925.change.rst b/changelog.d/2925.change.rst deleted file mode 100644 index c28f29cd..00000000 --- a/changelog.d/2925.change.rst +++ /dev/null @@ -1 +0,0 @@ -Merge with pypa/distutils@92082ee42c including introduction of deprecation warning on Version classes. diff --git a/setup.cfg b/setup.cfg index f1ffad7e..c04eff2d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 59.5.0 +version = 59.6.0 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From e773611d2d88d46872d3f5b1a84290955034c215 Mon Sep 17 00:00:00 2001 From: Matthew Suozzo Date: Thu, 16 Dec 2021 17:21:04 -0500 Subject: Add new test to document expected order stability. --- setuptools/tests/test_wheel.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/setuptools/tests/test_wheel.py b/setuptools/tests/test_wheel.py index 7345b135..a15c3a46 100644 --- a/setuptools/tests/test_wheel.py +++ b/setuptools/tests/test_wheel.py @@ -148,6 +148,7 @@ def _check_wheel_install(filename, install_dir, install_tree_includes, if requires_txt is None: assert not dist.has_metadata('requires.txt') else: + # Order must match to ensure reproducibility. assert requires_txt == dist.get_metadata('requires.txt').lstrip() @@ -419,6 +420,38 @@ WHEEL_INSTALL_TESTS = ( ), ), + dict( + id='requires_ensure_order', + install_requires=''' + foo + bar + baz + qux + ''', + extras_require={ + 'extra': ''' + foobar>3 + barbaz>4 + bazqux>5 + quxzap>6 + ''', + }, + requires_txt=DALS( + ''' + foo + bar + baz + qux + + [extra] + foobar>3 + barbaz>4 + bazqux>5 + quxzap>6 + ''' + ), + ), + dict( id='namespace_package', file_defs={ -- cgit v1.2.1 From 8c409a185b1a8258c32ba616fe9947043a04d184 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 17 Dec 2021 09:43:05 -0500 Subject: Extract 'incomplete_scheme' as a variable for readability. --- distutils/command/install.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/distutils/command/install.py b/distutils/command/install.py index 18b352fa..df181102 100644 --- a/distutils/command/install.py +++ b/distutils/command/install.py @@ -445,12 +445,17 @@ class install(Command): def finalize_unix(self): """Finalizes options for posix platforms.""" if self.install_base is not None or self.install_platbase is not None: - if ((self.install_lib is None and - self.install_purelib is None and - self.install_platlib is None) or + incomplete_scheme = ( + ( + self.install_lib is None and + self.install_purelib is None and + self.install_platlib is None + ) or self.install_headers is None or self.install_scripts is None or - self.install_data is None): + self.install_data is None + ) + if incomplete_scheme: raise DistutilsOptionError( "install-base or install-platbase supplied, but " "installation scheme is incomplete") -- cgit v1.2.1 From da15b6e30ec48a3472b518b63c63ce6363b4e1cd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 17 Dec 2021 10:15:07 -0500 Subject: Extract _pypy_hack method. --- distutils/command/install.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/distutils/command/install.py b/distutils/command/install.py index df181102..407b96d6 100644 --- a/distutils/command/install.py +++ b/distutils/command/install.py @@ -517,19 +517,20 @@ class install(Command): def select_scheme(self, name): """Sets the install directories by applying the install schemes.""" # it's the caller's problem if they supply a bad name! - if (hasattr(sys, 'pypy_version_info') and - sys.version_info < (3, 8) and - not name.endswith(('_user', '_home'))): - if os.name == 'nt': - name = 'pypy_nt' - else: - name = 'pypy' - scheme = _load_schemes()[name] + scheme = _load_schemes()[self._pypy_hack(name)] for key in SCHEME_KEYS: attrname = 'install_' + key if getattr(self, attrname) is None: setattr(self, attrname, scheme[key]) + @staticmethod + def _pypy_hack(name): + PY37 = sys.version_info < (3, 8) + old_pypy = hasattr(sys, 'pypy_version_info') and PY37 + prefix = not name.endswith(('_user', '_home')) + pypy_name = 'pypy' + '_nt' * (os.name == 'nt') + return pypy_name if old_pypy and prefix else name + def _expand_attrs(self, attrs): for attr in attrs: val = getattr(self, attr) -- cgit v1.2.1 From f32af4e73a3f994f3a76898bb404919c5722508d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 17 Dec 2021 10:13:33 -0500 Subject: Honor sysconfig.get_preferred_scheme when selecting install schemes. Fixes #76. --- distutils/command/install.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/distutils/command/install.py b/distutils/command/install.py index 407b96d6..80f5f8c7 100644 --- a/distutils/command/install.py +++ b/distutils/command/install.py @@ -81,6 +81,11 @@ if HAS_USER_SITE: 'data' : '{userbase}', } + INSTALL_SCHEMES['osx_framework_user'] = { + 'headers': + '{userbase}/include/{implementation_lower}{py_version_short}{abiflags}/{dist_name}', + } + # The keys to an installation scheme; if any new types of files are to be # installed, be sure to add an entry to every installation scheme above, # and to SCHEME_KEYS here. @@ -515,9 +520,17 @@ class install(Command): "I don't know how to install stuff on '%s'" % os.name) def select_scheme(self, name): + os_name, sep, key = name.partition('_') + try: + resolved = sysconfig.get_preferred_scheme(key) + except Exception: + resolved = self._pypy_hack(name) + return self._select_scheme(resolved) + + def _select_scheme(self, name): """Sets the install directories by applying the install schemes.""" # it's the caller's problem if they supply a bad name! - scheme = _load_schemes()[self._pypy_hack(name)] + scheme = _load_schemes()[name] for key in SCHEME_KEYS: attrname = 'install_' + key if getattr(self, attrname) is None: -- cgit v1.2.1 From eca1c4ca6e104c8add280c721cbb365196f55ac7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 24 Nov 2021 20:38:46 -0500 Subject: Remove filtered warnings, addressed upstream. --- pytest.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/pytest.ini b/pytest.ini index 9ecdba49..ec965b24 100644 --- a/pytest.ini +++ b/pytest.ini @@ -5,5 +5,3 @@ doctest_optionflags=ALLOW_UNICODE ELLIPSIS filterwarnings= # Suppress deprecation warning in flake8 ignore:SelectableGroups dict interface is deprecated::flake8 - # Suppress deprecation warning in pypa/packaging#433 - ignore:The distutils package is deprecated::packaging.tags -- cgit v1.2.1 From a9e48a650be03781cba76de2b2863c7136219bcf Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 17 Dec 2021 22:14:27 -0500 Subject: Try using pypy-3.7 as that's what the readme suggests. Fixes #2931. --- .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 07c560e3..6b546dc4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,7 +10,7 @@ jobs: - stdlib - local python: - - pypy3 + - pypy-3.7 - 3.7 - 3.9 - "3.10" -- cgit v1.2.1 From cbe70602e408670b306871fbd64f3255019d4b51 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 17 Dec 2021 23:28:58 -0500 Subject: Bump minimum version to rely on WindowsPath in subprocess. Fixes #2932. --- setuptools/tests/test_develop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setuptools/tests/test_develop.py b/setuptools/tests/test_develop.py index 70c5794c..1aeb7ffe 100644 --- a/setuptools/tests/test_develop.py +++ b/setuptools/tests/test_develop.py @@ -219,6 +219,6 @@ class TestNamespaces: # now run 'sample' with the prefix on the PYTHONPATH bin = 'Scripts' if platform.system() == 'Windows' else 'bin' exe = prefix / bin / 'sample' - if sys.version_info < (3, 7) and platform.system() == 'Windows': + if sys.version_info < (3, 8) and platform.system() == 'Windows': exe = str(exe) subprocess.check_call([exe], env=env) -- cgit v1.2.1 From 93562cb376a8666fe8672c7c070c4002554ddbf1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 17 Dec 2021 23:29:32 -0500 Subject: =?UTF-8?q?Bump=20version:=2059.6.0=20=E2=86=92=2059.7.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 9 +++++++++ changelog.d/2930.change.rst | 1 - setup.cfg | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/2930.change.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index ab0c95cf..44072dee 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 59.6.0 +current_version = 59.7.0 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index 9b38064e..5c898fa8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,12 @@ +v59.7.0 +------- + + +Changes +^^^^^^^ +* #2930: Require Python 3.7 + + v59.6.0 ------- diff --git a/changelog.d/2930.change.rst b/changelog.d/2930.change.rst deleted file mode 100644 index 5fc04945..00000000 --- a/changelog.d/2930.change.rst +++ /dev/null @@ -1 +0,0 @@ -Require Python 3.7 diff --git a/setup.cfg b/setup.cfg index c80efc24..343ea2ad 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 59.6.0 +version = 59.7.0 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From 7828197702541840fd61377434711b788de7a076 Mon Sep 17 00:00:00 2001 From: Christoph Reiter Date: Sat, 18 Dec 2021 08:15:26 +0100 Subject: msvc9compiler: Don't raise DistutilsPlatformError on import It gets raised when get_build_version() returns a too low version or when it fails to detect a MSVC version, which for exaple is the case when CPython is built with gcc/clang. In theory this module isn't needed on non-MSVC platforms, but setuptools imports it anyway when monkey patching, which in turn makes setuptools fail. To work around this only check the version when initialising MSVCCompiler(). This also mirrors the behaviour of the newer MSVCCompiler() (in _msvccompiler) which also raises DistutilsPlatformError() only on initialisation in case it can't find the right MSVC. --- distutils/msvc9compiler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/distutils/msvc9compiler.py b/distutils/msvc9compiler.py index a1b3b02f..14d13775 100644 --- a/distutils/msvc9compiler.py +++ b/distutils/msvc9compiler.py @@ -291,8 +291,6 @@ def query_vcvarsall(version, arch="x86"): # More globals VERSION = get_build_version() -if VERSION < 8.0: - raise DistutilsPlatformError("VC %0.1f is not supported by this module" % VERSION) # MACROS = MacroExpander(VERSION) class MSVCCompiler(CCompiler) : @@ -339,6 +337,8 @@ class MSVCCompiler(CCompiler) : 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" + if self.__version < 8.0: + raise DistutilsPlatformError("VC %0.1f is not supported by this module" % self.__version) if plat_name is None: plat_name = get_platform() # sanity check for platforms to prevent obscure errors later. -- cgit v1.2.1 From 4d4d32a78f0fcfbeff49e271377f735402ba0d74 Mon Sep 17 00:00:00 2001 From: Christoph Reiter Date: Sat, 18 Dec 2021 11:18:49 +0100 Subject: tests: skip uid/gid using tests under Cygwin There is no single user/group with UID/GID=0 under Cygwin unlike Unix. Skip the tests that assume this for now. --- distutils/tests/test_archive_util.py | 3 ++- distutils/tests/test_sdist.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/distutils/tests/test_archive_util.py b/distutils/tests/test_archive_util.py index ce6456dc..0f90ea15 100644 --- a/distutils/tests/test_archive_util.py +++ b/distutils/tests/test_archive_util.py @@ -339,7 +339,7 @@ class ArchiveUtilTestCase(support.TempdirManager, def test_make_archive_owner_group(self): # testing make_archive with owner and group, with various combinations # this works even if there's not gid/uid support - if UID_GID_SUPPORT: + if UID_GID_SUPPORT and sys.platform != "cygwin": group = grp.getgrgid(0)[0] owner = pwd.getpwuid(0)[0] else: @@ -365,6 +365,7 @@ class ArchiveUtilTestCase(support.TempdirManager, @unittest.skipUnless(ZLIB_SUPPORT, "Requires zlib") @unittest.skipUnless(UID_GID_SUPPORT, "Requires grp and pwd support") + @unittest.skipUnless(sys.platform != "cygwin", "Cygwin doesn't have UID=0") def test_tarfile_root_owner(self): tmpdir = self._create_files() base_name = os.path.join(self.mkdtemp(), 'archive') diff --git a/distutils/tests/test_sdist.py b/distutils/tests/test_sdist.py index b087a817..21e3974a 100644 --- a/distutils/tests/test_sdist.py +++ b/distutils/tests/test_sdist.py @@ -1,5 +1,6 @@ """Tests for distutils.command.sdist.""" import os +import sys import tarfile import unittest import warnings @@ -441,6 +442,7 @@ class SDistTestCase(BasePyPIRCCommandTestCase): @unittest.skipUnless(ZLIB_SUPPORT, "requires zlib") @unittest.skipUnless(UID_GID_SUPPORT, "Requires grp and pwd support") + @unittest.skipUnless(sys.platform != "cygwin", "Cygwin doesn't have UID=0") @unittest.skipIf(find_executable('tar') is None, "The tar command is not found") @unittest.skipIf(find_executable('gzip') is None, -- cgit v1.2.1 From 2a95a48dcee29f2396bee63de34e2ad0ef1247a3 Mon Sep 17 00:00:00 2001 From: Christoph Reiter Date: Sat, 18 Dec 2021 09:14:03 +0100 Subject: CI: add a CI job for testing under Cygwin There are some tests skipped because of missing docutils, but cygwin currently is missing a docutils package for Python 3.9 --- .github/workflows/main.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 947b8551..bd0e1992 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,6 +21,30 @@ jobs: - name: Run tests run: tox + test_cygwin: + strategy: + matrix: + python: [39] + platform: [windows-latest] + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v2 + - name: Install Cygwin + uses: cygwin/cygwin-install-action@v1 + with: + platform: x86_64 + packages: >- + python${{ matrix.python }}, + python${{ matrix.python }}-devel, + python${{ matrix.python }}-pytest, + 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 + ci_setuptools: # Integration testing with setuptools strategy: -- cgit v1.2.1 From 629f8ff0517d090dd6931794161d67d64673b016 Mon Sep 17 00:00:00 2001 From: Christoph Reiter Date: Sat, 18 Dec 2021 07:58:00 +0100 Subject: cygwinccompiler: Split CC env var before passing to subprocess 4113bc31a8e62 added support for clang by respecting the CC env variable. The content of the env var gets passed to check_output() as the compiler executable. But CC is not just a path to an executable but a command line, such as "ccache gcc" which makes checkout_output() fail in those cases because it will try to look for "ccache gcc" in PATH instead of "ccache". To fix the issue use shlex to parse the command line before passing it to check_output(). --- distutils/cygwinccompiler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/distutils/cygwinccompiler.py b/distutils/cygwinccompiler.py index ad6cc44b..09be5ba2 100644 --- a/distutils/cygwinccompiler.py +++ b/distutils/cygwinccompiler.py @@ -50,6 +50,7 @@ cygwin in no-cygwin mode). import os import sys import copy +import shlex from subprocess import Popen, PIPE, check_output import re @@ -421,5 +422,5 @@ def get_versions(): def is_cygwincc(cc): '''Try to determine if the compiler that would be used is from cygwin.''' - out_string = check_output([cc, '-dumpmachine']) + out_string = check_output(shlex.split(cc) + ['-dumpmachine']) return out_string.strip().endswith(b'cygwin') -- cgit v1.2.1 From 1d98a156026cbbb43588c5f307a3b8d66d40a1bb Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 18 Dec 2021 10:18:55 -0500 Subject: Extract grp/pwd handling to a unix_compat module. --- distutils/tests/test_archive_util.py | 13 ++++--------- distutils/tests/test_sdist.py | 13 +++---------- distutils/tests/unix_compat.py | 16 ++++++++++++++++ 3 files changed, 23 insertions(+), 19 deletions(-) create mode 100644 distutils/tests/unix_compat.py diff --git a/distutils/tests/test_archive_util.py b/distutils/tests/test_archive_util.py index 0f90ea15..c5560372 100644 --- a/distutils/tests/test_archive_util.py +++ b/distutils/tests/test_archive_util.py @@ -14,16 +14,11 @@ from distutils.archive_util import (check_archive_formats, make_tarball, from distutils.spawn import find_executable, spawn from distutils.tests import support from test.support import run_unittest, patch +from .unix_compat import require_unix_id, require_uid_0, grp, pwd, UID_0_SUPPORT from .py38compat import change_cwd from .py38compat import check_warnings -try: - import grp - import pwd - UID_GID_SUPPORT = True -except ImportError: - UID_GID_SUPPORT = False try: import zipfile @@ -339,7 +334,7 @@ class ArchiveUtilTestCase(support.TempdirManager, def test_make_archive_owner_group(self): # testing make_archive with owner and group, with various combinations # this works even if there's not gid/uid support - if UID_GID_SUPPORT and sys.platform != "cygwin": + if UID_0_SUPPORT: group = grp.getgrgid(0)[0] owner = pwd.getpwuid(0)[0] else: @@ -364,8 +359,8 @@ class ArchiveUtilTestCase(support.TempdirManager, self.assertTrue(os.path.exists(res)) @unittest.skipUnless(ZLIB_SUPPORT, "Requires zlib") - @unittest.skipUnless(UID_GID_SUPPORT, "Requires grp and pwd support") - @unittest.skipUnless(sys.platform != "cygwin", "Cygwin doesn't have UID=0") + @require_unix_id + @require_uid_0 def test_tarfile_root_owner(self): tmpdir = self._create_files() base_name = os.path.join(self.mkdtemp(), 'archive') diff --git a/distutils/tests/test_sdist.py b/distutils/tests/test_sdist.py index 21e3974a..880044fa 100644 --- a/distutils/tests/test_sdist.py +++ b/distutils/tests/test_sdist.py @@ -1,6 +1,5 @@ """Tests for distutils.command.sdist.""" import os -import sys import tarfile import unittest import warnings @@ -8,6 +7,7 @@ import zipfile from os.path import join from textwrap import dedent from test.support import captured_stdout, run_unittest +from .unix_compat import require_unix_id, require_uid_0, pwd, grp from .py38compat import check_warnings @@ -17,13 +17,6 @@ try: except ImportError: ZLIB_SUPPORT = False -try: - import grp - import pwd - UID_GID_SUPPORT = True -except ImportError: - UID_GID_SUPPORT = False - from distutils.command.sdist import sdist, show_formats from distutils.core import Distribution from distutils.tests.test_config import BasePyPIRCCommandTestCase @@ -441,8 +434,8 @@ class SDistTestCase(BasePyPIRCCommandTestCase): 'fake-1.0/README.manual']) @unittest.skipUnless(ZLIB_SUPPORT, "requires zlib") - @unittest.skipUnless(UID_GID_SUPPORT, "Requires grp and pwd support") - @unittest.skipUnless(sys.platform != "cygwin", "Cygwin doesn't have UID=0") + @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, diff --git a/distutils/tests/unix_compat.py b/distutils/tests/unix_compat.py new file mode 100644 index 00000000..b7718c26 --- /dev/null +++ b/distutils/tests/unix_compat.py @@ -0,0 +1,16 @@ +import sys +import unittest + +try: + import grp + import pwd +except ImportError: + grp = pwd = None + + +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") -- cgit v1.2.1 From 221a2f2888aa1a48887003c5afa10c1d18ae3a92 Mon Sep 17 00:00:00 2001 From: Naveen M K Date: Wed, 15 Dec 2021 19:48:03 +0530 Subject: MinGW/Cygwin: Remove checks for ancient gcc/binutils The versions checked here are 20 years old. Also dllwrap has started to emit a deprecation warning in the latest release spamming the build logs. Co-authored-by: Christoph Reiter Signed-off-by: Naveen M K --- distutils/cygwinccompiler.py | 122 ++++++-------------------------- distutils/tests/test_cygwinccompiler.py | 36 +--------- 2 files changed, 22 insertions(+), 136 deletions(-) diff --git a/distutils/cygwinccompiler.py b/distutils/cygwinccompiler.py index 09be5ba2..fda22bf3 100644 --- a/distutils/cygwinccompiler.py +++ b/distutils/cygwinccompiler.py @@ -51,16 +51,13 @@ import os import sys import copy import shlex -from subprocess import Popen, PIPE, check_output -import re +from subprocess import check_output -import distutils.version from distutils.unixccompiler import UnixCCompiler from distutils.file_util import write_file from distutils.errors import (DistutilsExecError, CCompilerError, CompileError, UnknownFileError) from distutils.version import LooseVersion -from distutils.spawn import find_executable def get_msvcr(): """Include the appropriate MSVC runtime library if Python was built @@ -125,33 +122,14 @@ class CygwinCCompiler(UnixCCompiler): self.cc = os.environ.get('CC', 'gcc') self.cxx = os.environ.get('CXX', 'g++') - if ('gcc' in self.cc): # Start gcc workaround - self.gcc_version, self.ld_version, self.dllwrap_version = \ - get_versions() - self.debug_print(self.compiler_type + ": gcc %s, ld %s, dllwrap %s\n" % - (self.gcc_version, - self.ld_version, - self.dllwrap_version) ) - - # ld_version >= "2.10.90" and < "2.13" should also be able to use - # gcc -mdll instead of dllwrap - # Older dllwraps had own version numbers, newer ones use the - # same as the rest of binutils ( also ld ) - # dllwrap 2.10.90 is buggy - if self.ld_version >= "2.10.90": - self.linker_dll = self.cc - else: - self.linker_dll = "dllwrap" + # Older numpy dependend on this existing to check for ancient + # gcc versions. This doesn't make much sense with clang etc so + # just hardcode to something recent. + # https://github.com/numpy/numpy/pull/20333 + self.gcc_version = LooseVersion("11.2.0") - # ld_version >= "2.13" support -shared so use it instead of - # -mdll -static - if self.ld_version >= "2.13": - shared_option = "-shared" - else: - shared_option = "-mdll -static" - else: # Assume linker is up to date - self.linker_dll = self.cc - shared_option = "-shared" + self.linker_dll = self.cc + shared_option = "-shared" self.set_executables(compiler='%s -mcygwin -O -Wall' % self.cc, compiler_so='%s -mcygwin -mdll -O -Wall' % self.cc, @@ -160,17 +138,9 @@ class CygwinCCompiler(UnixCCompiler): linker_so=('%s -mcygwin %s' % (self.linker_dll, shared_option))) - # cygwin and mingw32 need different sets of libraries - if ('gcc' in self.cc and self.gcc_version == "2.91.57"): - # cygwin shouldn't need msvcrt, but without the dlls will crash - # (gcc version 2.91.57) -- perhaps something about initialization - self.dll_libraries=["msvcrt"] - self.warn( - "Consider upgrading to a newer version of gcc") - else: - # Include the appropriate MSVC runtime library if Python was built - # with MSVC 7.0 or later. - self.dll_libraries = get_msvcr() + # Include the appropriate MSVC runtime library if Python was built + # with MSVC 7.0 or later. + self.dll_libraries = get_msvcr() def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts): """Compiles the source by spawning GCC and windres if needed.""" @@ -232,24 +202,17 @@ class CygwinCCompiler(UnixCCompiler): # next add options for def-file and to creating import libraries - # dllwrap uses different options than gcc/ld - if self.linker_dll == "dllwrap": - extra_preargs.extend(["--output-lib", lib_file]) - # for dllwrap we have to use a special option - extra_preargs.extend(["--def", def_file]) - # we use gcc/ld here and can be sure ld is >= 2.9.10 - else: - # doesn't work: bfd_close build\...\libfoo.a: Invalid operation - #extra_preargs.extend(["-Wl,--out-implib,%s" % lib_file]) - # for gcc/ld the def-file is specified as any object files - objects.append(def_file) + # doesn't work: bfd_close build\...\libfoo.a: Invalid operation + #extra_preargs.extend(["-Wl,--out-implib,%s" % lib_file]) + # for gcc/ld the def-file is specified as any object files + objects.append(def_file) #end: if ((export_symbols is not None) and # (target_desc != self.EXECUTABLE or self.linker_dll == "gcc")): # who wants symbols and a many times larger output file # should explicitly switch the debug mode on - # otherwise we let dllwrap/ld strip the output file + # otherwise we let ld strip the output file # (On my machine: 10KiB < stripped_file < ??100KiB # unstripped_file = stripped_file + XXX KiB # ( XXX=254 for a typical python extension)) @@ -297,19 +260,7 @@ class Mingw32CCompiler(CygwinCCompiler): CygwinCCompiler.__init__ (self, verbose, dry_run, force) - # ld_version >= "2.13" support -shared so use it instead of - # -mdll -static - if ('gcc' in self.cc and self.ld_version < "2.13"): - shared_option = "-mdll -static" - else: - shared_option = "-shared" - - # A real mingw32 doesn't need to specify a different entry point, - # but cygwin 2.91.57 in no-cygwin-mode needs it. - if ('gcc' in self.cc and self.gcc_version <= "2.91.57"): - entry_point = '--entry _DllMain@12' - else: - entry_point = '' + shared_option = "-shared" if is_cygwincc(self.cc): raise CCompilerError( @@ -319,9 +270,9 @@ class Mingw32CCompiler(CygwinCCompiler): compiler_so='%s -mdll -O -Wall' % self.cc, compiler_cxx='%s -O -Wall' % self.cxx, linker_exe='%s' % self.cc, - linker_so='%s %s %s' - % (self.linker_dll, shared_option, - entry_point)) + linker_so='%s %s' + % (self.linker_dll, shared_option)) + # Maybe we should also append -mthreads, but then the finished # dlls need another dll (mingwm10.dll see Mingw32 docs) # (-mthreads: Support thread-safe exception handling on `Mingw32') @@ -388,39 +339,8 @@ def check_config_h(): return (CONFIG_H_UNCERTAIN, "couldn't read '%s': %s" % (fn, exc.strerror)) -RE_VERSION = re.compile(br'(\d+\.\d+(\.\d+)*)') - -def _find_exe_version(cmd): - """Find the version of an executable by running `cmd` in the shell. - - If the command is not found, or the output does not match - `RE_VERSION`, returns None. - """ - executable = cmd.split()[0] - if find_executable(executable) is None: - return None - out = Popen(cmd, shell=True, stdout=PIPE).stdout - try: - out_string = out.read() - finally: - out.close() - result = RE_VERSION.search(out_string) - if result is None: - return None - # LooseVersion works with strings; decode - ver_str = result.group(1).decode() - with distutils.version.suppress_known_deprecation(): - return LooseVersion(ver_str) - -def get_versions(): - """ Try to find out the versions of gcc, ld and dllwrap. - - If not possible it returns None for it. - """ - commands = ['gcc -dumpversion', 'ld -v', 'dllwrap --version'] - return tuple([_find_exe_version(cmd) for cmd in commands]) - def is_cygwincc(cc): '''Try to determine if the compiler that would be used is from cygwin.''' out_string = check_output(shlex.split(cc) + ['-dumpmachine']) return out_string.strip().endswith(b'cygwin') + diff --git a/distutils/tests/test_cygwinccompiler.py b/distutils/tests/test_cygwinccompiler.py index 2a02eed4..c99582d9 100644 --- a/distutils/tests/test_cygwinccompiler.py +++ b/distutils/tests/test_cygwinccompiler.py @@ -8,7 +8,7 @@ from test.support import run_unittest from distutils import cygwinccompiler from distutils.cygwinccompiler import (check_config_h, CONFIG_H_OK, CONFIG_H_NOTOK, - CONFIG_H_UNCERTAIN, get_versions, + CONFIG_H_UNCERTAIN, get_msvcr) from distutils.tests import support @@ -81,40 +81,6 @@ class CygwinCCompilerTestCase(support.TempdirManager, self.write_file(self.python_h, 'xxx __GNUC__ xxx') self.assertEqual(check_config_h()[0], CONFIG_H_OK) - def test_get_versions(self): - - # get_versions calls distutils.spawn.find_executable on - # 'gcc', 'ld' and 'dllwrap' - self.assertEqual(get_versions(), (None, None, None)) - - # Let's fake we have 'gcc' and it returns '3.4.5' - self._exes['gcc'] = b'gcc (GCC) 3.4.5 (mingw special)\nFSF' - res = get_versions() - self.assertEqual(str(res[0]), '3.4.5') - - # and let's see what happens when the version - # doesn't match the regular expression - # (\d+\.\d+(\.\d+)*) - self._exes['gcc'] = b'very strange output' - res = get_versions() - self.assertEqual(res[0], None) - - # same thing for ld - self._exes['ld'] = b'GNU ld version 2.17.50 20060824' - res = get_versions() - self.assertEqual(str(res[1]), '2.17.50') - self._exes['ld'] = b'@(#)PROGRAM:ld PROJECT:ld64-77' - res = get_versions() - self.assertEqual(res[1], None) - - # and dllwrap - self._exes['dllwrap'] = b'GNU dllwrap 2.17.50 20060824\nFSF' - res = get_versions() - self.assertEqual(str(res[2]), '2.17.50') - self._exes['dllwrap'] = b'Cheese Wrap' - res = get_versions() - self.assertEqual(res[2], None) - def test_get_msvcr(self): # none -- cgit v1.2.1 From c09472f6ea1ebf6839f9a31562308568866c98b2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 18 Dec 2021 10:34:22 -0500 Subject: Remove unused imports in cygwincompiler and the reliance on them in tests. --- distutils/tests/test_cygwinccompiler.py | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/distutils/tests/test_cygwinccompiler.py b/distutils/tests/test_cygwinccompiler.py index c99582d9..0e52c88f 100644 --- a/distutils/tests/test_cygwinccompiler.py +++ b/distutils/tests/test_cygwinccompiler.py @@ -2,28 +2,14 @@ import unittest import sys import os -from io import BytesIO from test.support import run_unittest -from distutils import cygwinccompiler from distutils.cygwinccompiler import (check_config_h, CONFIG_H_OK, CONFIG_H_NOTOK, CONFIG_H_UNCERTAIN, get_msvcr) from distutils.tests import support -class FakePopen(object): - test_class = None - - def __init__(self, cmd, shell, stdout): - self.cmd = cmd.split()[0] - exes = self.test_class._exes - if self.cmd in exes: - # issue #6438 in Python 3.x, Popen returns bytes - self.stdout = BytesIO(exes[self.cmd]) - else: - self.stdout = os.popen(cmd, 'r') - class CygwinCCompilerTestCase(support.TempdirManager, unittest.TestCase): @@ -35,29 +21,16 @@ class CygwinCCompilerTestCase(support.TempdirManager, 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 - self.old_find_executable = cygwinccompiler.find_executable - cygwinccompiler.find_executable = self._find_executable - self._exes = {} - self.old_popen = cygwinccompiler.Popen - FakePopen.test_class = self - cygwinccompiler.Popen = FakePopen def tearDown(self): sys.version = self.version from distutils import sysconfig sysconfig.get_config_h_filename = self.old_get_config_h_filename - cygwinccompiler.find_executable = self.old_find_executable - cygwinccompiler.Popen = self.old_popen super(CygwinCCompilerTestCase, self).tearDown() def _get_config_h_filename(self): return self.python_h - def _find_executable(self, name): - if name in self._exes: - return name - return None - def test_check_config_h(self): # check_config_h looks for "GCC" in sys.version first -- cgit v1.2.1 From 4d1035f0d52fb6c07b8603781c4a81122c0f7930 Mon Sep 17 00:00:00 2001 From: Naveen M K Date: Sun, 19 Dec 2021 13:35:20 +0530 Subject: Raise a `DeprecationWarning` for `gcc_version` attribute --- distutils/cygwinccompiler.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/distutils/cygwinccompiler.py b/distutils/cygwinccompiler.py index fda22bf3..4a38dfda 100644 --- a/distutils/cygwinccompiler.py +++ b/distutils/cygwinccompiler.py @@ -51,13 +51,14 @@ import os import sys import copy import shlex +import warnings from subprocess import check_output from distutils.unixccompiler import UnixCCompiler from distutils.file_util import write_file from distutils.errors import (DistutilsExecError, CCompilerError, CompileError, UnknownFileError) -from distutils.version import LooseVersion +from distutils.version import LooseVersion, suppress_known_deprecation def get_msvcr(): """Include the appropriate MSVC runtime library if Python was built @@ -122,12 +123,6 @@ class CygwinCCompiler(UnixCCompiler): self.cc = os.environ.get('CC', 'gcc') self.cxx = os.environ.get('CXX', 'g++') - # Older numpy dependend on this existing to check for ancient - # gcc versions. This doesn't make much sense with clang etc so - # just hardcode to something recent. - # https://github.com/numpy/numpy/pull/20333 - self.gcc_version = LooseVersion("11.2.0") - self.linker_dll = self.cc shared_option = "-shared" @@ -142,6 +137,21 @@ class CygwinCCompiler(UnixCCompiler): # with MSVC 7.0 or later. self.dll_libraries = get_msvcr() + @property + def gcc_version(self): + # Older numpy dependend on this existing to check for ancient + # gcc versions. This doesn't make much sense with clang etc so + # just hardcode to something recent. + # https://github.com/numpy/numpy/pull/20333 + warnings.warn( + "gcc_version attribute of CygwinCCompiler is deprecated. " + "Instead of returning actual gcc version a fixed value 11.2.0 is returned.", + DeprecationWarning, + stacklevel=2, + ) + with suppress_known_deprecation(): + return LooseVersion("11.2.0") + def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts): """Compiles the source by spawning GCC and windres if needed.""" if ext == '.rc' or ext == '.res': -- cgit v1.2.1 From 2f520ab58a202228a35f532e18ccae0f935260da Mon Sep 17 00:00:00 2001 From: Alex Hedges Date: Sun, 19 Dec 2021 16:45:26 -0500 Subject: Render distutils commit links properly in docs --- docs/conf.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 3cc8e35b..f6ccff0f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -68,6 +68,10 @@ link_files = { pattern=r'pypa/distutils#(?P\d+)', url='{GH}/pypa/distutils/issues/{distutils}', ), + dict( + pattern=r'pypa/distutils@(?P[\da-f]+)', + url='{GH}/pypa/distutils/commit/{distutils_commit}', + ), dict( pattern=r'^(?m)((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n', with_scm='{text}\n{rev[timestamp]:%d %b %Y}\n', -- cgit v1.2.1 From 8a3d3c429f72e657b10f672031893e6631141491 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 19 Dec 2021 19:20:48 -0500 Subject: Add changelog --- changelog.d/2935.change.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/2935.change.rst diff --git a/changelog.d/2935.change.rst b/changelog.d/2935.change.rst new file mode 100644 index 00000000..6cef8564 --- /dev/null +++ b/changelog.d/2935.change.rst @@ -0,0 +1 @@ +Merge pypa/distutils@460b59f0e68dba17e2465e8dd421bbc14b994d1f. -- cgit v1.2.1 From 266ab067d1bfa19fa3897260dee1c813b97a0636 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 19 Dec 2021 19:21:27 -0500 Subject: =?UTF-8?q?Bump=20version:=2059.7.0=20=E2=86=92=2059.8.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 9 +++++++++ changelog.d/2935.change.rst | 1 - setup.cfg | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/2935.change.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 44072dee..562bc7d3 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 59.7.0 +current_version = 59.8.0 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index 5c898fa8..0b018b25 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,12 @@ +v59.8.0 +------- + + +Changes +^^^^^^^ +* #2935: Merge pypa/distutils@460b59f0e68dba17e2465e8dd421bbc14b994d1f. + + v59.7.0 ------- diff --git a/changelog.d/2935.change.rst b/changelog.d/2935.change.rst deleted file mode 100644 index 6cef8564..00000000 --- a/changelog.d/2935.change.rst +++ /dev/null @@ -1 +0,0 @@ -Merge pypa/distutils@460b59f0e68dba17e2465e8dd421bbc14b994d1f. diff --git a/setup.cfg b/setup.cfg index 343ea2ad..21bdf4a0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 59.7.0 +version = 59.8.0 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From 9288c6f3f039bf51f997a99ae8766ed02ed92cda Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 19 Dec 2021 19:26:17 -0500 Subject: =?UTF-8?q?Bump=20version:=2059.8.0=20=E2=86=92=2060.0.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 9 +++++++++ changelog.d/2896.breaking.rst | 1 - setup.cfg | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/2896.breaking.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 562bc7d3..b4585481 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 59.8.0 +current_version = 60.0.0 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index 0b018b25..3e658050 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,12 @@ +v60.0.0 +------- + + +Breaking Changes +^^^^^^^^^^^^^^^^ +* #2896: Setuptools once again makes its local copy of distutils the default. To override, set SETUPTOOLS_USE_DISTUTILS=stdlib. + + v59.8.0 ------- diff --git a/changelog.d/2896.breaking.rst b/changelog.d/2896.breaking.rst deleted file mode 100644 index e0aebdcb..00000000 --- a/changelog.d/2896.breaking.rst +++ /dev/null @@ -1 +0,0 @@ -Setuptools once again makes its local copy of distutils the default. To override, set SETUPTOOLS_USE_DISTUTILS=stdlib. diff --git a/setup.cfg b/setup.cfg index 21bdf4a0..6a67e79d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 59.8.0 +version = 60.0.0 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From aefe5d10cda2692f5ea63e5f130a9c8be288b0f4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 20 Dec 2021 11:46:03 -0500 Subject: Extract _select_scheme function to be re-used by Setuptools. --- distutils/command/install.py | 65 ++++++++++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/distutils/command/install.py b/distutils/command/install.py index 80f5f8c7..40be5ba6 100644 --- a/distutils/command/install.py +++ b/distutils/command/install.py @@ -123,6 +123,47 @@ def _get_implementation(): return 'Python' +def _select_scheme(ob, name): + vars(ob).update(_remove_set(ob, _scheme_attrs(_resolve_scheme(name)))) + + +def _remove_set(ob, attrs): + """ + Include only attrs that are None in ob. + """ + return { + key: value + for key, value in attrs.items() + if getattr(ob, key) is None + } + + +def _resolve_scheme(name): + os_name, sep, key = name.partition('_') + try: + resolved = sysconfig.get_preferred_scheme(key) + except Exception: + resolved = _pypy_hack(name) + return resolved + + +def _scheme_attrs(name): + """Resolve install directories by applying the install schemes.""" + scheme = _load_schemes()[name] + return { + f'install_{key}': scheme[key] + for key in SCHEME_KEYS + } + + +def _pypy_hack(name): + PY37 = sys.version_info < (3, 8) + old_pypy = hasattr(sys, 'pypy_version_info') and PY37 + prefix = not name.endswith(('_user', '_home')) + pypy_name = 'pypy' + '_nt' * (os.name == 'nt') + return pypy_name if old_pypy and prefix else name + + class install(Command): description = "install everything from build directory" @@ -520,29 +561,7 @@ class install(Command): "I don't know how to install stuff on '%s'" % os.name) def select_scheme(self, name): - os_name, sep, key = name.partition('_') - try: - resolved = sysconfig.get_preferred_scheme(key) - except Exception: - resolved = self._pypy_hack(name) - return self._select_scheme(resolved) - - def _select_scheme(self, name): - """Sets the install directories by applying the install schemes.""" - # it's the caller's problem if they supply a bad name! - scheme = _load_schemes()[name] - for key in SCHEME_KEYS: - attrname = 'install_' + key - if getattr(self, attrname) is None: - setattr(self, attrname, scheme[key]) - - @staticmethod - def _pypy_hack(name): - PY37 = sys.version_info < (3, 8) - old_pypy = hasattr(sys, 'pypy_version_info') and PY37 - prefix = not name.endswith(('_user', '_home')) - pypy_name = 'pypy' + '_nt' * (os.name == 'nt') - return pypy_name if old_pypy and prefix else name + _select_scheme(self, name) def _expand_attrs(self, attrs): for attr in attrs: -- cgit v1.2.1 From 76c9ab9efc3f13e5ef8681614bebbe437bf51012 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 20 Dec 2021 11:50:50 -0500 Subject: In easy_install, re-use scheme selection from distutils if available. --- setuptools/command/easy_install.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index fc848d0d..00b59904 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -17,10 +17,10 @@ from distutils.errors import ( DistutilsArgError, DistutilsOptionError, DistutilsError, DistutilsPlatformError, ) -from distutils.command.install import INSTALL_SCHEMES, SCHEME_KEYS from distutils import log, dir_util from distutils.command.build_scripts import first_line_re from distutils.spawn import find_executable +from distutils.command import install import sys import os import zipimport @@ -251,6 +251,9 @@ class easy_install(Command): 'exec_prefix': exec_prefix, # Only python 3.2+ has abiflags 'abiflags': getattr(sys, 'abiflags', ''), + 'platlibdir': getattr(sys, 'platlibdir', 'lib'), + 'implementation_lower': install._get_implementation().lower(), + 'implementation': install._get_implementation(), } if site.ENABLE_USER_SITE: @@ -711,13 +714,7 @@ class easy_install(Command): return dist def select_scheme(self, name): - """Sets the install directories by applying the install schemes.""" - # it's the caller's problem if they supply a bad name! - scheme = INSTALL_SCHEMES[name] - for key in SCHEME_KEYS: - attrname = 'install_' + key - if getattr(self, attrname) is None: - setattr(self, attrname, scheme[key]) + install._select_scheme(self, name) # FIXME: 'easy_install.process_distribution' is too complex (12) def process_distribution( # noqa: C901 -- cgit v1.2.1 From 60b78468341d52bc033f0ad77e890a656ccb9a72 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 20 Dec 2021 12:01:37 -0500 Subject: Add fallback support for distutils in stdlib. --- setuptools/command/easy_install.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index 00b59904..e8150057 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -252,9 +252,13 @@ class easy_install(Command): # Only python 3.2+ has abiflags 'abiflags': getattr(sys, 'abiflags', ''), 'platlibdir': getattr(sys, 'platlibdir', 'lib'), - 'implementation_lower': install._get_implementation().lower(), - 'implementation': install._get_implementation(), } + with contextlib.suppress(AttributeError): + # only for distutils outside stdlib + self.config_vars.update({ + 'implementation_lower': install._get_implementation().lower(), + 'implementation': install._get_implementation(), + }) if site.ENABLE_USER_SITE: self.config_vars['userbase'] = self.install_userbase @@ -714,7 +718,11 @@ class easy_install(Command): return dist def select_scheme(self, name): - install._select_scheme(self, name) + try: + install._select_scheme(self, name) + except AttributeError: + # stdlib distutils + install.install.select_scheme(self, name) # FIXME: 'easy_install.process_distribution' is too complex (12) def process_distribution( # noqa: C901 -- cgit v1.2.1 From 4a9d555733924ce7ebbaa4d4c26e0f3b1e834eaf Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 20 Dec 2021 12:03:56 -0500 Subject: Update changelog. --- changelog.d/2944.misc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/2944.misc.rst diff --git a/changelog.d/2944.misc.rst b/changelog.d/2944.misc.rst new file mode 100644 index 00000000..9cfd7b6a --- /dev/null +++ b/changelog.d/2944.misc.rst @@ -0,0 +1 @@ +Add support for extended install schemes in easy_install. -- cgit v1.2.1 From aca3fe993e2f0159ed17ec5b8573e479bfa99617 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 20 Dec 2021 13:53:51 -0500 Subject: =?UTF-8?q?Bump=20version:=2060.0.0=20=E2=86=92=2060.0.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 9 +++++++++ changelog.d/2944.misc.rst | 1 - setup.cfg | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/2944.misc.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index b4585481..0532998c 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 60.0.0 +current_version = 60.0.1 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index 3e658050..b1119cff 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,12 @@ +v60.0.1 +------- + + +Misc +^^^^ +* #2944: Add support for extended install schemes in easy_install. + + v60.0.0 ------- diff --git a/changelog.d/2944.misc.rst b/changelog.d/2944.misc.rst deleted file mode 100644 index 9cfd7b6a..00000000 --- a/changelog.d/2944.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Add support for extended install schemes in easy_install. diff --git a/setup.cfg b/setup.cfg index 6a67e79d..378bc8bb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 60.0.0 +version = 60.0.1 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From edf116b1cc4a72075b9af06748df3b177d55d4dc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 20 Dec 2021 16:43:07 -0500 Subject: Select 'posix_user' for the scheme unless falling back to stdlib, then use 'unix_user'. Fixes #2938. --- changelog.d/2938.misc.rst | 1 + setuptools/command/easy_install.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/2938.misc.rst diff --git a/changelog.d/2938.misc.rst b/changelog.d/2938.misc.rst new file mode 100644 index 00000000..c8cdbc95 --- /dev/null +++ b/changelog.d/2938.misc.rst @@ -0,0 +1 @@ +Select 'posix_user' for the scheme unless falling back to stdlib, then use 'unix_user'. diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index e8150057..fb34d10e 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -378,7 +378,7 @@ class easy_install(Command): msg = "User base directory is not specified" raise DistutilsPlatformError(msg) self.install_base = self.install_platbase = self.install_userbase - scheme_name = os.name.replace('posix', 'unix') + '_user' + scheme_name = f'{os.name}_user' self.select_scheme(scheme_name) def _expand_attrs(self, attrs): @@ -722,7 +722,7 @@ class easy_install(Command): install._select_scheme(self, name) except AttributeError: # stdlib distutils - install.install.select_scheme(self, name) + install.install.select_scheme(self, name.replace('posix', 'unix')) # FIXME: 'easy_install.process_distribution' is too complex (12) def process_distribution( # noqa: C901 -- cgit v1.2.1 From 34006b386ef3c5f8cb6abb8c1f32ddf2f3cec4e0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 20 Dec 2021 16:43:58 -0500 Subject: =?UTF-8?q?Bump=20version:=2060.0.1=20=E2=86=92=2060.0.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 9 +++++++++ changelog.d/2938.misc.rst | 1 - setup.cfg | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/2938.misc.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 0532998c..aff2d444 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 60.0.1 +current_version = 60.0.2 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index b1119cff..4d66fa19 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,12 @@ +v60.0.2 +------- + + +Misc +^^^^ +* #2938: Select 'posix_user' for the scheme unless falling back to stdlib, then use 'unix_user'. + + v60.0.1 ------- diff --git a/changelog.d/2938.misc.rst b/changelog.d/2938.misc.rst deleted file mode 100644 index c8cdbc95..00000000 --- a/changelog.d/2938.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Select 'posix_user' for the scheme unless falling back to stdlib, then use 'unix_user'. diff --git a/setup.cfg b/setup.cfg index 378bc8bb..d6a59e18 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 60.0.1 +version = 60.0.2 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From 82726bbb8c799bd39e98422495e268bd7b50ca41 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 20 Dec 2021 19:30:13 -0500 Subject: Extract frame_file_is_setup. --- _distutils_hack/__init__.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/_distutils_hack/__init__.py b/_distutils_hack/__init__.py index ae97a0b2..da51b433 100644 --- a/_distutils_hack/__init__.py +++ b/_distutils_hack/__init__.py @@ -106,17 +106,21 @@ class DistutilsMetaFinder: clear_distutils() self.spec_for_distutils = lambda: None - @staticmethod - def pip_imported_during_build(): + @classmethod + def pip_imported_during_build(cls): """ Detect if pip is being imported in a build script. Ref #2355. """ import traceback return any( - frame.f_globals['__file__'].endswith('setup.py') + cls.frame_file_is_setup(frame) for frame, line in traceback.walk_stack(None) ) + @staticmethod + def frame_file_is_setup(frame): + return frame.f_globals['__file__'].endswith('setup.py') + DISTUTILS_FINDER = DistutilsMetaFinder() -- cgit v1.2.1 From 137ab9d684075f772c322f455b0dd1f992ddcd8f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 20 Dec 2021 19:46:08 -0500 Subject: Avoid KeyError in distutils hack when pip is imported during ensurepip. Fixes #2940. --- _distutils_hack/__init__.py | 6 +++++- changelog.d/2940.misc.rst | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelog.d/2940.misc.rst diff --git a/_distutils_hack/__init__.py b/_distutils_hack/__init__.py index da51b433..22bc9ed6 100644 --- a/_distutils_hack/__init__.py +++ b/_distutils_hack/__init__.py @@ -119,7 +119,11 @@ class DistutilsMetaFinder: @staticmethod def frame_file_is_setup(frame): - return frame.f_globals['__file__'].endswith('setup.py') + """ + Return True if the indicated frame suggests a setup.py file. + """ + # some frames may not have __file__ (#2940) + return frame.f_globals.get('__file__', '').endswith('setup.py') DISTUTILS_FINDER = DistutilsMetaFinder() diff --git a/changelog.d/2940.misc.rst b/changelog.d/2940.misc.rst new file mode 100644 index 00000000..74cd3859 --- /dev/null +++ b/changelog.d/2940.misc.rst @@ -0,0 +1 @@ +Avoid KeyError in distutils hack when pip is imported during ensurepip. -- cgit v1.2.1 From 390016f98244e8c11d6060c3aa27efbc36db8592 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 20 Dec 2021 19:56:14 -0500 Subject: =?UTF-8?q?Bump=20version:=2060.0.2=20=E2=86=92=2060.0.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 9 +++++++++ changelog.d/2940.misc.rst | 1 - setup.cfg | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/2940.misc.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index aff2d444..bb1c47a5 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 60.0.2 +current_version = 60.0.3 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index 4d66fa19..4fab510f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,12 @@ +v60.0.3 +------- + + +Misc +^^^^ +* #2940: Avoid KeyError in distutils hack when pip is imported during ensurepip. + + v60.0.2 ------- diff --git a/changelog.d/2940.misc.rst b/changelog.d/2940.misc.rst deleted file mode 100644 index 74cd3859..00000000 --- a/changelog.d/2940.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Avoid KeyError in distutils hack when pip is imported during ensurepip. diff --git a/setup.cfg b/setup.cfg index d6a59e18..77095b64 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 60.0.2 +version = 60.0.3 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From e645104656fda22f4c0c2b3d9841ed792b1e7103 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 8 Nov 2021 13:01:27 +0000 Subject: Configure pytest to enable/disable integration tests --- conftest.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/conftest.py b/conftest.py index d5e851fe..c4e4fec5 100644 --- a/conftest.py +++ b/conftest.py @@ -1,5 +1,7 @@ import sys +import pytest + pytest_plugins = 'setuptools.tests.fixtures' @@ -9,6 +11,18 @@ def pytest_addoption(parser): "--package_name", action="append", default=[], help="list of package_name to pass to test functions", ) + parser.addoption( + "--integration", action="store_true", default=False, + help="run integration tests (only)" + ) + + +def pytest_configure(config): + config.addinivalue_line("markers", "integration: indicate integration tests") + + if config.option.integration: + # Assume unit tests and flake already run + config.option.flake8 = False collect_ignore = [ @@ -27,3 +41,13 @@ collect_ignore = [ if sys.version_info < (3, 6): collect_ignore.append('docs/conf.py') # uses f-strings collect_ignore.append('pavement.py') + + +@pytest.fixture(autouse=True) +def _skip_integration(request): + running_integration_tests = request.config.getoption("--integration") + is_integration_test = request.node.get_closest_marker("integration") + if running_integration_tests and not is_integration_test: + pytest.skip("running integration tests only") + if not running_integration_tests and is_integration_test: + pytest.skip("skipping integration tests") -- cgit v1.2.1 From e1c1934c946704c74a8e83529e41d72e667b1d25 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 8 Nov 2021 13:03:13 +0000 Subject: Add integration test based on feedback from #2849 The selection of packages used in the integration test is arbitrary, and can be changed. The main criteria used was the time to build, and the number of "non-Python" dependencies. The only exception was numpy, due to its significance to the ecosystem. --- setup.cfg | 2 + setuptools/tests/integration/__init__.py | 0 .../tests/integration/test_pip_install_sdist.py | 272 +++++++++++++++++++++ 3 files changed, 274 insertions(+) create mode 100644 setuptools/tests/integration/__init__.py create mode 100644 setuptools/tests/integration/test_pip_install_sdist.py diff --git a/setup.cfg b/setup.cfg index 6a67e79d..79843258 100644 --- a/setup.cfg +++ b/setup.cfg @@ -64,6 +64,8 @@ testing = sphinx jaraco.path>=3.2.0 + tomli # used in integration test + docs = # upstream sphinx diff --git a/setuptools/tests/integration/__init__.py b/setuptools/tests/integration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/setuptools/tests/integration/test_pip_install_sdist.py b/setuptools/tests/integration/test_pip_install_sdist.py new file mode 100644 index 00000000..71969c0a --- /dev/null +++ b/setuptools/tests/integration/test_pip_install_sdist.py @@ -0,0 +1,272 @@ +"""Integration tests for setuptools that focus on building packages via pip. + +The idea behind these tests is not to exhaustively check all the possible +combinations of packages, operating systems, supporting libraries, etc, but +rather check a limited number of popular packages and how they interact with +the exposed public API. This way if any change in API is introduced, we hope to +identify backward compatibility problems before publishing a release. + +The number of tested packages is purposefully kept small, to minimise duration +and the associated maintenance cost (changes in the way these packages define +their build process may require changes in the tests). +""" +import importlib +import json +import os +import subprocess +import sys +import tarfile +from enum import Enum +from glob import glob +from hashlib import md5 +from itertools import chain +from urllib.request import urlopen +from zipfile import ZipFile + +import pytest +import tomli as toml +from packaging.requirements import Requirement + + +pytestmark = pytest.mark.integration + + +LATEST, = list(Enum("v", "LATEST")) +"""Default version to be checked""" +# There are positive and negative aspects of checking the latest version of the +# packages. +# The main positive aspect is that the latest version might have already +# removed the use of APIs deprecated in previous releases of setuptools. + + +# Packages to be tested: +# (Please notice the test environment cannot support EVERY library required for +# compiling binary extensions. In Ubuntu/Debian nomenclature, we only assume +# that `build-essential`, `gfortran` and `libopenblas-dev` are installed, +# due to their relevance to the numerical/scientific programming ecosystem) +EXAMPLES = [ + ("numpy", LATEST), # custom distutils-based commands + ("pandas", LATEST), # cython + custom build_ext + ("sphinx", LATEST), # custom setup.py + ("pip", LATEST), # just in case... + ("pytest", LATEST), # uses setuptools_scm + ("mypy", LATEST), # custom build_py + ext_modules + + # --- Popular packages: https://hugovk.github.io/top-pypi-packages/ --- + ("botocore", LATEST), + ("kiwisolver", "1.3.2"), # build_ext, version pinned due to setup_requires + ("brotli", LATEST), # not in the list but used by urllib3 +] + + +# Some packages have "optional" dependencies that modify their build behaviour +# and are not listed in pyproject.toml, others still use `setup_requires` +EXTRA_BUILD_DEPS = { + "sphinx": ("babel>=1.3",), + "kiwisolver": ("cppy>=1.1.0",) +} + + +# By default, pip will try to build packages in isolation (PEP 517), which +# means it will download the previous stable version of setuptools. +# `pip` flags can avoid that (the version of setuptools under test +# should be the one to be used) +PIP = (sys.executable, "-m", "pip") +SDIST_OPTIONS = ( + "--ignore-installed", + "--no-build-isolation", + # We don't need "--no-binary :all:" since we specify the path to the sdist. + # It also helps with performance, since dependencies can come from wheels. +) +# The downside of `--no-build-isolation` is that pip will not download build +# dependencies. The test script will have to also handle that. + + +@pytest.fixture(autouse=True) +def _prepare(tmp_path, monkeypatch, request): + (tmp_path / "lib").mkdir(exist_ok=True) + download_path = os.getenv("DOWNLOAD_PATH", str(tmp_path)) + os.makedirs(download_path, exist_ok=True) + + # Environment vars used for building some of the packages + monkeypatch.setenv("USE_MYPYC", "1") + + def _debug_info(): + # Let's provide the maximum amount of information possible in the case + # it is necessary to debug the tests directly from the CI logs. + print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") + print("Temporary directory:") + for entry in chain(tmp_path.glob("*"), tmp_path.glob("lib/*")): + print(entry) + request.addfinalizer(_debug_info) + + +ALREADY_LOADED = ("pytest", "mypy") # loaded by pytest/pytest-enabler + + +@pytest.mark.parametrize('package, version', EXAMPLES) +def test_install_sdist(package, version, tmp_path, monkeypatch): + lib = tmp_path / "lib" + sdist = retrieve_sdist(package, version, tmp_path) + deps = build_deps(package, sdist) + if deps: + print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") + print("Dependencies:", deps) + pip_install(*deps, target=lib) + + pip_install(*SDIST_OPTIONS, sdist, target=lib) + + if package in ALREADY_LOADED: + # We cannot import packages already in use from a different location + assert (lib / package).exists() + return + + # Make sure the package was installed correctly + with monkeypatch.context() as m: + m.syspath_prepend(str(lib)) # add installed packages to path + pkg = importlib.import_module(package) + if hasattr(pkg, '__version__'): + print(pkg.__version__) + for path in getattr(pkg, '__path__', []): + assert os.path.abspath(path).startswith(os.path.abspath(tmp_path)) + + +# ---- Helper Functions ---- + + +def pip_install(*args, target): + """Install packages in the ``target`` directory""" + cmd = [*PIP, 'install', '--target', str(target), *args] + env = {**os.environ, "PYTHONPATH": str(target)} + # ^-- use libs installed in the target for build, but keep + # compiling/build-related env variables + + try: + subprocess.run( + cmd, + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + env=env + ) + except subprocess.CalledProcessError as ex: + print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") + print("Command", repr(ex.cmd), "failed with code", ex.returncode) + print(ex.stdout) + print(ex.stderr) + raise + + +def retrieve_sdist(package, version, tmp_path): + """Either use cached sdist file or download it from PyPI""" + # `pip download` cannot be used due to + # https://github.com/pypa/pip/issues/1884 + # https://discuss.python.org/t/pep-625-file-name-of-a-source-distribution/4686 + # We have to find the correct distribution file and download it + download_path = os.getenv("DOWNLOAD_PATH", str(tmp_path)) + dist = retrieve_pypi_sdist_metadata(package, version) + + # Remove old files to prevent cache to grow indefinitely + for file in glob(os.path.join(download_path, f"{package}*")): + if dist["filename"] != file: + os.unlink(file) + + dist_file = os.path.join(download_path, dist["filename"]) + if not os.path.exists(dist_file): + download(dist["url"], dist_file, dist["md5_digest"]) + return dist_file + + +def retrieve_pypi_sdist_metadata(package, version): + # https://warehouse.pypa.io/api-reference/json.html + id_ = package if version is LATEST else f"{package}/{version}" + with urlopen(f"https://pypi.org/pypi/{id_}/json") as f: + metadata = json.load(f) + + if metadata["info"]["yanked"]: + raise ValueError(f"Release for {package} {version} was yanked") + + version = metadata["info"]["version"] + release = metadata["releases"][version] + dists = [d for d in release if d["packagetype"] == "sdist"] + if len(dists) == 0: + raise ValueError(f"No sdist found for {package} {version}") + + for dist in dists: + if dist["filename"].endswith(".tar.gz"): + return dist + + # Not all packages are publishing tar.gz, e.g. numpy==1.21.4 + return dist + + +def download(url, dest, md5_digest): + with urlopen(url) as f: + data = f.read() + + assert md5(data).hexdigest() == md5_digest + + with open(dest, "wb") as f: + f.write(data) + + assert os.path.exists(dest) + + +IN_TEST_VENV = ("setuptools", "wheel", "packaging") +"""Don't re-install""" + + +def build_deps(package, sdist_file): + """Find out what are the build dependencies for a package. + + We need to "manually" install them, since pip will not install build + deps with `--no-build-isolation`. + """ + archive = Archive(sdist_file) + pyproject = _read_pyproject(archive) + + info = toml.loads(pyproject) + deps = info.get("build-system", {}).get("requires", []) + deps += EXTRA_BUILD_DEPS.get(package, []) + # Remove setuptools from requirements (and deduplicate) + requirements = {Requirement(d).name: d for d in deps} + return [v for k, v in requirements.items() if k not in IN_TEST_VENV] + + +def _read_pyproject(archive): + for member in archive: + if os.path.basename(archive.get_name(member)) == "pyproject.toml": + return archive.get_content(member) + return "" + + +class Archive: + """Compatibility layer for ZipFile/Info and TarFile/Info""" + def __init__(self, filename): + self._filename = filename + if filename.endswith("tar.gz"): + self._obj = tarfile.open(filename, "r:gz") + elif filename.endswith("zip"): + self._obj = ZipFile(filename) + else: + raise ValueError(f"{filename} doesn't seem to be a zip or tar.gz") + + def __iter__(self): + if hasattr(self._obj, "infolist"): + return iter(self._obj.infolist()) + return iter(self._obj) + + def get_name(self, zip_or_tar_info): + if hasattr(zip_or_tar_info, "filename"): + return zip_or_tar_info.filename + return zip_or_tar_info.name + + def get_content(self, zip_or_tar_info): + if hasattr(self._obj, "extractfile"): + content = self._obj.extractfile(zip_or_tar_info) + if content is None: + msg = f"Invalid {zip_or_tar_info.name} in {self._filename}" + raise ValueError(msg) + return str(content.read(), "utf-8") + return str(self._obj.read(zip_or_tar_info), "utf-8") -- cgit v1.2.1 From 8e94277dc707eba2c4699eb656a48b0f032d89b5 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 8 Nov 2021 13:10:29 +0000 Subject: Add tox environment for integration tests --- tox.ini | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tox.ini b/tox.ini index 25b4eaf0..34b5abf6 100644 --- a/tox.ini +++ b/tox.ini @@ -20,6 +20,16 @@ passenv = SETUPTOOLS_USE_DISTUTILS windir # required for test_pkg_resources +[testenv:integration] +deps = {[testenv]deps} +extras = {[testenv]extras} +passenv = + {[testenv]passenv} + DOWNLOAD_PATH +commands = + pytest --integration {posargs:-vv --durations=10 --no-cov setuptools/tests/integration} + # use verbose mode by default to facilitate debugging from CI logs + [testenv:docs] extras = docs -- cgit v1.2.1 From 266b905b898c80c70bce0f0ed694e53900803fb2 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 8 Nov 2021 14:19:20 +0000 Subject: Run integration tests before release on CI --- .github/workflows/main.yml | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5a01e9a5..cb524369 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -59,9 +59,37 @@ jobs: run: | C:\\tools\\cygwin\\bin\\bash -l -x -c 'cd $(cygpath -u "$GITHUB_WORKSPACE") && tox -- --cov-report xml' - release: + integration-test: needs: test if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + # To avoid long times and high resource usage, we assume that: + # 1. The setuptools APIs used by packages don't vary too much with OS or + # Python implementation + # 2. Any circumstance for which the previous assumption is not valid is + # already tested via unit tests (or other tests not classified here as + # "integration") + # With that in mind, the integration tests can run for a single setup + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install OS-level dependencies + run: | + sudo apt-get update + sudo apt-get install build-essential gfortran libopenblas-dev + - name: Setup Python + uses: actions/setup-python@v2 + with: + # Use a release that is not very new but still have a long life: + python-version: "3.8" + - name: Install tox + run: | + python -m pip install tox + - name: Run integration tests + run: tox -e integration + + release: + needs: [test, integration-test] + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: -- cgit v1.2.1 From 159e9a141366d6f10eddc3ae3df20d8cf9c7afd0 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 8 Nov 2021 14:45:06 +0000 Subject: Add news fragment about integration tests --- changelog.d/2862.misc.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelog.d/2862.misc.rst diff --git a/changelog.d/2862.misc.rst b/changelog.d/2862.misc.rst new file mode 100644 index 00000000..77e80007 --- /dev/null +++ b/changelog.d/2862.misc.rst @@ -0,0 +1,2 @@ +Added integration tests that focus on building and installing some packages in +the Python ecosystem via ``pip`` -- by :user:`abravalheri` -- cgit v1.2.1 From c42cbb1c60c5b5d1798ebe470100ed26fb02506d Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 13 Nov 2021 09:40:47 +0000 Subject: Disable pytest plugins in integration tests via -p no:* --- conftest.py | 4 ---- tox.ini | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/conftest.py b/conftest.py index c4e4fec5..96431a57 100644 --- a/conftest.py +++ b/conftest.py @@ -20,10 +20,6 @@ def pytest_addoption(parser): def pytest_configure(config): config.addinivalue_line("markers", "integration: indicate integration tests") - if config.option.integration: - # Assume unit tests and flake already run - config.option.flake8 = False - collect_ignore = [ 'tests/manual_test.py', diff --git a/tox.ini b/tox.ini index 34b5abf6..fb4a72e4 100644 --- a/tox.ini +++ b/tox.ini @@ -27,7 +27,7 @@ passenv = {[testenv]passenv} DOWNLOAD_PATH commands = - pytest --integration {posargs:-vv --durations=10 --no-cov setuptools/tests/integration} + pytest --integration {posargs:-vv --durations=10 -p no:cov -p no:flake8 setuptools/tests/integration} # use verbose mode by default to facilitate debugging from CI logs [testenv:docs] -- cgit v1.2.1 From 698dd827f0e9882ad63ab3306f0ce7fc1bc9520e Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 13 Nov 2021 10:39:01 +0000 Subject: Use separated "extras" for integration tests Instead of disabling pytest plugins, simply don't install them --- setup.cfg | 8 +++++++- setuptools/tests/integration/test_pip_install_sdist.py | 5 ++++- tox.ini | 4 ++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index 79843258..44b58b6e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -64,7 +64,13 @@ testing = sphinx jaraco.path>=3.2.0 - tomli # used in integration test +testing-integration: + pytest + pytest-xdist + pytest-enabler + tomli + wheel + docs = # upstream diff --git a/setuptools/tests/integration/test_pip_install_sdist.py b/setuptools/tests/integration/test_pip_install_sdist.py index 71969c0a..7fbd8899 100644 --- a/setuptools/tests/integration/test_pip_install_sdist.py +++ b/setuptools/tests/integration/test_pip_install_sdist.py @@ -24,7 +24,6 @@ from urllib.request import urlopen from zipfile import ZipFile import pytest -import tomli as toml from packaging.requirements import Requirement @@ -223,6 +222,10 @@ def build_deps(package, sdist_file): We need to "manually" install them, since pip will not install build deps with `--no-build-isolation`. """ + import tomli as toml + # delay importing, since pytest discovery phase may hit this file from a + # testenv without tomli + archive = Archive(sdist_file) pyproject = _read_pyproject(archive) diff --git a/tox.ini b/tox.ini index fb4a72e4..e35bf618 100644 --- a/tox.ini +++ b/tox.ini @@ -22,12 +22,12 @@ passenv = [testenv:integration] deps = {[testenv]deps} -extras = {[testenv]extras} +extras = testing-integration passenv = {[testenv]passenv} DOWNLOAD_PATH commands = - pytest --integration {posargs:-vv --durations=10 -p no:cov -p no:flake8 setuptools/tests/integration} + pytest --integration {posargs:-vv --durations=10 setuptools/tests/integration} # use verbose mode by default to facilitate debugging from CI logs [testenv:docs] -- cgit v1.2.1 From 60dcba4c3b9f40f0dc9180bcf4f9954087ff9aad Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 13 Nov 2021 10:49:03 +0000 Subject: Fix flake8 errors for conftest.py --- conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conftest.py b/conftest.py index 96431a57..43f33ba4 100644 --- a/conftest.py +++ b/conftest.py @@ -18,7 +18,7 @@ def pytest_addoption(parser): def pytest_configure(config): - config.addinivalue_line("markers", "integration: indicate integration tests") + config.addinivalue_line("markers", "integration: integration tests") collect_ignore = [ -- cgit v1.2.1 From 48361da818928e4ffe609878a93a65e9210082dd Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 13 Nov 2021 16:33:23 +0000 Subject: Simulate pip's isolation using virtualenv --- setup.cfg | 1 + .../tests/integration/test_pip_install_sdist.py | 100 ++++++++++----------- tox.ini | 2 + 3 files changed, 52 insertions(+), 51 deletions(-) diff --git a/setup.cfg b/setup.cfg index 44b58b6e..2a5e10b9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -68,6 +68,7 @@ testing-integration: pytest pytest-xdist pytest-enabler + virtualenv tomli wheel diff --git a/setuptools/tests/integration/test_pip_install_sdist.py b/setuptools/tests/integration/test_pip_install_sdist.py index 7fbd8899..68f7f823 100644 --- a/setuptools/tests/integration/test_pip_install_sdist.py +++ b/setuptools/tests/integration/test_pip_install_sdist.py @@ -10,7 +10,6 @@ The number of tested packages is purposefully kept small, to minimise duration and the associated maintenance cost (changes in the way these packages define their build process may require changes in the tests). """ -import importlib import json import os import subprocess @@ -19,16 +18,17 @@ import tarfile from enum import Enum from glob import glob from hashlib import md5 -from itertools import chain from urllib.request import urlopen from zipfile import ZipFile import pytest +import setuptools from packaging.requirements import Requirement pytestmark = pytest.mark.integration +SETUPTOOLS_ROOT = os.path.dirname(next(iter(setuptools.__path__))) LATEST, = list(Enum("v", "LATEST")) """Default version to be checked""" @@ -55,6 +55,9 @@ EXAMPLES = [ ("botocore", LATEST), ("kiwisolver", "1.3.2"), # build_ext, version pinned due to setup_requires ("brotli", LATEST), # not in the list but used by urllib3 + + # When adding packages to this list, make sure they expose a `__version__` + # attribute, or modify the tests bellow ] @@ -66,11 +69,13 @@ EXTRA_BUILD_DEPS = { } +VIRTUALENV = (sys.executable, "-m", "virtualenv") + + # By default, pip will try to build packages in isolation (PEP 517), which # means it will download the previous stable version of setuptools. # `pip` flags can avoid that (the version of setuptools under test # should be the one to be used) -PIP = (sys.executable, "-m", "pip") SDIST_OPTIONS = ( "--ignore-installed", "--no-build-isolation", @@ -81,9 +86,14 @@ SDIST_OPTIONS = ( # dependencies. The test script will have to also handle that. +@pytest.fixture +def venv_python(tmp_path): + run_command([*VIRTUALENV, str(tmp_path / ".venv")]) + return str(next(tmp_path.glob(".venv/*/python"))) + + @pytest.fixture(autouse=True) -def _prepare(tmp_path, monkeypatch, request): - (tmp_path / "lib").mkdir(exist_ok=True) +def _prepare(tmp_path, venv_python, monkeypatch, request): download_path = os.getenv("DOWNLOAD_PATH", str(tmp_path)) os.makedirs(download_path, exist_ok=True) @@ -95,8 +105,9 @@ def _prepare(tmp_path, monkeypatch, request): # it is necessary to debug the tests directly from the CI logs. print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") print("Temporary directory:") - for entry in chain(tmp_path.glob("*"), tmp_path.glob("lib/*")): - print(entry) + map(print, tmp_path.glob("*")) + print("Virtual environment:") + run_command([venv_python, "-m", "pip", "freeze"]) request.addfinalizer(_debug_info) @@ -104,57 +115,48 @@ ALREADY_LOADED = ("pytest", "mypy") # loaded by pytest/pytest-enabler @pytest.mark.parametrize('package, version', EXAMPLES) -def test_install_sdist(package, version, tmp_path, monkeypatch): - lib = tmp_path / "lib" +def test_install_sdist(package, version, tmp_path, venv_python): + venv_pip = (venv_python, "-m", "pip") sdist = retrieve_sdist(package, version, tmp_path) deps = build_deps(package, sdist) if deps: print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") print("Dependencies:", deps) - pip_install(*deps, target=lib) - - pip_install(*SDIST_OPTIONS, sdist, target=lib) + run_command([*venv_pip, "install", *deps]) - if package in ALREADY_LOADED: - # We cannot import packages already in use from a different location - assert (lib / package).exists() - return + # Use a virtualenv to simulate PEP 517 isolation + # but install setuptools to force the version under development + correct_setuptools = os.getenv("PROJECT_ROOT") or SETUPTOOLS_ROOT + assert os.path.exists(os.path.join(correct_setuptools, "pyproject.toml")) + run_command([*venv_pip, "install", "-Ie", correct_setuptools]) + run_command([*venv_pip, "install", *SDIST_OPTIONS, sdist]) - # Make sure the package was installed correctly - with monkeypatch.context() as m: - m.syspath_prepend(str(lib)) # add installed packages to path - pkg = importlib.import_module(package) - if hasattr(pkg, '__version__'): - print(pkg.__version__) - for path in getattr(pkg, '__path__', []): - assert os.path.abspath(path).startswith(os.path.abspath(tmp_path)) + # Execute a simple script to make sure the package was installed correctly + script = f"import {package}; print(getattr({package}, '__version__', 0))" + run_command([venv_python, "-c", script]) # ---- Helper Functions ---- -def pip_install(*args, target): - """Install packages in the ``target`` directory""" - cmd = [*PIP, 'install', '--target', str(target), *args] - env = {**os.environ, "PYTHONPATH": str(target)} - # ^-- use libs installed in the target for build, but keep - # compiling/build-related env variables - - try: - subprocess.run( - cmd, - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True, - env=env - ) - except subprocess.CalledProcessError as ex: - print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") - print("Command", repr(ex.cmd), "failed with code", ex.returncode) - print(ex.stdout) - print(ex.stderr) - raise +def run_command(cmd, env=None): + r = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + env={**os.environ, **(env or {})} + # ^-- allow overwriting instead of discarding the current env + ) + + out = r.stdout + "\n" + r.stderr + # pytest omits stdout/err by default, if the test fails they help debugging + print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") + print(f"Command: {cmd}\nreturn code: {r.returncode}\n\n{out}") + + if r.returncode == 0: + return out + raise subprocess.CalledProcessError(r.returncode, cmd, r.stdout, r.stderr) def retrieve_sdist(package, version, tmp_path): @@ -212,10 +214,6 @@ def download(url, dest, md5_digest): assert os.path.exists(dest) -IN_TEST_VENV = ("setuptools", "wheel", "packaging") -"""Don't re-install""" - - def build_deps(package, sdist_file): """Find out what are the build dependencies for a package. @@ -234,7 +232,7 @@ def build_deps(package, sdist_file): deps += EXTRA_BUILD_DEPS.get(package, []) # Remove setuptools from requirements (and deduplicate) requirements = {Requirement(d).name: d for d in deps} - return [v for k, v in requirements.items() if k not in IN_TEST_VENV] + return [v for k, v in requirements.items() if k != "setuptools"] def _read_pyproject(archive): diff --git a/tox.ini b/tox.ini index e35bf618..c2725290 100644 --- a/tox.ini +++ b/tox.ini @@ -26,6 +26,8 @@ extras = testing-integration passenv = {[testenv]passenv} DOWNLOAD_PATH +setenv = + PROJECT_ROOT = {toxinidir} commands = pytest --integration {posargs:-vv --durations=10 setuptools/tests/integration} # use verbose mode by default to facilitate debugging from CI logs -- cgit v1.2.1 From c83f83be321a4f6b39460125788cf500459391bb Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sun, 14 Nov 2021 14:08:57 +0000 Subject: Use shutil to find executable This is more reliable then simply globing (and will also work in other operating systems) --- setuptools/tests/integration/test_pip_install_sdist.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/setuptools/tests/integration/test_pip_install_sdist.py b/setuptools/tests/integration/test_pip_install_sdist.py index 68f7f823..e838dd0c 100644 --- a/setuptools/tests/integration/test_pip_install_sdist.py +++ b/setuptools/tests/integration/test_pip_install_sdist.py @@ -12,6 +12,7 @@ their build process may require changes in the tests). """ import json import os +import shutil import subprocess import sys import tarfile @@ -22,9 +23,9 @@ from urllib.request import urlopen from zipfile import ZipFile import pytest -import setuptools from packaging.requirements import Requirement +import setuptools pytestmark = pytest.mark.integration @@ -89,7 +90,8 @@ SDIST_OPTIONS = ( @pytest.fixture def venv_python(tmp_path): run_command([*VIRTUALENV, str(tmp_path / ".venv")]) - return str(next(tmp_path.glob(".venv/*/python"))) + possible_path = (str(p.parent) for p in tmp_path.glob(".venv/*/python*")) + return shutil.which("python", path=os.pathsep.join(possible_path)) @pytest.fixture(autouse=True) @@ -221,6 +223,7 @@ def build_deps(package, sdist_file): deps with `--no-build-isolation`. """ import tomli as toml + # delay importing, since pytest discovery phase may hit this file from a # testenv without tomli -- cgit v1.2.1 From 01504a07f7312c3a0bce9d77dc702f31209e69f1 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 13 Nov 2021 17:01:31 +0000 Subject: Separate some reusable integration helpers --- setuptools/tests/integration/helpers.py | 61 +++++++++++++++++++ .../tests/integration/test_pip_install_sdist.py | 69 +++------------------- 2 files changed, 70 insertions(+), 60 deletions(-) create mode 100644 setuptools/tests/integration/helpers.py diff --git a/setuptools/tests/integration/helpers.py b/setuptools/tests/integration/helpers.py new file mode 100644 index 00000000..43f43902 --- /dev/null +++ b/setuptools/tests/integration/helpers.py @@ -0,0 +1,61 @@ +"""Reusable functions and classes for different types of integration tests. + +For example ``Archive`` can be used to check the contents of distribution built +with setuptools, and ``run`` will always try to be as verbose as possible to +facilitate debugging. +""" +import os +import subprocess +import tarfile +from zipfile import ZipFile + + +def run(cmd, env=None): + r = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + env={**os.environ, **(env or {})} + # ^-- allow overwriting instead of discarding the current env + ) + + out = r.stdout + "\n" + r.stderr + # pytest omits stdout/err by default, if the test fails they help debugging + print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") + print(f"Command: {cmd}\nreturn code: {r.returncode}\n\n{out}") + + if r.returncode == 0: + return out + raise subprocess.CalledProcessError(r.returncode, cmd, r.stdout, r.stderr) + + +class Archive: + """Compatibility layer for ZipFile/Info and TarFile/Info""" + def __init__(self, filename): + self._filename = filename + if filename.endswith("tar.gz"): + self._obj = tarfile.open(filename, "r:gz") + elif filename.endswith("zip"): + self._obj = ZipFile(filename) + else: + raise ValueError(f"{filename} doesn't seem to be a zip or tar.gz") + + def __iter__(self): + if hasattr(self._obj, "infolist"): + return iter(self._obj.infolist()) + return iter(self._obj) + + def get_name(self, zip_or_tar_info): + if hasattr(zip_or_tar_info, "filename"): + return zip_or_tar_info.filename + return zip_or_tar_info.name + + def get_content(self, zip_or_tar_info): + if hasattr(self._obj, "extractfile"): + content = self._obj.extractfile(zip_or_tar_info) + if content is None: + msg = f"Invalid {zip_or_tar_info.name} in {self._filename}" + raise ValueError(msg) + return str(content.read(), "utf-8") + return str(self._obj.read(zip_or_tar_info), "utf-8") diff --git a/setuptools/tests/integration/test_pip_install_sdist.py b/setuptools/tests/integration/test_pip_install_sdist.py index e838dd0c..23801bc4 100644 --- a/setuptools/tests/integration/test_pip_install_sdist.py +++ b/setuptools/tests/integration/test_pip_install_sdist.py @@ -13,20 +13,20 @@ their build process may require changes in the tests). import json import os import shutil -import subprocess import sys -import tarfile from enum import Enum from glob import glob from hashlib import md5 from urllib.request import urlopen -from zipfile import ZipFile import pytest from packaging.requirements import Requirement import setuptools +from .helpers import Archive, run + + pytestmark = pytest.mark.integration SETUPTOOLS_ROOT = os.path.dirname(next(iter(setuptools.__path__))) @@ -89,7 +89,7 @@ SDIST_OPTIONS = ( @pytest.fixture def venv_python(tmp_path): - run_command([*VIRTUALENV, str(tmp_path / ".venv")]) + run([*VIRTUALENV, str(tmp_path / ".venv")]) possible_path = (str(p.parent) for p in tmp_path.glob(".venv/*/python*")) return shutil.which("python", path=os.pathsep.join(possible_path)) @@ -109,7 +109,7 @@ def _prepare(tmp_path, venv_python, monkeypatch, request): print("Temporary directory:") map(print, tmp_path.glob("*")) print("Virtual environment:") - run_command([venv_python, "-m", "pip", "freeze"]) + run([venv_python, "-m", "pip", "freeze"]) request.addfinalizer(_debug_info) @@ -124,43 +124,23 @@ def test_install_sdist(package, version, tmp_path, venv_python): if deps: print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") print("Dependencies:", deps) - run_command([*venv_pip, "install", *deps]) + run([*venv_pip, "install", *deps]) # Use a virtualenv to simulate PEP 517 isolation # but install setuptools to force the version under development correct_setuptools = os.getenv("PROJECT_ROOT") or SETUPTOOLS_ROOT assert os.path.exists(os.path.join(correct_setuptools, "pyproject.toml")) - run_command([*venv_pip, "install", "-Ie", correct_setuptools]) - run_command([*venv_pip, "install", *SDIST_OPTIONS, sdist]) + run([*venv_pip, "install", "-Ie", correct_setuptools]) + run([*venv_pip, "install", *SDIST_OPTIONS, sdist]) # Execute a simple script to make sure the package was installed correctly script = f"import {package}; print(getattr({package}, '__version__', 0))" - run_command([venv_python, "-c", script]) + run([venv_python, "-c", script]) # ---- Helper Functions ---- -def run_command(cmd, env=None): - r = subprocess.run( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True, - env={**os.environ, **(env or {})} - # ^-- allow overwriting instead of discarding the current env - ) - - out = r.stdout + "\n" + r.stderr - # pytest omits stdout/err by default, if the test fails they help debugging - print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") - print(f"Command: {cmd}\nreturn code: {r.returncode}\n\n{out}") - - if r.returncode == 0: - return out - raise subprocess.CalledProcessError(r.returncode, cmd, r.stdout, r.stderr) - - def retrieve_sdist(package, version, tmp_path): """Either use cached sdist file or download it from PyPI""" # `pip download` cannot be used due to @@ -243,34 +223,3 @@ def _read_pyproject(archive): if os.path.basename(archive.get_name(member)) == "pyproject.toml": return archive.get_content(member) return "" - - -class Archive: - """Compatibility layer for ZipFile/Info and TarFile/Info""" - def __init__(self, filename): - self._filename = filename - if filename.endswith("tar.gz"): - self._obj = tarfile.open(filename, "r:gz") - elif filename.endswith("zip"): - self._obj = ZipFile(filename) - else: - raise ValueError(f"{filename} doesn't seem to be a zip or tar.gz") - - def __iter__(self): - if hasattr(self._obj, "infolist"): - return iter(self._obj.infolist()) - return iter(self._obj) - - def get_name(self, zip_or_tar_info): - if hasattr(zip_or_tar_info, "filename"): - return zip_or_tar_info.filename - return zip_or_tar_info.name - - def get_content(self, zip_or_tar_info): - if hasattr(self._obj, "extractfile"): - content = self._obj.extractfile(zip_or_tar_info) - if content is None: - msg = f"Invalid {zip_or_tar_info.name} in {self._filename}" - raise ValueError(msg) - return str(content.read(), "utf-8") - return str(self._obj.read(zip_or_tar_info), "utf-8") -- cgit v1.2.1 From 6f48cb7604302d50a40d9711d1928ccaaa2f009b Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 16 Nov 2021 11:24:33 +0000 Subject: Use '=' as setup.cfg delimiter for extras option --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 2a5e10b9..0998d8d5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -64,7 +64,7 @@ testing = sphinx jaraco.path>=3.2.0 -testing-integration: +testing-integration = pytest pytest-xdist pytest-enabler -- cgit v1.2.1 From 26b0f460817b1eb40684491daf41cae868a53696 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 21 Dec 2021 19:32:20 +0000 Subject: Change vendoring script to preserve license files As pointed out by #2950, it is probably a good idea to keep the license files for the vendored dependencies. This is done by changing the `pavement.py` tasks. --- pavement.py | 32 +++++ pkg_resources/_vendor/packaging/LICENSE | 3 + pkg_resources/_vendor/packaging/LICENSE.APACHE | 177 +++++++++++++++++++++++++ pkg_resources/_vendor/packaging/LICENSE.BSD | 23 ++++ pkg_resources/_vendor/pyparsing.LICENSE.txt | 18 +++ setuptools/_vendor/more_itertools/LICENSE | 19 +++ setuptools/_vendor/ordered_set.MIT-LICENSE | 19 +++ setuptools/_vendor/packaging/LICENSE | 3 + setuptools/_vendor/packaging/LICENSE.APACHE | 177 +++++++++++++++++++++++++ setuptools/_vendor/packaging/LICENSE.BSD | 23 ++++ setuptools/_vendor/pyparsing.LICENSE.txt | 18 +++ 11 files changed, 512 insertions(+) create mode 100644 pkg_resources/_vendor/packaging/LICENSE create mode 100644 pkg_resources/_vendor/packaging/LICENSE.APACHE create mode 100644 pkg_resources/_vendor/packaging/LICENSE.BSD create mode 100644 pkg_resources/_vendor/pyparsing.LICENSE.txt create mode 100644 setuptools/_vendor/more_itertools/LICENSE create mode 100644 setuptools/_vendor/ordered_set.MIT-LICENSE create mode 100644 setuptools/_vendor/packaging/LICENSE create mode 100644 setuptools/_vendor/packaging/LICENSE.APACHE create mode 100644 setuptools/_vendor/packaging/LICENSE.BSD create mode 100644 setuptools/_vendor/pyparsing.LICENSE.txt diff --git a/pavement.py b/pavement.py index 81ff6f12..1cebad79 100644 --- a/pavement.py +++ b/pavement.py @@ -1,6 +1,8 @@ import re import sys import subprocess +from itertools import chain +from fnmatch import fnmatch from paver.easy import task, path as Path @@ -52,12 +54,42 @@ def install(vendor): '-t', str(vendor), ] subprocess.check_call(install_args) + move_licenses(vendor) remove_all(vendor.glob('*.dist-info')) remove_all(vendor.glob('*.egg-info')) remove_all(vendor.glob('six.py')) (vendor / '__init__.py').write_text('') +def move_licenses(vendor): + license_patterns = ("*LICEN[CS]E*", "COPYING*", "NOTICE*", "AUTHORS*") + licenses = ( + entry + for path in chain(vendor.glob("*.dist-info"), vendor.glob("*.egg-info")) + for entry in path.glob("*") + if any(fnmatch(str(entry), p) for p in license_patterns) + ) + for file in licenses: + file.move(_find_license_dest(file, vendor)) + + +def _find_license_dest(license_file, vendor): + basename = license_file.basename() + pkg = license_file.dirname().replace(".dist-info", "").replace(".egg-info", "") + parts = pkg.split("-") + acc = [] + for part in parts: + acc.append(part) + for option in ("_".join(acc), "-".join(acc), ".".join(acc)): + candidate = Path(option) + if candidate.isdir(): + return candidate / basename + if Path(f"{candidate}.py").isfile(): + return Path(f"{candidate}.{basename}") + + raise FileNotFoundError(f"No destination found for {license_file}") + + def update_pkg_resources(): vendor = Path('pkg_resources/_vendor') install(vendor) diff --git a/pkg_resources/_vendor/packaging/LICENSE b/pkg_resources/_vendor/packaging/LICENSE new file mode 100644 index 00000000..6f62d44e --- /dev/null +++ b/pkg_resources/_vendor/packaging/LICENSE @@ -0,0 +1,3 @@ +This software is made available under the terms of *either* of the licenses +found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made +under the terms of *both* these licenses. diff --git a/pkg_resources/_vendor/packaging/LICENSE.APACHE b/pkg_resources/_vendor/packaging/LICENSE.APACHE new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/pkg_resources/_vendor/packaging/LICENSE.APACHE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/pkg_resources/_vendor/packaging/LICENSE.BSD b/pkg_resources/_vendor/packaging/LICENSE.BSD new file mode 100644 index 00000000..42ce7b75 --- /dev/null +++ b/pkg_resources/_vendor/packaging/LICENSE.BSD @@ -0,0 +1,23 @@ +Copyright (c) Donald Stufft and individual contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pkg_resources/_vendor/pyparsing.LICENSE.txt b/pkg_resources/_vendor/pyparsing.LICENSE.txt new file mode 100644 index 00000000..1bf98523 --- /dev/null +++ b/pkg_resources/_vendor/pyparsing.LICENSE.txt @@ -0,0 +1,18 @@ +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/setuptools/_vendor/more_itertools/LICENSE b/setuptools/_vendor/more_itertools/LICENSE new file mode 100644 index 00000000..0a523bec --- /dev/null +++ b/setuptools/_vendor/more_itertools/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2012 Erik Rose + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/setuptools/_vendor/ordered_set.MIT-LICENSE b/setuptools/_vendor/ordered_set.MIT-LICENSE new file mode 100644 index 00000000..25117ef4 --- /dev/null +++ b/setuptools/_vendor/ordered_set.MIT-LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018 Luminoso Technologies, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/setuptools/_vendor/packaging/LICENSE b/setuptools/_vendor/packaging/LICENSE new file mode 100644 index 00000000..6f62d44e --- /dev/null +++ b/setuptools/_vendor/packaging/LICENSE @@ -0,0 +1,3 @@ +This software is made available under the terms of *either* of the licenses +found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made +under the terms of *both* these licenses. diff --git a/setuptools/_vendor/packaging/LICENSE.APACHE b/setuptools/_vendor/packaging/LICENSE.APACHE new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/setuptools/_vendor/packaging/LICENSE.APACHE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/setuptools/_vendor/packaging/LICENSE.BSD b/setuptools/_vendor/packaging/LICENSE.BSD new file mode 100644 index 00000000..42ce7b75 --- /dev/null +++ b/setuptools/_vendor/packaging/LICENSE.BSD @@ -0,0 +1,23 @@ +Copyright (c) Donald Stufft and individual contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/setuptools/_vendor/pyparsing.LICENSE.txt b/setuptools/_vendor/pyparsing.LICENSE.txt new file mode 100644 index 00000000..1bf98523 --- /dev/null +++ b/setuptools/_vendor/pyparsing.LICENSE.txt @@ -0,0 +1,18 @@ +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -- cgit v1.2.1 From 9441be71d3041f30a503cf551a6ca70d8f9bc7cf Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 21 Dec 2021 19:42:09 +0000 Subject: Add news fragment --- changelog.d/2952.misc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/2952.misc.rst diff --git a/changelog.d/2952.misc.rst b/changelog.d/2952.misc.rst new file mode 100644 index 00000000..ccaf46b7 --- /dev/null +++ b/changelog.d/2952.misc.rst @@ -0,0 +1 @@ +Modified "vendoring" logic to keep license files. -- cgit v1.2.1 From 7479892c117dc739d26f8c957c1b761870d192fa Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 21 Dec 2021 19:52:37 +0000 Subject: Remove license lookup in *.egg-info I believe that only wheels install licenses in the *.dist-info. --- pavement.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pavement.py b/pavement.py index 1cebad79..d588e5ae 100644 --- a/pavement.py +++ b/pavement.py @@ -1,7 +1,6 @@ import re import sys import subprocess -from itertools import chain from fnmatch import fnmatch from paver.easy import task, path as Path @@ -65,7 +64,7 @@ def move_licenses(vendor): license_patterns = ("*LICEN[CS]E*", "COPYING*", "NOTICE*", "AUTHORS*") licenses = ( entry - for path in chain(vendor.glob("*.dist-info"), vendor.glob("*.egg-info")) + for path in vendor.glob("*.dist-info") for entry in path.glob("*") if any(fnmatch(str(entry), p) for p in license_patterns) ) @@ -75,7 +74,7 @@ def move_licenses(vendor): def _find_license_dest(license_file, vendor): basename = license_file.basename() - pkg = license_file.dirname().replace(".dist-info", "").replace(".egg-info", "") + pkg = license_file.dirname().replace(".dist-info", "") parts = pkg.split("-") acc = [] for part in parts: -- cgit v1.2.1 From eba2bcd310caa49b51aa640055e44ef8c74b5581 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 22 Dec 2021 04:13:01 -0500 Subject: Add support for 'platsubdir'. Fixes pypa/distutils#85. --- distutils/command/install.py | 1 + 1 file changed, 1 insertion(+) diff --git a/distutils/command/install.py b/distutils/command/install.py index 40be5ba6..4f944725 100644 --- a/distutils/command/install.py +++ b/distutils/command/install.py @@ -395,6 +395,7 @@ class install(Command): 'platlibdir': getattr(sys, 'platlibdir', 'lib'), 'implementation_lower': _get_implementation().lower(), 'implementation': _get_implementation(), + 'platsubdir': sysconfig.get_config_var('platsubdir'), } if HAS_USER_SITE: -- cgit v1.2.1 From 8c5e5d9b9d31e3a7599000a6c0ec6b4148fc408c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 22 Dec 2021 04:32:09 -0500 Subject: Update changelog. --- changelog.d/2954.misc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/2954.misc.rst diff --git a/changelog.d/2954.misc.rst b/changelog.d/2954.misc.rst new file mode 100644 index 00000000..e46b1f02 --- /dev/null +++ b/changelog.d/2954.misc.rst @@ -0,0 +1 @@ +Merge with pypa/distutils@eba2bcd310. Adds platsubdir to config vars available for substitution. -- cgit v1.2.1 From 52c990172fec37766b3566679724aa8bf70ae06d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 22 Dec 2021 04:41:54 -0500 Subject: =?UTF-8?q?Bump=20version:=2060.0.3=20=E2=86=92=2060.0.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 9 +++++++++ changelog.d/2954.misc.rst | 1 - setup.cfg | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/2954.misc.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index bb1c47a5..ce25a8c9 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 60.0.3 +current_version = 60.0.4 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index 4fab510f..9145ac94 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,12 @@ +v60.0.4 +------- + + +Misc +^^^^ +* #2954: Merge with pypa/distutils@eba2bcd310. Adds platsubdir to config vars available for substitution. + + v60.0.3 ------- diff --git a/changelog.d/2954.misc.rst b/changelog.d/2954.misc.rst deleted file mode 100644 index e46b1f02..00000000 --- a/changelog.d/2954.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Merge with pypa/distutils@eba2bcd310. Adds platsubdir to config vars available for substitution. diff --git a/setup.cfg b/setup.cfg index 77095b64..08eefc49 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 60.0.3 +version = 60.0.4 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From c70575d153f9e11a15b8ef6afd4248c6bda4888d Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 22 Dec 2021 10:07:47 +0000 Subject: Improve path handling on move_license task --- pavement.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pavement.py b/pavement.py index d588e5ae..6d5d519f 100644 --- a/pavement.py +++ b/pavement.py @@ -74,13 +74,14 @@ def move_licenses(vendor): def _find_license_dest(license_file, vendor): basename = license_file.basename() - pkg = license_file.dirname().replace(".dist-info", "") + pkg = license_file.dirname().basename().replace(".dist-info", "") parts = pkg.split("-") acc = [] for part in parts: + # Find actual name from normalized name + version acc.append(part) for option in ("_".join(acc), "-".join(acc), ".".join(acc)): - candidate = Path(option) + candidate = vendor / option if candidate.isdir(): return candidate / basename if Path(f"{candidate}.py").isfile(): -- cgit v1.2.1 From 5ed4fe50f31641937adf1da1a3f578365661b87e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 22 Dec 2021 17:30:22 -0500 Subject: Fix AttributeError when sysconfig does not supply platsubdir --- distutils/command/install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/command/install.py b/distutils/command/install.py index 4f944725..fceb36aa 100644 --- a/distutils/command/install.py +++ b/distutils/command/install.py @@ -395,7 +395,7 @@ class install(Command): 'platlibdir': getattr(sys, 'platlibdir', 'lib'), 'implementation_lower': _get_implementation().lower(), 'implementation': _get_implementation(), - 'platsubdir': sysconfig.get_config_var('platsubdir'), + 'platsubdir': sysconfig.get_config_var('platsubdir') or '', } if HAS_USER_SITE: -- cgit v1.2.1 From e3f53f93af07a0e04bfa7b853b3ba2795b349c90 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 23 Dec 2021 11:06:17 -0500 Subject: When headers are missing in a preferred scheme, use the default scheme as a fallback to resolve headers. Workaround for and fixes #88. --- distutils/command/install.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/distutils/command/install.py b/distutils/command/install.py index fceb36aa..380564b0 100644 --- a/distutils/command/install.py +++ b/distutils/command/install.py @@ -81,11 +81,6 @@ if HAS_USER_SITE: 'data' : '{userbase}', } - INSTALL_SCHEMES['osx_framework_user'] = { - 'headers': - '{userbase}/include/{implementation_lower}{py_version_short}{abiflags}/{dist_name}', - } - # The keys to an installation scheme; if any new types of files are to be # installed, be sure to add an entry to every installation scheme above, # and to SCHEME_KEYS here. @@ -124,7 +119,8 @@ def _get_implementation(): def _select_scheme(ob, name): - vars(ob).update(_remove_set(ob, _scheme_attrs(_resolve_scheme(name)))) + scheme = _inject_headers(name, _load_scheme(_resolve_scheme(name))) + vars(ob).update(_remove_set(ob, _scheme_attrs(scheme))) def _remove_set(ob, attrs): @@ -147,9 +143,26 @@ def _resolve_scheme(name): return resolved -def _scheme_attrs(name): +def _load_scheme(name): + return _load_schemes()[name] + + +def _inject_headers(name, scheme): + """ + Given a scheme name and the resolved scheme, + if the scheme does not include headers, resolve + the fallback scheme for the name and use headers + from it. pypa/distutils#88 + """ + # Bypass the preferred scheme, which may not + # have defined headers. + fallback = _load_scheme(_pypy_hack(name)) + scheme.setdefault('headers', fallback['headers']) + return scheme + + +def _scheme_attrs(scheme): """Resolve install directories by applying the install schemes.""" - scheme = _load_schemes()[name] return { f'install_{key}': scheme[key] for key in SCHEME_KEYS -- cgit v1.2.1 From 9867e2e42ad5468793b2e7c3bf69d99d95bcc5e1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 23 Dec 2021 11:32:40 -0500 Subject: Cast value to str and provide a comment explaining why. Ref pypa/distutils#87. --- distutils/command/install.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/distutils/command/install.py b/distutils/command/install.py index 380564b0..65844927 100644 --- a/distutils/command/install.py +++ b/distutils/command/install.py @@ -408,7 +408,8 @@ class install(Command): 'platlibdir': getattr(sys, 'platlibdir', 'lib'), 'implementation_lower': _get_implementation().lower(), 'implementation': _get_implementation(), - 'platsubdir': sysconfig.get_config_var('platsubdir') or '', + # all values must be str; see #86 + 'platsubdir': str(sysconfig.get_config_var('platsubdir')), } if HAS_USER_SITE: -- cgit v1.2.1 From 9c967af0f795c5b9aa5affc4f01adaeeb519695d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 23 Dec 2021 11:09:33 -0500 Subject: Update changelog --- changelog.d/2960.misc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/2960.misc.rst diff --git a/changelog.d/2960.misc.rst b/changelog.d/2960.misc.rst new file mode 100644 index 00000000..39d4656a --- /dev/null +++ b/changelog.d/2960.misc.rst @@ -0,0 +1 @@ +Install schemes fall back to default scheme for headers. -- cgit v1.2.1 From 56d267c6056185d930c3d46762210081fb71602b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 23 Dec 2021 12:07:42 -0500 Subject: Move 'str' cast into the one place where it's needed and allow config_vars to have None values. --- distutils/command/install.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/distutils/command/install.py b/distutils/command/install.py index 65844927..cdcc0528 100644 --- a/distutils/command/install.py +++ b/distutils/command/install.py @@ -408,8 +408,7 @@ class install(Command): 'platlibdir': getattr(sys, 'platlibdir', 'lib'), 'implementation_lower': _get_implementation().lower(), 'implementation': _get_implementation(), - # all values must be str; see #86 - 'platsubdir': str(sysconfig.get_config_var('platsubdir')), + 'platsubdir': sysconfig.get_config_var('platsubdir'), } if HAS_USER_SITE: @@ -650,7 +649,7 @@ class install(Command): return home = convert_path(os.path.expanduser("~")) for name, path in self.config_vars.items(): - if path.startswith(home) and not os.path.isdir(path): + if str(path).startswith(home) and not os.path.isdir(path): self.debug_print("os.makedirs('%s', 0o700)" % path) os.makedirs(path, 0o700) -- cgit v1.2.1 From 28f1d4751aedb1c36bec85554606eb12e4ab044d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 23 Dec 2021 12:16:27 -0500 Subject: =?UTF-8?q?Bump=20version:=2060.0.4=20=E2=86=92=2060.0.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 9 +++++++++ changelog.d/2960.misc.rst | 1 - setup.cfg | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/2960.misc.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index ce25a8c9..65eec0b0 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 60.0.4 +current_version = 60.0.5 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index 9145ac94..998dda4e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,12 @@ +v60.0.5 +------- + + +Misc +^^^^ +* #2960: Install schemes fall back to default scheme for headers. + + v60.0.4 ------- diff --git a/changelog.d/2960.misc.rst b/changelog.d/2960.misc.rst deleted file mode 100644 index 39d4656a..00000000 --- a/changelog.d/2960.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Install schemes fall back to default scheme for headers. diff --git a/setup.cfg b/setup.cfg index 08eefc49..f1a6fc52 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 60.0.4 +version = 60.0.5 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From 55e2134a4ac896210d93ba7079cb70ace43f4f3b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 23 Dec 2021 13:50:36 -0500 Subject: In distutils_hack, only add the metadata finder once. In ensure_local_distutils, rely on a context manager for reliable manipulation. Fixes #2958. --- _distutils_hack/__init__.py | 19 ++++++++++++++++--- changelog.d/2958.change.rst | 1 + setup.py | 2 +- 3 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 changelog.d/2958.change.rst diff --git a/_distutils_hack/__init__.py b/_distutils_hack/__init__.py index 22bc9ed6..85a51370 100644 --- a/_distutils_hack/__init__.py +++ b/_distutils_hack/__init__.py @@ -3,6 +3,7 @@ import os import re import importlib import warnings +import contextlib is_pypy = '__pypy__' in sys.builtin_module_names @@ -52,9 +53,8 @@ def ensure_local_distutils(): # With the DistutilsMetaFinder in place, # perform an import to cause distutils to be # loaded from setuptools._distutils. Ref #2906. - add_shim() - importlib.import_module('distutils') - remove_shim() + with shim(): + importlib.import_module('distutils') # check that submodules load as expected core = importlib.import_module('distutils.core') @@ -129,6 +129,19 @@ class DistutilsMetaFinder: DISTUTILS_FINDER = DistutilsMetaFinder() +def ensure_shim(): + DISTUTILS_FINDER in sys.meta_path or add_shim() + + +@contextlib.contextmanager +def shim(): + add_shim() + try: + yield + finally: + remove_shim() + + def add_shim(): sys.meta_path.insert(0, DISTUTILS_FINDER) diff --git a/changelog.d/2958.change.rst b/changelog.d/2958.change.rst new file mode 100644 index 00000000..9a3910e1 --- /dev/null +++ b/changelog.d/2958.change.rst @@ -0,0 +1 @@ +In distutils_hack, only add the metadata finder once. In ensure_local_distutils, rely on a context manager for reliable manipulation. diff --git a/setup.py b/setup.py index 4cda3d38..d15189db 100755 --- a/setup.py +++ b/setup.py @@ -55,7 +55,7 @@ class install_with_pth(install): import os var = 'SETUPTOOLS_USE_DISTUTILS' enabled = os.environ.get(var, 'local') == 'local' - enabled and __import__('_distutils_hack').add_shim() + enabled and __import__('_distutils_hack').ensure_shim() """).lstrip().replace('\n', '; ') def initialize_options(self): -- cgit v1.2.1 From 15340a3f1ec40e4d7267518d04487d7b4108ee83 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Thu, 23 Dec 2021 11:45:22 -0800 Subject: distutils shim should ignore setuptools on another path --- _distutils_hack/__init__.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/_distutils_hack/__init__.py b/_distutils_hack/__init__.py index 22bc9ed6..a5c8e1cf 100644 --- a/_distutils_hack/__init__.py +++ b/_distutils_hack/__init__.py @@ -58,7 +58,9 @@ def ensure_local_distutils(): # check that submodules load as expected core = importlib.import_module('distutils.core') - assert '_distutils' in core.__file__, core.__file__ + # FIXME: this assertion blows up if the MetaFinder below has no-opped on a + # setuptools from another path + # assert '_distutils' in core.__file__, core.__file__ def do_override(): @@ -85,6 +87,15 @@ class DistutilsMetaFinder: def spec_for_distutils(self): import importlib.abc import importlib.util + from pathlib import Path + + st_mod = importlib.import_module('setuptools') + + # prevent this import redirection shim from interacting with a possibly + # incompatible setuptools in another location on sys.path + # see pypa/setuptools#2957 + if not Path(__file__).parents[1].samefile(Path(st_mod.__file__).parents[1]): + return None class DistutilsLoader(importlib.abc.Loader): -- cgit v1.2.1 From 4e552491a216eeb56c70622264ab6dae1a30c4e7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 23 Dec 2021 14:47:04 -0500 Subject: Extend tests to capture expectation of only one DistutilsMetaFinder at a time. --- setuptools/tests/test_distutils_adoption.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/setuptools/tests/test_distutils_adoption.py b/setuptools/tests/test_distutils_adoption.py index b6b9c00e..27759b1d 100644 --- a/setuptools/tests/test_distutils_adoption.py +++ b/setuptools/tests/test_distutils_adoption.py @@ -3,6 +3,7 @@ import sys import functools import subprocess import platform +import textwrap import pytest import jaraco.envs @@ -49,12 +50,24 @@ def find_distutils(venv, imports='distutils', env=None, **kwargs): return popen_text(venv.run)(cmd, env=env, **kwargs) +def count_meta_path(venv, env=None): + py_cmd = textwrap.dedent( + """ + import sys + is_distutils = lambda finder: finder.__class__.__name__ == "DistutilsMetaFinder" + print(len(list(filter(is_distutils, sys.meta_path)))) + """) + cmd = ['python', '-c', py_cmd] + return int(popen_text(venv.run)(cmd, env=env)) + + def test_distutils_stdlib(venv): """ Ensure stdlib distutils is used when appropriate. """ env = dict(SETUPTOOLS_USE_DISTUTILS='stdlib') assert venv.name not in find_distutils(venv, env=env).split(os.sep) + assert count_meta_path(venv, env=env) == 0 def test_distutils_local_with_setuptools(venv): @@ -64,6 +77,7 @@ def test_distutils_local_with_setuptools(venv): env = dict(SETUPTOOLS_USE_DISTUTILS='local') loc = find_distutils(venv, imports='setuptools, distutils', env=env) assert venv.name in loc.split(os.sep) + assert count_meta_path(venv, env=env) <= 1 @pytest.mark.xfail('IS_PYPY', reason='pypy imports distutils on startup') @@ -74,3 +88,4 @@ def test_distutils_local(venv): """ env = dict(SETUPTOOLS_USE_DISTUTILS='local') assert venv.name in find_distutils(venv, env=env).split(os.sep) + assert count_meta_path(venv, env=env) <= 1 -- cgit v1.2.1 From 43f1475131a929536b08e4095c67630cace6ca2d Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Fri, 24 Dec 2021 04:08:37 +0530 Subject: Use mock.patch.dict to patch os.environ --- distutils/_msvccompiler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/_msvccompiler.py b/distutils/_msvccompiler.py index b7a06082..c41ea9ae 100644 --- a/distutils/_msvccompiler.py +++ b/distutils/_msvccompiler.py @@ -527,7 +527,7 @@ class MSVCCompiler(CCompiler) : return warnings.warn( "Fallback spawn triggered. Please update distutils monkeypatch.") - with unittest.mock.patch('os.environ', env): + with unittest.mock.patch.dict('os.environ', env): bag.value = super().spawn(cmd) # -- Miscellaneous methods ----------------------------------------- -- cgit v1.2.1 From 494d686474d90a9acc9fb0a3bffa54fd0202cf50 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 23 Dec 2021 19:29:34 -0500 Subject: Update changelog. --- changelog.d/2963.change.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/2963.change.rst diff --git a/changelog.d/2963.change.rst b/changelog.d/2963.change.rst new file mode 100644 index 00000000..d4e2a112 --- /dev/null +++ b/changelog.d/2963.change.rst @@ -0,0 +1 @@ +Merge with pypa/distutils@a5af364910. Includes revisited fix for pypa/distutils#15 and improved MinGW/Cygwin support from pypa/distutils#77. -- cgit v1.2.1 From 247de8a2fbb2d1c34369ee83588100dbbe5f82a2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 23 Dec 2021 19:30:24 -0500 Subject: =?UTF-8?q?Bump=20version:=2060.0.5=20=E2=86=92=2060.1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 10 ++++++++++ changelog.d/2958.change.rst | 1 - changelog.d/2963.change.rst | 1 - setup.cfg | 2 +- 5 files changed, 12 insertions(+), 4 deletions(-) delete mode 100644 changelog.d/2958.change.rst delete mode 100644 changelog.d/2963.change.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 65eec0b0..d5a104b0 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 60.0.5 +current_version = 60.1.0 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index 998dda4e..09692544 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,13 @@ +v60.1.0 +------- + + +Changes +^^^^^^^ +* #2958: In distutils_hack, only add the metadata finder once. In ensure_local_distutils, rely on a context manager for reliable manipulation. +* #2963: Merge with pypa/distutils@a5af364910. Includes revisited fix for pypa/distutils#15 and improved MinGW/Cygwin support from pypa/distutils#77. + + v60.0.5 ------- diff --git a/changelog.d/2958.change.rst b/changelog.d/2958.change.rst deleted file mode 100644 index 9a3910e1..00000000 --- a/changelog.d/2958.change.rst +++ /dev/null @@ -1 +0,0 @@ -In distutils_hack, only add the metadata finder once. In ensure_local_distutils, rely on a context manager for reliable manipulation. diff --git a/changelog.d/2963.change.rst b/changelog.d/2963.change.rst deleted file mode 100644 index d4e2a112..00000000 --- a/changelog.d/2963.change.rst +++ /dev/null @@ -1 +0,0 @@ -Merge with pypa/distutils@a5af364910. Includes revisited fix for pypa/distutils#15 and improved MinGW/Cygwin support from pypa/distutils#77. diff --git a/setup.cfg b/setup.cfg index f1a6fc52..3daca1df 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 60.0.5 +version = 60.1.0 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From 4834907218be6f104cac123caaf87c5d341e1683 Mon Sep 17 00:00:00 2001 From: eacheson Date: Fri, 24 Dec 2021 12:03:44 +0100 Subject: Update quickstart.rst fix various english issues --- docs/userguide/quickstart.rst | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst index 98e34c19..da904bab 100644 --- a/docs/userguide/quickstart.rst +++ b/docs/userguide/quickstart.rst @@ -14,9 +14,9 @@ Python packaging at a glance ============================ The landscape of Python packaging is shifting and ``Setuptools`` has evolved to only provide backend support, no longer being the de-facto packaging tool in -the market. All python package must provide a ``pyproject.toml`` and specify +the market. Every python package must provide a ``pyproject.toml`` and specify the backend (build system) it wants to use. The distribution can then -be generated with whatever tools that provides a ``build sdist``-alike +be generated with whatever tool that provides a ``build sdist``-like functionality. While this may appear cumbersome, given the added pieces, it in fact tremendously enhances the portability of your package. The change is driven under :pep:`PEP 517 <517#build-requirements>`. To learn more about Python packaging in general, @@ -76,7 +76,7 @@ This is what your project would look like:: setup.cfg # or setup.py mypackage/__init__.py -Then, you need an builder, such as :std:doc:`PyPA build ` +Then, you need a builder, such as :std:doc:`PyPA build ` which you can obtain via ``pip install build``. After downloading it, invoke the builder:: @@ -89,15 +89,15 @@ Of course, before you release your project to PyPI, you'll want to add a bit more information to your setup script to help people find or learn about your project. And maybe your project will have grown by then to include a few dependencies, and perhaps some data files and scripts. In the next few sections, -we will walk through those additional but essential information you need +we will walk through the additional but essential information you need to specify to properly package your project. 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. ``setuptools`` +the ``packages`` keyword in ``setup.cfg``. However, for very large projects, +it can be a big burden to keep the package list updated. ``setuptools`` therefore provides two convenient tools to ease the burden: :literal:`find:\ ` and :literal:`find_namespace:\ `. To use it in your project: @@ -110,11 +110,11 @@ therefore provides two convenient tools to ease the burden: :literal:`find:\ ` a include=pkg1, pkg2 exclude=pk3, pk4 -When you pass the above information, alongside other necessary ones, +When you pass the above information, alongside other necessary information, ``setuptools`` walks through the directory specified in ``where`` (omitted -here as the package reside in current directory) and filters the packages -it can find following the ``include`` (default to none), then remove -those that match the ``exclude`` and return a list of Python packages. Note +here as the package resides in the current directory) and filters the packages +it can find following the ``include`` (defaults to none), then removes +those that match the ``exclude`` and returns a list of Python packages. Note that each entry in the ``[options.packages.find]`` is optional. The above setup also allows you to adopt a ``src/`` layout. For more details and advanced use, go to :ref:`package_discovery` @@ -122,7 +122,7 @@ use, go to :ref:`package_discovery` Entry points and automatic script creation =========================================== -Setuptools support automatic creation of scripts upon installation, that runs +Setuptools supports automatic creation of scripts upon installation, that runs code within your package if you specify them with the ``entry_points`` keyword. This is what allows you to run commands like ``pip install`` instead of having to type ``python -m pip install``. To accomplish this, add the entry_points @@ -157,7 +157,7 @@ operators <, >, <=, >=, == or !=, followed by a version identifier): When your project is installed, all of the dependencies not already installed will be located (via PyPI), downloaded, built (if necessary), and installed. -This, of course, is a simplified scenarios. ``setuptools`` also provide +This, of course, is a simplified scenarios. ``setuptools`` also provides additional keywords such as ``setup_requires`` that allows you to install dependencies before running the script, and ``extras_require`` that take care of those needed by automatically generated scripts. It also provides @@ -207,7 +207,7 @@ associate with your source code. For more information, see :doc:`development_mod Uploading your package to PyPI ============================== -After generating the distribution files, next step would be to upload your +After generating the distribution files, the next step would be to upload your distribution so others can use it. This functionality is provided by `twine `_ and we will only demonstrate the basic use here. -- cgit v1.2.1 From 676e669a317d6a4804f8f8bf438fd1e31afbd306 Mon Sep 17 00:00:00 2001 From: Christoph Reiter Date: Sun, 26 Dec 2021 16:38:52 +0100 Subject: sysconfig: use get_config_h_filename() from the stdlib distutils.sysconfig provides various functions already present in the stdlib sysconfig module. Reusing the stdlib versions has the advantage that it is in control of the Python distributor/packager and distutils doesn't have to deal with platform details. This tries to start the transition using the simple get_config_h_filename() for starters by forwarding calls to the stdlib in case Python is considered to be installed. --- distutils/sysconfig.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py index d36d94f7..2300014d 100644 --- a/distutils/sysconfig.py +++ b/distutils/sysconfig.py @@ -13,6 +13,7 @@ import _imp import os import re import sys +import sysconfig from .errors import DistutilsPlatformError @@ -274,10 +275,10 @@ def get_config_h_filename(): inc_dir = os.path.join(_sys_home or project_base, "PC") else: inc_dir = _sys_home or project_base + return os.path.join(inc_dir, 'pyconfig.h') else: - inc_dir = get_python_inc(plat_specific=1) + return sysconfig.get_config_h_filename() - return os.path.join(inc_dir, 'pyconfig.h') # Allow this value to be patched by pkgsrc. Ref pypa/distutils#16. -- cgit v1.2.1 From 35f79dacc98eaf0e0715ef329a2d5d6bc1a59e64 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 26 Dec 2021 13:17:33 -0500 Subject: Add discord badge --- README.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.rst b/README.rst index fab41118..661edfb0 100644 --- a/README.rst +++ b/README.rst @@ -31,6 +31,10 @@ .. image:: https://tidelift.com/badges/github/pypa/setuptools?style=flat :target: https://tidelift.com/subscription/pkg/pypi-setuptools?utm_source=pypi-setuptools&utm_medium=readme +.. image:: https://img.shields.io/discord/803025117553754132 + :target: https://discord.com/channels/803025117553754132 + :alt: Discord + See the `Installation Instructions `_ in the Python Packaging User's Guide for instructions on installing, upgrading, and uninstalling -- cgit v1.2.1 From dfa0b76e0070a8e5314343f672298fcb80401691 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 26 Dec 2021 13:27:35 -0500 Subject: Include channel in the URL. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 661edfb0..7ea2b70e 100644 --- a/README.rst +++ b/README.rst @@ -32,7 +32,7 @@ :target: https://tidelift.com/subscription/pkg/pypi-setuptools?utm_source=pypi-setuptools&utm_medium=readme .. image:: https://img.shields.io/discord/803025117553754132 - :target: https://discord.com/channels/803025117553754132 + :target: https://discord.com/channels/803025117553754132/815945031150993468 :alt: Discord See the `Installation Instructions -- cgit v1.2.1 From b17dad6615d9b2f4fe255ec6750b2765d9b281f1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 26 Dec 2021 14:47:29 -0500 Subject: =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins=20(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- distutils/log.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/distutils/log.py b/distutils/log.py index 8ef6b28e..a68b156b 100644 --- a/distutils/log.py +++ b/distutils/log.py @@ -3,13 +3,14 @@ # The class here is styled after PEP 282 so that it could later be # replaced with a standard Python logging implementation. +import sys + DEBUG = 1 INFO = 2 WARN = 3 ERROR = 4 FATAL = 5 -import sys class Log: @@ -54,6 +55,7 @@ class Log: def fatal(self, msg, *args): self._log(FATAL, msg, args) + _global_log = Log() log = _global_log.log debug = _global_log.debug @@ -62,12 +64,14 @@ warn = _global_log.warn error = _global_log.error fatal = _global_log.fatal + def set_threshold(level): # return the old threshold for use from tests old = _global_log.threshold _global_log.threshold = level return old + def set_verbosity(v): if v <= 0: set_threshold(WARN) -- cgit v1.2.1 From dd118f755b88a4d8a193790a711c0003415f8dc0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 26 Dec 2021 15:42:52 -0500 Subject: Remove skipif for Python 3.6, no longer supported. --- setuptools/tests/test_virtualenv.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setuptools/tests/test_virtualenv.py b/setuptools/tests/test_virtualenv.py index 00f5f185..069076b2 100644 --- a/setuptools/tests/test_virtualenv.py +++ b/setuptools/tests/test_virtualenv.py @@ -82,7 +82,6 @@ def _get_pip_versions(): 'pip<22', mark( 'https://github.com/pypa/pip/archive/main.zip', - pytest.mark.skipif('sys.version_info < (3, 7)'), ), ] -- cgit v1.2.1 From dcdeedbed77711549f41e1f46f186c94ef9c4657 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 26 Dec 2021 15:43:28 -0500 Subject: Mark test as xfail. Fixes #2975. --- setuptools/tests/test_virtualenv.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setuptools/tests/test_virtualenv.py b/setuptools/tests/test_virtualenv.py index 069076b2..61d239aa 100644 --- a/setuptools/tests/test_virtualenv.py +++ b/setuptools/tests/test_virtualenv.py @@ -82,6 +82,7 @@ def _get_pip_versions(): 'pip<22', mark( 'https://github.com/pypa/pip/archive/main.zip', + pytest.mark.xfail(reason='#2975'), ), ] -- cgit v1.2.1 From 9d0b8cda4075e1f654b8e633957f76b921c608cd Mon Sep 17 00:00:00 2001 From: Christoph Reiter Date: Sun, 26 Dec 2021 19:40:43 +0100 Subject: sysconfig: use parse_config_h() from stdlib sysconfig The only difference is the argument name change --- distutils/sysconfig.py | 22 ++-------------------- distutils/tests/test_sysconfig.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py index d36d94f7..94993cc5 100644 --- a/distutils/sysconfig.py +++ b/distutils/sysconfig.py @@ -13,6 +13,7 @@ import _imp import os import re import sys +import sysconfig from .errors import DistutilsPlatformError @@ -308,26 +309,7 @@ def parse_config_h(fp, g=None): optional dictionary is passed in as the second argument, it is used instead of a new dictionary. """ - if g is None: - g = {} - define_rx = re.compile("#define ([A-Z][A-Za-z0-9_]+) (.*)\n") - undef_rx = re.compile("/[*] #undef ([A-Z][A-Za-z0-9_]+) [*]/\n") - # - while True: - line = fp.readline() - if not line: - break - m = define_rx.match(line) - if m: - n, v = m.group(1, 2) - try: v = int(v) - except ValueError: pass - g[n] = v - else: - m = undef_rx.match(line) - if m: - g[m.group(1)] = 0 - return g + return sysconfig.parse_config_h(fp, vars=g) # Regexes needed for parsing Makefile (and similar syntaxes, diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py index 80cd1599..d28f4712 100644 --- a/distutils/tests/test_sysconfig.py +++ b/distutils/tests/test_sysconfig.py @@ -283,6 +283,16 @@ class SysconfigTestCase(support.EnvironGuard, unittest.TestCase): outs, errs = p.communicate() self.assertEqual(0, p.returncode, "Subprocess failed: " + outs) + def test_parse_config_h(self): + config_h = sysconfig.get_config_h_filename() + input = {} + with open(config_h, encoding="utf-8") as f: + result = sysconfig.parse_config_h(f, g=input) + self.assertTrue(input) + self.assertTrue(input is result) + with open(config_h, encoding="utf-8") as f: + result = sysconfig.parse_config_h(f) + self.assertTrue(result) def test_suite(): suite = unittest.TestSuite() -- cgit v1.2.1 From 9c4d48c1312192af1e42d9baf5d3bad7e4fe7bae Mon Sep 17 00:00:00 2001 From: Christoph Reiter Date: Mon, 27 Dec 2021 09:48:30 +0100 Subject: tests: use loadTestsFromTestCase() instead of the deprecated makeSuite() See https://github.com/python/cpython/pull/28299 loadTestsFromTestCase() is available since Python 2.7 at least. --- distutils/tests/test_archive_util.py | 2 +- distutils/tests/test_bdist.py | 2 +- distutils/tests/test_bdist_dumb.py | 2 +- distutils/tests/test_bdist_msi.py | 2 +- distutils/tests/test_bdist_rpm.py | 2 +- distutils/tests/test_bdist_wininst.py | 2 +- distutils/tests/test_build.py | 2 +- distutils/tests/test_build_clib.py | 2 +- distutils/tests/test_build_ext.py | 4 ++-- distutils/tests/test_build_py.py | 2 +- distutils/tests/test_build_scripts.py | 2 +- distutils/tests/test_check.py | 2 +- distutils/tests/test_clean.py | 2 +- distutils/tests/test_cmd.py | 2 +- distutils/tests/test_config.py | 2 +- distutils/tests/test_config_cmd.py | 2 +- distutils/tests/test_core.py | 2 +- distutils/tests/test_cygwinccompiler.py | 2 +- distutils/tests/test_dep_util.py | 2 +- distutils/tests/test_dir_util.py | 2 +- distutils/tests/test_dist.py | 4 ++-- distutils/tests/test_extension.py | 2 +- distutils/tests/test_file_util.py | 2 +- distutils/tests/test_filelist.py | 4 ++-- distutils/tests/test_install.py | 2 +- distutils/tests/test_install_data.py | 2 +- distutils/tests/test_install_headers.py | 2 +- distutils/tests/test_install_lib.py | 2 +- distutils/tests/test_install_scripts.py | 2 +- distutils/tests/test_log.py | 2 +- distutils/tests/test_msvc9compiler.py | 2 +- distutils/tests/test_msvccompiler.py | 2 +- distutils/tests/test_register.py | 2 +- distutils/tests/test_sdist.py | 2 +- distutils/tests/test_spawn.py | 2 +- distutils/tests/test_sysconfig.py | 2 +- distutils/tests/test_text_file.py | 2 +- distutils/tests/test_unixccompiler.py | 2 +- distutils/tests/test_upload.py | 2 +- distutils/tests/test_util.py | 2 +- distutils/tests/test_version.py | 2 +- 41 files changed, 44 insertions(+), 44 deletions(-) diff --git a/distutils/tests/test_archive_util.py b/distutils/tests/test_archive_util.py index c5560372..800b9018 100644 --- a/distutils/tests/test_archive_util.py +++ b/distutils/tests/test_archive_util.py @@ -387,7 +387,7 @@ class ArchiveUtilTestCase(support.TempdirManager, archive.close() def test_suite(): - return unittest.makeSuite(ArchiveUtilTestCase) + return unittest.TestLoader().loadTestsFromTestCase(ArchiveUtilTestCase) if __name__ == "__main__": run_unittest(test_suite()) diff --git a/distutils/tests/test_bdist.py b/distutils/tests/test_bdist.py index 130d8bf1..8b7498e3 100644 --- a/distutils/tests/test_bdist.py +++ b/distutils/tests/test_bdist.py @@ -51,7 +51,7 @@ class BuildTestCase(support.TempdirManager, def test_suite(): - return unittest.makeSuite(BuildTestCase) + return unittest.TestLoader().loadTestsFromTestCase(BuildTestCase) if __name__ == '__main__': run_unittest(test_suite()) diff --git a/distutils/tests/test_bdist_dumb.py b/distutils/tests/test_bdist_dumb.py index 01a233bc..bb860c8a 100644 --- a/distutils/tests/test_bdist_dumb.py +++ b/distutils/tests/test_bdist_dumb.py @@ -91,7 +91,7 @@ class BuildDumbTestCase(support.TempdirManager, self.assertEqual(contents, sorted(wanted)) def test_suite(): - return unittest.makeSuite(BuildDumbTestCase) + return unittest.TestLoader().loadTestsFromTestCase(BuildDumbTestCase) if __name__ == '__main__': run_unittest(test_suite()) diff --git a/distutils/tests/test_bdist_msi.py b/distutils/tests/test_bdist_msi.py index 937266f8..b1831ef2 100644 --- a/distutils/tests/test_bdist_msi.py +++ b/distutils/tests/test_bdist_msi.py @@ -22,7 +22,7 @@ class BDistMSITestCase(support.TempdirManager, def test_suite(): - return unittest.makeSuite(BDistMSITestCase) + return unittest.TestLoader().loadTestsFromTestCase(BDistMSITestCase) if __name__ == '__main__': run_unittest(test_suite()) diff --git a/distutils/tests/test_bdist_rpm.py b/distutils/tests/test_bdist_rpm.py index 6453a02b..3b22af3a 100644 --- a/distutils/tests/test_bdist_rpm.py +++ b/distutils/tests/test_bdist_rpm.py @@ -129,7 +129,7 @@ class BuildRpmTestCase(support.TempdirManager, os.remove(os.path.join(pkg_dir, 'dist', 'foo-0.1-1.noarch.rpm')) def test_suite(): - return unittest.makeSuite(BuildRpmTestCase) + return unittest.TestLoader().loadTestsFromTestCase(BuildRpmTestCase) if __name__ == '__main__': run_unittest(test_suite()) diff --git a/distutils/tests/test_bdist_wininst.py b/distutils/tests/test_bdist_wininst.py index 31cf2628..59f25167 100644 --- a/distutils/tests/test_bdist_wininst.py +++ b/distutils/tests/test_bdist_wininst.py @@ -34,7 +34,7 @@ class BuildWinInstTestCase(support.TempdirManager, self.assertGreater(len(exe_file), 10) def test_suite(): - return unittest.makeSuite(BuildWinInstTestCase) + return unittest.TestLoader().loadTestsFromTestCase(BuildWinInstTestCase) if __name__ == '__main__': run_unittest(test_suite()) diff --git a/distutils/tests/test_build.py b/distutils/tests/test_build.py index b020a5ba..83a9e4f4 100644 --- a/distutils/tests/test_build.py +++ b/distutils/tests/test_build.py @@ -50,7 +50,7 @@ class BuildTestCase(support.TempdirManager, self.assertEqual(cmd.executable, os.path.normpath(sys.executable)) def test_suite(): - return unittest.makeSuite(BuildTestCase) + return unittest.TestLoader().loadTestsFromTestCase(BuildTestCase) if __name__ == "__main__": run_unittest(test_suite()) diff --git a/distutils/tests/test_build_clib.py b/distutils/tests/test_build_clib.py index 259c4352..d50ead7c 100644 --- a/distutils/tests/test_build_clib.py +++ b/distutils/tests/test_build_clib.py @@ -130,7 +130,7 @@ class BuildCLibTestCase(support.TempdirManager, self.assertIn('libfoo.a', os.listdir(build_temp)) def test_suite(): - return unittest.makeSuite(BuildCLibTestCase) + return unittest.TestLoader().loadTestsFromTestCase(BuildCLibTestCase) if __name__ == "__main__": run_unittest(test_suite()) diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index 85ecf4b7..cb0db2b5 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -538,8 +538,8 @@ class ParallelBuildExtTestCase(BuildExtTestCase): def test_suite(): suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(BuildExtTestCase)) - suite.addTest(unittest.makeSuite(ParallelBuildExtTestCase)) + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(BuildExtTestCase)) + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(ParallelBuildExtTestCase)) return suite if __name__ == '__main__': diff --git a/distutils/tests/test_build_py.py b/distutils/tests/test_build_py.py index 0712e92c..a590a485 100644 --- a/distutils/tests/test_build_py.py +++ b/distutils/tests/test_build_py.py @@ -173,7 +173,7 @@ class BuildPyTestCase(support.TempdirManager, def test_suite(): - return unittest.makeSuite(BuildPyTestCase) + return unittest.TestLoader().loadTestsFromTestCase(BuildPyTestCase) if __name__ == "__main__": run_unittest(test_suite()) diff --git a/distutils/tests/test_build_scripts.py b/distutils/tests/test_build_scripts.py index 954fc763..f299e51e 100644 --- a/distutils/tests/test_build_scripts.py +++ b/distutils/tests/test_build_scripts.py @@ -106,7 +106,7 @@ class BuildScriptsTestCase(support.TempdirManager, self.assertIn(name, built) def test_suite(): - return unittest.makeSuite(BuildScriptsTestCase) + return unittest.TestLoader().loadTestsFromTestCase(BuildScriptsTestCase) if __name__ == "__main__": run_unittest(test_suite()) diff --git a/distutils/tests/test_check.py b/distutils/tests/test_check.py index e534aca1..91bcdceb 100644 --- a/distutils/tests/test_check.py +++ b/distutils/tests/test_check.py @@ -157,7 +157,7 @@ class CheckTestCase(support.LoggingSilencer, 'restructuredtext': 1}) def test_suite(): - return unittest.makeSuite(CheckTestCase) + return unittest.TestLoader().loadTestsFromTestCase(CheckTestCase) if __name__ == "__main__": run_unittest(test_suite()) diff --git a/distutils/tests/test_clean.py b/distutils/tests/test_clean.py index c605afd8..92367499 100644 --- a/distutils/tests/test_clean.py +++ b/distutils/tests/test_clean.py @@ -43,7 +43,7 @@ class cleanTestCase(support.TempdirManager, cmd.run() def test_suite(): - return unittest.makeSuite(cleanTestCase) + return unittest.TestLoader().loadTestsFromTestCase(cleanTestCase) if __name__ == "__main__": run_unittest(test_suite()) diff --git a/distutils/tests/test_cmd.py b/distutils/tests/test_cmd.py index cf5197c3..2319214a 100644 --- a/distutils/tests/test_cmd.py +++ b/distutils/tests/test_cmd.py @@ -120,7 +120,7 @@ class CommandTestCase(unittest.TestCase): debug.DEBUG = False def test_suite(): - return unittest.makeSuite(CommandTestCase) + return unittest.TestLoader().loadTestsFromTestCase(CommandTestCase) if __name__ == '__main__': run_unittest(test_suite()) diff --git a/distutils/tests/test_config.py b/distutils/tests/test_config.py index 344084af..8ab70efb 100644 --- a/distutils/tests/test_config.py +++ b/distutils/tests/test_config.py @@ -135,7 +135,7 @@ class PyPIRCCommandTestCase(BasePyPIRCCommandTestCase): def test_suite(): - return unittest.makeSuite(PyPIRCCommandTestCase) + return unittest.TestLoader().loadTestsFromTestCase(PyPIRCCommandTestCase) if __name__ == "__main__": run_unittest(test_suite()) diff --git a/distutils/tests/test_config_cmd.py b/distutils/tests/test_config_cmd.py index 4cd9a6b9..2c84719a 100644 --- a/distutils/tests/test_config_cmd.py +++ b/distutils/tests/test_config_cmd.py @@ -92,7 +92,7 @@ class ConfigTestCase(support.LoggingSilencer, self.assertFalse(os.path.exists(f)) def test_suite(): - return unittest.makeSuite(ConfigTestCase) + return unittest.TestLoader().loadTestsFromTestCase(ConfigTestCase) if __name__ == "__main__": run_unittest(test_suite()) diff --git a/distutils/tests/test_core.py b/distutils/tests/test_core.py index d99cfd26..7270d699 100644 --- a/distutils/tests/test_core.py +++ b/distutils/tests/test_core.py @@ -159,7 +159,7 @@ class CoreTestCase(support.EnvironGuard, unittest.TestCase): self.assertEqual(stdout.readlines()[0], wanted) def test_suite(): - return unittest.makeSuite(CoreTestCase) + return unittest.TestLoader().loadTestsFromTestCase(CoreTestCase) if __name__ == "__main__": run_unittest(test_suite()) diff --git a/distutils/tests/test_cygwinccompiler.py b/distutils/tests/test_cygwinccompiler.py index 0e52c88f..8715a535 100644 --- a/distutils/tests/test_cygwinccompiler.py +++ b/distutils/tests/test_cygwinccompiler.py @@ -90,7 +90,7 @@ class CygwinCCompilerTestCase(support.TempdirManager, self.assertRaises(ValueError, get_msvcr) def test_suite(): - return unittest.makeSuite(CygwinCCompilerTestCase) + return unittest.TestLoader().loadTestsFromTestCase(CygwinCCompilerTestCase) if __name__ == '__main__': run_unittest(test_suite()) diff --git a/distutils/tests/test_dep_util.py b/distutils/tests/test_dep_util.py index c6fae39c..0d52740a 100644 --- a/distutils/tests/test_dep_util.py +++ b/distutils/tests/test_dep_util.py @@ -74,7 +74,7 @@ class DepUtilTestCase(support.TempdirManager, unittest.TestCase): def test_suite(): - return unittest.makeSuite(DepUtilTestCase) + return unittest.TestLoader().loadTestsFromTestCase(DepUtilTestCase) if __name__ == "__main__": run_unittest(test_suite()) diff --git a/distutils/tests/test_dir_util.py b/distutils/tests/test_dir_util.py index d436cf83..1b1f3bbb 100644 --- a/distutils/tests/test_dir_util.py +++ b/distutils/tests/test_dir_util.py @@ -133,7 +133,7 @@ class DirUtilTestCase(support.TempdirManager, unittest.TestCase): def test_suite(): - return unittest.makeSuite(DirUtilTestCase) + return unittest.TestLoader().loadTestsFromTestCase(DirUtilTestCase) if __name__ == "__main__": run_unittest(test_suite()) diff --git a/distutils/tests/test_dist.py b/distutils/tests/test_dist.py index 45eadee8..36155be1 100644 --- a/distutils/tests/test_dist.py +++ b/distutils/tests/test_dist.py @@ -525,8 +525,8 @@ class MetadataTestCase(support.TempdirManager, support.EnvironGuard, def test_suite(): suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(DistributionTestCase)) - suite.addTest(unittest.makeSuite(MetadataTestCase)) + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(DistributionTestCase)) + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(MetadataTestCase)) return suite if __name__ == "__main__": diff --git a/distutils/tests/test_extension.py b/distutils/tests/test_extension.py index 2eb5b422..78a55daa 100644 --- a/distutils/tests/test_extension.py +++ b/distutils/tests/test_extension.py @@ -65,7 +65,7 @@ class ExtensionTestCase(unittest.TestCase): "Unknown Extension options: 'chic'") def test_suite(): - return unittest.makeSuite(ExtensionTestCase) + return unittest.TestLoader().loadTestsFromTestCase(ExtensionTestCase) if __name__ == "__main__": run_unittest(test_suite()) diff --git a/distutils/tests/test_file_util.py b/distutils/tests/test_file_util.py index d2536075..81b90d6c 100644 --- a/distutils/tests/test_file_util.py +++ b/distutils/tests/test_file_util.py @@ -118,7 +118,7 @@ class FileUtilTestCase(support.TempdirManager, unittest.TestCase): def test_suite(): - return unittest.makeSuite(FileUtilTestCase) + return unittest.TestLoader().loadTestsFromTestCase(FileUtilTestCase) if __name__ == "__main__": run_unittest(test_suite()) diff --git a/distutils/tests/test_filelist.py b/distutils/tests/test_filelist.py index 9ec507b5..a90edcf1 100644 --- a/distutils/tests/test_filelist.py +++ b/distutils/tests/test_filelist.py @@ -344,8 +344,8 @@ class FindAllTestCase(unittest.TestCase): def test_suite(): return unittest.TestSuite([ - unittest.makeSuite(FileListTestCase), - unittest.makeSuite(FindAllTestCase), + unittest.TestLoader().loadTestsFromTestCase(FileListTestCase), + unittest.TestLoader().loadTestsFromTestCase(FindAllTestCase), ]) diff --git a/distutils/tests/test_install.py b/distutils/tests/test_install.py index cce973dc..75770b05 100644 --- a/distutils/tests/test_install.py +++ b/distutils/tests/test_install.py @@ -244,7 +244,7 @@ class InstallTestCase(support.TempdirManager, def test_suite(): - return unittest.makeSuite(InstallTestCase) + return unittest.TestLoader().loadTestsFromTestCase(InstallTestCase) if __name__ == "__main__": run_unittest(test_suite()) diff --git a/distutils/tests/test_install_data.py b/distutils/tests/test_install_data.py index 32ab296a..6191d2fa 100644 --- a/distutils/tests/test_install_data.py +++ b/distutils/tests/test_install_data.py @@ -69,7 +69,7 @@ class InstallDataTestCase(support.TempdirManager, self.assertTrue(os.path.exists(os.path.join(inst, rone))) def test_suite(): - return unittest.makeSuite(InstallDataTestCase) + return unittest.TestLoader().loadTestsFromTestCase(InstallDataTestCase) if __name__ == "__main__": run_unittest(test_suite()) diff --git a/distutils/tests/test_install_headers.py b/distutils/tests/test_install_headers.py index 2217b321..1aa4d09c 100644 --- a/distutils/tests/test_install_headers.py +++ b/distutils/tests/test_install_headers.py @@ -33,7 +33,7 @@ class InstallHeadersTestCase(support.TempdirManager, self.assertEqual(len(cmd.get_outputs()), 2) def test_suite(): - return unittest.makeSuite(InstallHeadersTestCase) + return unittest.TestLoader().loadTestsFromTestCase(InstallHeadersTestCase) if __name__ == "__main__": run_unittest(test_suite()) diff --git a/distutils/tests/test_install_lib.py b/distutils/tests/test_install_lib.py index fda6315b..652653f2 100644 --- a/distutils/tests/test_install_lib.py +++ b/distutils/tests/test_install_lib.py @@ -109,7 +109,7 @@ class InstallLibTestCase(support.TempdirManager, def test_suite(): - return unittest.makeSuite(InstallLibTestCase) + return unittest.TestLoader().loadTestsFromTestCase(InstallLibTestCase) if __name__ == "__main__": run_unittest(test_suite()) diff --git a/distutils/tests/test_install_scripts.py b/distutils/tests/test_install_scripts.py index 1f7b1038..648db3b1 100644 --- a/distutils/tests/test_install_scripts.py +++ b/distutils/tests/test_install_scripts.py @@ -76,7 +76,7 @@ class InstallScriptsTestCase(support.TempdirManager, def test_suite(): - return unittest.makeSuite(InstallScriptsTestCase) + return unittest.TestLoader().loadTestsFromTestCase(InstallScriptsTestCase) if __name__ == "__main__": run_unittest(test_suite()) diff --git a/distutils/tests/test_log.py b/distutils/tests/test_log.py index 75cf9006..ec2ae028 100644 --- a/distutils/tests/test_log.py +++ b/distutils/tests/test_log.py @@ -40,7 +40,7 @@ class TestLog(unittest.TestCase): 'Fαtal\t\\xc8rr\\u014dr') def test_suite(): - return unittest.makeSuite(TestLog) + return unittest.TestLoader().loadTestsFromTestCase(TestLog) if __name__ == "__main__": run_unittest(test_suite()) diff --git a/distutils/tests/test_msvc9compiler.py b/distutils/tests/test_msvc9compiler.py index 77a07ef3..6235405e 100644 --- a/distutils/tests/test_msvc9compiler.py +++ b/distutils/tests/test_msvc9compiler.py @@ -178,7 +178,7 @@ class msvc9compilerTestCase(support.TempdirManager, def test_suite(): - return unittest.makeSuite(msvc9compilerTestCase) + return unittest.TestLoader().loadTestsFromTestCase(msvc9compilerTestCase) if __name__ == "__main__": run_unittest(test_suite()) diff --git a/distutils/tests/test_msvccompiler.py b/distutils/tests/test_msvccompiler.py index 46a51cd0..aefdd8ba 100644 --- a/distutils/tests/test_msvccompiler.py +++ b/distutils/tests/test_msvccompiler.py @@ -132,7 +132,7 @@ class TestSpawn(unittest.TestCase): def test_suite(): - return unittest.makeSuite(msvccompilerTestCase) + return unittest.TestLoader().loadTestsFromTestCase(msvccompilerTestCase) if __name__ == "__main__": run_unittest(test_suite()) diff --git a/distutils/tests/test_register.py b/distutils/tests/test_register.py index 84607f99..5770ed58 100644 --- a/distutils/tests/test_register.py +++ b/distutils/tests/test_register.py @@ -319,7 +319,7 @@ class RegisterTestCase(BasePyPIRCCommandTestCase): def test_suite(): - return unittest.makeSuite(RegisterTestCase) + return unittest.TestLoader().loadTestsFromTestCase(RegisterTestCase) if __name__ == "__main__": run_unittest(test_suite()) diff --git a/distutils/tests/test_sdist.py b/distutils/tests/test_sdist.py index 880044fa..4c51717c 100644 --- a/distutils/tests/test_sdist.py +++ b/distutils/tests/test_sdist.py @@ -483,7 +483,7 @@ class SDistTestCase(BasePyPIRCCommandTestCase): archive.close() def test_suite(): - return unittest.makeSuite(SDistTestCase) + return unittest.TestLoader().loadTestsFromTestCase(SDistTestCase) if __name__ == "__main__": run_unittest(test_suite()) diff --git a/distutils/tests/test_spawn.py b/distutils/tests/test_spawn.py index f620da78..c5ed8e2b 100644 --- a/distutils/tests/test_spawn.py +++ b/distutils/tests/test_spawn.py @@ -133,7 +133,7 @@ class SpawnTestCase(support.TempdirManager, def test_suite(): - return unittest.makeSuite(SpawnTestCase) + return unittest.TestLoader().loadTestsFromTestCase(SpawnTestCase) if __name__ == "__main__": run_unittest(test_suite()) diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py index d28f4712..93509c71 100644 --- a/distutils/tests/test_sysconfig.py +++ b/distutils/tests/test_sysconfig.py @@ -296,7 +296,7 @@ class SysconfigTestCase(support.EnvironGuard, unittest.TestCase): def test_suite(): suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(SysconfigTestCase)) + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(SysconfigTestCase)) return suite diff --git a/distutils/tests/test_text_file.py b/distutils/tests/test_text_file.py index 7e76240a..ebac3d52 100644 --- a/distutils/tests/test_text_file.py +++ b/distutils/tests/test_text_file.py @@ -101,7 +101,7 @@ class TextFileTestCase(support.TempdirManager, unittest.TestCase): in_file.close() def test_suite(): - return unittest.makeSuite(TextFileTestCase) + return unittest.TestLoader().loadTestsFromTestCase(TextFileTestCase) if __name__ == "__main__": run_unittest(test_suite()) diff --git a/distutils/tests/test_unixccompiler.py b/distutils/tests/test_unixccompiler.py index 2ea93da1..4574f77f 100644 --- a/distutils/tests/test_unixccompiler.py +++ b/distutils/tests/test_unixccompiler.py @@ -246,7 +246,7 @@ class UnixCCompilerTestCase(support.TempdirManager, unittest.TestCase): def test_suite(): - return unittest.makeSuite(UnixCCompilerTestCase) + return unittest.TestLoader().loadTestsFromTestCase(UnixCCompilerTestCase) if __name__ == "__main__": run_unittest(test_suite()) diff --git a/distutils/tests/test_upload.py b/distutils/tests/test_upload.py index bca5516d..ce3e84a2 100644 --- a/distutils/tests/test_upload.py +++ b/distutils/tests/test_upload.py @@ -217,7 +217,7 @@ class uploadTestCase(BasePyPIRCCommandTestCase): def test_suite(): - return unittest.makeSuite(uploadTestCase) + return unittest.TestLoader().loadTestsFromTestCase(uploadTestCase) if __name__ == "__main__": run_unittest(test_suite()) diff --git a/distutils/tests/test_util.py b/distutils/tests/test_util.py index bf0d4333..12469e3d 100644 --- a/distutils/tests/test_util.py +++ b/distutils/tests/test_util.py @@ -303,7 +303,7 @@ class UtilTestCase(support.EnvironGuard, unittest.TestCase): def test_suite(): - return unittest.makeSuite(UtilTestCase) + return unittest.TestLoader().loadTestsFromTestCase(UtilTestCase) if __name__ == "__main__": run_unittest(test_suite()) diff --git a/distutils/tests/test_version.py b/distutils/tests/test_version.py index d50cca1f..8405aa3a 100644 --- a/distutils/tests/test_version.py +++ b/distutils/tests/test_version.py @@ -89,7 +89,7 @@ class VersionTestCase(unittest.TestCase): (v1, v2, res)) def test_suite(): - return unittest.makeSuite(VersionTestCase) + return unittest.TestLoader().loadTestsFromTestCase(VersionTestCase) if __name__ == "__main__": run_unittest(test_suite()) -- cgit v1.2.1 From a3a59994e15235885c40a6feb051a6bb8285f68d Mon Sep 17 00:00:00 2001 From: Christoph Reiter Date: Mon, 27 Dec 2021 09:59:19 +0100 Subject: tests: fix usage of requires_zlib() decorator It was passing the test function itself as an argument, skipping the test always. See https://github.com/python/cpython/pull/28305 for the same fix in CPython. --- distutils/tests/test_bdist_rpm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/distutils/tests/test_bdist_rpm.py b/distutils/tests/test_bdist_rpm.py index 6453a02b..ba4382fb 100644 --- a/distutils/tests/test_bdist_rpm.py +++ b/distutils/tests/test_bdist_rpm.py @@ -44,7 +44,7 @@ class BuildRpmTestCase(support.TempdirManager, # spurious sdtout/stderr output under Mac OS X @unittest.skipUnless(sys.platform.startswith('linux'), 'spurious sdtout/stderr output under Mac OS X') - @requires_zlib + @requires_zlib() @unittest.skipIf(find_executable('rpm') is None, 'the rpm command is not found') @unittest.skipIf(find_executable('rpmbuild') is None, @@ -87,7 +87,7 @@ class BuildRpmTestCase(support.TempdirManager, # spurious sdtout/stderr output under Mac OS X @unittest.skipUnless(sys.platform.startswith('linux'), 'spurious sdtout/stderr output under Mac OS X') - @requires_zlib + @requires_zlib() # http://bugs.python.org/issue1533164 @unittest.skipIf(find_executable('rpm') is None, 'the rpm command is not found') -- cgit v1.2.1 From 1736f537eb85bcfe58e1ae854a45d5b79fe12996 Mon Sep 17 00:00:00 2001 From: Christoph Reiter Date: Mon, 27 Dec 2021 10:19:30 +0100 Subject: tests: fix tests on Ubuntu 22.04 I added a test for sysconfig.parse_config_h() in 9d0b8cda407 which assumed pyconfig.h always has some macros defined and checked that the result was non-empty. On Ubuntu 22.04 the pyconfig.h is just a shim, including various platform specific configs and not defining anything directly. This changes the test to not assume anything about the output of parse_config_h() instead. --- distutils/tests/test_sysconfig.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py index d28f4712..c7989d68 100644 --- a/distutils/tests/test_sysconfig.py +++ b/distutils/tests/test_sysconfig.py @@ -288,11 +288,10 @@ class SysconfigTestCase(support.EnvironGuard, unittest.TestCase): input = {} with open(config_h, encoding="utf-8") as f: result = sysconfig.parse_config_h(f, g=input) - self.assertTrue(input) self.assertTrue(input is result) with open(config_h, encoding="utf-8") as f: result = sysconfig.parse_config_h(f) - self.assertTrue(result) + self.assertTrue(isinstance(result, dict)) def test_suite(): suite = unittest.TestSuite() -- cgit v1.2.1 From a8e6207c20912bdfd158421819889216e0413c47 Mon Sep 17 00:00:00 2001 From: Christoph Reiter Date: Mon, 27 Dec 2021 10:33:16 +0100 Subject: tests: use sys.executable instead of hardcoding "python" Don't assume a "python" is in PATH. Fixes this test when run from an uninstalled Python at least. --- distutils/tests/test_msvccompiler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/tests/test_msvccompiler.py b/distutils/tests/test_msvccompiler.py index 46a51cd0..a65537aa 100644 --- a/distutils/tests/test_msvccompiler.py +++ b/distutils/tests/test_msvccompiler.py @@ -98,7 +98,7 @@ class TestSpawn(unittest.TestCase): compiler = _msvccompiler.MSVCCompiler() compiler._paths = "expected" inner_cmd = 'import os; assert os.environ["PATH"] == "expected"' - command = ['python', '-c', inner_cmd] + command = [sys.executable, '-c', inner_cmd] threads = [ CheckThread(target=compiler.spawn, args=[command]) -- cgit v1.2.1 From 62ed95282bb1f79b8b7564ddac2c28967035300a Mon Sep 17 00:00:00 2001 From: Christoph Reiter Date: Sun, 26 Dec 2021 18:13:20 +0100 Subject: sysconfig: use get_makefile_filename() from stdlib sysconfig Instead of guessing the filename just refer to the stdlib. This also removes the "_makefile_tmpl" hook added in #16, but 1) It was never implemented by the distro requesting it from what I can see: https://github.com/NetBSD/pkgsrc/blob/586097714897b1b4d4a9/devel/py-setuptools/Makefile#L28 2) The stdlib version should return a proper result as it is patched by the distro requesting the change: https://github.com/NetBSD/pkgsrc/blob/6efa5763ec447864a7d4/lang/python38/patches/patch-Lib_sysconfig.py Also adds a small test checking that the file exists on Unix platforms --- distutils/sysconfig.py | 18 +----------------- distutils/tests/test_sysconfig.py | 6 ++++++ 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py index 6f1bbb48..4a77a431 100644 --- a/distutils/sysconfig.py +++ b/distutils/sysconfig.py @@ -281,25 +281,9 @@ def get_config_h_filename(): -# Allow this value to be patched by pkgsrc. Ref pypa/distutils#16. -_makefile_tmpl = 'config-{python_ver}{build_flags}{multiarch}' - - def get_makefile_filename(): """Return full pathname of installed Makefile from the Python build.""" - if python_build: - return os.path.join(_sys_home or project_base, "Makefile") - lib_dir = get_python_lib(plat_specific=0, standard_lib=1) - multiarch = ( - '-%s' % sys.implementation._multiarch - if hasattr(sys.implementation, '_multiarch') else '' - ) - config_file = _makefile_tmpl.format( - python_ver=get_python_version(), - build_flags=build_flags, - multiarch=multiarch, - ) - return os.path.join(lib_dir, config_file, 'Makefile') + return sysconfig.get_makefile_filename() def parse_config_h(fp, g=None): diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py index d28f4712..3fbacc2f 100644 --- a/distutils/tests/test_sysconfig.py +++ b/distutils/tests/test_sysconfig.py @@ -38,6 +38,12 @@ class SysconfigTestCase(support.EnvironGuard, unittest.TestCase): config_h = sysconfig.get_config_h_filename() self.assertTrue(os.path.isfile(config_h), config_h) + @unittest.skipIf(sys.platform == 'win32', + 'Makefile only exists on Unix like systems') + def test_get_makefile_filename(self): + makefile = sysconfig.get_makefile_filename() + self.assertTrue(os.path.isfile(makefile), makefile) + def test_get_python_lib(self): # XXX doesn't work on Linux when Python was never installed before #self.assertTrue(os.path.isdir(lib_dir), lib_dir) -- cgit v1.2.1 From 2a91fab31b909e36afcf6371697d3b17f9ec0ade Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 27 Dec 2021 22:11:07 +0000 Subject: Remove workaround for test/CI dependency on Sphinx It seems that Sphinx now supports Python 3.10, which means that we can remove the dependency workaround. --- setup.cfg | 4 ++-- tox.ini | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/setup.cfg b/setup.cfg index 3daca1df..30ca1945 100644 --- a/setup.cfg +++ b/setup.cfg @@ -61,12 +61,12 @@ testing = pip>=19.1 # For proper file:// URLs support. jaraco.envs>=2.2 pytest-xdist - sphinx + sphinx>=4.3.2 jaraco.path>=3.2.0 docs = # upstream - sphinx + sphinx >= 4.3.2 jaraco.packaging >= 8.2 rst.linker >= 1.9 jaraco.tidelift >= 1.4 diff --git a/tox.ini b/tox.ini index 25b4eaf0..145c2209 100644 --- a/tox.ini +++ b/tox.ini @@ -7,9 +7,6 @@ toxworkdir={env:TOX_WORK_DIR:.tox} [testenv] deps = - # workaround for sphinx-doc/sphinx#9562 - # TODO: remove after Sphinx>4.1.2 is available. - sphinx@git+https://github.com/sphinx-doc/sphinx; python_version>="3.10" # TODO: remove after man-group/pytest-plugins#188 is solved pytest-virtualenv @ git+https://github.com/jaraco/pytest-plugins@distutils-deprecated#subdirectory=pytest-virtualenv commands = -- cgit v1.2.1 From 9c9c91c0a3951a7c521bd6eddc3aa5f84f0cfd9f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 28 Dec 2021 14:34:37 -0500 Subject: Bypass distutils loader when setuptools module is no longer available on sys.path. Fixes #2980. --- _distutils_hack/__init__.py | 7 +++++++ changelog.d/2980.misc.rst | 1 + 2 files changed, 8 insertions(+) create mode 100644 changelog.d/2980.misc.rst diff --git a/_distutils_hack/__init__.py b/_distutils_hack/__init__.py index 85a51370..55ea0825 100644 --- a/_distutils_hack/__init__.py +++ b/_distutils_hack/__init__.py @@ -86,6 +86,13 @@ class DistutilsMetaFinder: import importlib.abc import importlib.util + # In cases of path manipulation during sitecustomize, + # Setuptools might actually not be present even though + # the hook has been loaded. Allow the caller to fall + # back to stdlib behavior. See #2980. + if not importlib.util.find_spec('setuptools'): + return + class DistutilsLoader(importlib.abc.Loader): def create_module(self, spec): diff --git a/changelog.d/2980.misc.rst b/changelog.d/2980.misc.rst new file mode 100644 index 00000000..222a3adb --- /dev/null +++ b/changelog.d/2980.misc.rst @@ -0,0 +1 @@ +Bypass distutils loader when setuptools module is no longer available on sys.path. -- cgit v1.2.1 From 7f795745bc23880ba8f40033a1ef3210c0718396 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 28 Dec 2021 14:35:41 -0500 Subject: =?UTF-8?q?Bump=20version:=2060.1.0=20=E2=86=92=2060.1.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 9 +++++++++ changelog.d/2980.misc.rst | 1 - setup.cfg | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/2980.misc.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index d5a104b0..e607b2fc 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 60.1.0 +current_version = 60.1.1 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index 09692544..554ee73e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,12 @@ +v60.1.1 +------- + + +Misc +^^^^ +* #2980: Bypass distutils loader when setuptools module is no longer available on sys.path. + + v60.1.0 ------- diff --git a/changelog.d/2980.misc.rst b/changelog.d/2980.misc.rst deleted file mode 100644 index 222a3adb..00000000 --- a/changelog.d/2980.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Bypass distutils loader when setuptools module is no longer available on sys.path. diff --git a/setup.cfg b/setup.cfg index 3daca1df..23c7d1ea 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 60.1.0 +version = 60.1.1 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From 79f6a3fa77407405259242cbdd85d4ff78ed7193 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 28 Dec 2021 15:24:39 -0500 Subject: Use line-based matrix values for nicer diffs. Remove Python 3.6 and bump to Python 3.10, matching jaraco/skeleton and pypa/setuptools approaches. --- .github/workflows/main.yml | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bd0e1992..bd2dc8f2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,8 +6,14 @@ jobs: test: strategy: matrix: - python: [3.6, 3.8, 3.9] - platform: [ubuntu-latest, macos-latest, windows-latest] + python: + - 3.7 + - 3.9 + - "3.10" + platform: + - ubuntu-latest + - macos-latest + - windows-latest runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v2 @@ -24,8 +30,10 @@ jobs: test_cygwin: strategy: matrix: - python: [39] - platform: [windows-latest] + python: + - "3.10" + platform: + - windows-latest runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v2 @@ -49,8 +57,10 @@ jobs: # Integration testing with setuptools strategy: matrix: - python: [3.9] - platform: [ubuntu-latest] + python: + - "3.10" + platform: + - ubuntu-latest runs-on: ${{ matrix.platform }} env: SETUPTOOLS_USE_DISTUTILS: local @@ -89,7 +99,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: "3.10" - name: Install tox run: | python -m pip install tox -- cgit v1.2.1 From 5c24a555a322e9fd987fb2d82782858971795361 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 28 Dec 2021 16:23:39 -0500 Subject: Leave sphinx unpinned as found upstream. --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 30ca1945..91359692 100644 --- a/setup.cfg +++ b/setup.cfg @@ -66,7 +66,7 @@ testing = docs = # upstream - sphinx >= 4.3.2 + sphinx jaraco.packaging >= 8.2 rst.linker >= 1.9 jaraco.tidelift >= 1.4 -- cgit v1.2.1 From 382037c446f45ebc2e91fd5144eda4dfa90c0281 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 26 Dec 2021 14:15:15 -0500 Subject: Add setuptools.log to supersede distutils.log. Ref #2973. --- changelog.d/2974.change.rst | 1 + setuptools/__init__.py | 2 ++ setuptools/logging.py | 22 ++++++++++++++++++++++ 3 files changed, 25 insertions(+) create mode 100644 changelog.d/2974.change.rst create mode 100644 setuptools/logging.py diff --git a/changelog.d/2974.change.rst b/changelog.d/2974.change.rst new file mode 100644 index 00000000..fcfa0049 --- /dev/null +++ b/changelog.d/2974.change.rst @@ -0,0 +1 @@ +Setuptools now relies on the Python logging infrastructure to log messages. Instead of using ``distutils.log.*``, use ``logging.getLogger(name).*``. diff --git a/setuptools/__init__.py b/setuptools/__init__.py index 9d6f0bc0..43d1c96e 100644 --- a/setuptools/__init__.py +++ b/setuptools/__init__.py @@ -18,6 +18,7 @@ from setuptools.extension import Extension from setuptools.dist import Distribution from setuptools.depends import Require from . import monkey +from . import logging __all__ = [ @@ -149,6 +150,7 @@ def _install_setup_requires(attrs): def setup(**attrs): # Make sure we have any requirements needed to interpret 'attrs'. + logging.configure() _install_setup_requires(attrs) return distutils.core.setup(**attrs) diff --git a/setuptools/logging.py b/setuptools/logging.py new file mode 100644 index 00000000..0ac47059 --- /dev/null +++ b/setuptools/logging.py @@ -0,0 +1,22 @@ +import sys +import logging + + +def _not_warning(record): + return record.levelno < logging.WARNING + + +def configure(): + """ + Configure logging to emit warning and above to stderr + and everything else to stdout. This behavior is provided + for compatibilty with distutils.log but may change in + the future. + """ + err_handler = logging.StreamHandler() + err_handler.setLevel(logging.WARNING) + out_handler = logging.StreamHandler(sys.stdout) + out_handler.addFilter(_not_warning) + handlers = err_handler, out_handler + logging.basicConfig( + format="{message}", style='{', handlers=handlers, level=logging.DEBUG) -- cgit v1.2.1 From b9cf7ff3ff907b5a805264b399b17e4ea6bec049 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 26 Dec 2021 15:23:28 -0500 Subject: Monkey patch distutils.log.set_threshold so the Python logger honors calls to it. --- setuptools/logging.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/setuptools/logging.py b/setuptools/logging.py index 0ac47059..dbead6e6 100644 --- a/setuptools/logging.py +++ b/setuptools/logging.py @@ -1,5 +1,7 @@ import sys import logging +import distutils.log +from . import monkey def _not_warning(record): @@ -20,3 +22,9 @@ def configure(): handlers = err_handler, out_handler logging.basicConfig( format="{message}", style='{', handlers=handlers, level=logging.DEBUG) + monkey.patch_func(set_threshold, distutils.log, 'set_threshold') + + +def set_threshold(level): + logging.root.setLevel(level*10) + return set_threshold.unpatched(level) -- cgit v1.2.1 From 41f61f3cce618b594c3c76b78162945f26fa6b61 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 29 Dec 2021 10:51:43 -0500 Subject: Restore assertion about expected distutils. --- _distutils_hack/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/_distutils_hack/__init__.py b/_distutils_hack/__init__.py index a5c8e1cf..cb694b34 100644 --- a/_distutils_hack/__init__.py +++ b/_distutils_hack/__init__.py @@ -58,9 +58,7 @@ def ensure_local_distutils(): # check that submodules load as expected core = importlib.import_module('distutils.core') - # FIXME: this assertion blows up if the MetaFinder below has no-opped on a - # setuptools from another path - # assert '_distutils' in core.__file__, core.__file__ + assert '_distutils' in core.__file__, core.__file__ def do_override(): -- cgit v1.2.1 From da8c68c49646d47b3946e882d402fa32131ade5d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 29 Dec 2021 10:59:29 -0500 Subject: Check early for the presence of local distutils and bail out if the found version of Setuptools doesn't have distutils. --- _distutils_hack/__init__.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/_distutils_hack/__init__.py b/_distutils_hack/__init__.py index cb694b34..f64f9fea 100644 --- a/_distutils_hack/__init__.py +++ b/_distutils_hack/__init__.py @@ -85,20 +85,18 @@ class DistutilsMetaFinder: def spec_for_distutils(self): import importlib.abc import importlib.util - from pathlib import Path - st_mod = importlib.import_module('setuptools') - - # prevent this import redirection shim from interacting with a possibly - # incompatible setuptools in another location on sys.path - # see pypa/setuptools#2957 - if not Path(__file__).parents[1].samefile(Path(st_mod.__file__).parents[1]): + try: + mod = importlib.import_module('setuptools._distutils') + except Exception: + # an older Setuptools without a local distutils is taking + # precedence, so fall back to stdlib. Ref #2957. return None class DistutilsLoader(importlib.abc.Loader): def create_module(self, spec): - return importlib.import_module('setuptools._distutils') + return mod def exec_module(self, module): pass -- cgit v1.2.1 From 6eb6350caebbb36537722c7a0ec532b9ff98168f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 29 Dec 2021 11:22:56 -0500 Subject: Update changelog. --- changelog.d/2962.misc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/2962.misc.rst diff --git a/changelog.d/2962.misc.rst b/changelog.d/2962.misc.rst new file mode 100644 index 00000000..5a553a77 --- /dev/null +++ b/changelog.d/2962.misc.rst @@ -0,0 +1 @@ +Avoid attempting to use local distutils when the presiding version of Setuptools on the path doesn't have one. -- cgit v1.2.1 From d0da0142609318e534d5874bfa0c44d1254f9e38 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 29 Dec 2021 11:31:51 -0500 Subject: Restore 'add_shim' as the way to invoke the hook. Avoids compatibility issues between different versions of Setuptools with the distutils local implementation. Renamed the former 'add_shim' as 'insert_shim'. Fixes #2983 --- _distutils_hack/__init__.py | 8 ++++---- changelog.d/2983.misc.rst | 1 + setup.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 changelog.d/2983.misc.rst diff --git a/_distutils_hack/__init__.py b/_distutils_hack/__init__.py index 55ea0825..0a8f0473 100644 --- a/_distutils_hack/__init__.py +++ b/_distutils_hack/__init__.py @@ -136,20 +136,20 @@ class DistutilsMetaFinder: DISTUTILS_FINDER = DistutilsMetaFinder() -def ensure_shim(): - DISTUTILS_FINDER in sys.meta_path or add_shim() +def add_shim(): + DISTUTILS_FINDER in sys.meta_path or insert_shim() @contextlib.contextmanager def shim(): - add_shim() + insert_shim() try: yield finally: remove_shim() -def add_shim(): +def insert_shim(): sys.meta_path.insert(0, DISTUTILS_FINDER) diff --git a/changelog.d/2983.misc.rst b/changelog.d/2983.misc.rst new file mode 100644 index 00000000..cc11f757 --- /dev/null +++ b/changelog.d/2983.misc.rst @@ -0,0 +1 @@ +Restore 'add_shim' as the way to invoke the hook. Avoids compatibility issues between different versions of Setuptools with the distutils local implementation. diff --git a/setup.py b/setup.py index d15189db..4cda3d38 100755 --- a/setup.py +++ b/setup.py @@ -55,7 +55,7 @@ class install_with_pth(install): import os var = 'SETUPTOOLS_USE_DISTUTILS' enabled = os.environ.get(var, 'local') == 'local' - enabled and __import__('_distutils_hack').ensure_shim() + enabled and __import__('_distutils_hack').add_shim() """).lstrip().replace('\n', '; ') def initialize_options(self): -- cgit v1.2.1 From 8c160a96931520044688471c8d1ea6148aacb719 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 29 Dec 2021 11:51:20 -0500 Subject: Restore 'get_versions' attribute, allowing older mpi4py to monkeypatch it. Fixes pypa/setuptools#2969. --- distutils/cygwinccompiler.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/distutils/cygwinccompiler.py b/distutils/cygwinccompiler.py index 4a38dfda..fd082f6d 100644 --- a/distutils/cygwinccompiler.py +++ b/distutils/cygwinccompiler.py @@ -354,3 +354,9 @@ def is_cygwincc(cc): out_string = check_output(shlex.split(cc) + ['-dumpmachine']) return out_string.strip().endswith(b'cygwin') + +get_versions = None +""" +A stand-in for the previous get_versions() function to prevent failures +when monkeypatched. See pypa/setuptools#2969. +""" -- cgit v1.2.1 From ddafa7b8aa83c2596391dcf65723e6e3b39ca1a6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 29 Dec 2021 13:01:32 -0500 Subject: Disable setuptools installation. Fixes pypa/distutils#99. --- .github/workflows/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bd2dc8f2..d9e219ae 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,6 +2,10 @@ name: tests on: [push, pull_request] +env: + # pypa/distutils#99 + VIRTUALENV_NO_SETUPTOOLS: 1 + jobs: test: strategy: -- cgit v1.2.1 From eb337f6440da3d9aaedf6ba92674c283ca34e2e2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 29 Dec 2021 13:09:56 -0500 Subject: Also use PEP 517 to build things like pytest-virtualenv. --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d9e219ae..60d07948 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,6 +5,7 @@ on: [push, pull_request] env: # pypa/distutils#99 VIRTUALENV_NO_SETUPTOOLS: 1 + PIP_USE_PEP517: 1 jobs: test: -- cgit v1.2.1 From 2842e802a42c7eb9fd8fc80336cb65f077dfd49c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 29 Dec 2021 13:15:09 -0500 Subject: 39 is actually required to get the right packages. --- .github/workflows/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 60d07948..dcd8b901 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -36,7 +36,7 @@ jobs: strategy: matrix: python: - - "3.10" + - 3.9 platform: - windows-latest runs-on: ${{ matrix.platform }} @@ -47,9 +47,9 @@ jobs: with: platform: x86_64 packages: >- - python${{ matrix.python }}, - python${{ matrix.python }}-devel, - python${{ matrix.python }}-pytest, + python${{ matrix.python//.// }}, + python${{ matrix.python//.// }}-devel, + python${{ matrix.python//.// }}-pytest, gcc-core, gcc-g++, ncompress -- cgit v1.2.1 From 9f2b840b218b6f46ede9d75b3c79db1042b433dd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 29 Dec 2021 13:17:53 -0500 Subject: Unset VIRTUALENV_NO_SETUPTOOLS for ci_setuptools because pytest-virtualenv can't install without it. Ref man-group/pytest-plugins#190 --- .github/workflows/main.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index dcd8b901..e404559b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,7 +5,6 @@ on: [push, pull_request] env: # pypa/distutils#99 VIRTUALENV_NO_SETUPTOOLS: 1 - PIP_USE_PEP517: 1 jobs: test: @@ -93,6 +92,8 @@ jobs: run: | cd integration/setuptools tox + env: + VIRTUALENV_NO_SETUPTOOLS: null release: needs: test -- cgit v1.2.1 From 0dfbfb81c67708aa55aa35bf612bd6769eb4eaa4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 29 Dec 2021 13:19:59 -0500 Subject: It really must be literally 39. --- .github/workflows/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e404559b..37087511 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -35,7 +35,7 @@ jobs: strategy: matrix: python: - - 3.9 + - 39 platform: - windows-latest runs-on: ${{ matrix.platform }} @@ -46,9 +46,9 @@ jobs: with: platform: x86_64 packages: >- - python${{ matrix.python//.// }}, - python${{ matrix.python//.// }}-devel, - python${{ matrix.python//.// }}-pytest, + python${{ matrix.python }}, + python${{ matrix.python }}-devel, + python${{ matrix.python }}-pytest, gcc-core, gcc-g++, ncompress -- cgit v1.2.1 From d9273a49872a2e605527bca44dfba5ea1f6e2aaf Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 29 Dec 2021 13:46:54 -0500 Subject: Add changelog. --- changelog.d/2987.change.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/2987.change.rst diff --git a/changelog.d/2987.change.rst b/changelog.d/2987.change.rst new file mode 100644 index 00000000..3b476eed --- /dev/null +++ b/changelog.d/2987.change.rst @@ -0,0 +1 @@ +Sync with pypa/distutils@2def21c5d74fdd2fe7996ee4030ac145a9d751bd, including fix for missing get_versions attribute (#2969), more reliance on sysconfig from stdlib. -- cgit v1.2.1 From 80045ce11c74d1b50892d15feef82415e6badfe9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 29 Dec 2021 15:34:39 -0500 Subject: =?UTF-8?q?Bump=20version:=2060.1.1=20=E2=86=92=2060.2.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 15 +++++++++++++++ changelog.d/2962.misc.rst | 1 - changelog.d/2974.change.rst | 1 - changelog.d/2983.misc.rst | 1 - changelog.d/2987.change.rst | 1 - setup.cfg | 2 +- 7 files changed, 17 insertions(+), 6 deletions(-) delete mode 100644 changelog.d/2962.misc.rst delete mode 100644 changelog.d/2974.change.rst delete mode 100644 changelog.d/2983.misc.rst delete mode 100644 changelog.d/2987.change.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index e607b2fc..f696b076 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 60.1.1 +current_version = 60.2.0 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index 554ee73e..59b29fb5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,18 @@ +v60.2.0 +------- + + +Changes +^^^^^^^ +* #2974: Setuptools now relies on the Python logging infrastructure to log messages. Instead of using ``distutils.log.*``, use ``logging.getLogger(name).*``. +* #2987: Sync with pypa/distutils@2def21c5d74fdd2fe7996ee4030ac145a9d751bd, including fix for missing get_versions attribute (#2969), more reliance on sysconfig from stdlib. + +Misc +^^^^ +* #2962: Avoid attempting to use local distutils when the presiding version of Setuptools on the path doesn't have one. +* #2983: Restore 'add_shim' as the way to invoke the hook. Avoids compatibility issues between different versions of Setuptools with the distutils local implementation. + + v60.1.1 ------- diff --git a/changelog.d/2962.misc.rst b/changelog.d/2962.misc.rst deleted file mode 100644 index 5a553a77..00000000 --- a/changelog.d/2962.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Avoid attempting to use local distutils when the presiding version of Setuptools on the path doesn't have one. diff --git a/changelog.d/2974.change.rst b/changelog.d/2974.change.rst deleted file mode 100644 index fcfa0049..00000000 --- a/changelog.d/2974.change.rst +++ /dev/null @@ -1 +0,0 @@ -Setuptools now relies on the Python logging infrastructure to log messages. Instead of using ``distutils.log.*``, use ``logging.getLogger(name).*``. diff --git a/changelog.d/2983.misc.rst b/changelog.d/2983.misc.rst deleted file mode 100644 index cc11f757..00000000 --- a/changelog.d/2983.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Restore 'add_shim' as the way to invoke the hook. Avoids compatibility issues between different versions of Setuptools with the distutils local implementation. diff --git a/changelog.d/2987.change.rst b/changelog.d/2987.change.rst deleted file mode 100644 index 3b476eed..00000000 --- a/changelog.d/2987.change.rst +++ /dev/null @@ -1 +0,0 @@ -Sync with pypa/distutils@2def21c5d74fdd2fe7996ee4030ac145a9d751bd, including fix for missing get_versions attribute (#2969), more reliance on sysconfig from stdlib. diff --git a/setup.cfg b/setup.cfg index 9ae3e02c..75a41c84 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 60.1.1 +version = 60.2.0 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From 9c6dcd56335d012d31b27fbcf94cd99f05480620 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 29 Dec 2021 17:07:48 -0500 Subject: In tests, add compatibility shim py38compat.requires_zlib. --- distutils/tests/py38compat.py | 9 +++++++++ distutils/tests/test_bdist_rpm.py | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/distutils/tests/py38compat.py b/distutils/tests/py38compat.py index 32269c7b..c949f58e 100644 --- a/distutils/tests/py38compat.py +++ b/distutils/tests/py38compat.py @@ -2,6 +2,11 @@ import contextlib import builtins +import sys + +from test.support import requires_zlib +import test.support + ModuleNotFoundError = getattr(builtins, 'ModuleNotFoundError', ImportError) @@ -51,3 +56,7 @@ try: from test.support.warnings_helper import save_restore_warnings_filters except (ModuleNotFoundError, ImportError): save_restore_warnings_filters = _save_restore_warnings_filters + + +if sys.version_info < (3, 9): + requires_zlib = lambda: test.support.requires_zlib diff --git a/distutils/tests/test_bdist_rpm.py b/distutils/tests/test_bdist_rpm.py index ba4382fb..cb0a0b86 100644 --- a/distutils/tests/test_bdist_rpm.py +++ b/distutils/tests/test_bdist_rpm.py @@ -3,13 +3,16 @@ import unittest import sys import os -from test.support import run_unittest, requires_zlib +from test.support import run_unittest 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 .py38compat import requires_zlib + + SETUP_PY = """\ from distutils.core import setup import foo -- cgit v1.2.1 From d8e82cbefdde64aad947f196477312f82e06ac1e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 29 Dec 2021 17:24:22 -0500 Subject: Include Python 3.8 in tests. Ref pypa/distutils#100. --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 37087511..35685723 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,6 +12,7 @@ jobs: matrix: python: - 3.7 + - 3.8 - 3.9 - "3.10" platform: -- cgit v1.2.1 From a0d77c2e3870c3e67753f7a8f7ab4d9545fc2b9d Mon Sep 17 00:00:00 2001 From: Ronald Oussoren Date: Sun, 8 Nov 2020 10:05:27 +0100 Subject: bpo-41100: Support macOS 11 and Apple Silicon (GH-22855) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Lawrence D’Anna * Add support for macOS 11 and Apple Silicon (aka arm64) As a side effect of this work use the system copy of libffi on macOS, and remove the vendored copy * Support building on recent versions of macOS while deploying to older versions This allows building installers on macOS 11 while still supporting macOS 10.9. --- 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 85ecf4b7..866dad46 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -493,7 +493,7 @@ class BuildExtTestCase(TempdirManager, # format the target value as defined in the Apple # Availability Macros. We can't use the macro names since # at least one value we test with will not exist yet. - if target[1] < 10: + if target[:2] < (10, 10): # for 10.1 through 10.9.x -> "10n0" target = '%02d%01d0' % target else: -- cgit v1.2.1 From 22c5f7de3563a0a6babbaba6d746810570cb7818 Mon Sep 17 00:00:00 2001 From: FX Coudert Date: Thu, 3 Dec 2020 04:20:18 +0100 Subject: bpo-42504: fix for MACOSX_DEPLOYMENT_TARGET=11 (GH-23556) macOS releases numbering has changed as of macOS 11 Big Sur. Previously, major releases were of the form 10.x, 10.x+1, 10.x+2, etc; as of Big Sur, they are now x, x+1, etc, so, for example, 10.15, 10.15.1, ..., 10.15.7, 11, 11.0.1, 11.1, ..., 12, 12.1, etc. Allow Python to build with single-digit deployment target values. Patch provided by FX Coudert. --- distutils/tests/test_build_ext.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index 866dad46..30454074 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -456,7 +456,7 @@ class BuildExtTestCase(TempdirManager, deptarget = sysconfig.get_config_var('MACOSX_DEPLOYMENT_TARGET') if deptarget: # increment the minor version number (i.e. 10.6 -> 10.7) - deptarget = [int(x) for x in deptarget.split('.')] + deptarget = [int(x) for x in str(deptarget).split('.')] deptarget[-1] += 1 deptarget = '.'.join(str(i) for i in deptarget) self._try_compile_deployment_target('<', deptarget) @@ -489,7 +489,7 @@ class BuildExtTestCase(TempdirManager, # get the deployment target that the interpreter was built with target = sysconfig.get_config_var('MACOSX_DEPLOYMENT_TARGET') - target = tuple(map(int, target.split('.')[0:2])) + target = tuple(map(int, str(target).split('.')[0:2])) # format the target value as defined in the Apple # Availability Macros. We can't use the macro names since # at least one value we test with will not exist yet. @@ -498,7 +498,11 @@ class BuildExtTestCase(TempdirManager, target = '%02d%01d0' % target else: # for 10.10 and beyond -> "10nn00" - target = '%02d%02d00' % target + if len(target) >= 2: + target = '%02d%02d00' % target + else: + # 11 and later can have no minor version (11 instead of 11.0) + target = '%02d0000' % target deptarget_ext = Extension( 'deptarget', [deptarget_c], -- cgit v1.2.1 From ecf5748f1322ca8aba78f38d6d67b8a383adb6e7 Mon Sep 17 00:00:00 2001 From: Ronald Oussoren Date: Mon, 1 Feb 2021 04:29:44 +0100 Subject: bpo-42504: Ensure that get_config_var('MACOSX_DEPLOYMENT_TARGET') is a string (GH-24341) * bpo-42504: Ensure that get_config_var('MACOSX_DEPLOYMENT_TARGET') is a string --- distutils/tests/test_build_ext.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/distutils/tests/test_build_ext.py b/distutils/tests/test_build_ext.py index 30454074..f101e39a 100644 --- a/distutils/tests/test_build_ext.py +++ b/distutils/tests/test_build_ext.py @@ -456,7 +456,7 @@ class BuildExtTestCase(TempdirManager, deptarget = sysconfig.get_config_var('MACOSX_DEPLOYMENT_TARGET') if deptarget: # increment the minor version number (i.e. 10.6 -> 10.7) - deptarget = [int(x) for x in str(deptarget).split('.')] + deptarget = [int(x) for x in deptarget.split('.')] deptarget[-1] += 1 deptarget = '.'.join(str(i) for i in deptarget) self._try_compile_deployment_target('<', deptarget) @@ -489,7 +489,7 @@ class BuildExtTestCase(TempdirManager, # get the deployment target that the interpreter was built with target = sysconfig.get_config_var('MACOSX_DEPLOYMENT_TARGET') - target = tuple(map(int, str(target).split('.')[0:2])) + target = tuple(map(int, target.split('.')[0:2])) # format the target value as defined in the Apple # Availability Macros. We can't use the macro names since # at least one value we test with will not exist yet. -- cgit v1.2.1 From 6039303f8890f4321f83803e7c960dd057bb8fd2 Mon Sep 17 00:00:00 2001 From: Naveen M K Date: Fri, 31 Dec 2021 13:30:10 +0530 Subject: util.get_host_platform: use sysconfig.get_platform sysconfig module has a copy of this function in `sysconfig.get_platform` and would be better to use that directly. This should help distributors to patch that function in a single place, ie sysconfig. Co-authored-by: Christoph Reiter Signed-off-by: Naveen M K --- distutils/tests/test_util.py | 120 +++++-------------------------------------- distutils/util.py | 81 ++--------------------------- 2 files changed, 18 insertions(+), 183 deletions(-) diff --git a/distutils/tests/test_util.py b/distutils/tests/test_util.py index 12469e3d..bfbc2ec6 100644 --- a/distutils/tests/test_util.py +++ b/distutils/tests/test_util.py @@ -2,6 +2,7 @@ import os import sys import unittest +import sysconfig as stdlib_sysconfig from copy import copy from test.support import run_unittest from unittest import mock @@ -10,12 +11,10 @@ from distutils.errors import DistutilsPlatformError, DistutilsByteCompileError from distutils.util import (get_platform, convert_path, change_root, check_environ, split_quoted, strtobool, rfc822_escape, byte_compile, - grok_environment_error) + grok_environment_error, get_host_platform) from distutils import util # used to patch _environ_checked -from distutils.sysconfig import get_config_vars from distutils import sysconfig from distutils.tests import support -import _osx_support class UtilTestCase(support.EnvironGuard, unittest.TestCase): @@ -63,110 +62,19 @@ class UtilTestCase(support.EnvironGuard, unittest.TestCase): def _get_uname(self): return self._uname - def test_get_platform(self): - - # windows XP, 32bits - os.name = 'nt' - sys.version = ('2.4.4 (#71, Oct 18 2006, 08:34:43) ' - '[MSC v.1310 32 bit (Intel)]') - sys.platform = 'win32' - self.assertEqual(get_platform(), 'win32') - - # windows XP, amd64 - os.name = 'nt' - sys.version = ('2.4.4 (#71, Oct 18 2006, 08:34:43) ' - '[MSC v.1310 32 bit (Amd64)]') - sys.platform = 'win32' - self.assertEqual(get_platform(), 'win-amd64') - - # macbook - os.name = 'posix' - sys.version = ('2.5 (r25:51918, Sep 19 2006, 08:49:13) ' - '\n[GCC 4.0.1 (Apple Computer, Inc. build 5341)]') - sys.platform = 'darwin' - self._set_uname(('Darwin', 'macziade', '8.11.1', - ('Darwin Kernel Version 8.11.1: ' - 'Wed Oct 10 18:23:28 PDT 2007; ' - 'root:xnu-792.25.20~1/RELEASE_I386'), 'i386')) - _osx_support._remove_original_values(get_config_vars()) - get_config_vars()['MACOSX_DEPLOYMENT_TARGET'] = '10.3' - - get_config_vars()['CFLAGS'] = ('-fno-strict-aliasing -DNDEBUG -g ' - '-fwrapv -O3 -Wall -Wstrict-prototypes') - - cursize = sys.maxsize - sys.maxsize = (2 ** 31)-1 - try: - self.assertEqual(get_platform(), 'macosx-10.3-i386') - finally: - sys.maxsize = cursize - - # macbook with fat binaries (fat, universal or fat64) - _osx_support._remove_original_values(get_config_vars()) - get_config_vars()['MACOSX_DEPLOYMENT_TARGET'] = '10.4' - get_config_vars()['CFLAGS'] = ('-arch ppc -arch i386 -isysroot ' - '/Developer/SDKs/MacOSX10.4u.sdk ' - '-fno-strict-aliasing -fno-common ' - '-dynamic -DNDEBUG -g -O3') - - self.assertEqual(get_platform(), 'macosx-10.4-fat') - - _osx_support._remove_original_values(get_config_vars()) - os.environ['MACOSX_DEPLOYMENT_TARGET'] = '10.1' - self.assertEqual(get_platform(), 'macosx-10.4-fat') - + def test_get_host_platform(self): + self.assertEqual(get_host_platform(), stdlib_sysconfig.get_platform()) - _osx_support._remove_original_values(get_config_vars()) - get_config_vars()['CFLAGS'] = ('-arch x86_64 -arch i386 -isysroot ' - '/Developer/SDKs/MacOSX10.4u.sdk ' - '-fno-strict-aliasing -fno-common ' - '-dynamic -DNDEBUG -g -O3') - - self.assertEqual(get_platform(), 'macosx-10.4-intel') - - _osx_support._remove_original_values(get_config_vars()) - get_config_vars()['CFLAGS'] = ('-arch x86_64 -arch ppc -arch i386 -isysroot ' - '/Developer/SDKs/MacOSX10.4u.sdk ' - '-fno-strict-aliasing -fno-common ' - '-dynamic -DNDEBUG -g -O3') - self.assertEqual(get_platform(), 'macosx-10.4-fat3') - - _osx_support._remove_original_values(get_config_vars()) - get_config_vars()['CFLAGS'] = ('-arch ppc64 -arch x86_64 -arch ppc -arch i386 -isysroot ' - '/Developer/SDKs/MacOSX10.4u.sdk ' - '-fno-strict-aliasing -fno-common ' - '-dynamic -DNDEBUG -g -O3') - self.assertEqual(get_platform(), 'macosx-10.4-universal') - - _osx_support._remove_original_values(get_config_vars()) - get_config_vars()['CFLAGS'] = ('-arch x86_64 -arch ppc64 -isysroot ' - '/Developer/SDKs/MacOSX10.4u.sdk ' - '-fno-strict-aliasing -fno-common ' - '-dynamic -DNDEBUG -g -O3') - - self.assertEqual(get_platform(), 'macosx-10.4-fat64') - - for arch in ('ppc', 'i386', 'x86_64', 'ppc64'): - _osx_support._remove_original_values(get_config_vars()) - get_config_vars()['CFLAGS'] = ('-arch %s -isysroot ' - '/Developer/SDKs/MacOSX10.4u.sdk ' - '-fno-strict-aliasing -fno-common ' - '-dynamic -DNDEBUG -g -O3'%(arch,)) - - self.assertEqual(get_platform(), 'macosx-10.4-%s'%(arch,)) - - - # linux debian sarge - os.name = 'posix' - sys.version = ('2.3.5 (#1, Jul 4 2007, 17:28:59) ' - '\n[GCC 4.1.2 20061115 (prerelease) (Debian 4.1.1-21)]') - sys.platform = 'linux2' - self._set_uname(('Linux', 'aglae', '2.6.21.1dedibox-r7', - '#1 Mon Apr 30 17:25:38 CEST 2007', 'i686')) - - self.assertEqual(get_platform(), 'linux-i686') - - # XXX more platforms to tests here + def test_get_platform(self): + with unittest.mock.patch('os.name', 'nt'): + with unittest.mock.patch.dict('os.environ', {'VSCMD_ARG_TGT_ARCH': 'x86'}): + self.assertEqual(get_platform(), 'win32') + with unittest.mock.patch.dict('os.environ', {'VSCMD_ARG_TGT_ARCH': 'x64'}): + self.assertEqual(get_platform(), 'win-amd64') + with unittest.mock.patch.dict('os.environ', {'VSCMD_ARG_TGT_ARCH': 'arm'}): + self.assertEqual(get_platform(), 'win-arm32') + with unittest.mock.patch.dict('os.environ', {'VSCMD_ARG_TGT_ARCH': 'arm64'}): + self.assertEqual(get_platform(), 'win-arm64') def test_convert_path(self): # linux/mac diff --git a/distutils/util.py b/distutils/util.py index ac6d446d..50633408 100644 --- a/distutils/util.py +++ b/distutils/util.py @@ -9,6 +9,7 @@ import re import importlib.util import string import sys +import sysconfig from distutils.errors import DistutilsPlatformError from distutils.dep_util import newer from distutils.spawn import spawn @@ -18,84 +19,10 @@ from .py35compat import _optim_args_from_interpreter_flags def get_host_platform(): - """Return a string that identifies the current platform. This is used mainly to - distinguish platform-specific build directories and platform-specific built - distributions. Typically includes the OS name and version and the - architecture (as supplied by 'os.uname()'), although the exact information - included depends on the OS; eg. on Linux, the kernel version isn't - particularly important. - - Examples of returned values: - linux-i586 - linux-alpha (?) - solaris-2.6-sun4u - - Windows will return one of: - win-amd64 (64bit Windows on AMD64 (aka x86_64, Intel64, EM64T, etc) - win32 (all others - specifically, sys.platform is returned) - - For other non-POSIX platforms, currently just returns 'sys.platform'. - + """Returns the same as `get_platform()` from sysconfig. """ - if os.name == 'nt': - if 'amd64' in sys.version.lower(): - return 'win-amd64' - if '(arm)' in sys.version.lower(): - return 'win-arm32' - if '(arm64)' in sys.version.lower(): - return 'win-arm64' - return sys.platform - - # Set for cross builds explicitly - if "_PYTHON_HOST_PLATFORM" in os.environ: - return os.environ["_PYTHON_HOST_PLATFORM"] - - if os.name != "posix" or not hasattr(os, 'uname'): - # XXX what about the architecture? NT is Intel or Alpha, - # Mac OS is M68k or PPC, etc. - return sys.platform - - # Try to distinguish various flavours of Unix - - (osname, host, release, version, machine) = os.uname() - - # Convert the OS name to lowercase, remove '/' characters, and translate - # spaces (for "Power Macintosh") - osname = osname.lower().replace('/', '') - machine = machine.replace(' ', '_') - machine = machine.replace('/', '-') - - if osname[:5] == "linux": - # At least on Linux/Intel, 'machine' is the processor -- - # i386, etc. - # XXX what about Alpha, SPARC, etc? - return "%s-%s" % (osname, machine) - elif osname[:5] == "sunos": - if release[0] >= "5": # SunOS 5 == Solaris 2 - osname = "solaris" - release = "%d.%s" % (int(release[0]) - 3, release[2:]) - # We can't use "platform.architecture()[0]" because a - # bootstrap problem. We use a dict to get an error - # if some suspicious happens. - bitness = {2147483647:"32bit", 9223372036854775807:"64bit"} - machine += ".%s" % bitness[sys.maxsize] - # fall through to standard osname-release-machine representation - elif osname[:3] == "aix": - from .py38compat import aix_platform - return aix_platform(osname, version, release) - elif osname[:6] == "cygwin": - osname = "cygwin" - rel_re = re.compile (r'[\d.]+', re.ASCII) - m = rel_re.match(release) - if m: - release = m.group() - elif osname[:6] == "darwin": - import _osx_support, distutils.sysconfig - osname, release, machine = _osx_support.get_platform_osx( - distutils.sysconfig.get_config_vars(), - osname, release, machine) - - return "%s-%s-%s" % (osname, release, machine) + + return sysconfig.get_platform() def get_platform(): if os.name == 'nt': -- cgit v1.2.1 From 41fa663c9da93ca1af98ce2ae397c02e4b3062e8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 2 Jan 2022 22:59:50 -0500 Subject: Add another exception for the exclusion for pip. Fixes #2993. --- _distutils_hack/__init__.py | 10 ++++++++++ changelog.d/2993.change.rst | 1 + 2 files changed, 11 insertions(+) create mode 100644 changelog.d/2993.change.rst diff --git a/_distutils_hack/__init__.py b/_distutils_hack/__init__.py index c0170d09..4745f8b9 100644 --- a/_distutils_hack/__init__.py +++ b/_distutils_hack/__init__.py @@ -116,6 +116,8 @@ class DistutilsMetaFinder: """ if self.pip_imported_during_build(): return + if self.is_get_pip(): + return clear_distutils() self.spec_for_distutils = lambda: None @@ -130,6 +132,14 @@ class DistutilsMetaFinder: for frame, line in traceback.walk_stack(None) ) + @classmethod + def is_get_pip(cls): + """ + Detect if get-pip is being invoked. Ref #2993. + """ + import __main__ + return os.path.basename(__main__.__file__) == 'get-pip.py' + @staticmethod def frame_file_is_setup(frame): """ diff --git a/changelog.d/2993.change.rst b/changelog.d/2993.change.rst new file mode 100644 index 00000000..cd528d57 --- /dev/null +++ b/changelog.d/2993.change.rst @@ -0,0 +1 @@ +In _distutils_hack, bypass the distutils exception for pip when get-pip is being invoked, because it imports setuptools. -- cgit v1.2.1 From ab36d21ed32b9414515027c942adac9b59a7881d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 3 Jan 2022 12:05:45 -0500 Subject: Add DictStack from jaraco.collections. --- distutils/_collections.py | 51 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 distutils/_collections.py diff --git a/distutils/_collections.py b/distutils/_collections.py new file mode 100644 index 00000000..7daff55e --- /dev/null +++ b/distutils/_collections.py @@ -0,0 +1,51 @@ +import collections +import itertools + + +# from jaraco.collections 3.5 +class DictStack(list, collections.abc.Mapping): + """ + A stack of dictionaries that behaves as a view on those dictionaries, + giving preference to the last. + + >>> stack = DictStack([dict(a=1, c=2), dict(b=2, a=2)]) + >>> stack['a'] + 2 + >>> stack['b'] + 2 + >>> stack['c'] + 2 + >>> stack.push(dict(a=3)) + >>> stack['a'] + 3 + >>> set(stack.keys()) == set(['a', 'b', 'c']) + True + >>> set(stack.items()) == set([('a', 3), ('b', 2), ('c', 2)]) + True + >>> dict(**stack) == dict(stack) == dict(a=3, c=2, b=2) + True + >>> d = stack.pop() + >>> stack['a'] + 2 + >>> d = stack.pop() + >>> stack['a'] + 1 + >>> stack.get('b', None) + >>> 'c' in stack + True + """ + + def __iter__(self): + dicts = list.__iter__(self) + return iter(set(itertools.chain.from_iterable(c.keys() for c in dicts))) + + def __getitem__(self, key): + for scope in reversed(self): + if key in scope: + return scope[key] + raise KeyError(key) + + push = list.append + + def __contains__(self, other): + return collections.abc.Mapping.__contains__(self, other) -- cgit v1.2.1 From 1560a1fe4e56e00392d3658a59e6fc7238e26a8a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 3 Jan 2022 12:10:40 -0500 Subject: In install command, honor config_vars from sysconfig. Fixes pypa/distutils#103. --- distutils/command/install.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/distutils/command/install.py b/distutils/command/install.py index cdcc0528..511938f4 100644 --- a/distutils/command/install.py +++ b/distutils/command/install.py @@ -17,6 +17,7 @@ from distutils.file_util import write_file from distutils.util import convert_path, subst_vars, change_root from distutils.util import get_platform from distutils.errors import DistutilsOptionError +from .. import _collections from site import USER_BASE from site import USER_SITE @@ -394,7 +395,8 @@ class install(Command): except AttributeError: # sys.abiflags may not be defined on all platforms. abiflags = '' - self.config_vars = {'dist_name': self.distribution.get_name(), + local_vars = { + 'dist_name': self.distribution.get_name(), 'dist_version': self.distribution.get_version(), 'dist_fullname': self.distribution.get_fullname(), 'py_version': py_version, @@ -408,12 +410,14 @@ class install(Command): 'platlibdir': getattr(sys, 'platlibdir', 'lib'), 'implementation_lower': _get_implementation().lower(), 'implementation': _get_implementation(), - 'platsubdir': sysconfig.get_config_var('platsubdir'), } if HAS_USER_SITE: - self.config_vars['userbase'] = self.install_userbase - self.config_vars['usersite'] = self.install_usersite + local_vars['userbase'] = self.install_userbase + local_vars['usersite'] = self.install_usersite + + self.config_vars = _collections.DictStack( + [sysconfig.get_config_vars(), local_vars]) self.expand_basedirs() @@ -421,15 +425,13 @@ class install(Command): # Now define config vars for the base directories so we can expand # everything else. - self.config_vars['base'] = self.install_base - self.config_vars['platbase'] = self.install_platbase - self.config_vars['installed_base'] = ( - sysconfig.get_config_vars()['installed_base']) + local_vars['base'] = self.install_base + local_vars['platbase'] = self.install_platbase if DEBUG: from pprint import pprint print("config vars:") - pprint(self.config_vars) + pprint(dict(self.config_vars)) # Expand "~" and configuration variables in the installation # directories. -- cgit v1.2.1 From b028d4636e6a9cb1737758f42f321340bf5abc25 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 3 Jan 2022 12:44:56 -0500 Subject: Use 'dict()' instead of '.copy()', for compatibility with DictStack. --- setuptools/command/easy_install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index fb34d10e..aad5794a 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -1350,7 +1350,7 @@ class easy_install(Command): if self.prefix: # Set default install_dir/scripts from --prefix - config_vars = config_vars.copy() + config_vars = dict(config_vars) config_vars['base'] = self.prefix scheme = self.INSTALL_SCHEMES.get(os.name, self.DEFAULT_SCHEME) for attr, val in scheme.items(): -- cgit v1.2.1 From 79209df4522eaa32848dfe2476aa14eba77b924c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 3 Jan 2022 12:46:26 -0500 Subject: Update changelog. --- changelog.d/2989.misc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/2989.misc.rst diff --git a/changelog.d/2989.misc.rst b/changelog.d/2989.misc.rst new file mode 100644 index 00000000..489a7823 --- /dev/null +++ b/changelog.d/2989.misc.rst @@ -0,0 +1 @@ +Merge with pypa/distutils@1560a1f. Includes fix for config vars missing from sysconfig. -- cgit v1.2.1 From 6092cdcbd659110e613e9841cdd5a0c43e61f33b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 3 Jan 2022 12:46:52 -0500 Subject: =?UTF-8?q?Bump=20version:=2060.2.0=20=E2=86=92=2060.3.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 13 +++++++++++++ changelog.d/2989.misc.rst | 1 - changelog.d/2993.change.rst | 1 - setup.cfg | 2 +- 5 files changed, 15 insertions(+), 4 deletions(-) delete mode 100644 changelog.d/2989.misc.rst delete mode 100644 changelog.d/2993.change.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index f696b076..36b71269 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 60.2.0 +current_version = 60.3.0 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index 59b29fb5..f6f8533b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,16 @@ +v60.3.0 +------- + + +Changes +^^^^^^^ +* #2993: In _distutils_hack, bypass the distutils exception for pip when get-pip is being invoked, because it imports setuptools. + +Misc +^^^^ +* #2989: Merge with pypa/distutils@1560a1f. Includes fix for config vars missing from sysconfig. + + v60.2.0 ------- diff --git a/changelog.d/2989.misc.rst b/changelog.d/2989.misc.rst deleted file mode 100644 index 489a7823..00000000 --- a/changelog.d/2989.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Merge with pypa/distutils@1560a1f. Includes fix for config vars missing from sysconfig. diff --git a/changelog.d/2993.change.rst b/changelog.d/2993.change.rst deleted file mode 100644 index cd528d57..00000000 --- a/changelog.d/2993.change.rst +++ /dev/null @@ -1 +0,0 @@ -In _distutils_hack, bypass the distutils exception for pip when get-pip is being invoked, because it imports setuptools. diff --git a/setup.cfg b/setup.cfg index 75a41c84..0af9cd5e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 60.2.0 +version = 60.3.0 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From 057adb220b6018ff5ad175a5f83e6173cc83f3ad Mon Sep 17 00:00:00 2001 From: Christoph Reiter Date: Mon, 3 Jan 2022 20:38:17 +0100 Subject: util.get_host_platform: restore Python 3.9 behaviour for Python 3.7/3.8 While get_host_platform() in distutils and sysconfig.get_platform() are more or less the same for the same Python version, this extracted distutils containing the Python 3.9 logic was exposed via setuptools to users using older Python versions. To avoid any breakage make sure to restore the 3.9 behaviour for 3.7/3.8. For any newer version get_host_platform() just delegates to sysconfig as before. Also partly revert the docstring to be more generic and no longer mention sysconfig.get_platform(), so no one depends on them matching. --- distutils/tests/test_util.py | 9 ++++++++- distutils/util.py | 23 ++++++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/distutils/tests/test_util.py b/distutils/tests/test_util.py index bfbc2ec6..2738388e 100644 --- a/distutils/tests/test_util.py +++ b/distutils/tests/test_util.py @@ -63,7 +63,14 @@ class UtilTestCase(support.EnvironGuard, unittest.TestCase): return self._uname def test_get_host_platform(self): - self.assertEqual(get_host_platform(), stdlib_sysconfig.get_platform()) + with unittest.mock.patch('os.name', 'nt'): + with unittest.mock.patch('sys.version', '... [... (ARM64)]'): + self.assertEqual(get_host_platform(), 'win-arm64') + with unittest.mock.patch('sys.version', '... [... (ARM)]'): + self.assertEqual(get_host_platform(), 'win-arm32') + + with unittest.mock.patch('sys.version_info', (3, 9, 0, 'final', 0)): + self.assertEqual(get_host_platform(), stdlib_sysconfig.get_platform()) def test_get_platform(self): with unittest.mock.patch('os.name', 'nt'): diff --git a/distutils/util.py b/distutils/util.py index 50633408..6d506d7e 100644 --- a/distutils/util.py +++ b/distutils/util.py @@ -19,9 +19,30 @@ from .py35compat import _optim_args_from_interpreter_flags def get_host_platform(): - """Returns the same as `get_platform()` from sysconfig. + """Return a string that identifies the current platform. This is used mainly to + distinguish platform-specific build directories and platform-specific built + distributions. """ + # We initially exposed platforms as defined in Python 3.9 + # even with older Python versions when distutils was split out. + # Now that we delegate to stdlib sysconfig we need to restore this + # in case anyone has started to depend on it. + + if sys.version_info < (3, 8): + if os.name == 'nt': + if '(arm)' in sys.version.lower(): + return 'win-arm32' + if '(arm64)' in sys.version.lower(): + return 'win-arm64' + + if sys.version_info < (3, 9): + if os.name == "posix" and hasattr(os, 'uname'): + osname, host, release, version, machine = os.uname() + if osname[:3] == "aix": + from .py38compat import aix_platform + return aix_platform(osname, version, release) + return sysconfig.get_platform() def get_platform(): -- cgit v1.2.1 From f441b60c6baa18da45ed205ca506fe2b5788c839 Mon Sep 17 00:00:00 2001 From: Giulio Procopio <61784957+Giuppox@users.noreply.github.com> Date: Wed, 5 Jan 2022 15:17:48 +0100 Subject: [maint] removed unneded exception inclusion --- distutils/spawn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distutils/spawn.py b/distutils/spawn.py index 6e1c89f1..b2d10e39 100644 --- a/distutils/spawn.py +++ b/distutils/spawn.py @@ -10,7 +10,7 @@ import sys import os import subprocess -from distutils.errors import DistutilsPlatformError, DistutilsExecError +from distutils.errors import DistutilsExecError from distutils.debug import DEBUG from distutils import log -- cgit v1.2.1 From 788cc159e4d734b972e22ccf06dbcd8ed8f94885 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 5 Jan 2022 18:29:29 -0500 Subject: Update DictStack implementation from jaraco.collections 3.5.1 --- distutils/_collections.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/distutils/_collections.py b/distutils/_collections.py index 7daff55e..98fce800 100644 --- a/distutils/_collections.py +++ b/distutils/_collections.py @@ -2,7 +2,7 @@ import collections import itertools -# from jaraco.collections 3.5 +# from jaraco.collections 3.5.1 class DictStack(list, collections.abc.Mapping): """ A stack of dictionaries that behaves as a view on those dictionaries, @@ -15,6 +15,8 @@ class DictStack(list, collections.abc.Mapping): 2 >>> stack['c'] 2 + >>> len(stack) + 3 >>> stack.push(dict(a=3)) >>> stack['a'] 3 @@ -40,7 +42,7 @@ class DictStack(list, collections.abc.Mapping): return iter(set(itertools.chain.from_iterable(c.keys() for c in dicts))) def __getitem__(self, key): - for scope in reversed(self): + for scope in reversed(tuple(list.__iter__(self))): if key in scope: return scope[key] raise KeyError(key) @@ -49,3 +51,6 @@ class DictStack(list, collections.abc.Mapping): def __contains__(self, other): return collections.abc.Mapping.__contains__(self, other) + + def __len__(self): + return len(list(iter(self))) -- cgit v1.2.1 From 4a5a4ddfc432f1fc569aa5a76b2b68ebb4b2062f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 5 Jan 2022 19:50:29 -0500 Subject: Update changelog. --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index f6f8533b..51fab99e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,7 +8,7 @@ Changes Misc ^^^^ -* #2989: Merge with pypa/distutils@1560a1f. Includes fix for config vars missing from sysconfig. +* #2989: Merge with pypa/distutils@788cc159. Includes fix for config vars missing from sysconfig. v60.2.0 -- cgit v1.2.1 From 387073fb4eab8f6680fa4382ed008d86103cf132 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 5 Jan 2022 23:17:45 -0500 Subject: Add test capturing failure. Ref #3002. --- setuptools/tests/test_distutils_adoption.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/setuptools/tests/test_distutils_adoption.py b/setuptools/tests/test_distutils_adoption.py index 27759b1d..ced41d29 100644 --- a/setuptools/tests/test_distutils_adoption.py +++ b/setuptools/tests/test_distutils_adoption.py @@ -89,3 +89,13 @@ def test_distutils_local(venv): env = dict(SETUPTOOLS_USE_DISTUTILS='local') assert venv.name in find_distutils(venv, env=env).split(os.sep) assert count_meta_path(venv, env=env) <= 1 + + +def test_pip_import(venv): + """ + Ensure pip can be imported with the hack installed. + Regression test for #3002. + """ + env = dict(SETUPTOOLS_USE_DISTUTILS='local') + cmd = ['python', '-c', 'import pip'] + popen_text(venv.run)(cmd, env=env) -- cgit v1.2.1 From 2504699e5f9acfdd1438512173f90b3f61114095 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 5 Jan 2022 23:21:43 -0500 Subject: Suppress AttributeError when detecting get-pip. Fixes #3002. --- _distutils_hack/__init__.py | 12 ++++++++++++ changelog.d/3002.misc.rst | 1 + 2 files changed, 13 insertions(+) create mode 100644 changelog.d/3002.misc.rst diff --git a/_distutils_hack/__init__.py b/_distutils_hack/__init__.py index 4745f8b9..75bc4463 100644 --- a/_distutils_hack/__init__.py +++ b/_distutils_hack/__init__.py @@ -73,6 +73,17 @@ def do_override(): ensure_local_distutils() +class suppress(contextlib.suppress, contextlib.ContextDecorator): + """ + A version of contextlib.suppress with decorator support. + + >>> @suppress(KeyError) + ... def key_error(): + ... {}[''] + >>> key_error() + """ + + class DistutilsMetaFinder: def find_spec(self, fullname, path, target=None): if path is not None: @@ -133,6 +144,7 @@ class DistutilsMetaFinder: ) @classmethod + @suppress(AttributeError) def is_get_pip(cls): """ Detect if get-pip is being invoked. Ref #2993. diff --git a/changelog.d/3002.misc.rst b/changelog.d/3002.misc.rst new file mode 100644 index 00000000..ed2c3936 --- /dev/null +++ b/changelog.d/3002.misc.rst @@ -0,0 +1 @@ +Suppress AttributeError when detecting get-pip. -- cgit v1.2.1 From 6a1a7e61343f054eda9d4c572ef723752dda7026 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 5 Jan 2022 23:50:47 -0500 Subject: Remove the env, as the test suite runs local by default but also tests stdlib. --- setuptools/tests/test_distutils_adoption.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/setuptools/tests/test_distutils_adoption.py b/setuptools/tests/test_distutils_adoption.py index ced41d29..cb26b77a 100644 --- a/setuptools/tests/test_distutils_adoption.py +++ b/setuptools/tests/test_distutils_adoption.py @@ -93,9 +93,8 @@ def test_distutils_local(venv): def test_pip_import(venv): """ - Ensure pip can be imported with the hack installed. + Ensure pip can be imported. Regression test for #3002. """ - env = dict(SETUPTOOLS_USE_DISTUTILS='local') cmd = ['python', '-c', 'import pip'] - popen_text(venv.run)(cmd, env=env) + popen_text(venv.run)(cmd) -- cgit v1.2.1 From c51f512d8c58516595f7aba7100326e7fdabb13a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 5 Jan 2022 23:52:40 -0500 Subject: Extract the SYSTEMROOT handler and document it. --- setuptools/tests/test_distutils_adoption.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/setuptools/tests/test_distutils_adoption.py b/setuptools/tests/test_distutils_adoption.py index cb26b77a..1e73f9aa 100644 --- a/setuptools/tests/test_distutils_adoption.py +++ b/setuptools/tests/test_distutils_adoption.py @@ -42,12 +42,24 @@ def popen_text(call): if sys.version_info < (3, 7) else functools.partial(call, text=True) +def win_sr(env): + """ + On Windows, SYSTEMROOT must be present to avoid + + > Fatal Python error: _Py_HashRandomization_Init: failed to + > get random numbers to initialize Python + """ + if env is None: + return + if platform.system() == 'Windows': + env['SYSTEMROOT'] = os.environ['SYSTEMROOT'] + return env + + def find_distutils(venv, imports='distutils', env=None, **kwargs): py_cmd = 'import {imports}; print(distutils.__file__)'.format(**locals()) cmd = ['python', '-c', py_cmd] - if platform.system() == 'Windows': - env['SYSTEMROOT'] = os.environ['SYSTEMROOT'] - return popen_text(venv.run)(cmd, env=env, **kwargs) + return popen_text(venv.run)(cmd, env=win_sr(env), **kwargs) def count_meta_path(venv, env=None): @@ -58,7 +70,7 @@ def count_meta_path(venv, env=None): print(len(list(filter(is_distutils, sys.meta_path)))) """) cmd = ['python', '-c', py_cmd] - return int(popen_text(venv.run)(cmd, env=env)) + return int(popen_text(venv.run)(cmd, env=win_sr(env))) def test_distutils_stdlib(venv): -- cgit v1.2.1 From 732731afd1e16145f9bf197b28b1e199b3f1fe9e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Jan 2022 10:52:55 -0500 Subject: =?UTF-8?q?Bump=20version:=2060.3.0=20=E2=86=92=2060.3.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 9 +++++++++ changelog.d/3002.misc.rst | 1 - setup.cfg | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/3002.misc.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 36b71269..ca91c30e 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 60.3.0 +current_version = 60.3.1 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index 51fab99e..2c52ecfc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,12 @@ +v60.3.1 +------- + + +Misc +^^^^ +* #3002: Suppress AttributeError when detecting get-pip. + + v60.3.0 ------- diff --git a/changelog.d/3002.misc.rst b/changelog.d/3002.misc.rst deleted file mode 100644 index ed2c3936..00000000 --- a/changelog.d/3002.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Suppress AttributeError when detecting get-pip. diff --git a/setup.cfg b/setup.cfg index 0af9cd5e..a726d2f6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 60.3.0 +version = 60.3.1 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From 5c7a84c63ae1d7f93d8cc0008311d47906a24117 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 6 Jan 2022 16:05:57 +0000 Subject: Rename `docs/{images/README => artwork}.rst` Unfortunately sphinx do not accept `.rst` files inside folders that are added to the `html_static_path`. (Adding `docs/images` to `html_static_path` will be required in a follow up commit to adopt the `sphinx-favicon` extension) --- docs/artwork.rst | 119 +++++++++++++++++++++++++++++++++++++++++++++++++ docs/images/README.rst | 119 ------------------------------------------------- docs/index.rst | 2 +- 3 files changed, 120 insertions(+), 120 deletions(-) create mode 100644 docs/artwork.rst delete mode 100644 docs/images/README.rst diff --git a/docs/artwork.rst b/docs/artwork.rst new file mode 100644 index 00000000..907e62a6 --- /dev/null +++ b/docs/artwork.rst @@ -0,0 +1,119 @@ +======= +Artwork +======= + +.. figure:: images/logo-over-white.svg + :align: center + + Setuptools logo, designed in 2021 by `Anderson Bravalheri`_ + +Elements of Design +================== + +The main colours of the design are a dark pastel azure (``#336790``) and a pale +orange (``#E5B62F``), referred in this document simply as "blue" and "yellow" +respectively. The text uses the *Monoid* typeface, an open source webfont that +was developed by Andreas Larsen and contributors in 2015 and is distributed +under the MIT or SIL licenses (more information at +https://github.com/larsenwork/monoid) + + +Usage +===== + +The preferred way of using the setuptools logo is over a white (or light) +background. Alternatively, the following options can be considered, depending +on the circumstances: + +- *"negative"* design - for dark backgrounds (e.g. website displayed in "dark + mode"): the white colour (``#FFFFFF``) of the background and the "blue" + (``#336790``) colour of the design can be swapped. +- *"monochrome"* - when colours are not available (e.g. black and white printed + media): a completely black or white version of the logo can also be used. +- *"banner"* mode: the symbol and text can be used alongside depending on the + available space. + +The following image illustrate these alternatives: + +.. image:: images/logo-demo.svg + :align: center + +Please refer to the SVG files in the `setuptools repository`_ for the specific +shapes and proportions between the elements of the design. + + +Working with the Design +======================= + +The `setuptools repository`_ contains a series of vector representations of the +design under the ``docs/images`` directory. These representations can be +manipulated via any graphic editor that support SVG files, +however the free and open-source software Inkscape_ is recommended for maximum +compatibility. + +When selecting the right file to work with, file names including +``editable-inkscape`` indicate "more editable" elements (e.g. editable text), +while the others prioritise SVG paths for maximum reproducibility. + +Also notice that you might have to `install the correct fonts`_ to be able to +visualise or edit some of the designs. + + +Inspiration +=========== + +This design was inspired by :user:`cajhne`'s `original proposal`_ and the +ancient symbol of the ouroboros_. +It features a snake moving in a circular trajectory not only as a reference to +the Python programming language but also to the `wheel package format`_ as one +of the distribution formats supported by setuptools. +The shape of the snake also resembles a cog, which together with the hammer is +a nod to the two words that compose the name of the project. + + +License +======= + + +This logo, design variations or a modified version may be used by anyone to +refer to setuptools, but does not indicate endorsement by the project. + +Redistribution, usage and derivative works are permitted under the same license +used by the setuptools software (MIT): + +.. code-block:: text + + Copyright (c) Anderson Bravalheri + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. + + THE USAGE OF THIS LOGO AND ARTWORK DOES NOT INDICATE ENDORSEMENT BY THE + SETUPTOOLS PROJECT. + +Whenever possible, please make the image a link to +https://github.com/pypa/setuptools or https://setuptools.pypa.io. + + +.. _Anderson Bravalheri: https://github.com/abravalheri +.. _Inkscape: https://inkscape.org +.. _setuptools repository: https://github.com/pypa/setuptools +.. _install the correct fonts: https://wiki.inkscape.org/wiki/Installing_fonts +.. _original proposal: https://github.com/pypa/setuptools/issues/2227#issuecomment-653628344 +.. _wheel package format: https://www.python.org/dev/peps/pep-0427/ +.. _ouroboros: https://en.wikipedia.org/wiki/Ouroboros diff --git a/docs/images/README.rst b/docs/images/README.rst deleted file mode 100644 index 55a5a602..00000000 --- a/docs/images/README.rst +++ /dev/null @@ -1,119 +0,0 @@ -======= -Artwork -======= - -.. figure:: logo-over-white.svg - :align: center - - Setuptools logo, designed in 2021 by `Anderson Bravalheri`_ - -Elements of Design -================== - -The main colours of the design are a dark pastel azure (``#336790``) and a pale -orange (``#E5B62F``), referred in this document simply as "blue" and "yellow" -respectively. The text uses the *Monoid* typeface, an open source webfont that -was developed by Andreas Larsen and contributors in 2015 and is distributed -under the MIT or SIL licenses (more information at -https://github.com/larsenwork/monoid) - - -Usage -===== - -The preferred way of using the setuptools logo is over a white (or light) -background. Alternatively, the following options can be considered, depending -on the circumstances: - -- *"negative"* design - for dark backgrounds (e.g. website displayed in "dark - mode"): the white colour (``#FFFFFF``) of the background and the "blue" - (``#336790``) colour of the design can be swapped. -- *"monochrome"* - when colours are not available (e.g. black and white printed - media): a completely black or white version of the logo can also be used. -- *"banner"* mode: the symbol and text can be used alongside depending on the - available space. - -The following image illustrate these alternatives: - -.. image:: logo-demo.svg - :align: center - -Please refer to the SVG files in the `setuptools repository`_ for the specific -shapes and proportions between the elements of the design. - - -Working with the Design -======================= - -The `setuptools repository`_ contains a series of vector representations of the -design under the ``docs/images`` directory. These representations can be -manipulated via any graphic editor that support SVG files, -however the free and open-source software Inkscape_ is recommended for maximum -compatibility. - -When selecting the right file to work with, file names including -``editable-inkscape`` indicate "more editable" elements (e.g. editable text), -while the others prioritise SVG paths for maximum reproducibility. - -Also notice that you might have to `install the correct fonts`_ to be able to -visualise or edit some of the designs. - - -Inspiration -=========== - -This design was inspired by :user:`cajhne`'s `original proposal`_ and the -ancient symbol of the ouroboros_. -It features a snake moving in a circular trajectory not only as a reference to -the Python programming language but also to the `wheel package format`_ as one -of the distribution formats supported by setuptools. -The shape of the snake also resembles a cog, which together with the hammer is -a nod to the two words that compose the name of the project. - - -License -======= - - -This logo, design variations or a modified version may be used by anyone to -refer to setuptools, but does not indicate endorsement by the project. - -Redistribution, usage and derivative works are permitted under the same license -used by the setuptools software (MIT): - -.. code-block:: text - - Copyright (c) Anderson Bravalheri - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to - deal in the Software without restriction, including without limitation the - rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - IN THE SOFTWARE. - - THE USAGE OF THIS LOGO AND ARTWORK DOES NOT INDICATE ENDORSEMENT BY THE - SETUPTOOLS PROJECT. - -Whenever possible, please make the image a link to -https://github.com/pypa/setuptools. - - -.. _Anderson Bravalheri: https://github.com/abravalheri -.. _Inkscape: https://inkscape.org -.. _setuptools repository: https://github.com/pypa/setuptools -.. _install the correct fonts: https://wiki.inkscape.org/wiki/Installing_fonts -.. _original proposal: https://github.com/pypa/setuptools/issues/2227#issuecomment-653628344 -.. _wheel package format: https://www.python.org/dev/peps/pep-0427/ -.. _ouroboros: https://en.wikipedia.org/wiki/Ouroboros diff --git a/docs/index.rst b/docs/index.rst index b886c8f8..0f52c360 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -26,6 +26,6 @@ designed to facilitate packaging Python projects. Development guide Backward compatibility & deprecated practice Changelog - Artwork + artwork .. tidelift-referral-banner:: -- cgit v1.2.1 From 6cff3789dd3cfb06b504cf42714c6ed6936cb23f Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 6 Jan 2022 16:17:31 +0000 Subject: Replace in-tree sphinx extension with sphinx-favicon This change has been previously proposed and agreed upon in #2869. The idea here is to avoid the burden of maintaining an in-tree sphinx extension, now that `sphinx-favicon` is available and covers setuptools use-case. --- docs/_ext/_custom_icons.py | 58 ---------------------------------------------- docs/conf.py | 13 +++++------ setup.cfg | 1 + 3 files changed, 7 insertions(+), 65 deletions(-) delete mode 100644 docs/_ext/_custom_icons.py diff --git a/docs/_ext/_custom_icons.py b/docs/_ext/_custom_icons.py deleted file mode 100644 index 245162c2..00000000 --- a/docs/_ext/_custom_icons.py +++ /dev/null @@ -1,58 +0,0 @@ -"""'In-tree' sphinx extension to add icons/favicons to documentation""" -import os -from sphinx.util.fileutil import copy_asset_file - - -IMAGES_DIR = "_images" # same used by .. image:: and .. picture:: - - -def _prepare_image(pathto, confdir, outdir, icon_attrs): - """Copy icon files to the ``IMAGES_DIR`` and return a modified version of - the icon attributes dict replacing ``file`` with the correct ``href``. - """ - icon = icon_attrs.copy() - src = os.path.join(confdir, icon.pop("file")) - if not os.path.exists(src): - raise FileNotFoundError(f"icon {src!r} not found") - - dest = os.path.join(outdir, IMAGES_DIR) - copy_asset_file(src, dest) # already compares if dest exists and is uptodate - - asset_name = os.path.basename(src) - icon["href"] = pathto(f"{IMAGES_DIR}/{asset_name}", resource=True) - return icon - - -def _link_tag(attrs): - return "" - - -def _add_icons(app, _pagename, _templatename, context, doctree): - """Add multiple "favicons", not limited to PNG/ICO files""" - # https://evilmartians.com/chronicles/how-to-favicon-in-2021-six-files-that-fit-most-needs - # https://caniuse.com/link-icon-svg - try: - pathto = context['pathto'] - except KeyError as ex: - msg = f"{__name__} extension is supposed to be call in HTML contexts" - raise ValueError(msg) from ex - - if doctree and "icons" in app.config: - icons = [ - _prepare_image(pathto, app.confdir, app.outdir, icon) - for icon in app.config["icons"] - ] - context["metatags"] += "\n".join(_link_tag(attrs) for attrs in icons) - - -def setup(app): - images_dir = os.path.join(app.outdir, IMAGES_DIR) - os.makedirs(images_dir, exist_ok=True) - - app.add_config_value("icons", None, "html") - app.connect("html-page-context", _add_icons) - - return { - 'parallel_read_safe': True, - 'parallel_write_safe': True, - } diff --git a/docs/conf.py b/docs/conf.py index f6ccff0f..4a14214f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -179,23 +179,22 @@ towncrier_draft_include_empty = False extensions += ['jaraco.tidelift'] # Add icons (aka "favicons") to documentation -sys.path.append(os.path.join(os.path.dirname(__file__), '_ext')) -extensions += ['_custom_icons'] +extensions += ['sphinx-favicon'] +html_static_path = ['images'] # should contain the folder with icons # List of dicts with HTML attributes -# as defined in https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link -# except that ``file`` gets replaced with the correct ``href`` -icons = [ +# static-file points to files in the html_static_path (href is computed) +favicons = [ { # "Catch-all" goes first, otherwise some browsers will overwrite "rel": "icon", "type": "image/svg+xml", - "file": "images/logo-symbol-only.svg", + "static-file": "logo-symbol-only.svg", "sizes": "any" }, { # Version with thicker strokes for better visibility at smaller sizes "rel": "icon", "type": "image/svg+xml", - "file": "images/favicon.svg", + "static-file": "favicon.svg", "sizes": "16x16 24x24 32x32 48x48" }, # rel="apple-touch-icon" does not support SVG yet diff --git a/setup.cfg b/setup.cfg index 0af9cd5e..41c9af4f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -73,6 +73,7 @@ docs = # local pygments-github-lexers==0.0.5 + sphinx-favicon sphinx-inline-tabs sphinxcontrib-towncrier furo -- cgit v1.2.1 From a1eec655bc40a92efdb86db38e361efef11d277e Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 6 Jan 2022 16:42:51 +0000 Subject: Use an SVG optimiser In this commit the SVG images (logo, banners, etc) were optimised with the help of https://pypi.org/project/scour/. This change has been previously requested/discussed and agreed upon on #2869. The `*editable-inkscape.svg` files are preserved in the original form. --- docs/images/banner-640x320.svg | 131 ++---- docs/images/banner-negative-640x320.svg | 140 ++----- docs/images/favicon.svg | 64 +-- docs/images/logo-demo.svg | 689 +++++++------------------------- docs/images/logo-inline-negative.svg | 136 ++----- docs/images/logo-inline.svg | 127 ++---- docs/images/logo-negative.svg | 137 ++----- docs/images/logo-over-white.svg | 137 ++----- docs/images/logo-symbol-only.svg | 62 +-- docs/images/logo-text-only.svg | 111 ++--- docs/images/logo.svg | 128 ++---- 11 files changed, 443 insertions(+), 1419 deletions(-) diff --git a/docs/images/banner-640x320.svg b/docs/images/banner-640x320.svg index 8222f645..4e908ea1 100644 --- a/docs/images/banner-640x320.svg +++ b/docs/images/banner-640x320.svg @@ -1,101 +1,36 @@ - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/banner-negative-640x320.svg b/docs/images/banner-negative-640x320.svg index fd5535fd..d45698ed 100644 --- a/docs/images/banner-negative-640x320.svg +++ b/docs/images/banner-negative-640x320.svg @@ -1,109 +1,37 @@ - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/favicon.svg b/docs/images/favicon.svg index 3ac5daf9..a1d31916 100644 --- a/docs/images/favicon.svg +++ b/docs/images/favicon.svg @@ -1,55 +1,23 @@ - - - + + + + image/svg+xml + + + + + + + + diff --git a/docs/images/logo-demo.svg b/docs/images/logo-demo.svg index 279b9088..6b78ebc3 100644 --- a/docs/images/logo-demo.svg +++ b/docs/images/logo-demo.svg @@ -1,543 +1,150 @@ - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/logo-inline-negative.svg b/docs/images/logo-inline-negative.svg index deed96e6..4bf63cfe 100644 --- a/docs/images/logo-inline-negative.svg +++ b/docs/images/logo-inline-negative.svg @@ -1,105 +1,35 @@ - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/logo-inline.svg b/docs/images/logo-inline.svg index 11ab7df7..6e45103d 100644 --- a/docs/images/logo-inline.svg +++ b/docs/images/logo-inline.svg @@ -1,97 +1,34 @@ - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/logo-negative.svg b/docs/images/logo-negative.svg index 23a553d3..d2142045 100644 --- a/docs/images/logo-negative.svg +++ b/docs/images/logo-negative.svg @@ -1,106 +1,37 @@ - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/logo-over-white.svg b/docs/images/logo-over-white.svg index 3ae3968e..1ed01380 100644 --- a/docs/images/logo-over-white.svg +++ b/docs/images/logo-over-white.svg @@ -1,106 +1,37 @@ - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/logo-symbol-only.svg b/docs/images/logo-symbol-only.svg index 7d839c65..2bbf2d58 100644 --- a/docs/images/logo-symbol-only.svg +++ b/docs/images/logo-symbol-only.svg @@ -1,46 +1,20 @@ - - - - - - - image/svg+xml - - - - - - - - - - - - + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/docs/images/logo-text-only.svg b/docs/images/logo-text-only.svg index a59731d4..2e92580d 100644 --- a/docs/images/logo-text-only.svg +++ b/docs/images/logo-text-only.svg @@ -1,85 +1,30 @@ - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/logo.svg b/docs/images/logo.svg index 103d294f..7c793a08 100644 --- a/docs/images/logo.svg +++ b/docs/images/logo.svg @@ -1,98 +1,36 @@ - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + -- cgit v1.2.1 From 8f85ce49712932556a1633a2ea7c1de39421fc79 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 6 Jan 2022 17:02:47 +0000 Subject: Add news fragment --- changelog.d/3008.doc.1.rst | 1 + changelog.d/3008.doc.2.rst | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 changelog.d/3008.doc.1.rst create mode 100644 changelog.d/3008.doc.2.rst diff --git a/changelog.d/3008.doc.1.rst b/changelog.d/3008.doc.1.rst new file mode 100644 index 00000000..dbc09bae --- /dev/null +++ b/changelog.d/3008.doc.1.rst @@ -0,0 +1 @@ +"In-tree" Sphinx extension for "favicons" replaced with ``sphinx-favicon``. diff --git a/changelog.d/3008.doc.2.rst b/changelog.d/3008.doc.2.rst new file mode 100644 index 00000000..b094359c --- /dev/null +++ b/changelog.d/3008.doc.2.rst @@ -0,0 +1,2 @@ +SVG images (logo, banners, ...) optimised with the help of the ``scour`` +package. -- cgit v1.2.1 From 0b3dad255564810bcd0f88f25544eb06c3b0acd0 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 6 Jan 2022 17:03:04 +0000 Subject: Remove unused imports from docs/conf.py --- docs/conf.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 4a14214f..1fb27716 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,3 @@ -import os -import sys - extensions = ['sphinx.ext.autodoc', 'jaraco.packaging.sphinx', 'rst.linker'] master_doc = "index" -- cgit v1.2.1 From 2c9205f5befee894ce3233958464633bfb79ce19 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 6 Jan 2022 18:55:43 +0000 Subject: Add SETUPTOOLS_USE_DISTUTILS configuration to integration tests --- .github/workflows/main.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fa51eeaf..a9f37c5a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -61,6 +61,11 @@ jobs: C:\\tools\\cygwin\\bin\\bash -l -x -c 'cd $(cygpath -u "$GITHUB_WORKSPACE") && tox -- --cov-report xml' integration-test: + strategy: + matrix: + distutils: + - stdlib + - local needs: test if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') # To avoid long times and high resource usage, we assume that: @@ -71,6 +76,8 @@ jobs: # "integration") # With that in mind, the integration tests can run for a single setup runs-on: ubuntu-latest + env: + SETUPTOOLS_USE_DISTUTILS: ${{ matrix.distutils }} steps: - uses: actions/checkout@v2 - name: Install OS-level dependencies -- cgit v1.2.1 From 818a6d973a86f43d60188b36e9f3b59781a94a37 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 6 Jan 2022 18:56:08 +0000 Subject: Allow integration tests to be manually triggered in GitHub Actions --- .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 a9f37c5a..e2441363 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,6 +1,6 @@ name: tests -on: [push, pull_request] +on: [push, pull_request, workflow_dispatch] jobs: test: @@ -67,7 +67,7 @@ jobs: - stdlib - local needs: test - if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && contains(github.ref, 'refs/tags/')) # To avoid long times and high resource usage, we assume that: # 1. The setuptools APIs used by packages don't vary too much with OS or # Python implementation -- cgit v1.2.1 From 2445fdd0d1be06d3d32155505c80831235b2af5e Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 24 Dec 2021 22:01:46 +0000 Subject: Add fixtures for sdist and wheel artifacts They should be build once for each session and be able to be re-used in parallel (assuming read-only) for all tests. This is useful when dealing with virtual environments --- setup.cfg | 2 ++ setuptools/tests/contexts.py | 13 +++++++++++++ setuptools/tests/fixtures.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/setup.cfg b/setup.cfg index 39d95461..9a14cc25 100644 --- a/setup.cfg +++ b/setup.cfg @@ -63,6 +63,8 @@ testing = pytest-xdist sphinx>=4.3.2 jaraco.path>=3.2.0 + build[virtualenv] + filelock>=3.4.0 testing-integration = pytest diff --git a/setuptools/tests/contexts.py b/setuptools/tests/contexts.py index 51ce8984..5316e599 100644 --- a/setuptools/tests/contexts.py +++ b/setuptools/tests/contexts.py @@ -7,6 +7,7 @@ import site import io import pkg_resources +from filelock import FileLock @contextlib.contextmanager @@ -96,3 +97,15 @@ def suppress_exceptions(*excs): yield except excs: pass + + +@contextlib.contextmanager +def session_locked_tmp_dir(tmp_path_factory, name): + """Uses a file lock to guarantee only one worker can access a temp dir""" + root_tmp_dir = tmp_path_factory.getbasetemp().parent + # ^-- get the temp directory shared by all workers + locked_dir = root_tmp_dir / name + with FileLock(locked_dir.with_suffix(".lock")): + # ^-- prevent multiple workers to access the directory at once + locked_dir.mkdir(exist_ok=True, parents=True) + yield locked_dir diff --git a/setuptools/tests/fixtures.py b/setuptools/tests/fixtures.py index a5a172e0..9219412f 100644 --- a/setuptools/tests/fixtures.py +++ b/setuptools/tests/fixtures.py @@ -72,3 +72,35 @@ def sample_project(tmp_path): except Exception: pytest.skip("Unable to clone sampleproject") return tmp_path / 'sampleproject' + + +# sdist and wheel artifacts should be stable across a round of tests +# so we can build them once per session and use the files as "readonly" + + +@pytest.fixture(scope="session") +def setuptools_sdist(tmp_path_factory, request): + with contexts.session_locked_tmp_dir(tmp_path_factory, "sdist_build") as tmp: + dist = next(tmp.glob("*.tar.gz"), None) + if dist: + return dist + + subprocess.check_call([ + sys.executable, "-m", "build", "--sdist", + "--outdir", str(tmp), str(request.config.rootdir) + ]) + return next(tmp.glob("*.tar.gz")) + + +@pytest.fixture(scope="session") +def setuptools_wheel(tmp_path_factory, request): + with contexts.session_locked_tmp_dir(tmp_path_factory, "wheel_build") as tmp: + dist = next(tmp.glob("*.whl"), None) + if dist: + return dist + + subprocess.check_call([ + sys.executable, "-m", "build", "--wheel", + "--outdir", str(tmp) , str(request.config.rootdir) + ]) + return next(tmp.glob("*.whl")) -- cgit v1.2.1 From 60e561c0a3747c2e862791f4cc5a4e530448a9a4 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 24 Dec 2021 22:34:34 +0000 Subject: Extract venv fixtures from test_distutils_adoption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … and change it to install the pre-build setuptools wheel (fixture) instead of installing from the source tree --- setuptools/tests/environment.py | 17 ++++++++++++++- setuptools/tests/fixtures.py | 32 ++++++++++++++++++++++++++++- setuptools/tests/test_distutils_adoption.py | 24 ---------------------- 3 files changed, 47 insertions(+), 26 deletions(-) diff --git a/setuptools/tests/environment.py b/setuptools/tests/environment.py index c0274c33..e3ced27c 100644 --- a/setuptools/tests/environment.py +++ b/setuptools/tests/environment.py @@ -1,9 +1,24 @@ import os import sys +import subprocess import unicodedata - from subprocess import Popen as _Popen, PIPE as _PIPE +import jaraco.envs + + +class VirtualEnv(jaraco.envs.VirtualEnv): + name = '.env' + # Some version of PyPy will import distutils on startup, implicitly + # importing setuptools, and thus leading to BackendInvalid errors + # when upgrading Setuptools. Bypass this behavior by avoiding the + # early availability and need to upgrade. + create_opts = ['--no-setuptools'] + + def run(self, cmd, *args, **kwargs): + cmd = [self.exe(cmd[0])] + cmd[1:] + return subprocess.check_output(cmd, *args, cwd=self.root, **kwargs) + def _which_dirs(cmd): result = set() diff --git a/setuptools/tests/fixtures.py b/setuptools/tests/fixtures.py index 9219412f..317254d9 100644 --- a/setuptools/tests/fixtures.py +++ b/setuptools/tests/fixtures.py @@ -4,8 +4,9 @@ import shutil import subprocess import pytest +import path -from . import contexts +from . import contexts, environment @pytest.fixture @@ -104,3 +105,32 @@ def setuptools_wheel(tmp_path_factory, request): "--outdir", str(tmp) , str(request.config.rootdir) ]) return next(tmp.glob("*.whl")) + + +@pytest.fixture +def venv(tmp_path, setuptools_wheel): + """Virtual env with the version of setuptools under test installed""" + env = environment.VirtualEnv() + env.root = path.Path(tmp_path / 'venv') + env.req = str(setuptools_wheel) + return env.create() + + +@pytest.fixture +def venv_without_setuptools(tmp_path): + """Virtual env without any version of setuptools installed""" + env = environment.VirtualEnv() + env.root = path.Path(tmp_path / 'venv_without_setuptools') + env.create_opts = ['--no-setuptools'] + env.ensure_env() + return env + + +@pytest.fixture +def bare_venv(tmp_path): + """Virtual env without any common packages installed""" + env = environment.VirtualEnv() + env.root = path.Path(tmp_path / 'bare_venv') + env.create_opts = ['--no-setuptools', '--no-pip', '--no-wheel', '--no-seed'] + env.ensure_env() + return env diff --git a/setuptools/tests/test_distutils_adoption.py b/setuptools/tests/test_distutils_adoption.py index 1e73f9aa..70075483 100644 --- a/setuptools/tests/test_distutils_adoption.py +++ b/setuptools/tests/test_distutils_adoption.py @@ -1,39 +1,15 @@ import os import sys import functools -import subprocess import platform import textwrap import pytest -import jaraco.envs -import path IS_PYPY = '__pypy__' in sys.builtin_module_names -class VirtualEnv(jaraco.envs.VirtualEnv): - name = '.env' - # Some version of PyPy will import distutils on startup, implicitly - # importing setuptools, and thus leading to BackendInvalid errors - # when upgrading Setuptools. Bypass this behavior by avoiding the - # early availability and need to upgrade. - create_opts = ['--no-setuptools'] - - def run(self, cmd, *args, **kwargs): - cmd = [self.exe(cmd[0])] + cmd[1:] - return subprocess.check_output(cmd, *args, cwd=self.root, **kwargs) - - -@pytest.fixture -def venv(tmp_path, tmp_src): - env = VirtualEnv() - env.root = path.Path(tmp_path / 'venv') - env.req = str(tmp_src) - return env.create() - - def popen_text(call): """ Augment the Popen call with the parameters to ensure unicode text. -- cgit v1.2.1 From 332a0532bf3293cf7c3d0fbfbb4664a5a98b9ec0 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 25 Dec 2021 00:18:33 +0000 Subject: Replace tmp_src fixture with the virtualenv fixtures Instead of re-building/installing setuptools from the source tree every time, the tests now rely on the venv, wheel and sdist fixtures (the venv fixture is populated from sdist/wheel). Moreover migrate `test_virtualenv` to use `jaraco.envs` (so it uses the same libraries ad `test_distutils_adoption`). --- setuptools/tests/environment.py | 3 +- setuptools/tests/fixtures.py | 17 ------- setuptools/tests/test_virtualenv.py | 96 +++++++++++++------------------------ 3 files changed, 34 insertions(+), 82 deletions(-) diff --git a/setuptools/tests/environment.py b/setuptools/tests/environment.py index e3ced27c..a0c0ec6e 100644 --- a/setuptools/tests/environment.py +++ b/setuptools/tests/environment.py @@ -17,7 +17,8 @@ class VirtualEnv(jaraco.envs.VirtualEnv): def run(self, cmd, *args, **kwargs): cmd = [self.exe(cmd[0])] + cmd[1:] - return subprocess.check_output(cmd, *args, cwd=self.root, **kwargs) + kwargs = {"cwd": self.root, **kwargs} # Allow overriding + return subprocess.check_output(cmd, *args, **kwargs) def _which_dirs(cmd): diff --git a/setuptools/tests/fixtures.py b/setuptools/tests/fixtures.py index 317254d9..9b91d7d7 100644 --- a/setuptools/tests/fixtures.py +++ b/setuptools/tests/fixtures.py @@ -1,6 +1,5 @@ import contextlib import sys -import shutil import subprocess import pytest @@ -29,22 +28,6 @@ def tmpdir_cwd(tmpdir): yield orig -@pytest.fixture -def tmp_src(request, tmp_path): - """Make a copy of the source dir under `$tmp/src`. - - This fixture is useful whenever it's necessary to run `setup.py` - or `pip install` against the source directory when there's no - control over the number of simultaneous invocations. Such - concurrent runs create and delete directories with the same names - under the target directory and so they influence each other's runs - when they are not being executed sequentially. - """ - tmp_src_path = tmp_path / 'src' - shutil.copytree(request.config.rootdir, tmp_src_path) - return tmp_src_path - - @pytest.fixture(autouse=True, scope="session") def workaround_xdist_376(request): """ diff --git a/setuptools/tests/test_virtualenv.py b/setuptools/tests/test_virtualenv.py index 61d239aa..0ba89643 100644 --- a/setuptools/tests/test_virtualenv.py +++ b/setuptools/tests/test_virtualenv.py @@ -1,52 +1,34 @@ -import glob import os import sys import itertools +import subprocess import pathlib import pytest -from pytest_fixture_config import yield_requires_config - -import pytest_virtualenv +from . import contexts from .textwrap import DALS from .test_easy_install import make_nspkg_sdist @pytest.fixture(autouse=True) -def pytest_virtualenv_works(virtualenv): +def pytest_virtualenv_works(venv): """ pytest_virtualenv may not work. if it doesn't, skip these tests. See #1284. """ - venv_prefix = virtualenv.run( - 'python -c "import sys; print(sys.prefix)"', - capture=True, - ).strip() + venv_prefix = venv.run(["python" , "-c", "import sys; print(sys.prefix)"]).strip() if venv_prefix == sys.prefix: pytest.skip("virtualenv is broken (see pypa/setuptools#1284)") -@yield_requires_config(pytest_virtualenv.CONFIG, ['virtualenv_executable']) -@pytest.fixture(scope='function') -def bare_virtualenv(): - """ Bare virtualenv (no pip/setuptools/wheel). - """ - with pytest_virtualenv.VirtualEnv(args=( - '--no-wheel', - '--no-pip', - '--no-setuptools', - )) as venv: - yield venv - - -def test_clean_env_install(bare_virtualenv, tmp_src): +def test_clean_env_install(venv_without_setuptools, setuptools_wheel): """ Check setuptools can be installed in a clean environment. """ - cmd = [bare_virtualenv.python, 'setup.py', 'install'] - bare_virtualenv.run(cmd, cd=tmp_src) + cmd = ["python", "-m", "pip", "install", str(setuptools_wheel)] + venv_without_setuptools.run(cmd) def _get_pip_versions(): @@ -99,41 +81,31 @@ def _get_pip_versions(): reason="https://github.com/pypa/setuptools/pull/2865#issuecomment-965834995", ) @pytest.mark.parametrize('pip_version', _get_pip_versions()) -def test_pip_upgrade_from_source(pip_version, tmp_src, virtualenv): +def test_pip_upgrade_from_source(pip_version, venv_without_setuptools, + setuptools_wheel, setuptools_sdist): """ Check pip can upgrade setuptools from source. """ - # Install pip/wheel, and remove setuptools (as it + # Install pip/wheel, in a venv without setuptools (as it # should not be needed for bootstraping from source) - if pip_version is None: - upgrade_pip = () - else: - upgrade_pip = ('python -m pip install -U "{pip_version}" --retries=1',) - virtualenv.run(' && '.join(( - 'pip uninstall -y setuptools', - 'pip install -U wheel', - ) + upgrade_pip).format(pip_version=pip_version)) - dist_dir = virtualenv.workspace - # Generate source distribution / wheel. - virtualenv.run(' && '.join(( - 'python setup.py -q sdist -d {dist}', - 'python setup.py -q bdist_wheel -d {dist}', - )).format(dist=dist_dir), cd=tmp_src) - sdist = glob.glob(os.path.join(dist_dir, '*.zip'))[0] - wheel = glob.glob(os.path.join(dist_dir, '*.whl'))[0] - # Then update from wheel. - virtualenv.run('pip install ' + wheel) + venv = venv_without_setuptools + venv.run(["pip", "install", "-U", "wheel"]) + if pip_version is not None: + venv.run(["python", "-m", "pip", "install", "-U", pip_version, "--retries=1"]) + with pytest.raises(subprocess.CalledProcessError): + # Meta-test to make sure setuptools is not installed + venv.run(["python", "-c", "import setuptools"]) + + # Then install from wheel. + venv.run(["pip", "install", str(setuptools_wheel)]) # And finally try to upgrade from source. - virtualenv.run('pip install --no-cache-dir --upgrade ' + sdist) + venv.run(["pip", "install", "--no-cache-dir", "--upgrade", str(setuptools_sdist)]) -def _check_test_command_install_requirements(virtualenv, tmpdir, cwd): +def _check_test_command_install_requirements(venv, tmpdir): """ Check the test command will install all required dependencies. """ - # Install setuptools. - virtualenv.run('python setup.py develop', cd=cwd) - def sdist(distname, version): dist_path = tmpdir.join('%s-%s.tar.gz' % (distname, version)) make_nspkg_sdist(str(dist_path), distname, version) @@ -182,28 +154,24 @@ def _check_test_command_install_requirements(virtualenv, tmpdir, cwd): open('success', 'w').close() ''')) - # Run test command for test package. - # use 'virtualenv.python' as workaround for man-group/pytest-plugins#166 - cmd = [virtualenv.python, 'setup.py', 'test', '-s', 'test'] - virtualenv.run(cmd, cd=str(tmpdir)) + + cmd = ["python", 'setup.py', 'test', '-s', 'test'] + venv.run(cmd, cwd=str(tmpdir)) assert tmpdir.join('success').check() -def test_test_command_install_requirements(virtualenv, tmpdir, request): +def test_test_command_install_requirements(venv, tmpdir, tmpdir_cwd): # Ensure pip/wheel packages are installed. - virtualenv.run( - "python -c \"__import__('pkg_resources').require(['pip', 'wheel'])\"") - # uninstall setuptools so that 'setup.py develop' works - virtualenv.run("python -m pip uninstall -y setuptools") + venv.run(["python", "-c", "__import__('pkg_resources').require(['pip', 'wheel'])"]) # disable index URL so bits and bobs aren't requested from PyPI - virtualenv.env['PIP_NO_INDEX'] = '1' - _check_test_command_install_requirements(virtualenv, tmpdir, request.config.rootdir) + with contexts.environment(PYTHONPATH=None, PIP_NO_INDEX="1"): + _check_test_command_install_requirements(venv, tmpdir) -def test_no_missing_dependencies(bare_virtualenv, request): +def test_no_missing_dependencies(bare_venv, request): """ Quick and dirty test to ensure all external dependencies are vendored. """ + setuptools_dir = request.config.rootdir for command in ('upload',): # sorted(distutils.command.__all__): - cmd = [bare_virtualenv.python, 'setup.py', command, '-h'] - bare_virtualenv.run(cmd, cd=request.config.rootdir) + bare_venv.run(['python', 'setup.py', command, '-h'], cwd=setuptools_dir) -- cgit v1.2.1 From 34e0fcbc8886b1940cfaeaf8c9de1b6f1cc199d7 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 25 Dec 2021 00:27:42 +0000 Subject: Remove dependency on pytest_virtualenv Now that all tests use `jaraco.envs`, there is no need to depend on `pytest_virtualenv`. --- setup.cfg | 1 - tox.ini | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 9a14cc25..2f027faf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -55,7 +55,6 @@ testing = mock flake8-2020 virtualenv>=13.0.0 - pytest-virtualenv>=1.2.7 # TODO: Update once man-group/pytest-plugins#188 is solved wheel paver pip>=19.1 # For proper file:// URLs support. diff --git a/tox.ini b/tox.ini index f4e28133..07c310bc 100644 --- a/tox.ini +++ b/tox.ini @@ -7,8 +7,7 @@ toxworkdir={env:TOX_WORK_DIR:.tox} [testenv] deps = - # TODO: remove after man-group/pytest-plugins#188 is solved - pytest-virtualenv @ git+https://github.com/jaraco/pytest-plugins@distutils-deprecated#subdirectory=pytest-virtualenv + # Ideally all the dependencies should be set as "extras" commands = pytest {posargs} usedevelop = True -- cgit v1.2.1 From dde9015db6a6e8b25e6a2df52a6859ddcedcd5b0 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 24 Dec 2021 18:54:11 +0000 Subject: Prevent some tests from inadvertently using the project root for builds Some tests are running the build process using setuptools own directory as cwd. This impacts the build process and also leave behind artifacts produced during tests (like .egg-info folders) --- .gitignore | 1 - setuptools/tests/test_bdist_deprecations.py | 2 +- setuptools/tests/test_setuptools.py | 5 +++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index dc14826e..90ae8050 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ docs/build include lib distribute.egg-info -foo.egg-info setuptools.egg-info .coverage .eggs diff --git a/setuptools/tests/test_bdist_deprecations.py b/setuptools/tests/test_bdist_deprecations.py index 28482fd0..1a900c67 100644 --- a/setuptools/tests/test_bdist_deprecations.py +++ b/setuptools/tests/test_bdist_deprecations.py @@ -11,7 +11,7 @@ from setuptools import SetuptoolsDeprecationWarning @pytest.mark.skipif(sys.platform == 'win32', reason='non-Windows only') @mock.patch('distutils.command.bdist_rpm.bdist_rpm') -def test_bdist_rpm_warning(distutils_cmd): +def test_bdist_rpm_warning(distutils_cmd, tmpdir_cwd): dist = Distribution( dict( script_name='setup.py', diff --git a/setuptools/tests/test_setuptools.py b/setuptools/tests/test_setuptools.py index 3609ab5e..3c429263 100644 --- a/setuptools/tests/test_setuptools.py +++ b/setuptools/tests/test_setuptools.py @@ -18,6 +18,11 @@ import setuptools.depends as dep from setuptools.depends import Require +@pytest.fixture(autouse=True) +def isolated_dir(tmpdir_cwd): + yield + + def makeSetup(**args): """Return distribution from 'setup(**args)', without executing commands""" -- cgit v1.2.1 From f4776f2e5c624f47b803221f3211c30b05f4000d Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 25 Dec 2021 12:03:19 +0000 Subject: Add news fragment --- changelog.d/2968.misc.1.rst | 9 +++++++++ changelog.d/2968.misc.2.rst | 4 ++++ changelog.d/2968.misc.3.rst | 4 ++++ 3 files changed, 17 insertions(+) create mode 100644 changelog.d/2968.misc.1.rst create mode 100644 changelog.d/2968.misc.2.rst create mode 100644 changelog.d/2968.misc.3.rst diff --git a/changelog.d/2968.misc.1.rst b/changelog.d/2968.misc.1.rst new file mode 100644 index 00000000..502ba612 --- /dev/null +++ b/changelog.d/2968.misc.1.rst @@ -0,0 +1,9 @@ +Removed ``tmp_src`` test fixture. Previously this fixture was copying all the +files and folders under the project root, including the ``.git`` directory, +which is error prone and increases testing time. + +Since ``tmp_src`` was used to populate virtual environments (installing the +version of ``setuptools`` under test via the source tree), it was replaced by +the new ``setuptools_sdist`` and ``setuptools_wheel`` fixtures (that are build +only once per session testing and can be shared between all the workers for +read-only usage). diff --git a/changelog.d/2968.misc.2.rst b/changelog.d/2968.misc.2.rst new file mode 100644 index 00000000..b575db13 --- /dev/null +++ b/changelog.d/2968.misc.2.rst @@ -0,0 +1,4 @@ +Introduced new test fixtures ``venv``, ``venv_without_setuptools``, +``bare_venv`` that rely on the ``jaraco.envs`` package. +These new test fixtures were also used to remove the (currently problematic) +dependency on the ``pytest_virtualenv`` plugin. diff --git a/changelog.d/2968.misc.3.rst b/changelog.d/2968.misc.3.rst new file mode 100644 index 00000000..9b04c131 --- /dev/null +++ b/changelog.d/2968.misc.3.rst @@ -0,0 +1,4 @@ +Improved isolation for some tests that where inadvertently using the project +root for builds, and therefore creating directories (e.g. ``build``, ``dist``, +``*.egg-info``) that could interfere with the outcome of other tests +-- by :user:`abravalheri`. -- cgit v1.2.1 From 6c5c22e357705b922b775ad1aeee663398eb1b8c Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 6 Jan 2022 21:55:17 +0000 Subject: Use setuptools wheel instead of source tree in integration tests --- setup.cfg | 6 +++++- setuptools/tests/integration/test_pip_install_sdist.py | 12 +++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/setup.cfg b/setup.cfg index 2f027faf..ce7dc5c8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -69,9 +69,13 @@ testing-integration = pytest pytest-xdist pytest-enabler - virtualenv + virtualenv>=13.0.0 tomli wheel + jaraco.path>=3.2.0 + jaraco.envs>=2.2 + build[virtualenv] + filelock>=3.4.0 docs = diff --git a/setuptools/tests/integration/test_pip_install_sdist.py b/setuptools/tests/integration/test_pip_install_sdist.py index 23801bc4..54955938 100644 --- a/setuptools/tests/integration/test_pip_install_sdist.py +++ b/setuptools/tests/integration/test_pip_install_sdist.py @@ -22,15 +22,11 @@ from urllib.request import urlopen import pytest from packaging.requirements import Requirement -import setuptools - from .helpers import Archive, run pytestmark = pytest.mark.integration -SETUPTOOLS_ROOT = os.path.dirname(next(iter(setuptools.__path__))) - LATEST, = list(Enum("v", "LATEST")) """Default version to be checked""" # There are positive and negative aspects of checking the latest version of the @@ -117,7 +113,7 @@ ALREADY_LOADED = ("pytest", "mypy") # loaded by pytest/pytest-enabler @pytest.mark.parametrize('package, version', EXAMPLES) -def test_install_sdist(package, version, tmp_path, venv_python): +def test_install_sdist(package, version, tmp_path, venv_python, setuptools_wheel): venv_pip = (venv_python, "-m", "pip") sdist = retrieve_sdist(package, version, tmp_path) deps = build_deps(package, sdist) @@ -127,10 +123,8 @@ def test_install_sdist(package, version, tmp_path, venv_python): run([*venv_pip, "install", *deps]) # Use a virtualenv to simulate PEP 517 isolation - # but install setuptools to force the version under development - correct_setuptools = os.getenv("PROJECT_ROOT") or SETUPTOOLS_ROOT - assert os.path.exists(os.path.join(correct_setuptools, "pyproject.toml")) - run([*venv_pip, "install", "-Ie", correct_setuptools]) + # but install fresh setuptools wheel to ensure the version under development + run([*venv_pip, "install", "-I", setuptools_wheel]) run([*venv_pip, "install", *SDIST_OPTIONS, sdist]) # Execute a simple script to make sure the package was installed correctly -- cgit v1.2.1 From 9c7e59c1e4f14c504b1c9016e35cec549d9f6b81 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 6 Jan 2022 21:58:27 +0000 Subject: Ensure test_cygwin pass before CI release action It seems that the release action in the CI was running even when the cygwin tests did not pass... It is likely we want it to pass first. --- .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 e2441363..dd6cef70 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -96,7 +96,7 @@ jobs: run: tox -e integration release: - needs: [test, integration-test] + needs: [test, test_cygwin, integration-test] if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') runs-on: ubuntu-latest -- cgit v1.2.1 From 6c18ba25ccdc3f3efc38e7b2c8293b89e4498ecf Mon Sep 17 00:00:00 2001 From: Matthew Suozzo Date: Thu, 6 Jan 2022 18:45:32 -0500 Subject: Use MappingProxyType to ensure immutability. --- pkg_resources/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index 6d0e19d6..f98516d1 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -3047,7 +3047,7 @@ class DistInfoDistribution(Distribution): if not req.marker or req.marker.evaluate({'extra': extra}): yield req - common = dict.fromkeys(reqs_for_extra(None)) + common = types.MappingProxyType(dict.fromkeys(reqs_for_extra(None))) dm[None].extend(common) for extra in self._parsed_pkg_info.get_all('Provides-Extra') or []: -- cgit v1.2.1 From b1e8e86b9de1d44f77d6d8c3a14af54e4c592a90 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 7 Jan 2022 10:13:35 +0000 Subject: Fix misplaced news fragment PR #2839 accidentally misplaced the news fragment file under root. This commit fix that. --- 2839.change.rst | 1 - changelog.d/2839.change.rst | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 2839.change.rst create mode 100644 changelog.d/2839.change.rst diff --git a/2839.change.rst b/2839.change.rst deleted file mode 100644 index 621fa667..00000000 --- a/2839.change.rst +++ /dev/null @@ -1 +0,0 @@ -Removed `requires` sorting when installing wheels as an egg dir. diff --git a/changelog.d/2839.change.rst b/changelog.d/2839.change.rst new file mode 100644 index 00000000..621fa667 --- /dev/null +++ b/changelog.d/2839.change.rst @@ -0,0 +1 @@ +Removed `requires` sorting when installing wheels as an egg dir. -- cgit v1.2.1 From d768c314a2f5627458ac08b813ccf316d9cabb44 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 7 Jan 2022 10:18:08 +0000 Subject: Remove `numpy` sdists from integration tests Numpy now uses a version cap for setuptools in their `pyproject.toml`, which defies a bit the value of including it in the integration tests. As revealed in a conversation with `numpy` maintainer, it seems that they not plan to remove this cap (maybe only update it from time to time, only if necessary). Moreover they are also studying other build backends. So I think that now the best thing to do would be stop trying to build numpy sdists with the current version of setuptools, otherwise we risk to break this test. --- setuptools/tests/integration/test_pip_install_sdist.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setuptools/tests/integration/test_pip_install_sdist.py b/setuptools/tests/integration/test_pip_install_sdist.py index 54955938..86cc4235 100644 --- a/setuptools/tests/integration/test_pip_install_sdist.py +++ b/setuptools/tests/integration/test_pip_install_sdist.py @@ -41,7 +41,6 @@ LATEST, = list(Enum("v", "LATEST")) # that `build-essential`, `gfortran` and `libopenblas-dev` are installed, # due to their relevance to the numerical/scientific programming ecosystem) EXAMPLES = [ - ("numpy", LATEST), # custom distutils-based commands ("pandas", LATEST), # cython + custom build_ext ("sphinx", LATEST), # custom setup.py ("pip", LATEST), # just in case... @@ -174,7 +173,7 @@ def retrieve_pypi_sdist_metadata(package, version): if dist["filename"].endswith(".tar.gz"): return dist - # Not all packages are publishing tar.gz, e.g. numpy==1.21.4 + # Not all packages are publishing tar.gz return dist -- cgit v1.2.1 From f51935be120c9f5361db02a5df7806b0837913fc Mon Sep 17 00:00:00 2001 From: Ananth Pattabiraman Date: Fri, 7 Jan 2022 19:14:45 +0530 Subject: add resources on packaging Section `Resources on Python packaging` did not point to any resource. Added as discussed on #2674 --- docs/userguide/quickstart.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst index da904bab..dc1c9e60 100644 --- a/docs/userguide/quickstart.rst +++ b/docs/userguide/quickstart.rst @@ -225,5 +225,6 @@ parsed by ``setuptool`` to ease the pain of transition. Resources on Python packaging ============================= -Packaging in Python is hard. Here we provide a list of links for those that -want to learn more. +Packaging in Python can be hard and is constantly evolving. +`Python Packaging User Guide `_ has tutorials and +up-to-date references. -- cgit v1.2.1 From e05982d312b9546c86918fe927b38d2a2c38f012 Mon Sep 17 00:00:00 2001 From: Ananth Pattabiraman Date: Fri, 7 Jan 2022 19:40:33 +0530 Subject: add 2674 changelog for pull request for #2674 as per developer guide --- changelog.d/2674.doc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/2674.doc.rst diff --git a/changelog.d/2674.doc.rst b/changelog.d/2674.doc.rst new file mode 100644 index 00000000..3f0757a0 --- /dev/null +++ b/changelog.d/2674.doc.rst @@ -0,0 +1 @@ +Added link to additional resources on packaging in Quickstart guide -- cgit v1.2.1 From 4d36adadc793c28e1df5bbb3a549ea608d55df76 Mon Sep 17 00:00:00 2001 From: Ananth Pattabiraman Date: Fri, 7 Jan 2022 19:59:03 +0530 Subject: Update docs/userguide/quickstart.rst Co-authored-by: Anderson Bravalheri --- docs/userguide/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst index dc1c9e60..4c62c6df 100644 --- a/docs/userguide/quickstart.rst +++ b/docs/userguide/quickstart.rst @@ -227,4 +227,4 @@ Resources on Python packaging ============================= Packaging in Python can be hard and is constantly evolving. `Python Packaging User Guide `_ has tutorials and -up-to-date references. +up-to-date references that can help you when it is time to distribute your work. -- cgit v1.2.1 From bfa75fc56d0bd47bd6c0edf9a0e579508c9fae9e Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 7 Jan 2022 13:48:36 +0000 Subject: Add test to make sure 3.10 is not interpreted as 3.1 --- setuptools/tests/test_easy_install.py | 49 +++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py index 6840d03b..4a2c2537 100644 --- a/setuptools/tests/test_easy_install.py +++ b/setuptools/tests/test_easy_install.py @@ -17,6 +17,8 @@ import time import re import subprocess import pathlib +import warnings +from collections import namedtuple import pytest from jaraco import path @@ -1058,3 +1060,50 @@ class TestWindowsScriptWriter: hdr = hdr.rstrip('\n') # header should not start with an escaped quote assert not hdr.startswith('\\"') + + +VersionStub = namedtuple("VersionStub", "major, minor, micro, releaselevel, serial") + + +@pytest.mark.skipif( + os.name == 'nt', + reason='Installation schemes for Windows may use values for interpolation ' + 'that come directly from sysconfig and are difficult to patch/mock' +) +def test_use_correct_python_version_string(tmpdir, tmpdir_cwd, monkeypatch): + # In issue #3001, easy_install wrongly uses the `python3.1` directory + # when the interpreter is `python3.10` and the `--user` option is given. + # See pypa/setuptools#3001. + dist = Distribution() + cmd = dist.get_command_obj('easy_install') + cmd.args = ['ok'] + cmd.optimize = 0 + cmd.user = True + cmd.install_userbase = str(tmpdir) + cmd.install_usersite = None + install_cmd = dist.get_command_obj('install') + install_cmd.install_userbase = str(tmpdir) + install_cmd.install_usersite = None + + with monkeypatch.context() as patch, warnings.catch_warnings(): + warnings.simplefilter("ignore") + version = '3.10.1 (main, Dec 21 2021, 09:17:12) [GCC 10.2.1 20210110]' + info = VersionStub(3, 10, 1, "final", 0) + patch.setattr('site.ENABLE_USER_SITE', True) + patch.setattr('sys.version', version) + patch.setattr('sys.version_info', info) + patch.setattr(cmd, 'create_home_path', mock.Mock()) + cmd.finalize_options() + + if os.getenv('SETUPTOOLS_USE_DISTUTILS', 'local') == 'local': + # Installation schemes in stdlib distutils might be outdated/bugged + name = "pypy" if hasattr(sys, 'pypy_version_info') else "python" + install_dir = cmd.install_dir.lower() + assert f"{name}3.10" in install_dir or f"{name}310" in install_dir + + # The following "variables" are used for interpolation in distutils + # installation schemes, so it should be fair to treat them as "semi-public", + # or at least public enough so we can have a test to make sure they are correct + assert cmd.config_vars['py_version'] == '3.10.1' + assert cmd.config_vars['py_version_short'] == '3.10' + assert cmd.config_vars['py_version_nodot'] == '310' -- cgit v1.2.1 From a257f0cb1f960e1d37933c5009da39a49a4622bc Mon Sep 17 00:00:00 2001 From: liuzhe Date: Wed, 22 Dec 2021 13:48:04 +0800 Subject: fix version parsing --- changelog.d/2953.change.rst | 1 + setuptools/command/easy_install.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/2953.change.rst diff --git a/changelog.d/2953.change.rst b/changelog.d/2953.change.rst new file mode 100644 index 00000000..bc06b045 --- /dev/null +++ b/changelog.d/2953.change.rst @@ -0,0 +1 @@ +Fixed a bug that easy install incorrectly parsed Python 3.10 version string. diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index aad5794a..a2962a7d 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -243,8 +243,8 @@ class easy_install(Command): 'dist_version': self.distribution.get_version(), 'dist_fullname': self.distribution.get_fullname(), 'py_version': py_version, - 'py_version_short': py_version[0:3], - 'py_version_nodot': py_version[0] + py_version[2], + 'py_version_short': f'{sys.version_info.major}.{sys.version_info.minor}', + 'py_version_nodot': f'{sys.version_info.major}{sys.version_info.minor}', 'sys_prefix': prefix, 'prefix': prefix, 'sys_exec_prefix': exec_prefix, -- cgit v1.2.1 From 02f788e73556b90f40acf4b08b27411eb6fc8deb Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 7 Jan 2022 23:13:16 -0500 Subject: Capture performance of interpreter startup. Ref #3006. --- exercises.py | 6 ++++++ setup.cfg | 1 + tox.ini | 2 ++ 3 files changed, 9 insertions(+) create mode 100644 exercises.py diff --git a/exercises.py b/exercises.py new file mode 100644 index 00000000..76176be5 --- /dev/null +++ b/exercises.py @@ -0,0 +1,6 @@ +def measure_startup_perf(): + # run by pytest_perf + import subprocess + import sys # end warmup + + subprocess.check_call([sys.executable, '-c', 'pass']) diff --git a/setup.cfg b/setup.cfg index ce7dc5c8..3bf0b618 100644 --- a/setup.cfg +++ b/setup.cfg @@ -50,6 +50,7 @@ testing = # workaround for jaraco/skeleton#22 python_implementation != "PyPy" pytest-enabler >= 1.0.1 + pytest-perf # local mock diff --git a/tox.ini b/tox.ini index 07c310bc..d6cc3f56 100644 --- a/tox.ini +++ b/tox.ini @@ -15,6 +15,8 @@ extras = testing passenv = SETUPTOOLS_USE_DISTUTILS windir # required for test_pkg_resources + # honor git config in pytest-perf + HOME [testenv:integration] deps = {[testenv]deps} -- cgit v1.2.1 From cf21303f1ef9b143e76d613b253730c61edd97c2 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Thu, 6 Jan 2022 23:30:19 +0100 Subject: Speedup startup of Python by importing less ``_distutils_hack`` is imported by a ``.pth`` file at every start of a Python interpreter. The import of costly modules like ``re`` and ``contextlib`` almost doubles the initial startup time of an interpreter. - replace ``contextlib`` with simple context manager and try/except - replace ``re`` with simple string match - move import of ``importlib`` into function body - remove ``warnings.filterwarnings()``, which imports ``re``, too. Fixes: #3006 Signed-off-by: Christian Heimes --- _distutils_hack/__init__.py | 50 ++++++++++++++++++++------------------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/_distutils_hack/__init__.py b/_distutils_hack/__init__.py index 75bc4463..5d2a6b20 100644 --- a/_distutils_hack/__init__.py +++ b/_distutils_hack/__init__.py @@ -1,17 +1,14 @@ +# don't import any costly modules import sys import os -import re -import importlib -import warnings -import contextlib is_pypy = '__pypy__' in sys.builtin_module_names -warnings.filterwarnings('ignore', - r'.+ distutils\b.+ deprecated', - DeprecationWarning) +# warnings.filterwarnings('ignore', +# r'.+ distutils\b.+ deprecated', +# DeprecationWarning) def warn_distutils_present(): @@ -21,6 +18,7 @@ def warn_distutils_present(): # PyPy for 3.6 unconditionally imports distutils, so bypass the warning # https://foss.heptapod.net/pypy/pypy/-/blob/be829135bc0d758997b3566062999ee8b23872b4/lib-python/3/site.py#L250 return + import warnings warnings.warn( "Distutils was imported before Setuptools, but importing Setuptools " "also replaces the `distutils` module in `sys.modules`. This may lead " @@ -33,8 +31,12 @@ def warn_distutils_present(): def clear_distutils(): if 'distutils' not in sys.modules: return + import warnings warnings.warn("Setuptools is replacing distutils.") - mods = [name for name in sys.modules if re.match(r'distutils\b', name)] + mods = [ + name for name in sys.modules + if name == "distutils" or name.startswith("distutils.") + ] for name in mods: del sys.modules[name] @@ -48,6 +50,7 @@ def enabled(): def ensure_local_distutils(): + import importlib clear_distutils() # With the DistutilsMetaFinder in place, @@ -73,17 +76,6 @@ def do_override(): ensure_local_distutils() -class suppress(contextlib.suppress, contextlib.ContextDecorator): - """ - A version of contextlib.suppress with decorator support. - - >>> @suppress(KeyError) - ... def key_error(): - ... {}[''] - >>> key_error() - """ - - class DistutilsMetaFinder: def find_spec(self, fullname, path, target=None): if path is not None: @@ -94,6 +86,7 @@ class DistutilsMetaFinder: return method() def spec_for_distutils(self): + import importlib import importlib.abc import importlib.util @@ -144,13 +137,15 @@ class DistutilsMetaFinder: ) @classmethod - @suppress(AttributeError) def is_get_pip(cls): """ Detect if get-pip is being invoked. Ref #2993. """ - import __main__ - return os.path.basename(__main__.__file__) == 'get-pip.py' + try: + import __main__ + return os.path.basename(__main__.__file__) == 'get-pip.py' + except AttributeError: + pass @staticmethod def frame_file_is_setup(frame): @@ -168,12 +163,11 @@ def add_shim(): DISTUTILS_FINDER in sys.meta_path or insert_shim() -@contextlib.contextmanager -def shim(): - insert_shim() - try: - yield - finally: +class shim: + def __enter__(self): + insert_shim() + + def __exit__(self, exc, value, tb): remove_shim() -- cgit v1.2.1 From 6587a0be16831dad180dedb9ca86dba81cad78af Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Thu, 6 Jan 2022 23:37:59 +0100 Subject: Add changelog entry --- changelog.d/3006.change.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelog.d/3006.change.rst diff --git a/changelog.d/3006.change.rst b/changelog.d/3006.change.rst new file mode 100644 index 00000000..d5f5d99d --- /dev/null +++ b/changelog.d/3006.change.rst @@ -0,0 +1,2 @@ +Fixed startup performance issue of Python interpreter due to imports of +costly modules in ``_distutils_hack`` -- by :user:`tiran` -- cgit v1.2.1 From f39c0631e5269c33b3544004439f8480a41fcd02 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Thu, 6 Jan 2022 23:48:41 +0100 Subject: Temporarily add back filter, tests fail without it --- _distutils_hack/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/_distutils_hack/__init__.py b/_distutils_hack/__init__.py index 5d2a6b20..47ea70d4 100644 --- a/_distutils_hack/__init__.py +++ b/_distutils_hack/__init__.py @@ -5,10 +5,10 @@ import os is_pypy = '__pypy__' in sys.builtin_module_names - -# warnings.filterwarnings('ignore', -# r'.+ distutils\b.+ deprecated', -# DeprecationWarning) +import warnings +warnings.filterwarnings('ignore', + r'.+ distutils\b.+ deprecated', + DeprecationWarning) def warn_distutils_present(): -- cgit v1.2.1 From 804b106a8c97efa2944b92918f120c636ecb6818 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Fri, 7 Jan 2022 00:17:15 +0100 Subject: Use internal warnings API and _TrivialRe hack to install filter --- _distutils_hack/__init__.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/_distutils_hack/__init__.py b/_distutils_hack/__init__.py index 47ea70d4..d324763d 100644 --- a/_distutils_hack/__init__.py +++ b/_distutils_hack/__init__.py @@ -1,14 +1,29 @@ # don't import any costly modules import sys import os +import warnings is_pypy = '__pypy__' in sys.builtin_module_names -import warnings -warnings.filterwarnings('ignore', - r'.+ distutils\b.+ deprecated', - DeprecationWarning) + +class _TrivialRe: + def __init__(self, *patterns): + self._patterns = patterns + + def match(self, string): + return all(pat in string for pat in self._patterns) + + +# warnings.filterwarnings() imports the re module +warnings._add_filter( + 'ignore', + _TrivialRe("distutils", "deprecated"), + DeprecationWarning, + None, + 0, + append=False +) def warn_distutils_present(): @@ -18,7 +33,6 @@ def warn_distutils_present(): # PyPy for 3.6 unconditionally imports distutils, so bypass the warning # https://foss.heptapod.net/pypy/pypy/-/blob/be829135bc0d758997b3566062999ee8b23872b4/lib-python/3/site.py#L250 return - import warnings warnings.warn( "Distutils was imported before Setuptools, but importing Setuptools " "also replaces the `distutils` module in `sys.modules`. This may lead " @@ -31,7 +45,6 @@ def warn_distutils_present(): def clear_distutils(): if 'distutils' not in sys.modules: return - import warnings warnings.warn("Setuptools is replacing distutils.") mods = [ name for name in sys.modules -- cgit v1.2.1 From 06d81cb32da88ec0b69d3bc01ad138a9c0d41520 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Fri, 7 Jan 2022 00:30:26 +0100 Subject: Move filter into meta finder --- _distutils_hack/__init__.py | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/_distutils_hack/__init__.py b/_distutils_hack/__init__.py index d324763d..0307734d 100644 --- a/_distutils_hack/__init__.py +++ b/_distutils_hack/__init__.py @@ -1,31 +1,11 @@ # don't import any costly modules import sys import os -import warnings is_pypy = '__pypy__' in sys.builtin_module_names -class _TrivialRe: - def __init__(self, *patterns): - self._patterns = patterns - - def match(self, string): - return all(pat in string for pat in self._patterns) - - -# warnings.filterwarnings() imports the re module -warnings._add_filter( - 'ignore', - _TrivialRe("distutils", "deprecated"), - DeprecationWarning, - None, - 0, - append=False -) - - def warn_distutils_present(): if 'distutils' not in sys.modules: return @@ -33,6 +13,7 @@ def warn_distutils_present(): # PyPy for 3.6 unconditionally imports distutils, so bypass the warning # https://foss.heptapod.net/pypy/pypy/-/blob/be829135bc0d758997b3566062999ee8b23872b4/lib-python/3/site.py#L250 return + import warnings warnings.warn( "Distutils was imported before Setuptools, but importing Setuptools " "also replaces the `distutils` module in `sys.modules`. This may lead " @@ -45,6 +26,7 @@ def warn_distutils_present(): def clear_distutils(): if 'distutils' not in sys.modules: return + import warnings warnings.warn("Setuptools is replacing distutils.") mods = [ name for name in sys.modules @@ -89,6 +71,14 @@ def do_override(): ensure_local_distutils() +class _TrivialRe: + def __init__(self, *patterns): + self._patterns = patterns + + def match(self, string): + return all(pat in string for pat in self._patterns) + + class DistutilsMetaFinder: def find_spec(self, fullname, path, target=None): if path is not None: @@ -102,6 +92,17 @@ class DistutilsMetaFinder: import importlib import importlib.abc import importlib.util + import warnings + + # warnings.filterwarnings() imports the re module + warnings._add_filter( + 'ignore', + _TrivialRe("distutils", "deprecated"), + DeprecationWarning, + None, + 0, + append=True + ) try: mod = importlib.import_module('setuptools._distutils') -- cgit v1.2.1 From 9c0d81e786978c5cc4cbfc50045d271e5b3ae0b9 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Fri, 7 Jan 2022 08:22:43 +0100 Subject: Suppress distutils deprecation warning --- pytest.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pytest.ini b/pytest.ini index 5c0ad039..2ffa4473 100644 --- a/pytest.ini +++ b/pytest.ini @@ -39,6 +39,8 @@ filterwarnings= # https://github.com/pytest-dev/pytest/discussions/9296 ignore:The distutils.sysconfig module is deprecated, use sysconfig instead + ignore:The distutils package is deprecated.* + # Workaround for pypa/setuptools#2868 # ideally would apply to PyPy only but for # https://github.com/pytest-dev/pytest/discussions/9296 -- cgit v1.2.1 From 7619852f4bfdf3065747c44ecc09c024d675c819 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Fri, 7 Jan 2022 10:18:05 +0100 Subject: pytest.ini uses tabs --- pytest.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pytest.ini b/pytest.ini index 2ffa4473..f522a45e 100644 --- a/pytest.ini +++ b/pytest.ini @@ -38,8 +38,7 @@ filterwarnings= # SETUPTOOLS_USE_DISTUTILS=stdlib but for # https://github.com/pytest-dev/pytest/discussions/9296 ignore:The distutils.sysconfig module is deprecated, use sysconfig instead - - ignore:The distutils package is deprecated.* + ignore:The distutils package is deprecated.* # Workaround for pypa/setuptools#2868 # ideally would apply to PyPy only but for -- cgit v1.2.1 From 91ce77e49670fb97ceaad7d7c3b414c488c65c62 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 8 Jan 2022 12:12:17 -0500 Subject: Check that distutils has an origin. Ref #2990. --- setuptools/tests/test_distutils_adoption.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/setuptools/tests/test_distutils_adoption.py b/setuptools/tests/test_distutils_adoption.py index 70075483..366f2928 100644 --- a/setuptools/tests/test_distutils_adoption.py +++ b/setuptools/tests/test_distutils_adoption.py @@ -86,3 +86,10 @@ def test_pip_import(venv): """ cmd = ['python', '-c', 'import pip'] popen_text(venv.run)(cmd) + + +def test_distutils_has_origin(): + """ + Distutils module spec should have an origin. #2990. + """ + assert __import__('distutils').__spec__.origin -- cgit v1.2.1 From bd8d4dbd9638988f09b061f5d94a678c0fd80f25 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 31 Dec 2021 09:32:20 -0800 Subject: set origin= for distutils.__spec__ set origin so spec finding reports the correct location -- https://docs.python.org/3/library/importlib.html#importlib.machinery.ModuleSpec --- _distutils_hack/__init__.py | 4 +++- changelog.d/2990.change.rst | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 changelog.d/2990.change.rst diff --git a/_distutils_hack/__init__.py b/_distutils_hack/__init__.py index 75bc4463..cc62dcf6 100644 --- a/_distutils_hack/__init__.py +++ b/_distutils_hack/__init__.py @@ -118,7 +118,9 @@ class DistutilsMetaFinder: def exec_module(self, module): pass - return importlib.util.spec_from_loader('distutils', DistutilsLoader()) + return importlib.util.spec_from_loader( + 'distutils', DistutilsLoader(), origin=mod.__file__ + ) def spec_for_pip(self): """ diff --git a/changelog.d/2990.change.rst b/changelog.d/2990.change.rst new file mode 100644 index 00000000..bb6e6032 --- /dev/null +++ b/changelog.d/2990.change.rst @@ -0,0 +1 @@ +Set the ``.origin`` attribute of the ``distutils`` module to the module's ``__file__``. -- cgit v1.2.1 From 82b9723caf47415aebd2a2371edb81e2f5a996c4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 8 Jan 2022 14:37:36 -0500 Subject: =?UTF-8?q?Bump=20version:=2060.3.1=20=E2=86=92=2060.4.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 42 ++++++++++++++++++++++++++++++++++++++++++ changelog.d/2674.doc.rst | 1 - changelog.d/2839.change.rst | 1 - changelog.d/2862.misc.rst | 2 -- changelog.d/2952.misc.rst | 1 - changelog.d/2953.change.rst | 1 - changelog.d/2968.misc.1.rst | 9 --------- changelog.d/2968.misc.2.rst | 4 ---- changelog.d/2968.misc.3.rst | 4 ---- changelog.d/3006.change.rst | 2 -- changelog.d/3008.doc.1.rst | 1 - changelog.d/3008.doc.2.rst | 2 -- setup.cfg | 2 +- 14 files changed, 44 insertions(+), 30 deletions(-) delete mode 100644 changelog.d/2674.doc.rst delete mode 100644 changelog.d/2839.change.rst delete mode 100644 changelog.d/2862.misc.rst delete mode 100644 changelog.d/2952.misc.rst delete mode 100644 changelog.d/2953.change.rst delete mode 100644 changelog.d/2968.misc.1.rst delete mode 100644 changelog.d/2968.misc.2.rst delete mode 100644 changelog.d/2968.misc.3.rst delete mode 100644 changelog.d/3006.change.rst delete mode 100644 changelog.d/3008.doc.1.rst delete mode 100644 changelog.d/3008.doc.2.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index ca91c30e..57c38239 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 60.3.1 +current_version = 60.4.0 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index 2c52ecfc..2f367678 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,45 @@ +v60.4.0 +------- + + +Changes +^^^^^^^ +* #2839: Removed `requires` sorting when installing wheels as an egg dir. +* #2953: Fixed a bug that easy install incorrectly parsed Python 3.10 version string. +* #3006: Fixed startup performance issue of Python interpreter due to imports of + costly modules in ``_distutils_hack`` -- by :user:`tiran` + +Documentation changes +^^^^^^^^^^^^^^^^^^^^^ +* #2674: Added link to additional resources on packaging in Quickstart guide +* #3008: "In-tree" Sphinx extension for "favicons" replaced with ``sphinx-favicon``. +* #3008: SVG images (logo, banners, ...) optimised with the help of the ``scour`` + package. + +Misc +^^^^ +* #2862: Added integration tests that focus on building and installing some packages in + the Python ecosystem via ``pip`` -- by :user:`abravalheri` +* #2952: Modified "vendoring" logic to keep license files. +* #2968: Improved isolation for some tests that where inadvertently using the project + root for builds, and therefore creating directories (e.g. ``build``, ``dist``, + ``*.egg-info``) that could interfere with the outcome of other tests + -- by :user:`abravalheri`. +* #2968: Introduced new test fixtures ``venv``, ``venv_without_setuptools``, + ``bare_venv`` that rely on the ``jaraco.envs`` package. + These new test fixtures were also used to remove the (currently problematic) + dependency on the ``pytest_virtualenv`` plugin. +* #2968: Removed ``tmp_src`` test fixture. Previously this fixture was copying all the + files and folders under the project root, including the ``.git`` directory, + which is error prone and increases testing time. + + Since ``tmp_src`` was used to populate virtual environments (installing the + version of ``setuptools`` under test via the source tree), it was replaced by + the new ``setuptools_sdist`` and ``setuptools_wheel`` fixtures (that are build + only once per session testing and can be shared between all the workers for + read-only usage). + + v60.3.1 ------- diff --git a/changelog.d/2674.doc.rst b/changelog.d/2674.doc.rst deleted file mode 100644 index 3f0757a0..00000000 --- a/changelog.d/2674.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Added link to additional resources on packaging in Quickstart guide diff --git a/changelog.d/2839.change.rst b/changelog.d/2839.change.rst deleted file mode 100644 index 621fa667..00000000 --- a/changelog.d/2839.change.rst +++ /dev/null @@ -1 +0,0 @@ -Removed `requires` sorting when installing wheels as an egg dir. diff --git a/changelog.d/2862.misc.rst b/changelog.d/2862.misc.rst deleted file mode 100644 index 77e80007..00000000 --- a/changelog.d/2862.misc.rst +++ /dev/null @@ -1,2 +0,0 @@ -Added integration tests that focus on building and installing some packages in -the Python ecosystem via ``pip`` -- by :user:`abravalheri` diff --git a/changelog.d/2952.misc.rst b/changelog.d/2952.misc.rst deleted file mode 100644 index ccaf46b7..00000000 --- a/changelog.d/2952.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Modified "vendoring" logic to keep license files. diff --git a/changelog.d/2953.change.rst b/changelog.d/2953.change.rst deleted file mode 100644 index bc06b045..00000000 --- a/changelog.d/2953.change.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed a bug that easy install incorrectly parsed Python 3.10 version string. diff --git a/changelog.d/2968.misc.1.rst b/changelog.d/2968.misc.1.rst deleted file mode 100644 index 502ba612..00000000 --- a/changelog.d/2968.misc.1.rst +++ /dev/null @@ -1,9 +0,0 @@ -Removed ``tmp_src`` test fixture. Previously this fixture was copying all the -files and folders under the project root, including the ``.git`` directory, -which is error prone and increases testing time. - -Since ``tmp_src`` was used to populate virtual environments (installing the -version of ``setuptools`` under test via the source tree), it was replaced by -the new ``setuptools_sdist`` and ``setuptools_wheel`` fixtures (that are build -only once per session testing and can be shared between all the workers for -read-only usage). diff --git a/changelog.d/2968.misc.2.rst b/changelog.d/2968.misc.2.rst deleted file mode 100644 index b575db13..00000000 --- a/changelog.d/2968.misc.2.rst +++ /dev/null @@ -1,4 +0,0 @@ -Introduced new test fixtures ``venv``, ``venv_without_setuptools``, -``bare_venv`` that rely on the ``jaraco.envs`` package. -These new test fixtures were also used to remove the (currently problematic) -dependency on the ``pytest_virtualenv`` plugin. diff --git a/changelog.d/2968.misc.3.rst b/changelog.d/2968.misc.3.rst deleted file mode 100644 index 9b04c131..00000000 --- a/changelog.d/2968.misc.3.rst +++ /dev/null @@ -1,4 +0,0 @@ -Improved isolation for some tests that where inadvertently using the project -root for builds, and therefore creating directories (e.g. ``build``, ``dist``, -``*.egg-info``) that could interfere with the outcome of other tests --- by :user:`abravalheri`. diff --git a/changelog.d/3006.change.rst b/changelog.d/3006.change.rst deleted file mode 100644 index d5f5d99d..00000000 --- a/changelog.d/3006.change.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fixed startup performance issue of Python interpreter due to imports of -costly modules in ``_distutils_hack`` -- by :user:`tiran` diff --git a/changelog.d/3008.doc.1.rst b/changelog.d/3008.doc.1.rst deleted file mode 100644 index dbc09bae..00000000 --- a/changelog.d/3008.doc.1.rst +++ /dev/null @@ -1 +0,0 @@ -"In-tree" Sphinx extension for "favicons" replaced with ``sphinx-favicon``. diff --git a/changelog.d/3008.doc.2.rst b/changelog.d/3008.doc.2.rst deleted file mode 100644 index b094359c..00000000 --- a/changelog.d/3008.doc.2.rst +++ /dev/null @@ -1,2 +0,0 @@ -SVG images (logo, banners, ...) optimised with the help of the ``scour`` -package. diff --git a/setup.cfg b/setup.cfg index 3bf0b618..20959166 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 60.3.1 +version = 60.4.0 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From 8b6e8f5f1f312fef00881ed015f2c0177bc7d61e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 8 Jan 2022 14:39:45 -0500 Subject: =?UTF-8?q?Bump=20version:=2060.4.0=20=E2=86=92=2060.5.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 9 +++++++++ changelog.d/2990.change.rst | 1 - setup.cfg | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/2990.change.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 57c38239..542ed845 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 60.4.0 +current_version = 60.5.0 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index 2f367678..2fee370f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,12 @@ +v60.5.0 +------- + + +Changes +^^^^^^^ +* #2990: Set the ``.origin`` attribute of the ``distutils`` module to the module's ``__file__``. + + v60.4.0 ------- diff --git a/changelog.d/2990.change.rst b/changelog.d/2990.change.rst deleted file mode 100644 index bb6e6032..00000000 --- a/changelog.d/2990.change.rst +++ /dev/null @@ -1 +0,0 @@ -Set the ``.origin`` attribute of the ``distutils`` module to the module's ``__file__``. diff --git a/setup.cfg b/setup.cfg index 20959166..e91d8aed 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 60.4.0 +version = 60.5.0 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From 464568d64469fb1ca9794ef26fe2288f16c15598 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 8 Jan 2022 16:43:07 -0500 Subject: Disable cygwin tests for now. Ref #3016. --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index dd6cef70..6ae4a264 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -42,6 +42,7 @@ jobs: ${{ matrix.python }} test_cygwin: + if: ${{ false }} # failing #3016 strategy: matrix: distutils: -- cgit v1.2.1 From d8af8adcf8db17a76284e245b6bc34c41098d913 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sun, 9 Jan 2022 01:07:08 +0000 Subject: Prevent test files from being included in setuptools own wheel --- setup.cfg | 1 + setuptools/tests/test_setuptools.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/setup.cfg b/setup.cfg index e91d8aed..a276254a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,6 +34,7 @@ exclude = docs* tests* *.tests + *.tests.* tools* [options.extras_require] diff --git a/setuptools/tests/test_setuptools.py b/setuptools/tests/test_setuptools.py index 3c429263..b97faf17 100644 --- a/setuptools/tests/test_setuptools.py +++ b/setuptools/tests/test_setuptools.py @@ -7,6 +7,7 @@ import distutils.cmd from distutils.errors import DistutilsOptionError from distutils.errors import DistutilsSetupError from distutils.core import Extension +from zipfile import ZipFile import pytest @@ -294,3 +295,11 @@ def test_findall_missing_symlink(tmpdir, can_symlink): os.symlink('foo', 'bar') found = list(setuptools.findall()) assert found == [] + + +def test_its_own_wheel_does_not_contain_tests(setuptools_wheel): + with ZipFile(setuptools_wheel) as zipfile: + contents = [f.replace(os.sep, '/') for f in zipfile.namelist()] + + for member in contents: + assert '/tests/' not in member -- cgit v1.2.1 From ca6152aad37eea45257ac7d86111c48026c49598 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 10 Jan 2022 17:16:25 -0500 Subject: Instead of detecting 'get-pip' during the import of pip, detect the attempt to 'import setuptools' during 'get-pip', and in that case, stub the import to signal the presence of setuptools. Ref #3022. Fixes #2993. --- _distutils_hack/__init__.py | 29 +++++++++++++++++++++++++++-- changelog.d/2993.misc.rst | 1 + 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 changelog.d/2993.misc.rst diff --git a/_distutils_hack/__init__.py b/_distutils_hack/__init__.py index ab462f95..0108d854 100644 --- a/_distutils_hack/__init__.py +++ b/_distutils_hack/__init__.py @@ -136,11 +136,36 @@ class DistutilsMetaFinder: """ if self.pip_imported_during_build(): return - if self.is_get_pip(): - return clear_distutils() self.spec_for_distutils = lambda: None + def spec_for_setuptools(self): + """ + get-pip imports setuptools solely for the purpose of + determining if it's installed. In this case, provide + a stubbed spec to represent setuptools being present + without invoking any behavior. + + Workaround for pypa/get-pip#137. + """ + if not self.is_get_pip(): + return + + import importlib + + class StubbedLoader(importlib.abc.Loader): + + def create_module(self, spec): + import types + return types.ModuleType('setuptools') + + def exec_module(self, module): + pass + + return importlib.util.spec_from_loader( + 'setuptools', StubbedLoader(), + ) + @classmethod def pip_imported_during_build(cls): """ diff --git a/changelog.d/2993.misc.rst b/changelog.d/2993.misc.rst new file mode 100644 index 00000000..c1d294d6 --- /dev/null +++ b/changelog.d/2993.misc.rst @@ -0,0 +1 @@ +In _distutils_hack, for get-pip, simulate existence of setuptools. -- cgit v1.2.1 From 9caec1fe2de94409a5928bc5b60366c83174c769 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 10 Jan 2022 20:43:52 -0500 Subject: Update changelog. --- changelog.d/2918.misc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/2918.misc.rst diff --git a/changelog.d/2918.misc.rst b/changelog.d/2918.misc.rst new file mode 100644 index 00000000..9f61d787 --- /dev/null +++ b/changelog.d/2918.misc.rst @@ -0,0 +1 @@ +Correct support for Python 3 native loaders. -- cgit v1.2.1 From 857d7779abcdb355e65b7d1cd7853e5c6d789d77 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 10 Jan 2022 20:51:23 -0500 Subject: =?UTF-8?q?Bump=20version:=2060.5.0=20=E2=86=92=2060.5.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 9 +++++++++ changelog.d/2918.misc.rst | 1 - setup.cfg | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/2918.misc.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 542ed845..5d0f65ac 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 60.5.0 +current_version = 60.5.1 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index 2fee370f..f07b27e4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,12 @@ +v60.5.1 +------- + + +Misc +^^^^ +* #2918: Correct support for Python 3 native loaders. + + v60.5.0 ------- diff --git a/changelog.d/2918.misc.rst b/changelog.d/2918.misc.rst deleted file mode 100644 index 9f61d787..00000000 --- a/changelog.d/2918.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Correct support for Python 3 native loaders. diff --git a/setup.cfg b/setup.cfg index e91d8aed..9676b131 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 60.5.0 +version = 60.5.1 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From 4f9ebdbe81cb81a75c94a93cc53551c558dfc5e7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 11 Jan 2022 17:27:39 -0500 Subject: =?UTF-8?q?Bump=20version:=2060.5.1=20=E2=86=92=2060.5.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 9 +++++++++ changelog.d/2993.misc.rst | 1 - setup.cfg | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/2993.misc.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 5d0f65ac..07bfe8de 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 60.5.1 +current_version = 60.5.2 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index f07b27e4..a512ee1d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,12 @@ +v60.5.2 +------- + + +Misc +^^^^ +* #2993: In _distutils_hack, for get-pip, simulate existence of setuptools. + + v60.5.1 ------- diff --git a/changelog.d/2993.misc.rst b/changelog.d/2993.misc.rst deleted file mode 100644 index c1d294d6..00000000 --- a/changelog.d/2993.misc.rst +++ /dev/null @@ -1 +0,0 @@ -In _distutils_hack, for get-pip, simulate existence of setuptools. diff --git a/setup.cfg b/setup.cfg index 9676b131..e895a3dc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 60.5.1 +version = 60.5.2 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From e3583870a5b56982cc6a4640bdcdd60ca4668b59 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 11 Jan 2022 17:41:11 -0500 Subject: Trim excess indentation. --- setuptools/_distutils/command/install.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/setuptools/_distutils/command/install.py b/setuptools/_distutils/command/install.py index 511938f4..0587ccd0 100644 --- a/setuptools/_distutils/command/install.py +++ b/setuptools/_distutils/command/install.py @@ -397,20 +397,20 @@ class install(Command): abiflags = '' local_vars = { 'dist_name': self.distribution.get_name(), - 'dist_version': self.distribution.get_version(), - 'dist_fullname': self.distribution.get_fullname(), - 'py_version': py_version, - 'py_version_short': '%d.%d' % sys.version_info[:2], - 'py_version_nodot': '%d%d' % sys.version_info[:2], - 'sys_prefix': prefix, - 'prefix': prefix, - 'sys_exec_prefix': exec_prefix, - 'exec_prefix': exec_prefix, - 'abiflags': abiflags, - 'platlibdir': getattr(sys, 'platlibdir', 'lib'), - 'implementation_lower': _get_implementation().lower(), - 'implementation': _get_implementation(), - } + 'dist_version': self.distribution.get_version(), + 'dist_fullname': self.distribution.get_fullname(), + 'py_version': py_version, + 'py_version_short': '%d.%d' % sys.version_info[:2], + 'py_version_nodot': '%d%d' % sys.version_info[:2], + 'sys_prefix': prefix, + 'prefix': prefix, + 'sys_exec_prefix': exec_prefix, + 'exec_prefix': exec_prefix, + 'abiflags': abiflags, + 'platlibdir': getattr(sys, 'platlibdir', 'lib'), + 'implementation_lower': _get_implementation().lower(), + 'implementation': _get_implementation(), + } if HAS_USER_SITE: local_vars['userbase'] = self.install_userbase -- cgit v1.2.1 From b789d313eeb514e9ba469e10f81a333a8f7acc47 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 12 Jan 2022 09:51:18 -0500 Subject: Honor sysconfig variables in easy_install. Fixes #3026. --- changelog.d/3026.misc.rst | 1 + setuptools/command/easy_install.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) create mode 100644 changelog.d/3026.misc.rst diff --git a/changelog.d/3026.misc.rst b/changelog.d/3026.misc.rst new file mode 100644 index 00000000..c9ef986b --- /dev/null +++ b/changelog.d/3026.misc.rst @@ -0,0 +1 @@ +Honor sysconfig variables in easy_install. diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index a2962a7d..514719de 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -39,9 +39,10 @@ import subprocess import shlex import io import configparser +import sysconfig -from sysconfig import get_config_vars, get_path +from sysconfig import get_path from setuptools import SetuptoolsDeprecationWarning @@ -236,23 +237,22 @@ class easy_install(Command): self.version and self._render_version() py_version = sys.version.split()[0] - prefix, exec_prefix = get_config_vars('prefix', 'exec_prefix') - self.config_vars = { + self.config_vars = dict(sysconfig.get_config_vars()) + + self.config_vars.update({ 'dist_name': self.distribution.get_name(), 'dist_version': self.distribution.get_version(), 'dist_fullname': self.distribution.get_fullname(), 'py_version': py_version, 'py_version_short': f'{sys.version_info.major}.{sys.version_info.minor}', 'py_version_nodot': f'{sys.version_info.major}{sys.version_info.minor}', - 'sys_prefix': prefix, - 'prefix': prefix, - 'sys_exec_prefix': exec_prefix, - 'exec_prefix': exec_prefix, + 'sys_prefix': self.config_vars['prefix'], + 'sys_exec_prefix': self.config_vars['exec_prefix'], # Only python 3.2+ has abiflags 'abiflags': getattr(sys, 'abiflags', ''), 'platlibdir': getattr(sys, 'platlibdir', 'lib'), - } + }) with contextlib.suppress(AttributeError): # only for distutils outside stdlib self.config_vars.update({ -- cgit v1.2.1 From f1ee2ad4a45739dc73f4de31a74ad97179b5fdca Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 12 Jan 2022 15:21:16 +0000 Subject: Fix failing test when user site-packages has no version number This error was first reported in https://github.com/pypa/setuptools/commit/bfa75fc56d0bd47bd6c0edf9a0e579508c9fae9e#commitcomment-63663642 The approach taken here is to check for the '3.10' substring only if '3.1' is present. --- setuptools/tests/test_easy_install.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py index 4a2c2537..83ce7f45 100644 --- a/setuptools/tests/test_easy_install.py +++ b/setuptools/tests/test_easy_install.py @@ -1065,11 +1065,6 @@ class TestWindowsScriptWriter: VersionStub = namedtuple("VersionStub", "major, minor, micro, releaselevel, serial") -@pytest.mark.skipif( - os.name == 'nt', - reason='Installation schemes for Windows may use values for interpolation ' - 'that come directly from sysconfig and are difficult to patch/mock' -) def test_use_correct_python_version_string(tmpdir, tmpdir_cwd, monkeypatch): # In issue #3001, easy_install wrongly uses the `python3.1` directory # when the interpreter is `python3.10` and the `--user` option is given. @@ -1095,11 +1090,18 @@ def test_use_correct_python_version_string(tmpdir, tmpdir_cwd, monkeypatch): patch.setattr(cmd, 'create_home_path', mock.Mock()) cmd.finalize_options() - if os.getenv('SETUPTOOLS_USE_DISTUTILS', 'local') == 'local': - # Installation schemes in stdlib distutils might be outdated/bugged - name = "pypy" if hasattr(sys, 'pypy_version_info') else "python" - install_dir = cmd.install_dir.lower() - assert f"{name}3.10" in install_dir or f"{name}310" in install_dir + name = "pypy" if hasattr(sys, 'pypy_version_info') else "python" + install_dir = cmd.install_dir.lower() + + # In some platforms (e.g. Windows), install_dir is mostly determined + # via `sysconfig`, which define constants eagerly at module creation. + # This means that monkeypatching `sys.version` to emulate 3.10 for testing + # may have no effect. + # The safest test here is to rely on the fact that 3.1 is no longer + # supported/tested, and make sure that if 'python3.1' ever appears in the string + # it is followed by another digit (e.g. 'python3.10'). + if re.search(name + r'3\.?1', install_dir): + assert re.search(name + r'3\.?1\d', install_dir) # The following "variables" are used for interpolation in distutils # installation schemes, so it should be fair to treat them as "semi-public", -- cgit v1.2.1 From 2ec2ba50320492dcb901ce2b6bc98765779bbffe Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 13 Jan 2022 21:32:52 -0500 Subject: =?UTF-8?q?Bump=20version:=2060.5.2=20=E2=86=92=2060.5.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 9 +++++++++ changelog.d/3026.misc.rst | 1 - setup.cfg | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/3026.misc.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 07bfe8de..04ab0a5d 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 60.5.2 +current_version = 60.5.3 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index a512ee1d..e76ec3e9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,12 @@ +v60.5.3 +------- + + +Misc +^^^^ +* #3026: Honor sysconfig variables in easy_install. + + v60.5.2 ------- diff --git a/changelog.d/3026.misc.rst b/changelog.d/3026.misc.rst deleted file mode 100644 index c9ef986b..00000000 --- a/changelog.d/3026.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Honor sysconfig variables in easy_install. diff --git a/setup.cfg b/setup.cfg index e895a3dc..8ee692a7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 60.5.2 +version = 60.5.3 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From a01b3da31afd3a6a8d3413ca9ec330068b95f0b0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 14 Jan 2022 20:23:14 -0500 Subject: Create tox routine for updating vendored packages. --- tox.ini | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tox.ini b/tox.ini index d6cc3f56..c43f2ef0 100644 --- a/tox.ini +++ b/tox.ini @@ -47,6 +47,13 @@ passenv = * commands = python tools/finalize.py +[testenv:vendor] +skip_install = True +deps = + paver +commands = + python -m paver -f pavement.py update_vendored + [testenv:release] skip_install = True deps = -- cgit v1.2.1 From 58aec58e7acf32fead05fd062145bd7f38894bc2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 14 Jan 2022 20:25:03 -0500 Subject: Move vendor routine to tools --- pavement.py | 102 ------------------------------------------------------ tools/vendored.py | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ tox.ini | 2 +- 3 files changed, 103 insertions(+), 103 deletions(-) delete mode 100644 pavement.py create mode 100644 tools/vendored.py diff --git a/pavement.py b/pavement.py deleted file mode 100644 index 6d5d519f..00000000 --- a/pavement.py +++ /dev/null @@ -1,102 +0,0 @@ -import re -import sys -import subprocess -from fnmatch import fnmatch - -from paver.easy import task, path as Path - - -def remove_all(paths): - for path in paths: - path.rmtree() if path.isdir() else path.remove() - - -@task -def update_vendored(): - update_pkg_resources() - update_setuptools() - - -def rewrite_packaging(pkg_files, new_root): - """ - Rewrite imports in packaging to redirect to vendored copies. - """ - for file in pkg_files.glob('*.py'): - text = file.text() - text = re.sub(r' (pyparsing)', rf' {new_root}.\1', text) - text = text.replace( - 'from six.moves.urllib import parse', - 'from urllib import parse', - ) - file.write_text(text) - - -def clean(vendor): - """ - Remove all files out of the vendor directory except the meta - data (as pip uninstall doesn't support -t). - """ - remove_all( - path - for path in vendor.glob('*') - if path.basename() != 'vendored.txt' - ) - - -def install(vendor): - clean(vendor) - install_args = [ - sys.executable, - '-m', 'pip', - 'install', - '-r', str(vendor / 'vendored.txt'), - '-t', str(vendor), - ] - subprocess.check_call(install_args) - move_licenses(vendor) - remove_all(vendor.glob('*.dist-info')) - remove_all(vendor.glob('*.egg-info')) - remove_all(vendor.glob('six.py')) - (vendor / '__init__.py').write_text('') - - -def move_licenses(vendor): - license_patterns = ("*LICEN[CS]E*", "COPYING*", "NOTICE*", "AUTHORS*") - licenses = ( - entry - for path in vendor.glob("*.dist-info") - for entry in path.glob("*") - if any(fnmatch(str(entry), p) for p in license_patterns) - ) - for file in licenses: - file.move(_find_license_dest(file, vendor)) - - -def _find_license_dest(license_file, vendor): - basename = license_file.basename() - pkg = license_file.dirname().basename().replace(".dist-info", "") - parts = pkg.split("-") - acc = [] - for part in parts: - # Find actual name from normalized name + version - acc.append(part) - for option in ("_".join(acc), "-".join(acc), ".".join(acc)): - candidate = vendor / option - if candidate.isdir(): - return candidate / basename - if Path(f"{candidate}.py").isfile(): - return Path(f"{candidate}.{basename}") - - raise FileNotFoundError(f"No destination found for {license_file}") - - -def update_pkg_resources(): - vendor = Path('pkg_resources/_vendor') - install(vendor) - rewrite_packaging(vendor / 'packaging', 'pkg_resources.extern') - - -def update_setuptools(): - vendor = Path('setuptools/_vendor') - install(vendor) - rewrite_packaging(vendor / 'packaging', 'setuptools.extern') diff --git a/tools/vendored.py b/tools/vendored.py new file mode 100644 index 00000000..6d5d519f --- /dev/null +++ b/tools/vendored.py @@ -0,0 +1,102 @@ +import re +import sys +import subprocess +from fnmatch import fnmatch + +from paver.easy import task, path as Path + + +def remove_all(paths): + for path in paths: + path.rmtree() if path.isdir() else path.remove() + + +@task +def update_vendored(): + update_pkg_resources() + update_setuptools() + + +def rewrite_packaging(pkg_files, new_root): + """ + Rewrite imports in packaging to redirect to vendored copies. + """ + for file in pkg_files.glob('*.py'): + text = file.text() + text = re.sub(r' (pyparsing)', rf' {new_root}.\1', text) + text = text.replace( + 'from six.moves.urllib import parse', + 'from urllib import parse', + ) + file.write_text(text) + + +def clean(vendor): + """ + Remove all files out of the vendor directory except the meta + data (as pip uninstall doesn't support -t). + """ + remove_all( + path + for path in vendor.glob('*') + if path.basename() != 'vendored.txt' + ) + + +def install(vendor): + clean(vendor) + install_args = [ + sys.executable, + '-m', 'pip', + 'install', + '-r', str(vendor / 'vendored.txt'), + '-t', str(vendor), + ] + subprocess.check_call(install_args) + move_licenses(vendor) + remove_all(vendor.glob('*.dist-info')) + remove_all(vendor.glob('*.egg-info')) + remove_all(vendor.glob('six.py')) + (vendor / '__init__.py').write_text('') + + +def move_licenses(vendor): + license_patterns = ("*LICEN[CS]E*", "COPYING*", "NOTICE*", "AUTHORS*") + licenses = ( + entry + for path in vendor.glob("*.dist-info") + for entry in path.glob("*") + if any(fnmatch(str(entry), p) for p in license_patterns) + ) + for file in licenses: + file.move(_find_license_dest(file, vendor)) + + +def _find_license_dest(license_file, vendor): + basename = license_file.basename() + pkg = license_file.dirname().basename().replace(".dist-info", "") + parts = pkg.split("-") + acc = [] + for part in parts: + # Find actual name from normalized name + version + acc.append(part) + for option in ("_".join(acc), "-".join(acc), ".".join(acc)): + candidate = vendor / option + if candidate.isdir(): + return candidate / basename + if Path(f"{candidate}.py").isfile(): + return Path(f"{candidate}.{basename}") + + raise FileNotFoundError(f"No destination found for {license_file}") + + +def update_pkg_resources(): + vendor = Path('pkg_resources/_vendor') + install(vendor) + rewrite_packaging(vendor / 'packaging', 'pkg_resources.extern') + + +def update_setuptools(): + vendor = Path('setuptools/_vendor') + install(vendor) + rewrite_packaging(vendor / 'packaging', 'setuptools.extern') diff --git a/tox.ini b/tox.ini index c43f2ef0..3340a9da 100644 --- a/tox.ini +++ b/tox.ini @@ -52,7 +52,7 @@ skip_install = True deps = paver commands = - python -m paver -f pavement.py update_vendored + python -m paver -f tools/vendored.py update_vendored [testenv:release] skip_install = True -- cgit v1.2.1 From 5f1e541b229832c43a97ad96eae4475b8a752fec Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 14 Jan 2022 20:28:15 -0500 Subject: Remove dependency on paver in vendored update. --- setup.cfg | 1 - tools/vendored.py | 6 ++++-- tox.ini | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/setup.cfg b/setup.cfg index bdccc548..eda747d8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -58,7 +58,6 @@ testing = flake8-2020 virtualenv>=13.0.0 wheel - paver pip>=19.1 # For proper file:// URLs support. jaraco.envs>=2.2 pytest-xdist diff --git a/tools/vendored.py b/tools/vendored.py index 6d5d519f..06d81cc9 100644 --- a/tools/vendored.py +++ b/tools/vendored.py @@ -3,7 +3,7 @@ import sys import subprocess from fnmatch import fnmatch -from paver.easy import task, path as Path +from path import Path def remove_all(paths): @@ -11,7 +11,6 @@ def remove_all(paths): path.rmtree() if path.isdir() else path.remove() -@task def update_vendored(): update_pkg_resources() update_setuptools() @@ -100,3 +99,6 @@ def update_setuptools(): vendor = Path('setuptools/_vendor') install(vendor) rewrite_packaging(vendor / 'packaging', 'setuptools.extern') + + +__name__ == '__main__' and update_vendored() diff --git a/tox.ini b/tox.ini index 3340a9da..26aefada 100644 --- a/tox.ini +++ b/tox.ini @@ -50,9 +50,9 @@ commands = [testenv:vendor] skip_install = True deps = - paver + path commands = - python -m paver -f tools/vendored.py update_vendored + python -m tools.vendored [testenv:release] skip_install = True -- cgit v1.2.1 From e08d1df7eec3426545fdcd46a5d5ea8c0492b2e6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 14 Jan 2022 20:48:43 -0500 Subject: Update vendored with no changes. --- pkg_resources/_vendor/pyparsing.LICENSE.txt | 36 ++++++++++++++--------------- setuptools/_vendor/pyparsing.LICENSE.txt | 36 ++++++++++++++--------------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/pkg_resources/_vendor/pyparsing.LICENSE.txt b/pkg_resources/_vendor/pyparsing.LICENSE.txt index 1bf98523..bbc959e0 100644 --- a/pkg_resources/_vendor/pyparsing.LICENSE.txt +++ b/pkg_resources/_vendor/pyparsing.LICENSE.txt @@ -1,18 +1,18 @@ -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/setuptools/_vendor/pyparsing.LICENSE.txt b/setuptools/_vendor/pyparsing.LICENSE.txt index 1bf98523..bbc959e0 100644 --- a/setuptools/_vendor/pyparsing.LICENSE.txt +++ b/setuptools/_vendor/pyparsing.LICENSE.txt @@ -1,18 +1,18 @@ -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -- cgit v1.2.1 From befd59a42b7cc6f0ea043a437b50a06234dffdbc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 14 Jan 2022 21:22:07 -0500 Subject: six isn't installed any longer --- tools/vendored.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/vendored.py b/tools/vendored.py index 06d81cc9..b4565d96 100644 --- a/tools/vendored.py +++ b/tools/vendored.py @@ -55,7 +55,6 @@ def install(vendor): move_licenses(vendor) remove_all(vendor.glob('*.dist-info')) remove_all(vendor.glob('*.egg-info')) - remove_all(vendor.glob('six.py')) (vendor / '__init__.py').write_text('') -- cgit v1.2.1 From 110af0bfb342edc145c2bde58abe876b1eead985 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 14 Jan 2022 21:24:07 -0500 Subject: Simplify vendored script to simply include the metadata. --- .../appdirs-1.4.3.dist-info/DESCRIPTION.rst | 227 ++++++++++ .../_vendor/appdirs-1.4.3.dist-info/INSTALLER | 1 + .../_vendor/appdirs-1.4.3.dist-info/METADATA | 254 +++++++++++ .../_vendor/appdirs-1.4.3.dist-info/RECORD | 10 + .../_vendor/appdirs-1.4.3.dist-info/REQUESTED | 0 .../_vendor/appdirs-1.4.3.dist-info/WHEEL | 6 + .../_vendor/appdirs-1.4.3.dist-info/metadata.json | 1 + .../_vendor/appdirs-1.4.3.dist-info/top_level.txt | 1 + .../_vendor/packaging-21.2.dist-info/INSTALLER | 1 + .../_vendor/packaging-21.2.dist-info/LICENSE | 3 + .../packaging-21.2.dist-info/LICENSE.APACHE | 177 ++++++++ .../_vendor/packaging-21.2.dist-info/LICENSE.BSD | 23 + .../_vendor/packaging-21.2.dist-info/METADATA | 446 ++++++++++++++++++++ .../_vendor/packaging-21.2.dist-info/RECORD | 32 ++ .../_vendor/packaging-21.2.dist-info/REQUESTED | 0 .../_vendor/packaging-21.2.dist-info/WHEEL | 5 + .../_vendor/packaging-21.2.dist-info/top_level.txt | 1 + pkg_resources/_vendor/packaging/LICENSE | 3 - pkg_resources/_vendor/packaging/LICENSE.APACHE | 177 -------- pkg_resources/_vendor/packaging/LICENSE.BSD | 23 - .../pyparsing-2.2.1.dist-info/DESCRIPTION.rst | 3 + .../_vendor/pyparsing-2.2.1.dist-info/INSTALLER | 1 + .../_vendor/pyparsing-2.2.1.dist-info/LICENSE.txt | 18 + .../_vendor/pyparsing-2.2.1.dist-info/METADATA | 30 ++ .../_vendor/pyparsing-2.2.1.dist-info/RECORD | 11 + .../_vendor/pyparsing-2.2.1.dist-info/REQUESTED | 0 .../_vendor/pyparsing-2.2.1.dist-info/WHEEL | 6 + .../pyparsing-2.2.1.dist-info/metadata.json | 1 + .../pyparsing-2.2.1.dist-info/top_level.txt | 1 + pkg_resources/_vendor/pyparsing.LICENSE.txt | 18 - .../more_itertools-8.8.0.dist-info/INSTALLER | 1 + .../_vendor/more_itertools-8.8.0.dist-info/LICENSE | 19 + .../more_itertools-8.8.0.dist-info/METADATA | 462 +++++++++++++++++++++ .../_vendor/more_itertools-8.8.0.dist-info/RECORD | 17 + .../more_itertools-8.8.0.dist-info/REQUESTED | 0 .../_vendor/more_itertools-8.8.0.dist-info/WHEEL | 5 + .../more_itertools-8.8.0.dist-info/top_level.txt | 1 + setuptools/_vendor/more_itertools/LICENSE | 19 - .../_vendor/ordered_set-3.1.1.dist-info/INSTALLER | 1 + .../_vendor/ordered_set-3.1.1.dist-info/METADATA | 157 +++++++ .../ordered_set-3.1.1.dist-info/MIT-LICENSE | 19 + .../_vendor/ordered_set-3.1.1.dist-info/RECORD | 9 + .../_vendor/ordered_set-3.1.1.dist-info/REQUESTED | 0 .../_vendor/ordered_set-3.1.1.dist-info/WHEEL | 6 + .../ordered_set-3.1.1.dist-info/top_level.txt | 1 + setuptools/_vendor/ordered_set.MIT-LICENSE | 19 - .../_vendor/packaging-21.2.dist-info/INSTALLER | 1 + .../_vendor/packaging-21.2.dist-info/LICENSE | 3 + .../packaging-21.2.dist-info/LICENSE.APACHE | 177 ++++++++ .../_vendor/packaging-21.2.dist-info/LICENSE.BSD | 23 + .../_vendor/packaging-21.2.dist-info/METADATA | 446 ++++++++++++++++++++ setuptools/_vendor/packaging-21.2.dist-info/RECORD | 32 ++ .../_vendor/packaging-21.2.dist-info/REQUESTED | 0 setuptools/_vendor/packaging-21.2.dist-info/WHEEL | 5 + .../_vendor/packaging-21.2.dist-info/top_level.txt | 1 + setuptools/_vendor/packaging/LICENSE | 3 - setuptools/_vendor/packaging/LICENSE.APACHE | 177 -------- setuptools/_vendor/packaging/LICENSE.BSD | 23 - .../pyparsing-2.2.1.dist-info/DESCRIPTION.rst | 3 + .../_vendor/pyparsing-2.2.1.dist-info/INSTALLER | 1 + .../_vendor/pyparsing-2.2.1.dist-info/LICENSE.txt | 18 + .../_vendor/pyparsing-2.2.1.dist-info/METADATA | 30 ++ .../_vendor/pyparsing-2.2.1.dist-info/RECORD | 11 + .../_vendor/pyparsing-2.2.1.dist-info/REQUESTED | 0 setuptools/_vendor/pyparsing-2.2.1.dist-info/WHEEL | 6 + .../pyparsing-2.2.1.dist-info/metadata.json | 1 + .../pyparsing-2.2.1.dist-info/top_level.txt | 1 + setuptools/_vendor/pyparsing.LICENSE.txt | 18 - tools/vendored.py | 34 -- 69 files changed, 2716 insertions(+), 514 deletions(-) create mode 100644 pkg_resources/_vendor/appdirs-1.4.3.dist-info/DESCRIPTION.rst create mode 100644 pkg_resources/_vendor/appdirs-1.4.3.dist-info/INSTALLER create mode 100644 pkg_resources/_vendor/appdirs-1.4.3.dist-info/METADATA create mode 100644 pkg_resources/_vendor/appdirs-1.4.3.dist-info/RECORD create mode 100644 pkg_resources/_vendor/appdirs-1.4.3.dist-info/REQUESTED create mode 100644 pkg_resources/_vendor/appdirs-1.4.3.dist-info/WHEEL create mode 100644 pkg_resources/_vendor/appdirs-1.4.3.dist-info/metadata.json create mode 100644 pkg_resources/_vendor/appdirs-1.4.3.dist-info/top_level.txt create mode 100644 pkg_resources/_vendor/packaging-21.2.dist-info/INSTALLER create mode 100644 pkg_resources/_vendor/packaging-21.2.dist-info/LICENSE create mode 100644 pkg_resources/_vendor/packaging-21.2.dist-info/LICENSE.APACHE create mode 100644 pkg_resources/_vendor/packaging-21.2.dist-info/LICENSE.BSD create mode 100644 pkg_resources/_vendor/packaging-21.2.dist-info/METADATA create mode 100644 pkg_resources/_vendor/packaging-21.2.dist-info/RECORD create mode 100644 pkg_resources/_vendor/packaging-21.2.dist-info/REQUESTED create mode 100644 pkg_resources/_vendor/packaging-21.2.dist-info/WHEEL create mode 100644 pkg_resources/_vendor/packaging-21.2.dist-info/top_level.txt delete mode 100644 pkg_resources/_vendor/packaging/LICENSE delete mode 100644 pkg_resources/_vendor/packaging/LICENSE.APACHE delete mode 100644 pkg_resources/_vendor/packaging/LICENSE.BSD create mode 100644 pkg_resources/_vendor/pyparsing-2.2.1.dist-info/DESCRIPTION.rst create mode 100644 pkg_resources/_vendor/pyparsing-2.2.1.dist-info/INSTALLER create mode 100644 pkg_resources/_vendor/pyparsing-2.2.1.dist-info/LICENSE.txt create mode 100644 pkg_resources/_vendor/pyparsing-2.2.1.dist-info/METADATA create mode 100644 pkg_resources/_vendor/pyparsing-2.2.1.dist-info/RECORD create mode 100644 pkg_resources/_vendor/pyparsing-2.2.1.dist-info/REQUESTED create mode 100644 pkg_resources/_vendor/pyparsing-2.2.1.dist-info/WHEEL create mode 100644 pkg_resources/_vendor/pyparsing-2.2.1.dist-info/metadata.json create mode 100644 pkg_resources/_vendor/pyparsing-2.2.1.dist-info/top_level.txt delete mode 100644 pkg_resources/_vendor/pyparsing.LICENSE.txt create mode 100644 setuptools/_vendor/more_itertools-8.8.0.dist-info/INSTALLER create mode 100644 setuptools/_vendor/more_itertools-8.8.0.dist-info/LICENSE create mode 100644 setuptools/_vendor/more_itertools-8.8.0.dist-info/METADATA create mode 100644 setuptools/_vendor/more_itertools-8.8.0.dist-info/RECORD create mode 100644 setuptools/_vendor/more_itertools-8.8.0.dist-info/REQUESTED create mode 100644 setuptools/_vendor/more_itertools-8.8.0.dist-info/WHEEL create mode 100644 setuptools/_vendor/more_itertools-8.8.0.dist-info/top_level.txt delete mode 100644 setuptools/_vendor/more_itertools/LICENSE create mode 100644 setuptools/_vendor/ordered_set-3.1.1.dist-info/INSTALLER create mode 100644 setuptools/_vendor/ordered_set-3.1.1.dist-info/METADATA create mode 100644 setuptools/_vendor/ordered_set-3.1.1.dist-info/MIT-LICENSE create mode 100644 setuptools/_vendor/ordered_set-3.1.1.dist-info/RECORD create mode 100644 setuptools/_vendor/ordered_set-3.1.1.dist-info/REQUESTED create mode 100644 setuptools/_vendor/ordered_set-3.1.1.dist-info/WHEEL create mode 100644 setuptools/_vendor/ordered_set-3.1.1.dist-info/top_level.txt delete mode 100644 setuptools/_vendor/ordered_set.MIT-LICENSE create mode 100644 setuptools/_vendor/packaging-21.2.dist-info/INSTALLER create mode 100644 setuptools/_vendor/packaging-21.2.dist-info/LICENSE create mode 100644 setuptools/_vendor/packaging-21.2.dist-info/LICENSE.APACHE create mode 100644 setuptools/_vendor/packaging-21.2.dist-info/LICENSE.BSD create mode 100644 setuptools/_vendor/packaging-21.2.dist-info/METADATA create mode 100644 setuptools/_vendor/packaging-21.2.dist-info/RECORD create mode 100644 setuptools/_vendor/packaging-21.2.dist-info/REQUESTED create mode 100644 setuptools/_vendor/packaging-21.2.dist-info/WHEEL create mode 100644 setuptools/_vendor/packaging-21.2.dist-info/top_level.txt delete mode 100644 setuptools/_vendor/packaging/LICENSE delete mode 100644 setuptools/_vendor/packaging/LICENSE.APACHE delete mode 100644 setuptools/_vendor/packaging/LICENSE.BSD create mode 100644 setuptools/_vendor/pyparsing-2.2.1.dist-info/DESCRIPTION.rst create mode 100644 setuptools/_vendor/pyparsing-2.2.1.dist-info/INSTALLER create mode 100644 setuptools/_vendor/pyparsing-2.2.1.dist-info/LICENSE.txt create mode 100644 setuptools/_vendor/pyparsing-2.2.1.dist-info/METADATA create mode 100644 setuptools/_vendor/pyparsing-2.2.1.dist-info/RECORD create mode 100644 setuptools/_vendor/pyparsing-2.2.1.dist-info/REQUESTED create mode 100644 setuptools/_vendor/pyparsing-2.2.1.dist-info/WHEEL create mode 100644 setuptools/_vendor/pyparsing-2.2.1.dist-info/metadata.json create mode 100644 setuptools/_vendor/pyparsing-2.2.1.dist-info/top_level.txt delete mode 100644 setuptools/_vendor/pyparsing.LICENSE.txt diff --git a/pkg_resources/_vendor/appdirs-1.4.3.dist-info/DESCRIPTION.rst b/pkg_resources/_vendor/appdirs-1.4.3.dist-info/DESCRIPTION.rst new file mode 100644 index 00000000..c605ec26 --- /dev/null +++ b/pkg_resources/_vendor/appdirs-1.4.3.dist-info/DESCRIPTION.rst @@ -0,0 +1,227 @@ + +.. image:: https://secure.travis-ci.org/ActiveState/appdirs.png + :target: http://travis-ci.org/ActiveState/appdirs + +the problem +=========== + +What directory should your app use for storing user data? If running on Mac OS X, you +should use:: + + ~/Library/Application Support/ + +If on Windows (at least English Win XP) that should be:: + + C:\Documents and Settings\\Application Data\Local Settings\\ + +or possibly:: + + C:\Documents and Settings\\Application Data\\ + +for `roaming profiles `_ but that is another story. + +On Linux (and other Unices) the dir, according to the `XDG +spec `_, is:: + + ~/.local/share/ + + +``appdirs`` to the rescue +========================= + +This kind of thing is what the ``appdirs`` module is for. ``appdirs`` will +help you choose an appropriate: + +- user data dir (``user_data_dir``) +- user config dir (``user_config_dir``) +- user cache dir (``user_cache_dir``) +- site data dir (``site_data_dir``) +- site config dir (``site_config_dir``) +- user log dir (``user_log_dir``) + +and also: + +- is a single module so other Python packages can include their own private copy +- is slightly opinionated on the directory names used. Look for "OPINION" in + documentation and code for when an opinion is being applied. + + +some example output +=================== + +On Mac OS X:: + + >>> from appdirs import * + >>> appname = "SuperApp" + >>> appauthor = "Acme" + >>> user_data_dir(appname, appauthor) + '/Users/trentm/Library/Application Support/SuperApp' + >>> site_data_dir(appname, appauthor) + '/Library/Application Support/SuperApp' + >>> user_cache_dir(appname, appauthor) + '/Users/trentm/Library/Caches/SuperApp' + >>> user_log_dir(appname, appauthor) + '/Users/trentm/Library/Logs/SuperApp' + +On Windows 7:: + + >>> from appdirs import * + >>> appname = "SuperApp" + >>> appauthor = "Acme" + >>> user_data_dir(appname, appauthor) + 'C:\\Users\\trentm\\AppData\\Local\\Acme\\SuperApp' + >>> user_data_dir(appname, appauthor, roaming=True) + 'C:\\Users\\trentm\\AppData\\Roaming\\Acme\\SuperApp' + >>> user_cache_dir(appname, appauthor) + 'C:\\Users\\trentm\\AppData\\Local\\Acme\\SuperApp\\Cache' + >>> user_log_dir(appname, appauthor) + 'C:\\Users\\trentm\\AppData\\Local\\Acme\\SuperApp\\Logs' + +On Linux:: + + >>> from appdirs import * + >>> appname = "SuperApp" + >>> appauthor = "Acme" + >>> user_data_dir(appname, appauthor) + '/home/trentm/.local/share/SuperApp + >>> site_data_dir(appname, appauthor) + '/usr/local/share/SuperApp' + >>> site_data_dir(appname, appauthor, multipath=True) + '/usr/local/share/SuperApp:/usr/share/SuperApp' + >>> user_cache_dir(appname, appauthor) + '/home/trentm/.cache/SuperApp' + >>> user_log_dir(appname, appauthor) + '/home/trentm/.cache/SuperApp/log' + >>> user_config_dir(appname) + '/home/trentm/.config/SuperApp' + >>> site_config_dir(appname) + '/etc/xdg/SuperApp' + >>> os.environ['XDG_CONFIG_DIRS'] = '/etc:/usr/local/etc' + >>> site_config_dir(appname, multipath=True) + '/etc/SuperApp:/usr/local/etc/SuperApp' + + +``AppDirs`` for convenience +=========================== + +:: + + >>> from appdirs import AppDirs + >>> dirs = AppDirs("SuperApp", "Acme") + >>> dirs.user_data_dir + '/Users/trentm/Library/Application Support/SuperApp' + >>> dirs.site_data_dir + '/Library/Application Support/SuperApp' + >>> dirs.user_cache_dir + '/Users/trentm/Library/Caches/SuperApp' + >>> dirs.user_log_dir + '/Users/trentm/Library/Logs/SuperApp' + + + +Per-version isolation +===================== + +If you have multiple versions of your app in use that you want to be +able to run side-by-side, then you may want version-isolation for these +dirs:: + + >>> from appdirs import AppDirs + >>> dirs = AppDirs("SuperApp", "Acme", version="1.0") + >>> dirs.user_data_dir + '/Users/trentm/Library/Application Support/SuperApp/1.0' + >>> dirs.site_data_dir + '/Library/Application Support/SuperApp/1.0' + >>> dirs.user_cache_dir + '/Users/trentm/Library/Caches/SuperApp/1.0' + >>> dirs.user_log_dir + '/Users/trentm/Library/Logs/SuperApp/1.0' + + + +appdirs Changelog +================= + +appdirs 1.4.3 +------------- +- [PR #76] Python 3.6 invalid escape sequence deprecation fixes +- Fix for Python 3.6 support + +appdirs 1.4.2 +------------- +- [PR #84] Allow installing without setuptools +- [PR #86] Fix string delimiters in setup.py description +- Add Python 3.6 support + +appdirs 1.4.1 +------------- +- [issue #38] Fix _winreg import on Windows Py3 +- [issue #55] Make appname optional + +appdirs 1.4.0 +------------- +- [PR #42] AppAuthor is now optional on Windows +- [issue 41] Support Jython on Windows, Mac, and Unix-like platforms. Windows + support requires `JNA `_. +- [PR #44] Fix incorrect behaviour of the site_config_dir method + +appdirs 1.3.0 +------------- +- [Unix, issue 16] Conform to XDG standard, instead of breaking it for + everybody +- [Unix] Removes gratuitous case mangling of the case, since \*nix-es are + usually case sensitive, so mangling is not wise +- [Unix] Fixes the utterly wrong behaviour in ``site_data_dir``, return result + based on XDG_DATA_DIRS and make room for respecting the standard which + specifies XDG_DATA_DIRS is a multiple-value variable +- [Issue 6] Add ``*_config_dir`` which are distinct on nix-es, according to + XDG specs; on Windows and Mac return the corresponding ``*_data_dir`` + +appdirs 1.2.0 +------------- + +- [Unix] Put ``user_log_dir`` under the *cache* dir on Unix. Seems to be more + typical. +- [issue 9] Make ``unicode`` work on py3k. + +appdirs 1.1.0 +------------- + +- [issue 4] Add ``AppDirs.user_log_dir``. +- [Unix, issue 2, issue 7] appdirs now conforms to `XDG base directory spec + `_. +- [Mac, issue 5] Fix ``site_data_dir()`` on Mac. +- [Mac] Drop use of 'Carbon' module in favour of hardcoded paths; supports + Python3 now. +- [Windows] Append "Cache" to ``user_cache_dir`` on Windows by default. Use + ``opinion=False`` option to disable this. +- Add ``appdirs.AppDirs`` convenience class. Usage: + + >>> dirs = AppDirs("SuperApp", "Acme", version="1.0") + >>> dirs.user_data_dir + '/Users/trentm/Library/Application Support/SuperApp/1.0' + +- [Windows] Cherry-pick Komodo's change to downgrade paths to the Windows short + paths if there are high bit chars. +- [Linux] Change default ``user_cache_dir()`` on Linux to be singular, e.g. + "~/.superapp/cache". +- [Windows] Add ``roaming`` option to ``user_data_dir()`` (for use on Windows only) + and change the default ``user_data_dir`` behaviour to use a *non*-roaming + profile dir (``CSIDL_LOCAL_APPDATA`` instead of ``CSIDL_APPDATA``). Why? Because + a large roaming profile can cause login speed issues. The "only syncs on + logout" behaviour can cause surprises in appdata info. + + +appdirs 1.0.1 (never released) +------------------------------ + +Started this changelog 27 July 2010. Before that this module originated in the +`Komodo `_ product as ``applib.py`` and then +as `applib/location.py +`_ (used by +`PyPM `_ in `ActivePython +`_). This is basically a fork of +applib.py 1.0.1 and applib/location.py 1.0.1. + + + diff --git a/pkg_resources/_vendor/appdirs-1.4.3.dist-info/INSTALLER b/pkg_resources/_vendor/appdirs-1.4.3.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/pkg_resources/_vendor/appdirs-1.4.3.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/pkg_resources/_vendor/appdirs-1.4.3.dist-info/METADATA b/pkg_resources/_vendor/appdirs-1.4.3.dist-info/METADATA new file mode 100644 index 00000000..69ddf934 --- /dev/null +++ b/pkg_resources/_vendor/appdirs-1.4.3.dist-info/METADATA @@ -0,0 +1,254 @@ +Metadata-Version: 2.0 +Name: appdirs +Version: 1.4.3 +Summary: A small Python module for determining appropriate platform-specific dirs, e.g. a "user data dir". +Home-page: http://github.com/ActiveState/appdirs +Author: Trent Mick; Sridhar Ratnakumar; Jeff Rouse +Author-email: trentm@gmail.com; github@srid.name; jr@its.to +License: MIT +Keywords: application directory log cache user +Platform: UNKNOWN +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.2 +Classifier: Programming Language :: Python :: 3.3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Topic :: Software Development :: Libraries :: Python Modules + + +.. image:: https://secure.travis-ci.org/ActiveState/appdirs.png + :target: http://travis-ci.org/ActiveState/appdirs + +the problem +=========== + +What directory should your app use for storing user data? If running on Mac OS X, you +should use:: + + ~/Library/Application Support/ + +If on Windows (at least English Win XP) that should be:: + + C:\Documents and Settings\\Application Data\Local Settings\\ + +or possibly:: + + C:\Documents and Settings\\Application Data\\ + +for `roaming profiles `_ but that is another story. + +On Linux (and other Unices) the dir, according to the `XDG +spec `_, is:: + + ~/.local/share/ + + +``appdirs`` to the rescue +========================= + +This kind of thing is what the ``appdirs`` module is for. ``appdirs`` will +help you choose an appropriate: + +- user data dir (``user_data_dir``) +- user config dir (``user_config_dir``) +- user cache dir (``user_cache_dir``) +- site data dir (``site_data_dir``) +- site config dir (``site_config_dir``) +- user log dir (``user_log_dir``) + +and also: + +- is a single module so other Python packages can include their own private copy +- is slightly opinionated on the directory names used. Look for "OPINION" in + documentation and code for when an opinion is being applied. + + +some example output +=================== + +On Mac OS X:: + + >>> from appdirs import * + >>> appname = "SuperApp" + >>> appauthor = "Acme" + >>> user_data_dir(appname, appauthor) + '/Users/trentm/Library/Application Support/SuperApp' + >>> site_data_dir(appname, appauthor) + '/Library/Application Support/SuperApp' + >>> user_cache_dir(appname, appauthor) + '/Users/trentm/Library/Caches/SuperApp' + >>> user_log_dir(appname, appauthor) + '/Users/trentm/Library/Logs/SuperApp' + +On Windows 7:: + + >>> from appdirs import * + >>> appname = "SuperApp" + >>> appauthor = "Acme" + >>> user_data_dir(appname, appauthor) + 'C:\\Users\\trentm\\AppData\\Local\\Acme\\SuperApp' + >>> user_data_dir(appname, appauthor, roaming=True) + 'C:\\Users\\trentm\\AppData\\Roaming\\Acme\\SuperApp' + >>> user_cache_dir(appname, appauthor) + 'C:\\Users\\trentm\\AppData\\Local\\Acme\\SuperApp\\Cache' + >>> user_log_dir(appname, appauthor) + 'C:\\Users\\trentm\\AppData\\Local\\Acme\\SuperApp\\Logs' + +On Linux:: + + >>> from appdirs import * + >>> appname = "SuperApp" + >>> appauthor = "Acme" + >>> user_data_dir(appname, appauthor) + '/home/trentm/.local/share/SuperApp + >>> site_data_dir(appname, appauthor) + '/usr/local/share/SuperApp' + >>> site_data_dir(appname, appauthor, multipath=True) + '/usr/local/share/SuperApp:/usr/share/SuperApp' + >>> user_cache_dir(appname, appauthor) + '/home/trentm/.cache/SuperApp' + >>> user_log_dir(appname, appauthor) + '/home/trentm/.cache/SuperApp/log' + >>> user_config_dir(appname) + '/home/trentm/.config/SuperApp' + >>> site_config_dir(appname) + '/etc/xdg/SuperApp' + >>> os.environ['XDG_CONFIG_DIRS'] = '/etc:/usr/local/etc' + >>> site_config_dir(appname, multipath=True) + '/etc/SuperApp:/usr/local/etc/SuperApp' + + +``AppDirs`` for convenience +=========================== + +:: + + >>> from appdirs import AppDirs + >>> dirs = AppDirs("SuperApp", "Acme") + >>> dirs.user_data_dir + '/Users/trentm/Library/Application Support/SuperApp' + >>> dirs.site_data_dir + '/Library/Application Support/SuperApp' + >>> dirs.user_cache_dir + '/Users/trentm/Library/Caches/SuperApp' + >>> dirs.user_log_dir + '/Users/trentm/Library/Logs/SuperApp' + + + +Per-version isolation +===================== + +If you have multiple versions of your app in use that you want to be +able to run side-by-side, then you may want version-isolation for these +dirs:: + + >>> from appdirs import AppDirs + >>> dirs = AppDirs("SuperApp", "Acme", version="1.0") + >>> dirs.user_data_dir + '/Users/trentm/Library/Application Support/SuperApp/1.0' + >>> dirs.site_data_dir + '/Library/Application Support/SuperApp/1.0' + >>> dirs.user_cache_dir + '/Users/trentm/Library/Caches/SuperApp/1.0' + >>> dirs.user_log_dir + '/Users/trentm/Library/Logs/SuperApp/1.0' + + + +appdirs Changelog +================= + +appdirs 1.4.3 +------------- +- [PR #76] Python 3.6 invalid escape sequence deprecation fixes +- Fix for Python 3.6 support + +appdirs 1.4.2 +------------- +- [PR #84] Allow installing without setuptools +- [PR #86] Fix string delimiters in setup.py description +- Add Python 3.6 support + +appdirs 1.4.1 +------------- +- [issue #38] Fix _winreg import on Windows Py3 +- [issue #55] Make appname optional + +appdirs 1.4.0 +------------- +- [PR #42] AppAuthor is now optional on Windows +- [issue 41] Support Jython on Windows, Mac, and Unix-like platforms. Windows + support requires `JNA `_. +- [PR #44] Fix incorrect behaviour of the site_config_dir method + +appdirs 1.3.0 +------------- +- [Unix, issue 16] Conform to XDG standard, instead of breaking it for + everybody +- [Unix] Removes gratuitous case mangling of the case, since \*nix-es are + usually case sensitive, so mangling is not wise +- [Unix] Fixes the utterly wrong behaviour in ``site_data_dir``, return result + based on XDG_DATA_DIRS and make room for respecting the standard which + specifies XDG_DATA_DIRS is a multiple-value variable +- [Issue 6] Add ``*_config_dir`` which are distinct on nix-es, according to + XDG specs; on Windows and Mac return the corresponding ``*_data_dir`` + +appdirs 1.2.0 +------------- + +- [Unix] Put ``user_log_dir`` under the *cache* dir on Unix. Seems to be more + typical. +- [issue 9] Make ``unicode`` work on py3k. + +appdirs 1.1.0 +------------- + +- [issue 4] Add ``AppDirs.user_log_dir``. +- [Unix, issue 2, issue 7] appdirs now conforms to `XDG base directory spec + `_. +- [Mac, issue 5] Fix ``site_data_dir()`` on Mac. +- [Mac] Drop use of 'Carbon' module in favour of hardcoded paths; supports + Python3 now. +- [Windows] Append "Cache" to ``user_cache_dir`` on Windows by default. Use + ``opinion=False`` option to disable this. +- Add ``appdirs.AppDirs`` convenience class. Usage: + + >>> dirs = AppDirs("SuperApp", "Acme", version="1.0") + >>> dirs.user_data_dir + '/Users/trentm/Library/Application Support/SuperApp/1.0' + +- [Windows] Cherry-pick Komodo's change to downgrade paths to the Windows short + paths if there are high bit chars. +- [Linux] Change default ``user_cache_dir()`` on Linux to be singular, e.g. + "~/.superapp/cache". +- [Windows] Add ``roaming`` option to ``user_data_dir()`` (for use on Windows only) + and change the default ``user_data_dir`` behaviour to use a *non*-roaming + profile dir (``CSIDL_LOCAL_APPDATA`` instead of ``CSIDL_APPDATA``). Why? Because + a large roaming profile can cause login speed issues. The "only syncs on + logout" behaviour can cause surprises in appdata info. + + +appdirs 1.0.1 (never released) +------------------------------ + +Started this changelog 27 July 2010. Before that this module originated in the +`Komodo `_ product as ``applib.py`` and then +as `applib/location.py +`_ (used by +`PyPM `_ in `ActivePython +`_). This is basically a fork of +applib.py 1.0.1 and applib/location.py 1.0.1. + + + diff --git a/pkg_resources/_vendor/appdirs-1.4.3.dist-info/RECORD b/pkg_resources/_vendor/appdirs-1.4.3.dist-info/RECORD new file mode 100644 index 00000000..3f45ff59 --- /dev/null +++ b/pkg_resources/_vendor/appdirs-1.4.3.dist-info/RECORD @@ -0,0 +1,10 @@ +__pycache__/appdirs.cpython-310.pyc,, +appdirs-1.4.3.dist-info/DESCRIPTION.rst,sha256=77Fe8OIOLSjDSNdLiL5xywMKO-AGE42rdXkqKo4Ee-k,7531 +appdirs-1.4.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +appdirs-1.4.3.dist-info/METADATA,sha256=3IFw6jTfImdOqsCb2GYvVR157tL7KEzfRAszn382csk,8773 +appdirs-1.4.3.dist-info/RECORD,, +appdirs-1.4.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +appdirs-1.4.3.dist-info/WHEEL,sha256=o2k-Qa-RMNIJmUdIc7KU6VWR_ErNRbWNlxDIpl7lm34,110 +appdirs-1.4.3.dist-info/metadata.json,sha256=fL_Q-GuFJu3PJxMrwU7SdsI8RGqjIfi2AvouCSF5DSA,1359 +appdirs-1.4.3.dist-info/top_level.txt,sha256=nKncE8CUqZERJ6VuQWL4_bkunSPDNfn7KZqb4Tr5YEM,8 +appdirs.py,sha256=MievUEuv3l_mQISH5SF0shDk_BNhHHzYiAPrT3ITN4I,24701 diff --git a/pkg_resources/_vendor/appdirs-1.4.3.dist-info/REQUESTED b/pkg_resources/_vendor/appdirs-1.4.3.dist-info/REQUESTED new file mode 100644 index 00000000..e69de29b diff --git a/pkg_resources/_vendor/appdirs-1.4.3.dist-info/WHEEL b/pkg_resources/_vendor/appdirs-1.4.3.dist-info/WHEEL new file mode 100644 index 00000000..8b6dd1b5 --- /dev/null +++ b/pkg_resources/_vendor/appdirs-1.4.3.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.29.0) +Root-Is-Purelib: true +Tag: py2-none-any +Tag: py3-none-any + diff --git a/pkg_resources/_vendor/appdirs-1.4.3.dist-info/metadata.json b/pkg_resources/_vendor/appdirs-1.4.3.dist-info/metadata.json new file mode 100644 index 00000000..da1e5f3a --- /dev/null +++ b/pkg_resources/_vendor/appdirs-1.4.3.dist-info/metadata.json @@ -0,0 +1 @@ +{"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Software Development :: Libraries :: Python Modules"], "extensions": {"python.details": {"contacts": [{"email": "trentm@gmail.com; github@srid.name; jr@its.to", "name": "Trent Mick; Sridhar Ratnakumar; Jeff Rouse", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "http://github.com/ActiveState/appdirs"}}}, "generator": "bdist_wheel (0.29.0)", "keywords": ["application", "directory", "log", "cache", "user"], "license": "MIT", "metadata_version": "2.0", "name": "appdirs", "summary": "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\".", "test_requires": [{"requires": []}], "version": "1.4.3"} \ No newline at end of file diff --git a/pkg_resources/_vendor/appdirs-1.4.3.dist-info/top_level.txt b/pkg_resources/_vendor/appdirs-1.4.3.dist-info/top_level.txt new file mode 100644 index 00000000..d64bc321 --- /dev/null +++ b/pkg_resources/_vendor/appdirs-1.4.3.dist-info/top_level.txt @@ -0,0 +1 @@ +appdirs diff --git a/pkg_resources/_vendor/packaging-21.2.dist-info/INSTALLER b/pkg_resources/_vendor/packaging-21.2.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/pkg_resources/_vendor/packaging-21.2.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/pkg_resources/_vendor/packaging-21.2.dist-info/LICENSE b/pkg_resources/_vendor/packaging-21.2.dist-info/LICENSE new file mode 100644 index 00000000..6f62d44e --- /dev/null +++ b/pkg_resources/_vendor/packaging-21.2.dist-info/LICENSE @@ -0,0 +1,3 @@ +This software is made available under the terms of *either* of the licenses +found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made +under the terms of *both* these licenses. diff --git a/pkg_resources/_vendor/packaging-21.2.dist-info/LICENSE.APACHE b/pkg_resources/_vendor/packaging-21.2.dist-info/LICENSE.APACHE new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/pkg_resources/_vendor/packaging-21.2.dist-info/LICENSE.APACHE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/pkg_resources/_vendor/packaging-21.2.dist-info/LICENSE.BSD b/pkg_resources/_vendor/packaging-21.2.dist-info/LICENSE.BSD new file mode 100644 index 00000000..42ce7b75 --- /dev/null +++ b/pkg_resources/_vendor/packaging-21.2.dist-info/LICENSE.BSD @@ -0,0 +1,23 @@ +Copyright (c) Donald Stufft and individual contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pkg_resources/_vendor/packaging-21.2.dist-info/METADATA b/pkg_resources/_vendor/packaging-21.2.dist-info/METADATA new file mode 100644 index 00000000..e8ff54d7 --- /dev/null +++ b/pkg_resources/_vendor/packaging-21.2.dist-info/METADATA @@ -0,0 +1,446 @@ +Metadata-Version: 2.1 +Name: packaging +Version: 21.2 +Summary: Core utilities for Python packages +Home-page: https://github.com/pypa/packaging +Author: Donald Stufft and individual contributors +Author-email: donald@stufft.io +License: BSD-2-Clause or Apache-2.0 +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Apache Software License +Classifier: License :: OSI Approved :: BSD License +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Requires-Python: >=3.6 +Description-Content-Type: text/x-rst +License-File: LICENSE +License-File: LICENSE.APACHE +License-File: LICENSE.BSD +Requires-Dist: pyparsing (<3,>=2.0.2) + +packaging +========= + +.. start-intro + +Reusable core utilities for various Python Packaging +`interoperability specifications `_. + +This library provides utilities that implement the interoperability +specifications which have clearly one correct behaviour (eg: :pep:`440`) +or benefit greatly from having a single shared implementation (eg: :pep:`425`). + +.. end-intro + +The ``packaging`` project includes the following: version handling, specifiers, +markers, requirements, tags, utilities. + +Documentation +------------- + +The `documentation`_ provides information and the API for the following: + +- Version Handling +- Specifiers +- Markers +- Requirements +- Tags +- Utilities + +Installation +------------ + +Use ``pip`` to install these utilities:: + + pip install packaging + +Discussion +---------- + +If you run into bugs, you can file them in our `issue tracker`_. + +You can also join ``#pypa`` on Freenode to ask questions or get involved. + + +.. _`documentation`: https://packaging.pypa.io/ +.. _`issue tracker`: https://github.com/pypa/packaging/issues + + +Code of Conduct +--------------- + +Everyone interacting in the packaging project's codebases, issue trackers, chat +rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_. + +.. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md + +Contributing +------------ + +The ``CONTRIBUTING.rst`` file outlines how to contribute to this project as +well as how to report a potential security issue. The documentation for this +project also covers information about `project development`_ and `security`_. + +.. _`project development`: https://packaging.pypa.io/en/latest/development/ +.. _`security`: https://packaging.pypa.io/en/latest/security/ + +Project History +--------------- + +Please review the ``CHANGELOG.rst`` file or the `Changelog documentation`_ for +recent changes and project history. + +.. _`Changelog documentation`: https://packaging.pypa.io/en/latest/changelog/ + +Changelog +--------- + +21.2 - 2021-10-29 +~~~~~~~~~~~~~~~~~ + +* Update documentation entry for 21.1. + +21.1 - 2021-10-29 +~~~~~~~~~~~~~~~~~ + +* Update pin to pyparsing to exclude 3.0.0. + +21.0 - 2021-07-03 +~~~~~~~~~~~~~~~~~ + +* PEP 656: musllinux support (`#411 `__) +* Drop support for Python 2.7, Python 3.4 and Python 3.5. +* Replace distutils usage with sysconfig (`#396 `__) +* Add support for zip files in ``parse_sdist_filename`` (`#429 `__) +* Use cached ``_hash`` attribute to short-circuit tag equality comparisons (`#417 `__) +* Specify the default value for the ``specifier`` argument to ``SpecifierSet`` (`#437 `__) +* Proper keyword-only "warn" argument in packaging.tags (`#403 `__) +* Correctly remove prerelease suffixes from ~= check (`#366 `__) +* Fix type hints for ``Version.post`` and ``Version.dev`` (`#393 `__) +* Use typing alias ``UnparsedVersion`` (`#398 `__) +* Improve type inference for ``packaging.specifiers.filter()`` (`#430 `__) +* Tighten the return type of ``canonicalize_version()`` (`#402 `__) + +20.9 - 2021-01-29 +~~~~~~~~~~~~~~~~~ + +* Run `isort `_ over the code base (`#377 `__) +* Add support for the ``macosx_10_*_universal2`` platform tags (`#379 `__) +* Introduce ``packaging.utils.parse_wheel_filename()`` and ``parse_sdist_filename()`` + (`#387 `__ and `#389 `__) + +20.8 - 2020-12-11 +~~~~~~~~~~~~~~~~~ + +* Revert back to setuptools for compatibility purposes for some Linux distros (`#363 `__) +* Do not insert an underscore in wheel tags when the interpreter version number + is more than 2 digits (`#372 `__) + +20.7 - 2020-11-28 +~~~~~~~~~~~~~~~~~ + +No unreleased changes. + +20.6 - 2020-11-28 +~~~~~~~~~~~~~~~~~ + +.. note:: This release was subsequently yanked, and these changes were included in 20.7. + +* Fix flit configuration, to include LICENSE files (`#357 `__) +* Make `intel` a recognized CPU architecture for the `universal` macOS platform tag (`#361 `__) +* Add some missing type hints to `packaging.requirements` (issue:`350`) + +20.5 - 2020-11-27 +~~~~~~~~~~~~~~~~~ + +* Officially support Python 3.9 (`#343 `__) +* Deprecate the ``LegacyVersion`` and ``LegacySpecifier`` classes (`#321 `__) +* Handle ``OSError`` on non-dynamic executables when attempting to resolve + the glibc version string. + +20.4 - 2020-05-19 +~~~~~~~~~~~~~~~~~ + +* Canonicalize version before comparing specifiers. (`#282 `__) +* Change type hint for ``canonicalize_name`` to return + ``packaging.utils.NormalizedName``. + This enables the use of static typing tools (like mypy) to detect mixing of + normalized and un-normalized names. + +20.3 - 2020-03-05 +~~~~~~~~~~~~~~~~~ + +* Fix changelog for 20.2. + +20.2 - 2020-03-05 +~~~~~~~~~~~~~~~~~ + +* Fix a bug that caused a 32-bit OS that runs on a 64-bit ARM CPU (e.g. ARM-v8, + aarch64), to report the wrong bitness. + +20.1 - 2020-01-24 +~~~~~~~~~~~~~~~~~~~ + +* Fix a bug caused by reuse of an exhausted iterator. (`#257 `__) + +20.0 - 2020-01-06 +~~~~~~~~~~~~~~~~~ + +* Add type hints (`#191 `__) + +* Add proper trove classifiers for PyPy support (`#198 `__) + +* Scale back depending on ``ctypes`` for manylinux support detection (`#171 `__) + +* Use ``sys.implementation.name`` where appropriate for ``packaging.tags`` (`#193 `__) + +* Expand upon the API provided by ``packaging.tags``: ``interpreter_name()``, ``mac_platforms()``, ``compatible_tags()``, ``cpython_tags()``, ``generic_tags()`` (`#187 `__) + +* Officially support Python 3.8 (`#232 `__) + +* Add ``major``, ``minor``, and ``micro`` aliases to ``packaging.version.Version`` (`#226 `__) + +* Properly mark ``packaging`` has being fully typed by adding a `py.typed` file (`#226 `__) + +19.2 - 2019-09-18 +~~~~~~~~~~~~~~~~~ + +* Remove dependency on ``attrs`` (`#178 `__, `#179 `__) + +* Use appropriate fallbacks for CPython ABI tag (`#181 `__, `#185 `__) + +* Add manylinux2014 support (`#186 `__) + +* Improve ABI detection (`#181 `__) + +* Properly handle debug wheels for Python 3.8 (`#172 `__) + +* Improve detection of debug builds on Windows (`#194 `__) + +19.1 - 2019-07-30 +~~~~~~~~~~~~~~~~~ + +* Add the ``packaging.tags`` module. (`#156 `__) + +* Correctly handle two-digit versions in ``python_version`` (`#119 `__) + + +19.0 - 2019-01-20 +~~~~~~~~~~~~~~~~~ + +* Fix string representation of PEP 508 direct URL requirements with markers. + +* Better handling of file URLs + + This allows for using ``file:///absolute/path``, which was previously + prevented due to the missing ``netloc``. + + This allows for all file URLs that ``urlunparse`` turns back into the + original URL to be valid. + + +18.0 - 2018-09-26 +~~~~~~~~~~~~~~~~~ + +* Improve error messages when invalid requirements are given. (`#129 `__) + + +17.1 - 2017-02-28 +~~~~~~~~~~~~~~~~~ + +* Fix ``utils.canonicalize_version`` when supplying non PEP 440 versions. + + +17.0 - 2017-02-28 +~~~~~~~~~~~~~~~~~ + +* Drop support for python 2.6, 3.2, and 3.3. + +* Define minimal pyparsing version to 2.0.2 (`#91 `__). + +* Add ``epoch``, ``release``, ``pre``, ``dev``, and ``post`` attributes to + ``Version`` and ``LegacyVersion`` (`#34 `__). + +* Add ``Version().is_devrelease`` and ``LegacyVersion().is_devrelease`` to + make it easy to determine if a release is a development release. + +* Add ``utils.canonicalize_version`` to canonicalize version strings or + ``Version`` instances (`#121 `__). + + +16.8 - 2016-10-29 +~~~~~~~~~~~~~~~~~ + +* Fix markers that utilize ``in`` so that they render correctly. + +* Fix an erroneous test on Python RC releases. + + +16.7 - 2016-04-23 +~~~~~~~~~~~~~~~~~ + +* Add support for the deprecated ``python_implementation`` marker which was + an undocumented setuptools marker in addition to the newer markers. + + +16.6 - 2016-03-29 +~~~~~~~~~~~~~~~~~ + +* Add support for the deprecated, PEP 345 environment markers in addition to + the newer markers. + + +16.5 - 2016-02-26 +~~~~~~~~~~~~~~~~~ + +* Fix a regression in parsing requirements with whitespaces between the comma + separators. + + +16.4 - 2016-02-22 +~~~~~~~~~~~~~~~~~ + +* Fix a regression in parsing requirements like ``foo (==4)``. + + +16.3 - 2016-02-21 +~~~~~~~~~~~~~~~~~ + +* Fix a bug where ``packaging.requirements:Requirement`` was overly strict when + matching legacy requirements. + + +16.2 - 2016-02-09 +~~~~~~~~~~~~~~~~~ + +* Add a function that implements the name canonicalization from PEP 503. + + +16.1 - 2016-02-07 +~~~~~~~~~~~~~~~~~ + +* Implement requirement specifiers from PEP 508. + + +16.0 - 2016-01-19 +~~~~~~~~~~~~~~~~~ + +* Relicense so that packaging is available under *either* the Apache License, + Version 2.0 or a 2 Clause BSD license. + +* Support installation of packaging when only distutils is available. + +* Fix ``==`` comparison when there is a prefix and a local version in play. + (`#41 `__). + +* Implement environment markers from PEP 508. + + +15.3 - 2015-08-01 +~~~~~~~~~~~~~~~~~ + +* Normalize post-release spellings for rev/r prefixes. `#35 `__ + + +15.2 - 2015-05-13 +~~~~~~~~~~~~~~~~~ + +* Fix an error where the arbitrary specifier (``===``) was not correctly + allowing pre-releases when it was being used. + +* Expose the specifier and version parts through properties on the + ``Specifier`` classes. + +* Allow iterating over the ``SpecifierSet`` to get access to all of the + ``Specifier`` instances. + +* Allow testing if a version is contained within a specifier via the ``in`` + operator. + + +15.1 - 2015-04-13 +~~~~~~~~~~~~~~~~~ + +* Fix a logic error that was causing inconsistent answers about whether or not + a pre-release was contained within a ``SpecifierSet`` or not. + + +15.0 - 2015-01-02 +~~~~~~~~~~~~~~~~~ + +* Add ``Version().is_postrelease`` and ``LegacyVersion().is_postrelease`` to + make it easy to determine if a release is a post release. + +* Add ``Version().base_version`` and ``LegacyVersion().base_version`` to make + it easy to get the public version without any pre or post release markers. + +* Support the update to PEP 440 which removed the implied ``!=V.*`` when using + either ``>V`` or ``V`` or ````) operator. + + +14.3 - 2014-11-19 +~~~~~~~~~~~~~~~~~ + +* **BACKWARDS INCOMPATIBLE** Refactor specifier support so that it can sanely + handle legacy specifiers as well as PEP 440 specifiers. + +* **BACKWARDS INCOMPATIBLE** Move the specifier support out of + ``packaging.version`` into ``packaging.specifiers``. + + +14.2 - 2014-09-10 +~~~~~~~~~~~~~~~~~ + +* Add prerelease support to ``Specifier``. +* Remove the ability to do ``item in Specifier()`` and replace it with + ``Specifier().contains(item)`` in order to allow flags that signal if a + prerelease should be accepted or not. +* Add a method ``Specifier().filter()`` which will take an iterable and returns + an iterable with items that do not match the specifier filtered out. + + +14.1 - 2014-09-08 +~~~~~~~~~~~~~~~~~ + +* Allow ``LegacyVersion`` and ``Version`` to be sorted together. +* Add ``packaging.version.parse()`` to enable easily parsing a version string + as either a ``Version`` or a ``LegacyVersion`` depending on it's PEP 440 + validity. + + +14.0 - 2014-09-05 +~~~~~~~~~~~~~~~~~ + +* Initial release. + + +.. _`master`: https://github.com/pypa/packaging/ + + diff --git a/pkg_resources/_vendor/packaging-21.2.dist-info/RECORD b/pkg_resources/_vendor/packaging-21.2.dist-info/RECORD new file mode 100644 index 00000000..ed2291ac --- /dev/null +++ b/pkg_resources/_vendor/packaging-21.2.dist-info/RECORD @@ -0,0 +1,32 @@ +packaging-21.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +packaging-21.2.dist-info/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197 +packaging-21.2.dist-info/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174 +packaging-21.2.dist-info/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344 +packaging-21.2.dist-info/METADATA,sha256=N4A8uSYrQwV9byem7YuI9OtVkbqiNzFlDhcDVT-suAo,14754 +packaging-21.2.dist-info/RECORD,, +packaging-21.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +packaging-21.2.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92 +packaging-21.2.dist-info/top_level.txt,sha256=zFdHrhWnPslzsiP455HutQsqPB6v0KCtNUMtUtrefDw,10 +packaging/__about__.py,sha256=IIRHpOsJlJSgkjq1UoeBoMTqhvNp3gN9FyMb5Kf8El4,661 +packaging/__init__.py,sha256=b9Kk5MF7KxhhLgcDmiUWukN-LatWFxPdNug0joPhHSk,497 +packaging/__pycache__/__about__.cpython-310.pyc,, +packaging/__pycache__/__init__.cpython-310.pyc,, +packaging/__pycache__/_manylinux.cpython-310.pyc,, +packaging/__pycache__/_musllinux.cpython-310.pyc,, +packaging/__pycache__/_structures.cpython-310.pyc,, +packaging/__pycache__/markers.cpython-310.pyc,, +packaging/__pycache__/requirements.cpython-310.pyc,, +packaging/__pycache__/specifiers.cpython-310.pyc,, +packaging/__pycache__/tags.cpython-310.pyc,, +packaging/__pycache__/utils.cpython-310.pyc,, +packaging/__pycache__/version.cpython-310.pyc,, +packaging/_manylinux.py,sha256=XcbiXB-qcjv3bcohp6N98TMpOP4_j3m-iOA8ptK2GWY,11488 +packaging/_musllinux.py,sha256=z5yeG1ygOPx4uUyLdqj-p8Dk5UBb5H_b0NIjW9yo8oA,4378 +packaging/_structures.py,sha256=TMiAgFbdUOPmIfDIfiHc3KFhSJ8kMjof2QS5I-2NyQ8,1629 +packaging/markers.py,sha256=Fygi3_eZnjQ-3VJizW5AhI5wvo0Hb6RMk4DidsKpOC0,8475 +packaging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +packaging/requirements.py,sha256=rjaGRCMepZS1mlYMjJ5Qh6rfq3gtsCRQUQmftGZ_bu8,4664 +packaging/specifiers.py,sha256=MZ-fYcNL3u7pNrt-6g2EQO7AbRXkjc-SPEYwXMQbLmc,30964 +packaging/tags.py,sha256=vGybAUQYlPKMcukzX_2e65fmafnFFuMbD25naYTEwtc,15710 +packaging/utils.py,sha256=dJjeat3BS-TYn1RrUFVwufUMasbtzLfYRoy_HXENeFQ,4200 +packaging/version.py,sha256=_fLRNrFrxYcHVfyo8vk9j8s6JM8N_xsSxVFr6RJyco8,14665 diff --git a/pkg_resources/_vendor/packaging-21.2.dist-info/REQUESTED b/pkg_resources/_vendor/packaging-21.2.dist-info/REQUESTED new file mode 100644 index 00000000..e69de29b diff --git a/pkg_resources/_vendor/packaging-21.2.dist-info/WHEEL b/pkg_resources/_vendor/packaging-21.2.dist-info/WHEEL new file mode 100644 index 00000000..5bad85fd --- /dev/null +++ b/pkg_resources/_vendor/packaging-21.2.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/pkg_resources/_vendor/packaging-21.2.dist-info/top_level.txt b/pkg_resources/_vendor/packaging-21.2.dist-info/top_level.txt new file mode 100644 index 00000000..748809f7 --- /dev/null +++ b/pkg_resources/_vendor/packaging-21.2.dist-info/top_level.txt @@ -0,0 +1 @@ +packaging diff --git a/pkg_resources/_vendor/packaging/LICENSE b/pkg_resources/_vendor/packaging/LICENSE deleted file mode 100644 index 6f62d44e..00000000 --- a/pkg_resources/_vendor/packaging/LICENSE +++ /dev/null @@ -1,3 +0,0 @@ -This software is made available under the terms of *either* of the licenses -found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made -under the terms of *both* these licenses. diff --git a/pkg_resources/_vendor/packaging/LICENSE.APACHE b/pkg_resources/_vendor/packaging/LICENSE.APACHE deleted file mode 100644 index f433b1a5..00000000 --- a/pkg_resources/_vendor/packaging/LICENSE.APACHE +++ /dev/null @@ -1,177 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS diff --git a/pkg_resources/_vendor/packaging/LICENSE.BSD b/pkg_resources/_vendor/packaging/LICENSE.BSD deleted file mode 100644 index 42ce7b75..00000000 --- a/pkg_resources/_vendor/packaging/LICENSE.BSD +++ /dev/null @@ -1,23 +0,0 @@ -Copyright (c) Donald Stufft and individual contributors. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pkg_resources/_vendor/pyparsing-2.2.1.dist-info/DESCRIPTION.rst b/pkg_resources/_vendor/pyparsing-2.2.1.dist-info/DESCRIPTION.rst new file mode 100644 index 00000000..e1187231 --- /dev/null +++ b/pkg_resources/_vendor/pyparsing-2.2.1.dist-info/DESCRIPTION.rst @@ -0,0 +1,3 @@ +UNKNOWN + + diff --git a/pkg_resources/_vendor/pyparsing-2.2.1.dist-info/INSTALLER b/pkg_resources/_vendor/pyparsing-2.2.1.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/pkg_resources/_vendor/pyparsing-2.2.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/pkg_resources/_vendor/pyparsing-2.2.1.dist-info/LICENSE.txt b/pkg_resources/_vendor/pyparsing-2.2.1.dist-info/LICENSE.txt new file mode 100644 index 00000000..bbc959e0 --- /dev/null +++ b/pkg_resources/_vendor/pyparsing-2.2.1.dist-info/LICENSE.txt @@ -0,0 +1,18 @@ +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/pkg_resources/_vendor/pyparsing-2.2.1.dist-info/METADATA b/pkg_resources/_vendor/pyparsing-2.2.1.dist-info/METADATA new file mode 100644 index 00000000..a15c350e --- /dev/null +++ b/pkg_resources/_vendor/pyparsing-2.2.1.dist-info/METADATA @@ -0,0 +1,30 @@ +Metadata-Version: 2.0 +Name: pyparsing +Version: 2.2.1 +Summary: Python parsing module +Home-page: https://github.com/pyparsing/pyparsing/ +Author: Paul McGuire +Author-email: ptmcg@users.sourceforge.net +License: MIT License +Download-URL: https://pypi.org/project/pyparsing/ +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Information Technology +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Requires-Python: >=2.6, !=3.0.*, !=3.1.*, !=3.2.* + +UNKNOWN + + diff --git a/pkg_resources/_vendor/pyparsing-2.2.1.dist-info/RECORD b/pkg_resources/_vendor/pyparsing-2.2.1.dist-info/RECORD new file mode 100644 index 00000000..09cc30e3 --- /dev/null +++ b/pkg_resources/_vendor/pyparsing-2.2.1.dist-info/RECORD @@ -0,0 +1,11 @@ +__pycache__/pyparsing.cpython-310.pyc,, +pyparsing-2.2.1.dist-info/DESCRIPTION.rst,sha256=OCTuuN6LcWulhHS3d5rfjdsQtW22n7HENFRh6jC6ego,10 +pyparsing-2.2.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +pyparsing-2.2.1.dist-info/LICENSE.txt,sha256=081Pq74Spe1XdwrGkewNKSqa078kLIh7UWI-wVjdj8I,1041 +pyparsing-2.2.1.dist-info/METADATA,sha256=I0jhx9vpUYlQXjn4gVDnFFoAt3nNrxwR4iuqA_pknYs,1091 +pyparsing-2.2.1.dist-info/RECORD,, +pyparsing-2.2.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +pyparsing-2.2.1.dist-info/WHEEL,sha256=kdsN-5OJAZIiHN-iO4Rhl82KyS0bDWf4uBwMbkNafr8,110 +pyparsing-2.2.1.dist-info/metadata.json,sha256=v1_77-dSdajUZSItSJg8Ov9M713STY3PzhyrRvs1ax4,1185 +pyparsing-2.2.1.dist-info/top_level.txt,sha256=eUOjGzJVhlQ3WS2rFAy2mN3LX_7FKTM5GSJ04jfnLmU,10 +pyparsing.py,sha256=tmrp-lu-qO1i75ZzIN5A12nKRRD1Cm4Vpk-5LR9rims,232055 diff --git a/pkg_resources/_vendor/pyparsing-2.2.1.dist-info/REQUESTED b/pkg_resources/_vendor/pyparsing-2.2.1.dist-info/REQUESTED new file mode 100644 index 00000000..e69de29b diff --git a/pkg_resources/_vendor/pyparsing-2.2.1.dist-info/WHEEL b/pkg_resources/_vendor/pyparsing-2.2.1.dist-info/WHEEL new file mode 100644 index 00000000..7332a419 --- /dev/null +++ b/pkg_resources/_vendor/pyparsing-2.2.1.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.30.0) +Root-Is-Purelib: true +Tag: py2-none-any +Tag: py3-none-any + diff --git a/pkg_resources/_vendor/pyparsing-2.2.1.dist-info/metadata.json b/pkg_resources/_vendor/pyparsing-2.2.1.dist-info/metadata.json new file mode 100644 index 00000000..b760b766 --- /dev/null +++ b/pkg_resources/_vendor/pyparsing-2.2.1.dist-info/metadata.json @@ -0,0 +1 @@ +{"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7"], "download_url": "https://pypi.org/project/pyparsing/", "extensions": {"python.details": {"contacts": [{"email": "ptmcg@users.sourceforge.net", "name": "Paul McGuire", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst", "license": "LICENSE.txt"}, "project_urls": {"Home": "https://github.com/pyparsing/pyparsing/"}}}, "generator": "bdist_wheel (0.30.0)", "license": "MIT License", "metadata_version": "2.0", "name": "pyparsing", "requires_python": ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*", "summary": "Python parsing module", "version": "2.2.1"} \ No newline at end of file diff --git a/pkg_resources/_vendor/pyparsing-2.2.1.dist-info/top_level.txt b/pkg_resources/_vendor/pyparsing-2.2.1.dist-info/top_level.txt new file mode 100644 index 00000000..210dfec5 --- /dev/null +++ b/pkg_resources/_vendor/pyparsing-2.2.1.dist-info/top_level.txt @@ -0,0 +1 @@ +pyparsing diff --git a/pkg_resources/_vendor/pyparsing.LICENSE.txt b/pkg_resources/_vendor/pyparsing.LICENSE.txt deleted file mode 100644 index bbc959e0..00000000 --- a/pkg_resources/_vendor/pyparsing.LICENSE.txt +++ /dev/null @@ -1,18 +0,0 @@ -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/setuptools/_vendor/more_itertools-8.8.0.dist-info/INSTALLER b/setuptools/_vendor/more_itertools-8.8.0.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/setuptools/_vendor/more_itertools-8.8.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/setuptools/_vendor/more_itertools-8.8.0.dist-info/LICENSE b/setuptools/_vendor/more_itertools-8.8.0.dist-info/LICENSE new file mode 100644 index 00000000..0a523bec --- /dev/null +++ b/setuptools/_vendor/more_itertools-8.8.0.dist-info/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2012 Erik Rose + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/setuptools/_vendor/more_itertools-8.8.0.dist-info/METADATA b/setuptools/_vendor/more_itertools-8.8.0.dist-info/METADATA new file mode 100644 index 00000000..bdaee655 --- /dev/null +++ b/setuptools/_vendor/more_itertools-8.8.0.dist-info/METADATA @@ -0,0 +1,462 @@ +Metadata-Version: 2.1 +Name: more-itertools +Version: 8.8.0 +Summary: More routines for operating on iterables, beyond itertools +Home-page: https://github.com/more-itertools/more-itertools +Author: Erik Rose +Author-email: erikrose@grinchcentral.com +License: MIT +Keywords: itertools,iterator,iteration,filter,peek,peekable,collate,chunk,chunked +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Natural Language :: English +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Software Development :: Libraries +Requires-Python: >=3.5 +Description-Content-Type: text/x-rst + +============== +More Itertools +============== + +.. image:: https://readthedocs.org/projects/more-itertools/badge/?version=latest + :target: https://more-itertools.readthedocs.io/en/stable/ + +Python's ``itertools`` library is a gem - you can compose elegant solutions +for a variety of problems with the functions it provides. In ``more-itertools`` +we collect additional building blocks, recipes, and routines for working with +Python iterables. + ++------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Grouping | `chunked `_, | +| | `ichunked `_, | +| | `sliced `_, | +| | `distribute `_, | +| | `divide `_, | +| | `split_at `_, | +| | `split_before `_, | +| | `split_after `_, | +| | `split_into `_, | +| | `split_when `_, | +| | `bucket `_, | +| | `unzip `_, | +| | `grouper `_, | +| | `partition `_ | ++------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Lookahead and lookback | `spy `_, | +| | `peekable `_, | +| | `seekable `_ | ++------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Windowing | `windowed `_, | +| | `substrings `_, | +| | `substrings_indexes `_, | +| | `stagger `_, | +| | `windowed_complete `_, | +| | `pairwise `_ | ++------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Augmenting | `count_cycle `_, | +| | `intersperse `_, | +| | `padded `_, | +| | `mark_ends `_, | +| | `repeat_last `_, | +| | `adjacent `_, | +| | `groupby_transform `_, | +| | `padnone `_, | +| | `ncycles `_ | ++------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Combining | `collapse `_, | +| | `sort_together `_, | +| | `interleave `_, | +| | `interleave_longest `_, | +| | `zip_offset `_, | +| | `zip_equal `_, | +| | `dotproduct `_, | +| | `convolve `_, | +| | `flatten `_, | +| | `roundrobin `_, | +| | `prepend `_, | +| | `value_chain `_ | ++------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Summarizing | `ilen `_, | +| | `unique_to_each `_, | +| | `sample `_, | +| | `consecutive_groups `_, | +| | `run_length `_, | +| | `map_reduce `_, | +| | `exactly_n `_, | +| | `is_sorted `_, | +| | `all_equal `_, | +| | `all_unique `_, | +| | `first_true `_, | +| | `quantify `_ | ++------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Selecting | `islice_extended `_, | +| | `first `_, | +| | `last `_, | +| | `one `_, | +| | `only `_, | +| | `strip `_, | +| | `lstrip `_, | +| | `rstrip `_, | +| | `filter_except `_ | +| | `map_except `_ | +| | `nth_or_last `_, | +| | `nth `_, | +| | `take `_, | +| | `tail `_, | +| | `unique_everseen `_, | +| | `unique_justseen `_ | ++------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Combinatorics | `distinct_permutations `_, | +| | `distinct_combinations `_, | +| | `circular_shifts `_, | +| | `partitions `_, | +| | `set_partitions `_, | +| | `product_index `_, | +| | `combination_index `_, | +| | `permutation_index `_, | +| | `powerset `_, | +| | `random_product `_, | +| | `random_permutation `_, | +| | `random_combination `_, | +| | `random_combination_with_replacement `_, | +| | `nth_product `_ | +| | `nth_permutation `_ | +| | `nth_combination `_ | ++------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Wrapping | `always_iterable `_, | +| | `always_reversible `_, | +| | `countable `_, | +| | `consumer `_, | +| | `with_iter `_, | +| | `iter_except `_ | ++------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Others | `locate `_, | +| | `rlocate `_, | +| | `replace `_, | +| | `numeric_range `_, | +| | `side_effect `_, | +| | `iterate `_, | +| | `difference `_, | +| | `make_decorator `_, | +| | `SequenceView `_, | +| | `time_limited `_, | +| | `consume `_, | +| | `tabulate `_, | +| | `repeatfunc `_ | ++------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + + +Getting started +=============== + +To get started, install the library with `pip `_: + +.. code-block:: shell + + pip install more-itertools + +The recipes from the `itertools docs `_ +are included in the top-level package: + +.. code-block:: python + + >>> from more_itertools import flatten + >>> iterable = [(0, 1), (2, 3)] + >>> list(flatten(iterable)) + [0, 1, 2, 3] + +Several new recipes are available as well: + +.. code-block:: python + + >>> from more_itertools import chunked + >>> iterable = [0, 1, 2, 3, 4, 5, 6, 7, 8] + >>> list(chunked(iterable, 3)) + [[0, 1, 2], [3, 4, 5], [6, 7, 8]] + + >>> from more_itertools import spy + >>> iterable = (x * x for x in range(1, 6)) + >>> head, iterable = spy(iterable, n=3) + >>> list(head) + [1, 4, 9] + >>> list(iterable) + [1, 4, 9, 16, 25] + + + +For the full listing of functions, see the `API documentation `_. + + +Links elsewhere +=============== + +Blog posts about ``more-itertools``: + +* `Yo, I heard you like decorators `__ +* `Tour of Python Itertools `__ (`Alternate `__) + + +Development +=========== + +``more-itertools`` is maintained by `@erikrose `_ +and `@bbayles `_, with help from `many others `_. +If you have a problem or suggestion, please file a bug or pull request in this +repository. Thanks for contributing! + + +Version History +=============== + + + :noindex: + +8.8.0 +----- + +* New functions + * countable (thanks to krzysieq) + +* Changes to existing functions + * split_before was updated to handle empy collections (thanks to TiunovNN) + * unique_everseen got a performance boost (thanks to Numerlor) + * The type hint for value_chain was corrected (thanks to vr2262) + +8.7.0 +----- + +* New functions + * convolve (from the Python itertools docs) + * product_index, combination_index, and permutation_index (thanks to N8Brooks) + * value_chain (thanks to jenstroeger) + +* Changes to existing functions + * distinct_combinations now uses a non-recursive algorithm (thanks to knutdrand) + * pad_none is now the preferred name for padnone, though the latter remains available. + * pairwise will now use the Python standard library implementation on Python 3.10+ + * sort_together now accepts a ``key`` argument (thanks to brianmaissy) + * seekable now has a ``peek`` method, and can indicate whether the iterator it's wrapping is exhausted (thanks to gsakkis) + * time_limited can now indicate whether its iterator has expired (thanks to roysmith) + * The implementation of unique_everseen was improved (thanks to plammens) + +* Other changes: + * Various documentation updates (thanks to cthoyt, Evantm, and cyphase) + +8.6.0 +----- + +* New itertools + * all_unique (thanks to brianmaissy) + * nth_product and nth_permutation (thanks to N8Brooks) + +* Changes to existing itertools + * chunked and sliced now accept a ``strict`` parameter (thanks to shlomif and jtwool) + +* Other changes + * Python 3.5 has reached its end of life and is no longer supported. + * Python 3.9 is officially supported. + * Various documentation fixes (thanks to timgates42) + +8.5.0 +----- + +* New itertools + * windowed_complete (thanks to MarcinKonowalczyk) + +* Changes to existing itertools: + * The is_sorted implementation was improved (thanks to cool-RR) + * The groupby_transform now accepts a ``reducefunc`` parameter. + * The last implementation was improved (thanks to brianmaissy) + +* Other changes + * Various documentation fixes (thanks to craigrosie, samuelstjean, PiCT0) + * The tests for distinct_combinations were improved (thanks to Minabsapi) + * Automated tests now run on GitHub Actions. All commits now check: + * That unit tests pass + * That the examples in docstrings work + * That test coverage remains high (using `coverage`) + * For linting errors (using `flake8`) + * For consistent style (using `black`) + * That the type stubs work (using `mypy`) + * That the docs build correctly (using `sphinx`) + * That packages build correctly (using `twine`) + +8.4.0 +----- + +* New itertools + * mark_ends (thanks to kalekundert) + * is_sorted + +* Changes to existing itertools: + * islice_extended can now be used with real slices (thanks to cool-RR) + * The implementations for filter_except and map_except were improved (thanks to SergBobrovsky) + +* Other changes + * Automated tests now enforce code style (using `black `__) + * The various signatures of islice_extended and numeric_range now appear in the docs (thanks to dsfulf) + * The test configuration for mypy was updated (thanks to blueyed) + + +8.3.0 +----- + +* New itertools + * zip_equal (thanks to frankier and alexmojaki) + +* Changes to existing itertools: + * split_at, split_before, split_after, and split_when all got a ``maxsplit`` paramter (thanks to jferard and ilai-deutel) + * split_at now accepts a ``keep_separator`` parameter (thanks to jferard) + * distinct_permutations can now generate ``r``-length permutations (thanks to SergBobrovsky and ilai-deutel) + * The windowed implementation was improved (thanks to SergBobrovsky) + * The spy implementation was improved (thanks to has2k1) + +* Other changes + * Type stubs are now tested with ``stubtest`` (thanks to ilai-deutel) + * Tests now run with ``python -m unittest`` instead of ``python setup.py test`` (thanks to jdufresne) + +8.2.0 +----- + +* Bug fixes + * The .pyi files for typing were updated. (thanks to blueyed and ilai-deutel) + +* Changes to existing itertools: + * numeric_range now behaves more like the built-in range. (thanks to jferard) + * bucket now allows for enumerating keys. (thanks to alexchandel) + * sliced now should now work for numpy arrays. (thanks to sswingle) + * seekable now has a ``maxlen`` parameter. + +8.1.0 +----- + +* Bug fixes + * partition works with ``pred=None`` again. (thanks to MSeifert04) + +* New itertools + * sample (thanks to tommyod) + * nth_or_last (thanks to d-ryzhikov) + +* Changes to existing itertools: + * The implementation for divide was improved. (thanks to jferard) + +8.0.2 +----- + +* Bug fixes + * The type stub files are now part of the wheel distribution (thanks to keisheiled) + +8.0.1 +----- + +* Bug fixes + * The type stub files now work for functions imported from the + root package (thanks to keisheiled) + +8.0.0 +----- + +* New itertools and other additions + * This library now ships type hints for use with mypy. + (thanks to ilai-deutel for the implementation, and to gabbard and fmagin for assistance) + * split_when (thanks to jferard) + * repeat_last (thanks to d-ryzhikov) + +* Changes to existing itertools: + * The implementation for set_partitions was improved. (thanks to jferard) + * partition was optimized for expensive predicates. (thanks to stevecj) + * unique_everseen and groupby_transform were re-factored. (thanks to SergBobrovsky) + * The implementation for difference was improved. (thanks to Jabbey92) + +* Other changes + * Python 3.4 has reached its end of life and is no longer supported. + * Python 3.8 is officially supported. (thanks to jdufresne) + * The ``collate`` function has been deprecated. + It raises a ``DeprecationWarning`` if used, and will be removed in a future release. + * one and only now provide more informative error messages. (thanks to gabbard) + * Unit tests were moved outside of the main package (thanks to jdufresne) + * Various documentation fixes (thanks to kriomant, gabbard, jdufresne) + + +7.2.0 +----- + +* New itertools + * distinct_combinations + * set_partitions (thanks to kbarrett) + * filter_except + * map_except + +7.1.0 +----- + +* New itertools + * ichunked (thanks davebelais and youtux) + * only (thanks jaraco) + +* Changes to existing itertools: + * numeric_range now supports ranges specified by + ``datetime.datetime`` and ``datetime.timedelta`` objects (thanks to MSeifert04 for tests). + * difference now supports an *initial* keyword argument. + + +* Other changes + * Various documentation fixes (thanks raimon49, pylang) + +7.0.0 +----- + +* New itertools: + * time_limited + * partitions (thanks to rominf and Saluev) + * substrings_indexes (thanks to rominf) + +* Changes to existing itertools: + * collapse now treats ``bytes`` objects the same as ``str`` objects. (thanks to Sweenpet) + +The major version update is due to the change in the default behavior of +collapse. It now treats ``bytes`` objects the same as ``str`` objects. +This aligns its behavior with always_iterable. + +.. code-block:: python + + >>> from more_itertools import collapse + >>> iterable = [[1, 2], b'345', [6]] + >>> print(list(collapse(iterable))) + [1, 2, b'345', 6] + +6.0.0 +----- + +* Major changes: + * Python 2.7 is no longer supported. The 5.0.0 release will be the last + version targeting Python 2.7. + * All future releases will target the active versions of Python 3. + As of 2019, those are Python 3.4 and above. + * The ``six`` library is no longer a dependency. + * The accumulate function is no longer part of this library. You + may import a better version from the standard ``itertools`` module. + +* Changes to existing itertools: + * The order of the parameters in grouper have changed to match + the latest recipe in the itertools documentation. Use of the old order + will be supported in this release, but emit a ``DeprecationWarning``. + The legacy behavior will be dropped in a future release. (thanks to jaraco) + * distinct_permutations was improved (thanks to jferard - see also `permutations with unique values `_ at StackOverflow.) + * An unused parameter was removed from substrings. (thanks to pylang) + +* Other changes: + * The docs for unique_everseen were improved. (thanks to jferard and MSeifert04) + * Several Python 2-isms were removed. (thanks to jaraco, MSeifert04, and hugovk) + + diff --git a/setuptools/_vendor/more_itertools-8.8.0.dist-info/RECORD b/setuptools/_vendor/more_itertools-8.8.0.dist-info/RECORD new file mode 100644 index 00000000..36ffbd86 --- /dev/null +++ b/setuptools/_vendor/more_itertools-8.8.0.dist-info/RECORD @@ -0,0 +1,17 @@ +more_itertools-8.8.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +more_itertools-8.8.0.dist-info/LICENSE,sha256=CfHIyelBrz5YTVlkHqm4fYPAyw_QB-te85Gn4mQ8GkY,1053 +more_itertools-8.8.0.dist-info/METADATA,sha256=Gke9w7RnfiAvveik_iBBrzd0RjrDhsQ8uRYNBJdo4qQ,40482 +more_itertools-8.8.0.dist-info/RECORD,, +more_itertools-8.8.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +more_itertools-8.8.0.dist-info/WHEEL,sha256=OqRkF0eY5GHssMorFjlbTIq072vpHpF60fIQA6lS9xA,92 +more_itertools-8.8.0.dist-info/top_level.txt,sha256=fAuqRXu9LPhxdB9ujJowcFOu1rZ8wzSpOW9_jlKis6M,15 +more_itertools/__init__.py,sha256=C7sXffHTXM3P-iaLPPfqfmDoxOflQMJLcM7ed9p3jak,82 +more_itertools/__init__.pyi,sha256=5B3eTzON1BBuOLob1vCflyEb2lSd6usXQQ-Cv-hXkeA,43 +more_itertools/__pycache__/__init__.cpython-310.pyc,, +more_itertools/__pycache__/more.cpython-310.pyc,, +more_itertools/__pycache__/recipes.cpython-310.pyc,, +more_itertools/more.py,sha256=DlZa8v6JihVwfQ5zHidOA-xDE0orcQIUyxVnCaUoDKE,117968 +more_itertools/more.pyi,sha256=r32pH2raBC1zih3evK4fyvAXvrUamJqc6dgV7QCRL_M,14977 +more_itertools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +more_itertools/recipes.py,sha256=UkNkrsZyqiwgLHANBTmvMhCvaNSvSNYhyOpz_Jc55DY,16256 +more_itertools/recipes.pyi,sha256=9BpeKd5_qalYVSnuHfqPSCfoGgqnQY2Xu9pNwrDlHU8,3551 diff --git a/setuptools/_vendor/more_itertools-8.8.0.dist-info/REQUESTED b/setuptools/_vendor/more_itertools-8.8.0.dist-info/REQUESTED new file mode 100644 index 00000000..e69de29b diff --git a/setuptools/_vendor/more_itertools-8.8.0.dist-info/WHEEL b/setuptools/_vendor/more_itertools-8.8.0.dist-info/WHEEL new file mode 100644 index 00000000..385faab0 --- /dev/null +++ b/setuptools/_vendor/more_itertools-8.8.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.36.2) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/setuptools/_vendor/more_itertools-8.8.0.dist-info/top_level.txt b/setuptools/_vendor/more_itertools-8.8.0.dist-info/top_level.txt new file mode 100644 index 00000000..a5035bef --- /dev/null +++ b/setuptools/_vendor/more_itertools-8.8.0.dist-info/top_level.txt @@ -0,0 +1 @@ +more_itertools diff --git a/setuptools/_vendor/more_itertools/LICENSE b/setuptools/_vendor/more_itertools/LICENSE deleted file mode 100644 index 0a523bec..00000000 --- a/setuptools/_vendor/more_itertools/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2012 Erik Rose - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/setuptools/_vendor/ordered_set-3.1.1.dist-info/INSTALLER b/setuptools/_vendor/ordered_set-3.1.1.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/setuptools/_vendor/ordered_set-3.1.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/setuptools/_vendor/ordered_set-3.1.1.dist-info/METADATA b/setuptools/_vendor/ordered_set-3.1.1.dist-info/METADATA new file mode 100644 index 00000000..db6e12f2 --- /dev/null +++ b/setuptools/_vendor/ordered_set-3.1.1.dist-info/METADATA @@ -0,0 +1,157 @@ +Metadata-Version: 2.1 +Name: ordered-set +Version: 3.1.1 +Summary: A MutableSet that remembers its order, so that every entry has an index. +Home-page: https://github.com/LuminosoInsight/ordered-set +Maintainer: Robyn Speer +Maintainer-email: rspeer@luminoso.com +License: MIT-LICENSE +Platform: any +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Requires-Python: >=2.7 +Description-Content-Type: text/markdown +License-File: MIT-LICENSE + +[![Travis](https://img.shields.io/travis/LuminosoInsight/ordered-set/master.svg?label=Travis%20CI)](https://travis-ci.org/LuminosoInsight/ordered-set) +[![Codecov](https://codecov.io/github/LuminosoInsight/ordered-set/badge.svg?branch=master&service=github)](https://codecov.io/github/LuminosoInsight/ordered-set?branch=master) +[![Pypi](https://img.shields.io/pypi/v/ordered-set.svg)](https://pypi.python.org/pypi/ordered-set) + +An OrderedSet is a mutable data structure that is a hybrid of a list and a set. +It remembers the order of its entries, and every entry has an index number that +can be looked up. + + +## Usage examples + +An OrderedSet is created and used like a set: + + >>> from ordered_set import OrderedSet + + >>> letters = OrderedSet('abracadabra') + + >>> letters + OrderedSet(['a', 'b', 'r', 'c', 'd']) + + >>> 'r' in letters + True + +It is efficient to find the index of an entry in an OrderedSet, or find an +entry by its index. To help with this use case, the `.add()` method returns +the index of the added item, whether it was already in the set or not. + + >>> letters.index('r') + 2 + + >>> letters[2] + 'r' + + >>> letters.add('r') + 2 + + >>> letters.add('x') + 5 + +OrderedSets implement the union (`|`), intersection (`&`), and difference (`-`) +operators like sets do. + + >>> letters |= OrderedSet('shazam') + + >>> letters + OrderedSet(['a', 'b', 'r', 'c', 'd', 'x', 's', 'h', 'z', 'm']) + + >>> letters & set('aeiou') + OrderedSet(['a']) + + >>> letters -= 'abcd' + + >>> letters + OrderedSet(['r', 'x', 's', 'h', 'z', 'm']) + +The `__getitem__()` and `index()` methods have been extended to accept any +iterable except a string, returning a list, to perform NumPy-like "fancy +indexing". + + >>> letters = OrderedSet('abracadabra') + + >>> letters[[0, 2, 3]] + ['a', 'r', 'c'] + + >>> letters.index(['a', 'r', 'c']) + [0, 2, 3] + +OrderedSet implements `__getstate__` and `__setstate__` so it can be pickled, +and implements the abstract base classes `collections.MutableSet` and +`collections.Sequence`. + + +## Interoperability with NumPy and Pandas + +An OrderedSet can be used as a bi-directional mapping between a sparse +vocabulary and dense index numbers. As of version 3.1, it accepts NumPy arrays +of index numbers as well as lists. + +This combination of features makes OrderedSet a simple implementation of many +of the things that `pandas.Index` is used for, and many of its operations are +faster than the equivalent pandas operations. + +For further compatibility with pandas.Index, `get_loc` (the pandas method for +looking up a single index) and `get_indexer` (the pandas method for fancy +indexing in reverse) are both aliases for `index` (which handles both cases +in OrderedSet). + + +## Type hinting +To use type hinting features install `ordered-set-stubs` package from +[PyPI](https://pypi.org/project/ordered-set-stubs/): + + $ pip install ordered-set-stubs + + +## Authors + +OrderedSet was implemented by Robyn Speer. Jon Crall contributed changes and +tests to make it fit the Python set API. + + +## Comparisons + +The original implementation of OrderedSet was a [recipe posted to ActiveState +Recipes][recipe] by Raymond Hettiger, released under the MIT license. + +[recipe]: https://code.activestate.com/recipes/576694-orderedset/ + +Hettiger's implementation kept its content in a doubly-linked list referenced by a +dict. As a result, looking up an item by its index was an O(N) operation, while +deletion was O(1). + +This version makes different trade-offs for the sake of efficient lookups. Its +content is a standard Python list instead of a doubly-linked list. This +provides O(1) lookups by index at the expense of O(N) deletion, as well as +slightly faster iteration. + +In Python 3.6 and later, the built-in `dict` type is inherently ordered. If you +ignore the dictionary values, that also gives you a simple ordered set, with +fast O(1) insertion, deletion, iteration and membership testing. However, `dict` +does not provide the list-like random access features of OrderedSet. You +would have to convert it to a list in O(N) to look up the index of an entry or +look up an entry by its index. + + +## Compatibility + +OrderedSet is automatically tested on Python 2.7, 3.4, 3.5, 3.6, and 3.7. +We've checked more informally that it works on PyPy and PyPy3. + + diff --git a/setuptools/_vendor/ordered_set-3.1.1.dist-info/MIT-LICENSE b/setuptools/_vendor/ordered_set-3.1.1.dist-info/MIT-LICENSE new file mode 100644 index 00000000..25117ef4 --- /dev/null +++ b/setuptools/_vendor/ordered_set-3.1.1.dist-info/MIT-LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018 Luminoso Technologies, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/setuptools/_vendor/ordered_set-3.1.1.dist-info/RECORD b/setuptools/_vendor/ordered_set-3.1.1.dist-info/RECORD new file mode 100644 index 00000000..89579a07 --- /dev/null +++ b/setuptools/_vendor/ordered_set-3.1.1.dist-info/RECORD @@ -0,0 +1,9 @@ +__pycache__/ordered_set.cpython-310.pyc,, +ordered_set-3.1.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +ordered_set-3.1.1.dist-info/METADATA,sha256=uGvfFaNmhcl69lGdHmyOXc30N3U6Jn8DByfh_VHEPpw,5359 +ordered_set-3.1.1.dist-info/MIT-LICENSE,sha256=TvRE7qUSUBcd0ols7wgNf3zDEEJWW7kv7WDRySrMBBE,1071 +ordered_set-3.1.1.dist-info/RECORD,, +ordered_set-3.1.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +ordered_set-3.1.1.dist-info/WHEEL,sha256=z9j0xAa_JmUKMpmz72K0ZGALSM_n-wQVmGbleXx2VHg,110 +ordered_set-3.1.1.dist-info/top_level.txt,sha256=NTY2_aDi1Do9fl3Z9EmWPxasFkUeW2dzO2D3RDx5CfM,12 +ordered_set.py,sha256=dbaCcs27dyN9gnMWGF5nA_BrVn6Q-NrjKYJpV9_fgBs,15130 diff --git a/setuptools/_vendor/ordered_set-3.1.1.dist-info/REQUESTED b/setuptools/_vendor/ordered_set-3.1.1.dist-info/REQUESTED new file mode 100644 index 00000000..e69de29b diff --git a/setuptools/_vendor/ordered_set-3.1.1.dist-info/WHEEL b/setuptools/_vendor/ordered_set-3.1.1.dist-info/WHEEL new file mode 100644 index 00000000..0b18a281 --- /dev/null +++ b/setuptools/_vendor/ordered_set-3.1.1.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.1) +Root-Is-Purelib: true +Tag: py2-none-any +Tag: py3-none-any + diff --git a/setuptools/_vendor/ordered_set-3.1.1.dist-info/top_level.txt b/setuptools/_vendor/ordered_set-3.1.1.dist-info/top_level.txt new file mode 100644 index 00000000..1c191eef --- /dev/null +++ b/setuptools/_vendor/ordered_set-3.1.1.dist-info/top_level.txt @@ -0,0 +1 @@ +ordered_set diff --git a/setuptools/_vendor/ordered_set.MIT-LICENSE b/setuptools/_vendor/ordered_set.MIT-LICENSE deleted file mode 100644 index 25117ef4..00000000 --- a/setuptools/_vendor/ordered_set.MIT-LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2018 Luminoso Technologies, Inc. - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. diff --git a/setuptools/_vendor/packaging-21.2.dist-info/INSTALLER b/setuptools/_vendor/packaging-21.2.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/setuptools/_vendor/packaging-21.2.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/setuptools/_vendor/packaging-21.2.dist-info/LICENSE b/setuptools/_vendor/packaging-21.2.dist-info/LICENSE new file mode 100644 index 00000000..6f62d44e --- /dev/null +++ b/setuptools/_vendor/packaging-21.2.dist-info/LICENSE @@ -0,0 +1,3 @@ +This software is made available under the terms of *either* of the licenses +found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made +under the terms of *both* these licenses. diff --git a/setuptools/_vendor/packaging-21.2.dist-info/LICENSE.APACHE b/setuptools/_vendor/packaging-21.2.dist-info/LICENSE.APACHE new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/setuptools/_vendor/packaging-21.2.dist-info/LICENSE.APACHE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/setuptools/_vendor/packaging-21.2.dist-info/LICENSE.BSD b/setuptools/_vendor/packaging-21.2.dist-info/LICENSE.BSD new file mode 100644 index 00000000..42ce7b75 --- /dev/null +++ b/setuptools/_vendor/packaging-21.2.dist-info/LICENSE.BSD @@ -0,0 +1,23 @@ +Copyright (c) Donald Stufft and individual contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/setuptools/_vendor/packaging-21.2.dist-info/METADATA b/setuptools/_vendor/packaging-21.2.dist-info/METADATA new file mode 100644 index 00000000..e8ff54d7 --- /dev/null +++ b/setuptools/_vendor/packaging-21.2.dist-info/METADATA @@ -0,0 +1,446 @@ +Metadata-Version: 2.1 +Name: packaging +Version: 21.2 +Summary: Core utilities for Python packages +Home-page: https://github.com/pypa/packaging +Author: Donald Stufft and individual contributors +Author-email: donald@stufft.io +License: BSD-2-Clause or Apache-2.0 +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Apache Software License +Classifier: License :: OSI Approved :: BSD License +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Requires-Python: >=3.6 +Description-Content-Type: text/x-rst +License-File: LICENSE +License-File: LICENSE.APACHE +License-File: LICENSE.BSD +Requires-Dist: pyparsing (<3,>=2.0.2) + +packaging +========= + +.. start-intro + +Reusable core utilities for various Python Packaging +`interoperability specifications `_. + +This library provides utilities that implement the interoperability +specifications which have clearly one correct behaviour (eg: :pep:`440`) +or benefit greatly from having a single shared implementation (eg: :pep:`425`). + +.. end-intro + +The ``packaging`` project includes the following: version handling, specifiers, +markers, requirements, tags, utilities. + +Documentation +------------- + +The `documentation`_ provides information and the API for the following: + +- Version Handling +- Specifiers +- Markers +- Requirements +- Tags +- Utilities + +Installation +------------ + +Use ``pip`` to install these utilities:: + + pip install packaging + +Discussion +---------- + +If you run into bugs, you can file them in our `issue tracker`_. + +You can also join ``#pypa`` on Freenode to ask questions or get involved. + + +.. _`documentation`: https://packaging.pypa.io/ +.. _`issue tracker`: https://github.com/pypa/packaging/issues + + +Code of Conduct +--------------- + +Everyone interacting in the packaging project's codebases, issue trackers, chat +rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_. + +.. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md + +Contributing +------------ + +The ``CONTRIBUTING.rst`` file outlines how to contribute to this project as +well as how to report a potential security issue. The documentation for this +project also covers information about `project development`_ and `security`_. + +.. _`project development`: https://packaging.pypa.io/en/latest/development/ +.. _`security`: https://packaging.pypa.io/en/latest/security/ + +Project History +--------------- + +Please review the ``CHANGELOG.rst`` file or the `Changelog documentation`_ for +recent changes and project history. + +.. _`Changelog documentation`: https://packaging.pypa.io/en/latest/changelog/ + +Changelog +--------- + +21.2 - 2021-10-29 +~~~~~~~~~~~~~~~~~ + +* Update documentation entry for 21.1. + +21.1 - 2021-10-29 +~~~~~~~~~~~~~~~~~ + +* Update pin to pyparsing to exclude 3.0.0. + +21.0 - 2021-07-03 +~~~~~~~~~~~~~~~~~ + +* PEP 656: musllinux support (`#411 `__) +* Drop support for Python 2.7, Python 3.4 and Python 3.5. +* Replace distutils usage with sysconfig (`#396 `__) +* Add support for zip files in ``parse_sdist_filename`` (`#429 `__) +* Use cached ``_hash`` attribute to short-circuit tag equality comparisons (`#417 `__) +* Specify the default value for the ``specifier`` argument to ``SpecifierSet`` (`#437 `__) +* Proper keyword-only "warn" argument in packaging.tags (`#403 `__) +* Correctly remove prerelease suffixes from ~= check (`#366 `__) +* Fix type hints for ``Version.post`` and ``Version.dev`` (`#393 `__) +* Use typing alias ``UnparsedVersion`` (`#398 `__) +* Improve type inference for ``packaging.specifiers.filter()`` (`#430 `__) +* Tighten the return type of ``canonicalize_version()`` (`#402 `__) + +20.9 - 2021-01-29 +~~~~~~~~~~~~~~~~~ + +* Run `isort `_ over the code base (`#377 `__) +* Add support for the ``macosx_10_*_universal2`` platform tags (`#379 `__) +* Introduce ``packaging.utils.parse_wheel_filename()`` and ``parse_sdist_filename()`` + (`#387 `__ and `#389 `__) + +20.8 - 2020-12-11 +~~~~~~~~~~~~~~~~~ + +* Revert back to setuptools for compatibility purposes for some Linux distros (`#363 `__) +* Do not insert an underscore in wheel tags when the interpreter version number + is more than 2 digits (`#372 `__) + +20.7 - 2020-11-28 +~~~~~~~~~~~~~~~~~ + +No unreleased changes. + +20.6 - 2020-11-28 +~~~~~~~~~~~~~~~~~ + +.. note:: This release was subsequently yanked, and these changes were included in 20.7. + +* Fix flit configuration, to include LICENSE files (`#357 `__) +* Make `intel` a recognized CPU architecture for the `universal` macOS platform tag (`#361 `__) +* Add some missing type hints to `packaging.requirements` (issue:`350`) + +20.5 - 2020-11-27 +~~~~~~~~~~~~~~~~~ + +* Officially support Python 3.9 (`#343 `__) +* Deprecate the ``LegacyVersion`` and ``LegacySpecifier`` classes (`#321 `__) +* Handle ``OSError`` on non-dynamic executables when attempting to resolve + the glibc version string. + +20.4 - 2020-05-19 +~~~~~~~~~~~~~~~~~ + +* Canonicalize version before comparing specifiers. (`#282 `__) +* Change type hint for ``canonicalize_name`` to return + ``packaging.utils.NormalizedName``. + This enables the use of static typing tools (like mypy) to detect mixing of + normalized and un-normalized names. + +20.3 - 2020-03-05 +~~~~~~~~~~~~~~~~~ + +* Fix changelog for 20.2. + +20.2 - 2020-03-05 +~~~~~~~~~~~~~~~~~ + +* Fix a bug that caused a 32-bit OS that runs on a 64-bit ARM CPU (e.g. ARM-v8, + aarch64), to report the wrong bitness. + +20.1 - 2020-01-24 +~~~~~~~~~~~~~~~~~~~ + +* Fix a bug caused by reuse of an exhausted iterator. (`#257 `__) + +20.0 - 2020-01-06 +~~~~~~~~~~~~~~~~~ + +* Add type hints (`#191 `__) + +* Add proper trove classifiers for PyPy support (`#198 `__) + +* Scale back depending on ``ctypes`` for manylinux support detection (`#171 `__) + +* Use ``sys.implementation.name`` where appropriate for ``packaging.tags`` (`#193 `__) + +* Expand upon the API provided by ``packaging.tags``: ``interpreter_name()``, ``mac_platforms()``, ``compatible_tags()``, ``cpython_tags()``, ``generic_tags()`` (`#187 `__) + +* Officially support Python 3.8 (`#232 `__) + +* Add ``major``, ``minor``, and ``micro`` aliases to ``packaging.version.Version`` (`#226 `__) + +* Properly mark ``packaging`` has being fully typed by adding a `py.typed` file (`#226 `__) + +19.2 - 2019-09-18 +~~~~~~~~~~~~~~~~~ + +* Remove dependency on ``attrs`` (`#178 `__, `#179 `__) + +* Use appropriate fallbacks for CPython ABI tag (`#181 `__, `#185 `__) + +* Add manylinux2014 support (`#186 `__) + +* Improve ABI detection (`#181 `__) + +* Properly handle debug wheels for Python 3.8 (`#172 `__) + +* Improve detection of debug builds on Windows (`#194 `__) + +19.1 - 2019-07-30 +~~~~~~~~~~~~~~~~~ + +* Add the ``packaging.tags`` module. (`#156 `__) + +* Correctly handle two-digit versions in ``python_version`` (`#119 `__) + + +19.0 - 2019-01-20 +~~~~~~~~~~~~~~~~~ + +* Fix string representation of PEP 508 direct URL requirements with markers. + +* Better handling of file URLs + + This allows for using ``file:///absolute/path``, which was previously + prevented due to the missing ``netloc``. + + This allows for all file URLs that ``urlunparse`` turns back into the + original URL to be valid. + + +18.0 - 2018-09-26 +~~~~~~~~~~~~~~~~~ + +* Improve error messages when invalid requirements are given. (`#129 `__) + + +17.1 - 2017-02-28 +~~~~~~~~~~~~~~~~~ + +* Fix ``utils.canonicalize_version`` when supplying non PEP 440 versions. + + +17.0 - 2017-02-28 +~~~~~~~~~~~~~~~~~ + +* Drop support for python 2.6, 3.2, and 3.3. + +* Define minimal pyparsing version to 2.0.2 (`#91 `__). + +* Add ``epoch``, ``release``, ``pre``, ``dev``, and ``post`` attributes to + ``Version`` and ``LegacyVersion`` (`#34 `__). + +* Add ``Version().is_devrelease`` and ``LegacyVersion().is_devrelease`` to + make it easy to determine if a release is a development release. + +* Add ``utils.canonicalize_version`` to canonicalize version strings or + ``Version`` instances (`#121 `__). + + +16.8 - 2016-10-29 +~~~~~~~~~~~~~~~~~ + +* Fix markers that utilize ``in`` so that they render correctly. + +* Fix an erroneous test on Python RC releases. + + +16.7 - 2016-04-23 +~~~~~~~~~~~~~~~~~ + +* Add support for the deprecated ``python_implementation`` marker which was + an undocumented setuptools marker in addition to the newer markers. + + +16.6 - 2016-03-29 +~~~~~~~~~~~~~~~~~ + +* Add support for the deprecated, PEP 345 environment markers in addition to + the newer markers. + + +16.5 - 2016-02-26 +~~~~~~~~~~~~~~~~~ + +* Fix a regression in parsing requirements with whitespaces between the comma + separators. + + +16.4 - 2016-02-22 +~~~~~~~~~~~~~~~~~ + +* Fix a regression in parsing requirements like ``foo (==4)``. + + +16.3 - 2016-02-21 +~~~~~~~~~~~~~~~~~ + +* Fix a bug where ``packaging.requirements:Requirement`` was overly strict when + matching legacy requirements. + + +16.2 - 2016-02-09 +~~~~~~~~~~~~~~~~~ + +* Add a function that implements the name canonicalization from PEP 503. + + +16.1 - 2016-02-07 +~~~~~~~~~~~~~~~~~ + +* Implement requirement specifiers from PEP 508. + + +16.0 - 2016-01-19 +~~~~~~~~~~~~~~~~~ + +* Relicense so that packaging is available under *either* the Apache License, + Version 2.0 or a 2 Clause BSD license. + +* Support installation of packaging when only distutils is available. + +* Fix ``==`` comparison when there is a prefix and a local version in play. + (`#41 `__). + +* Implement environment markers from PEP 508. + + +15.3 - 2015-08-01 +~~~~~~~~~~~~~~~~~ + +* Normalize post-release spellings for rev/r prefixes. `#35 `__ + + +15.2 - 2015-05-13 +~~~~~~~~~~~~~~~~~ + +* Fix an error where the arbitrary specifier (``===``) was not correctly + allowing pre-releases when it was being used. + +* Expose the specifier and version parts through properties on the + ``Specifier`` classes. + +* Allow iterating over the ``SpecifierSet`` to get access to all of the + ``Specifier`` instances. + +* Allow testing if a version is contained within a specifier via the ``in`` + operator. + + +15.1 - 2015-04-13 +~~~~~~~~~~~~~~~~~ + +* Fix a logic error that was causing inconsistent answers about whether or not + a pre-release was contained within a ``SpecifierSet`` or not. + + +15.0 - 2015-01-02 +~~~~~~~~~~~~~~~~~ + +* Add ``Version().is_postrelease`` and ``LegacyVersion().is_postrelease`` to + make it easy to determine if a release is a post release. + +* Add ``Version().base_version`` and ``LegacyVersion().base_version`` to make + it easy to get the public version without any pre or post release markers. + +* Support the update to PEP 440 which removed the implied ``!=V.*`` when using + either ``>V`` or ``V`` or ````) operator. + + +14.3 - 2014-11-19 +~~~~~~~~~~~~~~~~~ + +* **BACKWARDS INCOMPATIBLE** Refactor specifier support so that it can sanely + handle legacy specifiers as well as PEP 440 specifiers. + +* **BACKWARDS INCOMPATIBLE** Move the specifier support out of + ``packaging.version`` into ``packaging.specifiers``. + + +14.2 - 2014-09-10 +~~~~~~~~~~~~~~~~~ + +* Add prerelease support to ``Specifier``. +* Remove the ability to do ``item in Specifier()`` and replace it with + ``Specifier().contains(item)`` in order to allow flags that signal if a + prerelease should be accepted or not. +* Add a method ``Specifier().filter()`` which will take an iterable and returns + an iterable with items that do not match the specifier filtered out. + + +14.1 - 2014-09-08 +~~~~~~~~~~~~~~~~~ + +* Allow ``LegacyVersion`` and ``Version`` to be sorted together. +* Add ``packaging.version.parse()`` to enable easily parsing a version string + as either a ``Version`` or a ``LegacyVersion`` depending on it's PEP 440 + validity. + + +14.0 - 2014-09-05 +~~~~~~~~~~~~~~~~~ + +* Initial release. + + +.. _`master`: https://github.com/pypa/packaging/ + + diff --git a/setuptools/_vendor/packaging-21.2.dist-info/RECORD b/setuptools/_vendor/packaging-21.2.dist-info/RECORD new file mode 100644 index 00000000..ed2291ac --- /dev/null +++ b/setuptools/_vendor/packaging-21.2.dist-info/RECORD @@ -0,0 +1,32 @@ +packaging-21.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +packaging-21.2.dist-info/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197 +packaging-21.2.dist-info/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174 +packaging-21.2.dist-info/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344 +packaging-21.2.dist-info/METADATA,sha256=N4A8uSYrQwV9byem7YuI9OtVkbqiNzFlDhcDVT-suAo,14754 +packaging-21.2.dist-info/RECORD,, +packaging-21.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +packaging-21.2.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92 +packaging-21.2.dist-info/top_level.txt,sha256=zFdHrhWnPslzsiP455HutQsqPB6v0KCtNUMtUtrefDw,10 +packaging/__about__.py,sha256=IIRHpOsJlJSgkjq1UoeBoMTqhvNp3gN9FyMb5Kf8El4,661 +packaging/__init__.py,sha256=b9Kk5MF7KxhhLgcDmiUWukN-LatWFxPdNug0joPhHSk,497 +packaging/__pycache__/__about__.cpython-310.pyc,, +packaging/__pycache__/__init__.cpython-310.pyc,, +packaging/__pycache__/_manylinux.cpython-310.pyc,, +packaging/__pycache__/_musllinux.cpython-310.pyc,, +packaging/__pycache__/_structures.cpython-310.pyc,, +packaging/__pycache__/markers.cpython-310.pyc,, +packaging/__pycache__/requirements.cpython-310.pyc,, +packaging/__pycache__/specifiers.cpython-310.pyc,, +packaging/__pycache__/tags.cpython-310.pyc,, +packaging/__pycache__/utils.cpython-310.pyc,, +packaging/__pycache__/version.cpython-310.pyc,, +packaging/_manylinux.py,sha256=XcbiXB-qcjv3bcohp6N98TMpOP4_j3m-iOA8ptK2GWY,11488 +packaging/_musllinux.py,sha256=z5yeG1ygOPx4uUyLdqj-p8Dk5UBb5H_b0NIjW9yo8oA,4378 +packaging/_structures.py,sha256=TMiAgFbdUOPmIfDIfiHc3KFhSJ8kMjof2QS5I-2NyQ8,1629 +packaging/markers.py,sha256=Fygi3_eZnjQ-3VJizW5AhI5wvo0Hb6RMk4DidsKpOC0,8475 +packaging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +packaging/requirements.py,sha256=rjaGRCMepZS1mlYMjJ5Qh6rfq3gtsCRQUQmftGZ_bu8,4664 +packaging/specifiers.py,sha256=MZ-fYcNL3u7pNrt-6g2EQO7AbRXkjc-SPEYwXMQbLmc,30964 +packaging/tags.py,sha256=vGybAUQYlPKMcukzX_2e65fmafnFFuMbD25naYTEwtc,15710 +packaging/utils.py,sha256=dJjeat3BS-TYn1RrUFVwufUMasbtzLfYRoy_HXENeFQ,4200 +packaging/version.py,sha256=_fLRNrFrxYcHVfyo8vk9j8s6JM8N_xsSxVFr6RJyco8,14665 diff --git a/setuptools/_vendor/packaging-21.2.dist-info/REQUESTED b/setuptools/_vendor/packaging-21.2.dist-info/REQUESTED new file mode 100644 index 00000000..e69de29b diff --git a/setuptools/_vendor/packaging-21.2.dist-info/WHEEL b/setuptools/_vendor/packaging-21.2.dist-info/WHEEL new file mode 100644 index 00000000..5bad85fd --- /dev/null +++ b/setuptools/_vendor/packaging-21.2.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/setuptools/_vendor/packaging-21.2.dist-info/top_level.txt b/setuptools/_vendor/packaging-21.2.dist-info/top_level.txt new file mode 100644 index 00000000..748809f7 --- /dev/null +++ b/setuptools/_vendor/packaging-21.2.dist-info/top_level.txt @@ -0,0 +1 @@ +packaging diff --git a/setuptools/_vendor/packaging/LICENSE b/setuptools/_vendor/packaging/LICENSE deleted file mode 100644 index 6f62d44e..00000000 --- a/setuptools/_vendor/packaging/LICENSE +++ /dev/null @@ -1,3 +0,0 @@ -This software is made available under the terms of *either* of the licenses -found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made -under the terms of *both* these licenses. diff --git a/setuptools/_vendor/packaging/LICENSE.APACHE b/setuptools/_vendor/packaging/LICENSE.APACHE deleted file mode 100644 index f433b1a5..00000000 --- a/setuptools/_vendor/packaging/LICENSE.APACHE +++ /dev/null @@ -1,177 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS diff --git a/setuptools/_vendor/packaging/LICENSE.BSD b/setuptools/_vendor/packaging/LICENSE.BSD deleted file mode 100644 index 42ce7b75..00000000 --- a/setuptools/_vendor/packaging/LICENSE.BSD +++ /dev/null @@ -1,23 +0,0 @@ -Copyright (c) Donald Stufft and individual contributors. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/setuptools/_vendor/pyparsing-2.2.1.dist-info/DESCRIPTION.rst b/setuptools/_vendor/pyparsing-2.2.1.dist-info/DESCRIPTION.rst new file mode 100644 index 00000000..e1187231 --- /dev/null +++ b/setuptools/_vendor/pyparsing-2.2.1.dist-info/DESCRIPTION.rst @@ -0,0 +1,3 @@ +UNKNOWN + + diff --git a/setuptools/_vendor/pyparsing-2.2.1.dist-info/INSTALLER b/setuptools/_vendor/pyparsing-2.2.1.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/setuptools/_vendor/pyparsing-2.2.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/setuptools/_vendor/pyparsing-2.2.1.dist-info/LICENSE.txt b/setuptools/_vendor/pyparsing-2.2.1.dist-info/LICENSE.txt new file mode 100644 index 00000000..bbc959e0 --- /dev/null +++ b/setuptools/_vendor/pyparsing-2.2.1.dist-info/LICENSE.txt @@ -0,0 +1,18 @@ +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/setuptools/_vendor/pyparsing-2.2.1.dist-info/METADATA b/setuptools/_vendor/pyparsing-2.2.1.dist-info/METADATA new file mode 100644 index 00000000..a15c350e --- /dev/null +++ b/setuptools/_vendor/pyparsing-2.2.1.dist-info/METADATA @@ -0,0 +1,30 @@ +Metadata-Version: 2.0 +Name: pyparsing +Version: 2.2.1 +Summary: Python parsing module +Home-page: https://github.com/pyparsing/pyparsing/ +Author: Paul McGuire +Author-email: ptmcg@users.sourceforge.net +License: MIT License +Download-URL: https://pypi.org/project/pyparsing/ +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Information Technology +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Requires-Python: >=2.6, !=3.0.*, !=3.1.*, !=3.2.* + +UNKNOWN + + diff --git a/setuptools/_vendor/pyparsing-2.2.1.dist-info/RECORD b/setuptools/_vendor/pyparsing-2.2.1.dist-info/RECORD new file mode 100644 index 00000000..09cc30e3 --- /dev/null +++ b/setuptools/_vendor/pyparsing-2.2.1.dist-info/RECORD @@ -0,0 +1,11 @@ +__pycache__/pyparsing.cpython-310.pyc,, +pyparsing-2.2.1.dist-info/DESCRIPTION.rst,sha256=OCTuuN6LcWulhHS3d5rfjdsQtW22n7HENFRh6jC6ego,10 +pyparsing-2.2.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +pyparsing-2.2.1.dist-info/LICENSE.txt,sha256=081Pq74Spe1XdwrGkewNKSqa078kLIh7UWI-wVjdj8I,1041 +pyparsing-2.2.1.dist-info/METADATA,sha256=I0jhx9vpUYlQXjn4gVDnFFoAt3nNrxwR4iuqA_pknYs,1091 +pyparsing-2.2.1.dist-info/RECORD,, +pyparsing-2.2.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +pyparsing-2.2.1.dist-info/WHEEL,sha256=kdsN-5OJAZIiHN-iO4Rhl82KyS0bDWf4uBwMbkNafr8,110 +pyparsing-2.2.1.dist-info/metadata.json,sha256=v1_77-dSdajUZSItSJg8Ov9M713STY3PzhyrRvs1ax4,1185 +pyparsing-2.2.1.dist-info/top_level.txt,sha256=eUOjGzJVhlQ3WS2rFAy2mN3LX_7FKTM5GSJ04jfnLmU,10 +pyparsing.py,sha256=tmrp-lu-qO1i75ZzIN5A12nKRRD1Cm4Vpk-5LR9rims,232055 diff --git a/setuptools/_vendor/pyparsing-2.2.1.dist-info/REQUESTED b/setuptools/_vendor/pyparsing-2.2.1.dist-info/REQUESTED new file mode 100644 index 00000000..e69de29b diff --git a/setuptools/_vendor/pyparsing-2.2.1.dist-info/WHEEL b/setuptools/_vendor/pyparsing-2.2.1.dist-info/WHEEL new file mode 100644 index 00000000..7332a419 --- /dev/null +++ b/setuptools/_vendor/pyparsing-2.2.1.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.30.0) +Root-Is-Purelib: true +Tag: py2-none-any +Tag: py3-none-any + diff --git a/setuptools/_vendor/pyparsing-2.2.1.dist-info/metadata.json b/setuptools/_vendor/pyparsing-2.2.1.dist-info/metadata.json new file mode 100644 index 00000000..b760b766 --- /dev/null +++ b/setuptools/_vendor/pyparsing-2.2.1.dist-info/metadata.json @@ -0,0 +1 @@ +{"classifiers": ["Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7"], "download_url": "https://pypi.org/project/pyparsing/", "extensions": {"python.details": {"contacts": [{"email": "ptmcg@users.sourceforge.net", "name": "Paul McGuire", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst", "license": "LICENSE.txt"}, "project_urls": {"Home": "https://github.com/pyparsing/pyparsing/"}}}, "generator": "bdist_wheel (0.30.0)", "license": "MIT License", "metadata_version": "2.0", "name": "pyparsing", "requires_python": ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*", "summary": "Python parsing module", "version": "2.2.1"} \ No newline at end of file diff --git a/setuptools/_vendor/pyparsing-2.2.1.dist-info/top_level.txt b/setuptools/_vendor/pyparsing-2.2.1.dist-info/top_level.txt new file mode 100644 index 00000000..210dfec5 --- /dev/null +++ b/setuptools/_vendor/pyparsing-2.2.1.dist-info/top_level.txt @@ -0,0 +1 @@ +pyparsing diff --git a/setuptools/_vendor/pyparsing.LICENSE.txt b/setuptools/_vendor/pyparsing.LICENSE.txt deleted file mode 100644 index bbc959e0..00000000 --- a/setuptools/_vendor/pyparsing.LICENSE.txt +++ /dev/null @@ -1,18 +0,0 @@ -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/tools/vendored.py b/tools/vendored.py index b4565d96..ee34dc0f 100644 --- a/tools/vendored.py +++ b/tools/vendored.py @@ -1,7 +1,6 @@ import re import sys import subprocess -from fnmatch import fnmatch from path import Path @@ -52,42 +51,9 @@ def install(vendor): '-t', str(vendor), ] subprocess.check_call(install_args) - move_licenses(vendor) - remove_all(vendor.glob('*.dist-info')) - remove_all(vendor.glob('*.egg-info')) (vendor / '__init__.py').write_text('') -def move_licenses(vendor): - license_patterns = ("*LICEN[CS]E*", "COPYING*", "NOTICE*", "AUTHORS*") - licenses = ( - entry - for path in vendor.glob("*.dist-info") - for entry in path.glob("*") - if any(fnmatch(str(entry), p) for p in license_patterns) - ) - for file in licenses: - file.move(_find_license_dest(file, vendor)) - - -def _find_license_dest(license_file, vendor): - basename = license_file.basename() - pkg = license_file.dirname().basename().replace(".dist-info", "") - parts = pkg.split("-") - acc = [] - for part in parts: - # Find actual name from normalized name + version - acc.append(part) - for option in ("_".join(acc), "-".join(acc), ".".join(acc)): - candidate = vendor / option - if candidate.isdir(): - return candidate / basename - if Path(f"{candidate}.py").isfile(): - return Path(f"{candidate}.{basename}") - - raise FileNotFoundError(f"No destination found for {license_file}") - - def update_pkg_resources(): vendor = Path('pkg_resources/_vendor') install(vendor) -- cgit v1.2.1 From e4dc986d1f5e91073f29fa381326a75068982422 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 14 Jan 2022 21:25:16 -0500 Subject: Move towncrier template to tools --- pyproject.toml | 2 +- tools/towncrier_template.rst | 35 +++++++++++++++++++++++++++++++++++ towncrier_template.rst | 35 ----------------------------------- 3 files changed, 36 insertions(+), 36 deletions(-) create mode 100644 tools/towncrier_template.rst delete mode 100644 towncrier_template.rst diff --git a/pyproject.toml b/pyproject.toml index 03c40125..6b426a37 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ addopts = "-n auto" directory = "changelog.d" title_format = "v{version}" issue_format = "#{issue}" - template = "towncrier_template.rst" + template = "tools/towncrier_template.rst" underlines = ["-", "^"] [[tool.towncrier.type]] diff --git a/tools/towncrier_template.rst b/tools/towncrier_template.rst new file mode 100644 index 00000000..7f507342 --- /dev/null +++ b/tools/towncrier_template.rst @@ -0,0 +1,35 @@ +{% if top_line %} +{{ top_line }} +{{ top_underline * ((top_line)|length)}} +{% endif %} +{% for section, _ in sections.items() %} +{% set underline = underlines[0] %}{% if section %}{{section}} +{{ underline * section|length }} +{% set underline = underlines[1] %} +{% endif %} + +{% if sections[section] %} +{% for category, val in definitions.items() if category in sections[section]%} + +{{ definitions[category]['name'] }} +{{ underline * definitions[category]['name']|length }} +{% if definitions[category]['showcontent'] %} +{% for text, values in sections[section][category].items() %} +* {{ values|join(', ') }}: {{ text }} +{% endfor %} +{% else %} +* {{ sections[section][category]['']|join(', ') }} + +{% endif %} +{% if sections[section][category]|length == 0 %} +No significant changes. +{% else %} +{% endif %} +{% endfor %} + +{% else %} +No significant changes. + + +{% endif %} +{% endfor %} diff --git a/towncrier_template.rst b/towncrier_template.rst deleted file mode 100644 index 7f507342..00000000 --- a/towncrier_template.rst +++ /dev/null @@ -1,35 +0,0 @@ -{% if top_line %} -{{ top_line }} -{{ top_underline * ((top_line)|length)}} -{% endif %} -{% for section, _ in sections.items() %} -{% set underline = underlines[0] %}{% if section %}{{section}} -{{ underline * section|length }} -{% set underline = underlines[1] %} -{% endif %} - -{% if sections[section] %} -{% for category, val in definitions.items() if category in sections[section]%} - -{{ definitions[category]['name'] }} -{{ underline * definitions[category]['name']|length }} -{% if definitions[category]['showcontent'] %} -{% for text, values in sections[section][category].items() %} -* {{ values|join(', ') }}: {{ text }} -{% endfor %} -{% else %} -* {{ sections[section][category]['']|join(', ') }} - -{% endif %} -{% if sections[section][category]|length == 0 %} -No significant changes. -{% else %} -{% endif %} -{% endfor %} - -{% else %} -No significant changes. - - -{% endif %} -{% endfor %} -- cgit v1.2.1 From bd89669149b4bf93be44fa947d54b193851471e3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 14 Jan 2022 21:26:28 -0500 Subject: Move launcher build scripts to tools --- msvc-build-launcher-arm64.cmd | 19 ------------------ msvc-build-launcher.cmd | 39 ------------------------------------- tools/msvc-build-launcher-arm64.cmd | 19 ++++++++++++++++++ tools/msvc-build-launcher.cmd | 39 +++++++++++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 58 deletions(-) delete mode 100644 msvc-build-launcher-arm64.cmd delete mode 100644 msvc-build-launcher.cmd create mode 100644 tools/msvc-build-launcher-arm64.cmd create mode 100644 tools/msvc-build-launcher.cmd diff --git a/msvc-build-launcher-arm64.cmd b/msvc-build-launcher-arm64.cmd deleted file mode 100644 index 8e63506b..00000000 --- a/msvc-build-launcher-arm64.cmd +++ /dev/null @@ -1,19 +0,0 @@ -@echo off - -REM Build with jaraco/windows Docker image - -set PATH_OLD=%PATH% -set PATH=C:\BuildTools\VC\Auxiliary\Build;%PATH_OLD% - -REM now for arm 64-bit -REM Cross compile for arm64 -call VCVARSx86_arm64 -if "%ERRORLEVEL%"=="0" ( - cl /D "GUI=0" /D "WIN32_LEAN_AND_MEAN" launcher.c /O2 /link /MACHINE:arm64 /SUBSYSTEM:CONSOLE /out:setuptools/cli-arm64.exe - cl /D "GUI=1" /D "WIN32_LEAN_AND_MEAN" launcher.c /O2 /link /MACHINE:arm64 /SUBSYSTEM:WINDOWS /out:setuptools/gui-arm64.exe -) else ( - echo Visual Studio 2019 with arm64 toolchain not installed -) - -set PATH=%PATH_OLD% - diff --git a/msvc-build-launcher.cmd b/msvc-build-launcher.cmd deleted file mode 100644 index 92da290e..00000000 --- a/msvc-build-launcher.cmd +++ /dev/null @@ -1,39 +0,0 @@ -@echo off - -REM Use old Windows SDK 6.1 so created .exe will be compatible with -REM old Windows versions. -REM Windows SDK 6.1 may be downloaded at: -REM http://www.microsoft.com/en-us/download/details.aspx?id=11310 -set PATH_OLD=%PATH% - -REM The SDK creates a false install of Visual Studio at one of these locations -set PATH=C:\Program Files\Microsoft Visual Studio 9.0\VC\bin;%PATH% -set PATH=C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\bin;%PATH% - -REM set up the environment to compile to x86 -call VCVARS32 -if "%ERRORLEVEL%"=="0" ( - cl /D "GUI=0" /D "WIN32_LEAN_AND_MEAN" launcher.c /O2 /link /MACHINE:x86 /SUBSYSTEM:CONSOLE /out:setuptools/cli-32.exe - cl /D "GUI=1" /D "WIN32_LEAN_AND_MEAN" launcher.c /O2 /link /MACHINE:x86 /SUBSYSTEM:WINDOWS /out:setuptools/gui-32.exe -) else ( - echo Windows SDK 6.1 not found to build Windows 32-bit version -) - -REM buildout (and possibly other implementations) currently depend on -REM the 32-bit launcher scripts without the -32 in the filename, so copy them -REM there for now. -copy setuptools/cli-32.exe setuptools/cli.exe -copy setuptools/gui-32.exe setuptools/gui.exe - -REM now for 64-bit -REM Use the x86_amd64 profile, which is the 32-bit cross compiler for amd64 -call VCVARSx86_amd64 -if "%ERRORLEVEL%"=="0" ( - cl /D "GUI=0" /D "WIN32_LEAN_AND_MEAN" launcher.c /O2 /link /MACHINE:x64 /SUBSYSTEM:CONSOLE /out:setuptools/cli-64.exe - cl /D "GUI=1" /D "WIN32_LEAN_AND_MEAN" launcher.c /O2 /link /MACHINE:x64 /SUBSYSTEM:WINDOWS /out:setuptools/gui-64.exe -) else ( - echo Windows SDK 6.1 not found to build Windows 64-bit version -) - -set PATH=%PATH_OLD% - diff --git a/tools/msvc-build-launcher-arm64.cmd b/tools/msvc-build-launcher-arm64.cmd new file mode 100644 index 00000000..8e63506b --- /dev/null +++ b/tools/msvc-build-launcher-arm64.cmd @@ -0,0 +1,19 @@ +@echo off + +REM Build with jaraco/windows Docker image + +set PATH_OLD=%PATH% +set PATH=C:\BuildTools\VC\Auxiliary\Build;%PATH_OLD% + +REM now for arm 64-bit +REM Cross compile for arm64 +call VCVARSx86_arm64 +if "%ERRORLEVEL%"=="0" ( + cl /D "GUI=0" /D "WIN32_LEAN_AND_MEAN" launcher.c /O2 /link /MACHINE:arm64 /SUBSYSTEM:CONSOLE /out:setuptools/cli-arm64.exe + cl /D "GUI=1" /D "WIN32_LEAN_AND_MEAN" launcher.c /O2 /link /MACHINE:arm64 /SUBSYSTEM:WINDOWS /out:setuptools/gui-arm64.exe +) else ( + echo Visual Studio 2019 with arm64 toolchain not installed +) + +set PATH=%PATH_OLD% + diff --git a/tools/msvc-build-launcher.cmd b/tools/msvc-build-launcher.cmd new file mode 100644 index 00000000..92da290e --- /dev/null +++ b/tools/msvc-build-launcher.cmd @@ -0,0 +1,39 @@ +@echo off + +REM Use old Windows SDK 6.1 so created .exe will be compatible with +REM old Windows versions. +REM Windows SDK 6.1 may be downloaded at: +REM http://www.microsoft.com/en-us/download/details.aspx?id=11310 +set PATH_OLD=%PATH% + +REM The SDK creates a false install of Visual Studio at one of these locations +set PATH=C:\Program Files\Microsoft Visual Studio 9.0\VC\bin;%PATH% +set PATH=C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\bin;%PATH% + +REM set up the environment to compile to x86 +call VCVARS32 +if "%ERRORLEVEL%"=="0" ( + cl /D "GUI=0" /D "WIN32_LEAN_AND_MEAN" launcher.c /O2 /link /MACHINE:x86 /SUBSYSTEM:CONSOLE /out:setuptools/cli-32.exe + cl /D "GUI=1" /D "WIN32_LEAN_AND_MEAN" launcher.c /O2 /link /MACHINE:x86 /SUBSYSTEM:WINDOWS /out:setuptools/gui-32.exe +) else ( + echo Windows SDK 6.1 not found to build Windows 32-bit version +) + +REM buildout (and possibly other implementations) currently depend on +REM the 32-bit launcher scripts without the -32 in the filename, so copy them +REM there for now. +copy setuptools/cli-32.exe setuptools/cli.exe +copy setuptools/gui-32.exe setuptools/gui.exe + +REM now for 64-bit +REM Use the x86_amd64 profile, which is the 32-bit cross compiler for amd64 +call VCVARSx86_amd64 +if "%ERRORLEVEL%"=="0" ( + cl /D "GUI=0" /D "WIN32_LEAN_AND_MEAN" launcher.c /O2 /link /MACHINE:x64 /SUBSYSTEM:CONSOLE /out:setuptools/cli-64.exe + cl /D "GUI=1" /D "WIN32_LEAN_AND_MEAN" launcher.c /O2 /link /MACHINE:x64 /SUBSYSTEM:WINDOWS /out:setuptools/gui-64.exe +) else ( + echo Windows SDK 6.1 not found to build Windows 64-bit version +) + +set PATH=%PATH_OLD% + -- cgit v1.2.1 From ecfcf0787fadb38b6b93c1c6e33fff985efd7f8c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 16 Jan 2022 17:45:05 -0500 Subject: Remove filtering of distutils warnings. Ref #3009. --- _distutils_hack/__init__.py | 11 ----------- changelog.d/3009.misc.rst | 1 + 2 files changed, 1 insertion(+), 11 deletions(-) create mode 100644 changelog.d/3009.misc.rst diff --git a/_distutils_hack/__init__.py b/_distutils_hack/__init__.py index 0108d854..834a062c 100644 --- a/_distutils_hack/__init__.py +++ b/_distutils_hack/__init__.py @@ -92,17 +92,6 @@ class DistutilsMetaFinder: import importlib import importlib.abc import importlib.util - import warnings - - # warnings.filterwarnings() imports the re module - warnings._add_filter( - 'ignore', - _TrivialRe("distutils", "deprecated"), - DeprecationWarning, - None, - 0, - append=True - ) try: mod = importlib.import_module('setuptools._distutils') diff --git a/changelog.d/3009.misc.rst b/changelog.d/3009.misc.rst new file mode 100644 index 00000000..b2e2f9ef --- /dev/null +++ b/changelog.d/3009.misc.rst @@ -0,0 +1 @@ +Remove filtering of distutils warnings. -- cgit v1.2.1 From 448b84062e84ed918818d4d56fb3571f3f045848 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 16 Jan 2022 18:21:04 -0500 Subject: Refactor to generalize script detection. --- _distutils_hack/__init__.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/_distutils_hack/__init__.py b/_distutils_hack/__init__.py index 834a062c..399d22f1 100644 --- a/_distutils_hack/__init__.py +++ b/_distutils_hack/__init__.py @@ -135,9 +135,9 @@ class DistutilsMetaFinder: a stubbed spec to represent setuptools being present without invoking any behavior. - Workaround for pypa/get-pip#137. + Workaround for pypa/get-pip#137. Ref #2993. """ - if not self.is_get_pip(): + if not self.is_script('get-pip'): return import importlib @@ -166,14 +166,11 @@ class DistutilsMetaFinder: for frame, line in traceback.walk_stack(None) ) - @classmethod - def is_get_pip(cls): - """ - Detect if get-pip is being invoked. Ref #2993. - """ + @staticmethod + def is_script(name): try: import __main__ - return os.path.basename(__main__.__file__) == 'get-pip.py' + return os.path.basename(__main__.__file__) == f'{name}.py' except AttributeError: pass -- cgit v1.2.1 From 336facde9301e041b2e33a7b46fe1d37991a7ed3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 16 Jan 2022 18:09:28 -0500 Subject: Suppress distutils replacement when building or testing CPython. Fixes #2965. Fixes #3007. --- _distutils_hack/__init__.py | 11 +++++++++++ changelog.d/3031.misc.rst | 1 + changelog.d/NNN.misc.rst | 0 3 files changed, 12 insertions(+) create mode 100644 changelog.d/3031.misc.rst create mode 100644 changelog.d/NNN.misc.rst diff --git a/_distutils_hack/__init__.py b/_distutils_hack/__init__.py index 399d22f1..1f8daf49 100644 --- a/_distutils_hack/__init__.py +++ b/_distutils_hack/__init__.py @@ -89,6 +89,9 @@ class DistutilsMetaFinder: return method() def spec_for_distutils(self): + if self.is_cpython(): + return + import importlib import importlib.abc import importlib.util @@ -118,6 +121,14 @@ class DistutilsMetaFinder: 'distutils', DistutilsLoader(), origin=mod.__file__ ) + @staticmethod + def is_cpython(): + """ + Suppress supplying distutils for CPython (build and tests). + Ref #2965 and #3007. + """ + return os.path.isfile('pybuilddir.txt') + def spec_for_pip(self): """ Ensure stdlib distutils when running under pip. diff --git a/changelog.d/3031.misc.rst b/changelog.d/3031.misc.rst new file mode 100644 index 00000000..b5af0169 --- /dev/null +++ b/changelog.d/3031.misc.rst @@ -0,0 +1 @@ +Suppress distutils replacement when building or testing CPython. diff --git a/changelog.d/NNN.misc.rst b/changelog.d/NNN.misc.rst new file mode 100644 index 00000000..e69de29b -- cgit v1.2.1 From 5e94859bf197500e6953c90668eb4cee159d25cf Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 16 Jan 2022 19:40:51 -0500 Subject: =?UTF-8?q?Bump=20version:=2060.5.3=20=E2=86=92=2060.5.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 11 +++++++++++ changelog.d/3009.misc.rst | 1 - changelog.d/3031.misc.rst | 1 - changelog.d/NNN.misc.rst | 0 setup.cfg | 2 +- 6 files changed, 13 insertions(+), 4 deletions(-) delete mode 100644 changelog.d/3009.misc.rst delete mode 100644 changelog.d/3031.misc.rst delete mode 100644 changelog.d/NNN.misc.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 04ab0a5d..6534cde9 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 60.5.3 +current_version = 60.5.4 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index e76ec3e9..e9f8a70f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,14 @@ +v60.5.4 +------- + + +Misc +^^^^ +* #NNN: +* #3009: Remove filtering of distutils warnings. +* #3031: Suppress distutils replacement when building or testing CPython. + + v60.5.3 ------- diff --git a/changelog.d/3009.misc.rst b/changelog.d/3009.misc.rst deleted file mode 100644 index b2e2f9ef..00000000 --- a/changelog.d/3009.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Remove filtering of distutils warnings. diff --git a/changelog.d/3031.misc.rst b/changelog.d/3031.misc.rst deleted file mode 100644 index b5af0169..00000000 --- a/changelog.d/3031.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Suppress distutils replacement when building or testing CPython. diff --git a/changelog.d/NNN.misc.rst b/changelog.d/NNN.misc.rst deleted file mode 100644 index e69de29b..00000000 diff --git a/setup.cfg b/setup.cfg index eda747d8..b83d3763 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 60.5.3 +version = 60.5.4 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From 197570db5eb56e41dfb0ca98bfc01d6240349a1d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 17 Jan 2022 10:21:39 -0500 Subject: Update documentation to match and remove requirements file with stale references. --- docs/development/developer-guide.rst | 11 ++++------- setuptools/tests/requirements.txt | 14 -------------- 2 files changed, 4 insertions(+), 21 deletions(-) delete mode 100644 setuptools/tests/requirements.txt diff --git a/docs/development/developer-guide.rst b/docs/development/developer-guide.rst index f29c1a80..e274c417 100644 --- a/docs/development/developer-guide.rst +++ b/docs/development/developer-guide.rst @@ -125,12 +125,9 @@ cannot declare dependencies other than through ``setuptools/_vendor/vendored.txt`` and ``pkg_resources/_vendor/vendored.txt``. -All the dependencies specified in these files are "vendorized" using Paver_, a -simple Python-based project scripting and task running tool. +All the dependencies specified in these files are "vendorized" using a +simple Python script ``tools/vendor.py``. -To refresh the dependencies, you can run the following command (defined in -``pavement.py``):: +To refresh the dependencies, run the following command:: - $ paver update_vendored - -.. _Paver: https://pythonhosted.org/Paver/ + $ tox -e vendor diff --git a/setuptools/tests/requirements.txt b/setuptools/tests/requirements.txt deleted file mode 100644 index b2d84a94..00000000 --- a/setuptools/tests/requirements.txt +++ /dev/null @@ -1,14 +0,0 @@ -mock -pytest-flake8 -flake8-2020; python_version>="3.6" -virtualenv>=13.0.0 -pytest-virtualenv>=1.2.7 -pytest>=3.7 -wheel -coverage>=4.5.1 -pytest-cov>=2.5.1 -paver; python_version>="3.6" -futures; python_version=="2.7" -pip>=19.1 # For proper file:// URLs support. -jaraco.envs -sphinx -- cgit v1.2.1 From 78aa84b23fe2ea8a2c041d99f80ec0a86d0ce588 Mon Sep 17 00:00:00 2001 From: Christoph Reiter Date: Mon, 17 Jan 2022 18:38:24 +0100 Subject: CI/cygwin: use cygwin/cygwin-install-action instead of choco Use the official GH action for installing cygwin instead of going through choco. See #3016 --- .github/workflows/main.yml | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6ae4a264..3a15246e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -53,13 +53,24 @@ jobs: SETUPTOOLS_USE_DISTUTILS: ${{ matrix.distutils }} steps: - uses: actions/checkout@v2 - - name: Install Cygwin with Python and tox + - name: Install Cygwin with Python + uses: cygwin/cygwin-install-action@v1 + with: + platform: x86_64 + packages: >- + git, + 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: | - choco install git gcc-core python38-devel python38-pip --source cygwin - C:\\tools\\cygwin\\bin\\bash -l -x -c 'python3.8 -m pip install tox' + python3.8 -m pip install tox - 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: | - C:\\tools\\cygwin\\bin\\bash -l -x -c 'cd $(cygpath -u "$GITHUB_WORKSPACE") && tox -- --cov-report xml' + tox -- --cov-report xml integration-test: strategy: -- cgit v1.2.1 From 0a647139866d97c469fa436bccfd4bbae594ab53 Mon Sep 17 00:00:00 2001 From: Christoph Reiter Date: Mon, 17 Jan 2022 19:57:18 +0100 Subject: CI: re-enable the cygwin jobs again --- .github/workflows/main.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3a15246e..821cf883 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -42,7 +42,6 @@ jobs: ${{ matrix.python }} test_cygwin: - if: ${{ false }} # failing #3016 strategy: matrix: distutils: -- cgit v1.2.1 From 00eb51498605cbdb4be7a78350672e8848877a89 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 18 Jan 2022 09:11:53 -0500 Subject: Remove noise in the changelog. --- CHANGES.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index e9f8a70f..b6938ce7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,7 +4,6 @@ v60.5.4 Misc ^^^^ -* #NNN: * #3009: Remove filtering of distutils warnings. * #3031: Suppress distutils replacement when building or testing CPython. -- cgit v1.2.1 From 3d3ab74d05959c22583d4d7caf0ddef4684a33a4 Mon Sep 17 00:00:00 2001 From: Jeroen Ruigrok van der Werven Date: Thu, 20 Jan 2022 09:26:38 +0100 Subject: Replace the defunct distutils-sig ml with forum pointer --- README.rst | 6 +++--- changelog.d/3034.docs.rst | 4 ++++ docs/development/developer-guide.rst | 9 +++++---- docs/setuptools.rst | 10 +++++----- 4 files changed, 17 insertions(+), 12 deletions(-) create mode 100644 changelog.d/3034.docs.rst diff --git a/README.rst b/README.rst index 7ea2b70e..fe2e749e 100644 --- a/README.rst +++ b/README.rst @@ -40,8 +40,8 @@ See the `Installation Instructions User's Guide for instructions on installing, upgrading, and uninstalling Setuptools. -Questions and comments should be directed to the `distutils-sig -mailing list `_. +Questions and comments should be directed to `GitHub Discussions +`_. Bug reports and especially tested patches may be submitted directly to the `bug tracker `_. @@ -51,7 +51,7 @@ Code of Conduct =============== Everyone interacting in the setuptools project's codebases, issue trackers, -chat rooms, and mailing lists is expected to follow the +chat rooms, and fora is expected to follow the `PSF Code of Conduct `_. diff --git a/changelog.d/3034.docs.rst b/changelog.d/3034.docs.rst new file mode 100644 index 00000000..6106e0ff --- /dev/null +++ b/changelog.d/3034.docs.rst @@ -0,0 +1,4 @@ +Replaced occurrences of the defunct distutils-sig mailing list with pointers +to GitHub Discussions. +-- by :user:`ashemedai` + diff --git a/docs/development/developer-guide.rst b/docs/development/developer-guide.rst index e274c417..6131b703 100644 --- a/docs/development/developer-guide.rst +++ b/docs/development/developer-guide.rst @@ -26,11 +26,12 @@ Python Packaging Authority (PyPA) with several core contributors. All bugs for Setuptools are filed and the canonical source is maintained in GitHub. User support and discussions are done through the issue tracker (for specific) -issues, through the `distutils-sig mailing list `_, or on IRC (Freenode) at -#pypa. +issues, through `GitHub Discussions `_, +or on IRC (Freenode) at #pypa. -Discussions about development happen on the distutils-sig mailing list or on -`Gitter `_. +Discussions about development happen on GitHub Discussions, +`Gitter `_ or +the ``setuptools`` channel on `PyPA Discord `_. ----------------- Authoring Tickets diff --git a/docs/setuptools.rst b/docs/setuptools.rst index c5a89adc..d0fb9a9c 100644 --- a/docs/setuptools.rst +++ b/docs/setuptools.rst @@ -201,13 +201,13 @@ As a consequence, the resulting dictionary will include no such options. -Mailing List and Bug Tracker -============================ +Forum and Bug Tracker +===================== -Please use the `distutils-sig mailing list`_ for questions and discussion about +Please use `GitHub Discussions`_ for questions and discussion about setuptools, and the `setuptools bug tracker`_ ONLY for issues you have -confirmed via the list are actual bugs, and which you have reduced to a minimal +confirmed via the forum are actual bugs, and which you have reduced to a minimal set of steps to reproduce. -.. _distutils-sig mailing list: http://mail.python.org/pipermail/distutils-sig/ +.. _GitHub Discussions: https://github.com/pypa/setuptools/discussions .. _setuptools bug tracker: https://github.com/pypa/setuptools/ -- cgit v1.2.1 From 275bd48f231de87c365bd870a0916db480883e52 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 20 Jan 2022 11:38:48 +0000 Subject: Remove deprecated communication platforms --- docs/development/developer-guide.rst | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/development/developer-guide.rst b/docs/development/developer-guide.rst index 6131b703..d2cf1592 100644 --- a/docs/development/developer-guide.rst +++ b/docs/development/developer-guide.rst @@ -25,12 +25,11 @@ Setuptools is maintained primarily in GitHub at `this home Python Packaging Authority (PyPA) with several core contributors. All bugs for Setuptools are filed and the canonical source is maintained in GitHub. -User support and discussions are done through the issue tracker (for specific) -issues, through `GitHub Discussions `_, -or on IRC (Freenode) at #pypa. +User support and discussions are done through +`GitHub Discussions `_, +or the issue tracker (for specific issues). -Discussions about development happen on GitHub Discussions, -`Gitter `_ or +Discussions about development happen on GitHub Discussions or the ``setuptools`` channel on `PyPA Discord `_. ----------------- -- cgit v1.2.1 From 518004161ff408826428b0584f8f91b8e08fdc45 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 22 Jan 2022 12:55:34 +0000 Subject: Add test to ensure the correct log level is set --- setuptools/tests/test_logging.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 setuptools/tests/test_logging.py diff --git a/setuptools/tests/test_logging.py b/setuptools/tests/test_logging.py new file mode 100644 index 00000000..4677dc24 --- /dev/null +++ b/setuptools/tests/test_logging.py @@ -0,0 +1,31 @@ +import logging + +import pytest + + +setup_py = """\ +from setuptools import setup + +setup( + name="test_logging", + version="0.0" +) +""" + + +@pytest.mark.parametrize( + "flag, expected_level", [("--dry-run", "INFO"), ("--verbose", "DEBUG")] +) +def test_verbosity_level(tmp_path, flag, expected_level): + """Make sure the correct verbosity level is set (issue #3038)""" + import setuptools # noqa: Import setuptools to monkeypatch distutils + import distutils # <- load distutils after all the patches take place + + setup_script = tmp_path / "setup.py" + setup_script.write_text(setup_py) + dist = distutils.core.run_setup(setup_script, stop_after="init") + dist.script_args = [flag, "sdist"] + dist.parse_command_line() # <- where the log level is set + log_level = logging.root.getEffectiveLevel() # <- setuptools uses the root logger + log_level_name = logging.getLevelName(log_level) + assert log_level_name == expected_level -- cgit v1.2.1 From 0d491c616284933e35bb5d61a94828aed0c8d3f2 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 22 Jan 2022 12:56:09 +0000 Subject: Fix weird distutils.log reloading/caching situation For some reason `distutils.log` module is getting cached in `distutils.dist` and then loaded again when we have the opportunity to patch it. This implies: id(distutils.log) != id(distutils.dist.log). We need to make sure the same module object is used everywhere. --- setuptools/logging.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/setuptools/logging.py b/setuptools/logging.py index dbead6e6..56669c96 100644 --- a/setuptools/logging.py +++ b/setuptools/logging.py @@ -24,6 +24,12 @@ def configure(): format="{message}", style='{', handlers=handlers, level=logging.DEBUG) monkey.patch_func(set_threshold, distutils.log, 'set_threshold') + # For some reason `distutils.log` module is getting cached in `distutils.dist` + # and then loaded again when we have the opportunity to patch it. + # This implies: id(distutils.log) != id(distutils.dist.log). + # We need to make sure the same module object is used everywhere: + distutils.dist.log = distutils.log + def set_threshold(level): logging.root.setLevel(level*10) -- cgit v1.2.1 From 633171c6bb8e75983ea405ae529806ff6dc849fa Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 22 Jan 2022 13:29:11 +0000 Subject: Avoid replacing the global log level in test --- setuptools/tests/test_logging.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/setuptools/tests/test_logging.py b/setuptools/tests/test_logging.py index 4677dc24..a5ddd56d 100644 --- a/setuptools/tests/test_logging.py +++ b/setuptools/tests/test_logging.py @@ -16,16 +16,21 @@ setup( @pytest.mark.parametrize( "flag, expected_level", [("--dry-run", "INFO"), ("--verbose", "DEBUG")] ) -def test_verbosity_level(tmp_path, flag, expected_level): +def test_verbosity_level(tmp_path, monkeypatch, flag, expected_level): """Make sure the correct verbosity level is set (issue #3038)""" import setuptools # noqa: Import setuptools to monkeypatch distutils import distutils # <- load distutils after all the patches take place + logger = logging.Logger(__name__) + monkeypatch.setattr(logging, "root", logger) + unset_log_level = logger.getEffectiveLevel() + assert logging.getLevelName(unset_log_level) == "NOTSET" + setup_script = tmp_path / "setup.py" setup_script.write_text(setup_py) dist = distutils.core.run_setup(setup_script, stop_after="init") dist.script_args = [flag, "sdist"] dist.parse_command_line() # <- where the log level is set - log_level = logging.root.getEffectiveLevel() # <- setuptools uses the root logger + log_level = logger.getEffectiveLevel() log_level_name = logging.getLevelName(log_level) assert log_level_name == expected_level -- cgit v1.2.1 From c4d780731ac1331667a46f8de3a80fbff33db2c7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 22 Jan 2022 20:45:08 -0500 Subject: Rely on pip_run.launch to install sitecustomize. --- setup.cfg | 1 + setuptools/tests/test_develop.py | 23 ++++------------------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/setup.cfg b/setup.cfg index b83d3763..0dc90438 100644 --- a/setup.cfg +++ b/setup.cfg @@ -65,6 +65,7 @@ testing = jaraco.path>=3.2.0 build[virtualenv] filelock>=3.4.0 + pip_run>=8.8 testing-integration = pytest diff --git a/setuptools/tests/test_develop.py b/setuptools/tests/test_develop.py index 1aeb7ffe..c52072ac 100644 --- a/setuptools/tests/test_develop.py +++ b/setuptools/tests/test_develop.py @@ -6,11 +6,11 @@ import sys import subprocess import platform import pathlib -import textwrap from setuptools.command import test import pytest +import pip_run.launch from setuptools.command.develop import develop from setuptools.dist import Distribution @@ -166,21 +166,6 @@ class TestNamespaces: with test.test.paths_on_pythonpath([str(target)]): subprocess.check_call(pkg_resources_imp) - @staticmethod - def install_workaround(site_packages): - site_packages.mkdir(parents=True) - sc = site_packages / 'sitecustomize.py' - sc.write_text( - textwrap.dedent( - """ - import site - import pathlib - here = pathlib.Path(__file__).parent - site.addsitedir(str(here)) - """ - ).lstrip() - ) - @pytest.mark.xfail( platform.python_implementation() == 'PyPy', reason="Workaround fails on PyPy (why?)", @@ -190,7 +175,6 @@ class TestNamespaces: Editable install to a prefix should be discoverable. """ prefix = tmp_path / 'prefix' - prefix.mkdir() # figure out where pip will likely install the package site_packages = prefix / next( @@ -198,9 +182,10 @@ class TestNamespaces: for path in sys.path if 'site-packages' in path and path.startswith(sys.prefix) ) + site_packages.mkdir(parents=True) - # install the workaround - self.install_workaround(site_packages) + # install workaround + pip_run.launch.inject_sitecustomize(str(site_packages)) env = dict(os.environ, PYTHONPATH=str(site_packages)) cmd = [ -- cgit v1.2.1 From 4b313f0d8600e8957736df7d0ffb795d187ab955 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 23 Jan 2022 09:03:30 -0800 Subject: Use sysconfig to provide get_config_vars --- distutils/sysconfig.py | 100 +------------------------------------------------ 1 file changed, 1 insertion(+), 99 deletions(-) diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py index 4a77a431..9fad3835 100644 --- a/distutils/sysconfig.py +++ b/distutils/sysconfig.py @@ -436,51 +436,6 @@ def expand_makefile_vars(s, vars): _config_vars = None -_sysconfig_name_tmpl = '_sysconfigdata_{abi}_{platform}_{multiarch}' - - -def _init_posix(): - """Initialize the module as appropriate for POSIX systems.""" - # _sysconfigdata is generated at build time, see the sysconfig module - name = os.environ.get( - '_PYTHON_SYSCONFIGDATA_NAME', - _sysconfig_name_tmpl.format( - abi=sys.abiflags, - platform=sys.platform, - multiarch=getattr(sys.implementation, '_multiarch', ''), - ), - ) - try: - _temp = __import__(name, globals(), locals(), ['build_time_vars'], 0) - except ImportError: - # Python 3.5 and pypy 7.3.1 - _temp = __import__( - '_sysconfigdata', globals(), locals(), ['build_time_vars'], 0) - build_time_vars = _temp.build_time_vars - global _config_vars - _config_vars = {} - _config_vars.update(build_time_vars) - - -def _init_nt(): - """Initialize the module as appropriate for NT""" - g = {} - # set basic install directories - g['LIBDEST'] = get_python_lib(plat_specific=0, standard_lib=1) - g['BINLIBDEST'] = get_python_lib(plat_specific=1, standard_lib=1) - - # XXX hmmm.. a normal install puts include files here - g['INCLUDEPY'] = get_python_inc(plat_specific=0) - - g['EXT_SUFFIX'] = _imp.extension_suffixes()[0] - g['EXE'] = ".exe" - g['VERSION'] = get_python_version().replace(".", "") - g['BINDIR'] = os.path.dirname(os.path.abspath(sys.executable)) - - global _config_vars - _config_vars = g - - def get_config_vars(*args): """With no arguments, return a dictionary of all configuration variables relevant for the current platform. Generally this includes @@ -493,60 +448,7 @@ def get_config_vars(*args): """ global _config_vars if _config_vars is None: - func = globals().get("_init_" + os.name) - if func: - func() - else: - _config_vars = {} - - # Normalized versions of prefix and exec_prefix are handy to have; - # in fact, these are the standard versions used most places in the - # Distutils. - _config_vars['prefix'] = PREFIX - _config_vars['exec_prefix'] = EXEC_PREFIX - - if not IS_PYPY: - # For backward compatibility, see issue19555 - SO = _config_vars.get('EXT_SUFFIX') - if SO is not None: - _config_vars['SO'] = SO - - # Always convert srcdir to an absolute path - srcdir = _config_vars.get('srcdir', project_base) - if os.name == 'posix': - if python_build: - # If srcdir is a relative path (typically '.' or '..') - # then it should be interpreted relative to the directory - # containing Makefile. - base = os.path.dirname(get_makefile_filename()) - srcdir = os.path.join(base, srcdir) - else: - # srcdir is not meaningful since the installation is - # spread about the filesystem. We choose the - # directory containing the Makefile since we know it - # exists. - srcdir = os.path.dirname(get_makefile_filename()) - _config_vars['srcdir'] = os.path.abspath(os.path.normpath(srcdir)) - - # Convert srcdir into an absolute path if it appears necessary. - # Normally it is relative to the build directory. However, during - # testing, for example, we might be running a non-installed python - # from a different directory. - if python_build and os.name == "posix": - base = project_base - if (not os.path.isabs(_config_vars['srcdir']) and - base != os.getcwd()): - # srcdir is relative and we are not in the same directory - # as the executable. Assume executable is in the build - # directory and make srcdir absolute. - srcdir = os.path.join(base, _config_vars['srcdir']) - _config_vars['srcdir'] = os.path.normpath(srcdir) - - # OS X platforms require special customization to handle - # multi-architecture, multi-os-version installers - if sys.platform == 'darwin': - import _osx_support - _osx_support.customize_config_vars(_config_vars) + _config_vars = sysconfig.get_config_vars().copy() if args: vals = [] -- cgit v1.2.1 From 05bd74c933c1471525e50b1e21d4d4ed1f492343 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 23 Jan 2022 20:21:51 -0500 Subject: Replace IRC link with Discord. Restore default for blank issues. --- .github/ISSUE_TEMPLATE/config.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index dde102ca..6d62a68b 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,3 @@ -# Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser -blank_issues_enabled: false # default: true contact_links: - name: 🤔 Have questions or need support? url: https://github.com/pypa/setuptools/discussions @@ -9,7 +7,6 @@ contact_links: about: | Please ask typical Q&A here: general ideas for Python packaging, questions about structuring projects and so on -- name: >- - 💬 IRC: #pypa @ Freenode - url: https://webchat.freenode.net/#pypa +- name: 💬 Discord (chat) + url: https://discord.com/channels/803025117553754132/815945031150993468 about: Chat with devs -- cgit v1.2.1 From 23ce525c5c4294b703a1ff8dd3cc5bc71365306b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 23 Jan 2022 20:24:18 -0500 Subject: Use the generic invite link to pypa discord. Has better redirect behavior. --- .github/ISSUE_TEMPLATE/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 6d62a68b..ebc2d339 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -8,5 +8,5 @@ contact_links: Please ask typical Q&A here: general ideas for Python packaging, questions about structuring projects and so on - name: 💬 Discord (chat) - url: https://discord.com/channels/803025117553754132/815945031150993468 + url: https://discord.com/invite/pypa about: Chat with devs -- cgit v1.2.1 From 6b58198db2ed3e3a60a83457c28f229d31c7c529 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 23 Jan 2022 20:30:19 -0500 Subject: Remove CoC checkbox as unnecessary friction. [ref](https://discuss.python.org/t/what-to-do-with-contributors-who-refuse-code-of-conduct/13287) --- .github/ISSUE_TEMPLATE/bug-report.yml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 73911ec8..672acd18 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -115,15 +115,4 @@ body: validations: required: true - -- type: checkboxes - attributes: - label: Code of Conduct - description: | - Read the [PSF Code of Conduct][CoC] first. - - [CoC]: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md - options: - - label: I agree to follow the PSF Code of Conduct - required: true ... -- cgit v1.2.1 From 944861f2e58d4c0755e3aa1e79fbce831318b9d5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 23 Jan 2022 21:06:20 -0500 Subject: Add tests for yield_lines. --- pkg_resources/__init__.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index 9cc6b0a4..0ed1c08f 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -2404,7 +2404,20 @@ def _nonblank(str): @functools.singledispatch def yield_lines(iterable): - """Yield valid lines of a string or iterable""" + r""" + Yield valid lines of a string or iterable. + + >>> list(yield_lines('')) + [] + >>> list(yield_lines(['foo', 'bar'])) + ['foo', 'bar'] + >>> list(yield_lines('foo\nbar')) + ['foo', 'bar'] + >>> list(yield_lines('\nfoo\n#bar\nbaz #comment')) + ['foo', 'baz #comment'] + >>> list(yield_lines(['foo\nbar', 'baz', 'bing\n\n\n'])) + ['foo', 'bar', 'baz', 'bing'] + """ return itertools.chain.from_iterable(map(yield_lines, iterable)) -- cgit v1.2.1 From 42ce1398e1576537966a9dbfbf63e5757dbb95e8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 23 Jan 2022 21:18:02 -0500 Subject: Extract function for dropping comments. --- pkg_resources/__init__.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index 0ed1c08f..c8dad54b 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -3092,18 +3092,30 @@ def issue_warning(*args, **kw): warnings.warn(stacklevel=level + 1, *args, **kw) +def drop_comment(line): + """ + Drop comments. + + >>> drop_comment('foo # bar') + 'foo' + + A hash without a space may be in a URL. + + >>> drop_comment('http://example.com/foo#bar') + 'http://example.com/foo#bar' + """ + return line.partition(' #')[0] + + def parse_requirements(strs): """Yield ``Requirement`` objects for each specification in `strs` `strs` must be a string, or a (possibly-nested) iterable thereof. """ # create a steppable iterator, so we can handle \-continuations - lines = iter(yield_lines(strs)) + lines = iter(map(drop_comment, yield_lines(strs))) for line in lines: - # Drop comments -- a hash without a space may be in a URL. - if ' #' in line: - line = line[:line.find(' #')] # If there is a line continuation, drop it, and append the next line. if line.endswith('\\'): line = line[:-2].strip() -- cgit v1.2.1 From 2de9ab95cea180f1e24ff98988fc6abbb02b0b8a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 23 Jan 2022 21:47:11 -0500 Subject: Extract function for joining continuations in lines. --- pkg_resources/__init__.py | 47 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index c8dad54b..7f8cc93d 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -3107,23 +3107,46 @@ def drop_comment(line): return line.partition(' #')[0] -def parse_requirements(strs): - """Yield ``Requirement`` objects for each specification in `strs` +def join_continuation(lines): + r""" + Join lines continued by a trailing backslash. - `strs` must be a string, or a (possibly-nested) iterable thereof. - """ - # create a steppable iterator, so we can handle \-continuations - lines = iter(map(drop_comment, yield_lines(strs))) + >>> list(join_continuation(['foo \\', 'bar', 'baz'])) + ['foobar', 'baz'] + >>> list(join_continuation(['foo \\', 'bar', 'baz'])) + ['foobar', 'baz'] + >>> list(join_continuation(['foo \\', 'bar \\', 'baz'])) + ['foobarbaz'] + + Not sure why, but... + The character preceeding the backslash is also elided. + + >>> list(join_continuation(['goo\\', 'dly'])) + ['godly'] + + A terrible idea, but... + If no line is available to continue, suppress the lines. - for line in lines: - # If there is a line continuation, drop it, and append the next line. - if line.endswith('\\'): - line = line[:-2].strip() + >>> list(join_continuation(['foo', 'bar\\', 'baz\\'])) + ['foo'] + """ + lines = iter(lines) + for item in lines: + while item.endswith('\\'): try: - line += next(lines) + item = item[:-2].strip() + next(lines) except StopIteration: return - yield Requirement(line) + yield item + + +def parse_requirements(strs): + """Yield ``Requirement`` objects for each specification in `strs` + + `strs` must be a string, or a (possibly-nested) iterable thereof. + """ + lines = map(drop_comment, yield_lines(strs)) + return map(Requirement, join_continuation(lines)) class RequirementParseError(packaging.requirements.InvalidRequirement): -- cgit v1.2.1 From 3eca9923d34ffab7be96802eed8029d0d7ab1fbf Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 23 Jan 2022 21:51:51 -0500 Subject: Consolidate behavior now that it fits on one line. --- pkg_resources/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index 7f8cc93d..b0704965 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -3141,12 +3141,12 @@ def join_continuation(lines): def parse_requirements(strs): - """Yield ``Requirement`` objects for each specification in `strs` + """ + Yield ``Requirement`` objects for each specification in `strs`. `strs` must be a string, or a (possibly-nested) iterable thereof. """ - lines = map(drop_comment, yield_lines(strs)) - return map(Requirement, join_continuation(lines)) + return map(Requirement, join_continuation(map(drop_comment, yield_lines(strs)))) class RequirementParseError(packaging.requirements.InvalidRequirement): -- cgit v1.2.1 From 711b5267109a4b6799d002aa96fa913ab8bee231 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 22 Jan 2022 17:30:36 +0000 Subject: Update setuptools/logging.py Co-authored-by: Jason R. Coombs --- setuptools/logging.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setuptools/logging.py b/setuptools/logging.py index 56669c96..15b57613 100644 --- a/setuptools/logging.py +++ b/setuptools/logging.py @@ -25,9 +25,9 @@ def configure(): monkey.patch_func(set_threshold, distutils.log, 'set_threshold') # For some reason `distutils.log` module is getting cached in `distutils.dist` - # and then loaded again when we have the opportunity to patch it. - # This implies: id(distutils.log) != id(distutils.dist.log). - # We need to make sure the same module object is used everywhere: + # and then loaded again when patched, + # implying: id(distutils.log) != id(distutils.dist.log). + # Make sure the same module object is used everywhere: distutils.dist.log = distutils.log -- cgit v1.2.1 From f3a76a101fce465284a37453b03bff29da09c4f4 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 22 Jan 2022 17:30:05 +0000 Subject: Add tests about duplicated distutils imports --- setuptools/tests/test_distutils_adoption.py | 65 +++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/setuptools/tests/test_distutils_adoption.py b/setuptools/tests/test_distutils_adoption.py index 366f2928..8ebb6681 100644 --- a/setuptools/tests/test_distutils_adoption.py +++ b/setuptools/tests/test_distutils_adoption.py @@ -93,3 +93,68 @@ def test_distutils_has_origin(): Distutils module spec should have an origin. #2990. """ assert __import__('distutils').__spec__.origin + + +ENSURE_IMPORTS_ARE_NOT_DUPLICATED = r""" +# Depending on the importlib machinery and _distutils_hack, some imports can be +# duplicated resulting in different module objects being loaded, which prevents +# patches from being applied as shown in #3042. +# This script provides a way of verifying if this duplication is happening. + +import distutils.command.sdist as sdist + +# import last to prevent caching +from distutils import dir_util, file_util, archive_util + +assert sdist.dir_util == dir_util, ( + f"\n{sdist.dir_util}\n!=\n{dir_util}" +) + +assert sdist.file_util == file_util, ( + f"\n{sdist.file_util}\n!=\n{file_util}" +) + +assert sdist.archive_util == archive_util, ( + f"\n{sdist.archive_util}\n!=\n{archive_util}" +) + +print("success") +""" + + +@pytest.mark.parametrize("distutils_version", ("local", "stdlib")) +def test_modules_are_not_duplicated_on_import(distutils_version, tmpdir_cwd, venv): + env = dict(SETUPTOOLS_USE_DISTUTILS=distutils_version) + cmd = ['python', '-c', ENSURE_IMPORTS_ARE_NOT_DUPLICATED] + output = popen_text(venv.run)(cmd, env=win_sr(env)).strip() + assert output == "success" + + +ENSURE_LOG_IMPORT_IS_NOT_DUPLICATED = r""" +# Similar to ENSURE_IMPORTS_ARE_NOT_DUPLICATED +import distutils.dist as dist +from distutils import log + +assert dist.log == log, ( + f"\n{dist.log}\n!=\n{log}" +) + +print("success") +""" + + +@pytest.mark.parametrize( + "distutils_version", + [ + pytest.param( + "local", + marks=pytest.mark.xfail(reason="duplicated distutils.log, #3038 #3042") + ), + "stdlib" + ] +) +def test_log_module_is_not_duplicated_on_import(distutils_version, tmpdir_cwd, venv): + env = dict(SETUPTOOLS_USE_DISTUTILS=distutils_version) + cmd = ['python', '-c', ENSURE_LOG_IMPORT_IS_NOT_DUPLICATED] + output = popen_text(venv.run)(cmd, env=win_sr(env)).strip() + assert output == "success" -- cgit v1.2.1 From c169a006e8fb2d678f4fa6d3e64a20268d227697 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 22 Jan 2022 18:02:44 +0000 Subject: Use distutils.cmd to test duplicated imports --- setuptools/tests/test_distutils_adoption.py | 44 ++++++++++++++++++----------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/setuptools/tests/test_distutils_adoption.py b/setuptools/tests/test_distutils_adoption.py index 8ebb6681..9aca16dc 100644 --- a/setuptools/tests/test_distutils_adoption.py +++ b/setuptools/tests/test_distutils_adoption.py @@ -96,36 +96,46 @@ def test_distutils_has_origin(): ENSURE_IMPORTS_ARE_NOT_DUPLICATED = r""" -# Depending on the importlib machinery and _distutils_hack, some imports can be +# Depending on the importlib machinery and _distutils_hack, some imports are # duplicated resulting in different module objects being loaded, which prevents -# patches from being applied as shown in #3042. +# patches as shown in #3042. # This script provides a way of verifying if this duplication is happening. +from distutils import cmd import distutils.command.sdist as sdist # import last to prevent caching -from distutils import dir_util, file_util, archive_util +from distutils import {imported_module} -assert sdist.dir_util == dir_util, ( - f"\n{sdist.dir_util}\n!=\n{dir_util}" -) - -assert sdist.file_util == file_util, ( - f"\n{sdist.file_util}\n!=\n{file_util}" -) - -assert sdist.archive_util == archive_util, ( - f"\n{sdist.archive_util}\n!=\n{archive_util}" -) +for mod in (cmd, sdist): + assert mod.{imported_module} == {imported_module}, ( + f"\n{{mod.dir_util}}\n!=\n{{{imported_module}}}" + ) print("success") """ -@pytest.mark.parametrize("distutils_version", ("local", "stdlib")) -def test_modules_are_not_duplicated_on_import(distutils_version, tmpdir_cwd, venv): +@pytest.mark.parametrize( + "distutils_version, imported_module", + [ + ("stdlib", "dir_util"), + ("stdlib", "file_util"), + ("stdlib", "archive_util"), + ("local", "dir_util"), + pytest.param( + "local", "file_util", + marks=pytest.mark.xfail(reason="duplicated distutils.file_util, #3042") + ), + ("local", "archive_util"), + ] +) +def test_modules_are_not_duplicated_on_import( + distutils_version, imported_module, tmpdir_cwd, venv +): env = dict(SETUPTOOLS_USE_DISTUTILS=distutils_version) - cmd = ['python', '-c', ENSURE_IMPORTS_ARE_NOT_DUPLICATED] + script = ENSURE_IMPORTS_ARE_NOT_DUPLICATED.format(imported_module=imported_module) + cmd = ['python', '-c', script] output = popen_text(venv.run)(cmd, env=win_sr(env)).strip() assert output == "success" -- cgit v1.2.1 From 6706f2f56133446d012753c178a4ed61b9d8ac8b Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Mon, 24 Jan 2022 17:07:56 +0000 Subject: Fixes #112 install command doesn't use platform in nt_user scheme --- distutils/command/install.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/distutils/command/install.py b/distutils/command/install.py index 511938f4..0c280f1c 100644 --- a/distutils/command/install.py +++ b/distutils/command/install.py @@ -68,8 +68,8 @@ if HAS_USER_SITE: INSTALL_SCHEMES['nt_user'] = { 'purelib': '{usersite}', 'platlib': '{usersite}', - 'headers': '{userbase}/{implementation}{py_version_nodot}/Include/{dist_name}', - 'scripts': '{userbase}/{implementation}{py_version_nodot}/Scripts', + 'headers': '{userbase}/{implementation}{py_version_nodot_plat}/Include/{dist_name}', + 'scripts': '{userbase}/{implementation}{py_version_nodot_plat}/Scripts', 'data' : '{userbase}', } @@ -411,6 +411,10 @@ class install(Command): 'implementation_lower': _get_implementation().lower(), 'implementation': _get_implementation(), } + try: + local_vars['py_version_nodot_plat'] = sys.winver.replace('.', '') + except AttributeError: + local_vars['py_version_nodot_plat'] = '' if HAS_USER_SITE: local_vars['userbase'] = self.install_userbase -- cgit v1.2.1 From a855f07676a3a925797d6bce49af730dfc7e62c8 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Mon, 24 Jan 2022 17:58:53 +0000 Subject: Fix fake expanduser() and verify calculated headers path --- distutils/tests/test_install.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/distutils/tests/test_install.py b/distutils/tests/test_install.py index 75770b05..a60db139 100644 --- a/distutils/tests/test_install.py +++ b/distutils/tests/test_install.py @@ -81,7 +81,9 @@ class InstallTestCase(support.TempdirManager, install_module.USER_SITE = self.user_site def _expanduser(path): - return self.tmpdir + if path.startswith('~'): + return os.path.normpath(self.tmpdir + path[1:]) + return path self.old_expand = os.path.expanduser os.path.expanduser = _expanduser @@ -122,6 +124,15 @@ class InstallTestCase(support.TempdirManager, self.assertIn('userbase', cmd.config_vars) self.assertIn('usersite', cmd.config_vars) + actual_headers = os.path.relpath(cmd.install_headers, self.user_base) + expect_headers = os.path.join( + os.path.relpath(os.path.dirname(self.old_user_site), self.old_user_base), + "Include", + "xx", + ) + + self.assertEqual(os.path.normcase(actual_headers), os.path.normcase(expect_headers)) + def test_handle_extra_path(self): dist = Distribution({'name': 'xx', 'extra_path': 'path,dirs'}) cmd = install(dist) -- cgit v1.2.1 From 2cadaba69aa3d8bef49a5dc8225b4cf5139446a2 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Mon, 24 Jan 2022 18:13:13 +0000 Subject: Add non-NT path for expected path --- distutils/tests/test_install.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/distutils/tests/test_install.py b/distutils/tests/test_install.py index a60db139..2cc9d31c 100644 --- a/distutils/tests/test_install.py +++ b/distutils/tests/test_install.py @@ -125,11 +125,18 @@ class InstallTestCase(support.TempdirManager, self.assertIn('usersite', cmd.config_vars) actual_headers = os.path.relpath(cmd.install_headers, self.user_base) - expect_headers = os.path.join( - os.path.relpath(os.path.dirname(self.old_user_site), self.old_user_base), - "Include", - "xx", - ) + if os.name == 'nt': + expect_headers = os.path.join( + os.path.relpath(os.path.dirname(self.old_user_site), self.old_user_base), + 'Include', + 'xx', + ) + else: + expect_headers = os.path.join( + 'include', + os.path.relpath(os.path.dirname(self.old_user_site), self.old_user_base).rpartition(os.sep)[2], + 'xx', + ) self.assertEqual(os.path.normcase(actual_headers), os.path.normcase(expect_headers)) -- cgit v1.2.1 From aac97591cf99214eb9a7493028ebbbea5b44209c Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Mon, 24 Jan 2022 20:19:38 +0000 Subject: Change API --- distutils/tests/test_install.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/distutils/tests/test_install.py b/distutils/tests/test_install.py index 2cc9d31c..554b2770 100644 --- a/distutils/tests/test_install.py +++ b/distutils/tests/test_install.py @@ -125,18 +125,7 @@ class InstallTestCase(support.TempdirManager, self.assertIn('usersite', cmd.config_vars) actual_headers = os.path.relpath(cmd.install_headers, self.user_base) - if os.name == 'nt': - expect_headers = os.path.join( - os.path.relpath(os.path.dirname(self.old_user_site), self.old_user_base), - 'Include', - 'xx', - ) - else: - expect_headers = os.path.join( - 'include', - os.path.relpath(os.path.dirname(self.old_user_site), self.old_user_base).rpartition(os.sep)[2], - 'xx', - ) + expect_headers = os.path.join(sysconfig.get_python_inc(0, ''), 'xx') self.assertEqual(os.path.normcase(actual_headers), os.path.normcase(expect_headers)) -- cgit v1.2.1 From 198cd3b1c38ac0419933484fecc833581e4b5de3 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Mon, 24 Jan 2022 20:54:58 +0000 Subject: Special case for Windows --- distutils/tests/test_install.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/distutils/tests/test_install.py b/distutils/tests/test_install.py index 554b2770..e8ef1caf 100644 --- a/distutils/tests/test_install.py +++ b/distutils/tests/test_install.py @@ -125,7 +125,14 @@ class InstallTestCase(support.TempdirManager, self.assertIn('usersite', cmd.config_vars) actual_headers = os.path.relpath(cmd.install_headers, self.user_base) - expect_headers = os.path.join(sysconfig.get_python_inc(0, ''), 'xx') + if os.name == 'nt': + expect_headers = os.path.join( + os.path.relpath(os.path.dirname(self.old_user_site), self.old_user_base), + 'Include', + 'xx', + ) + else: + expect_headers = os.path.join(sysconfig.get_python_inc(0, ''), 'xx') self.assertEqual(os.path.normcase(actual_headers), os.path.normcase(expect_headers)) -- cgit v1.2.1 From b48ef32c926052695008b2f4a8a2c68ca0d88678 Mon Sep 17 00:00:00 2001 From: Julien Palard Date: Mon, 24 Jan 2022 23:47:19 +0100 Subject: doc: Fix trailing spaces, tabs, and missing newlines at end of file. --- docs/build_meta.rst | 28 ++++++++++++++-------------- docs/deprecated/distutils-legacy.rst | 2 +- docs/deprecated/functionalities.rst | 2 +- docs/userguide/declarative_config.rst | 4 ++-- docs/userguide/distribution.rst | 2 +- docs/userguide/quickstart.rst | 8 ++++---- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/build_meta.rst b/docs/build_meta.rst index 27df70a2..9c77f9f3 100644 --- a/docs/build_meta.rst +++ b/docs/build_meta.rst @@ -9,29 +9,29 @@ Python packaging has come `a long way `_ The traditional ``setuptools`` way of packaging Python modules uses a ``setup()`` function within the ``setup.py`` script. Commands such as -``python setup.py bdist`` or ``python setup.py bdist_wheel`` generate a -distribution bundle and ``python setup.py install`` installs the distribution. -This interface makes it difficult to choose other packaging tools without an +``python setup.py bdist`` or ``python setup.py bdist_wheel`` generate a +distribution bundle and ``python setup.py install`` installs the distribution. +This interface makes it difficult to choose other packaging tools without an overhaul. Because ``setup.py`` scripts allowed for arbitrary execution, it proved difficult to provide a reliable user experience across environments and history. `PEP 517 `_ therefore came to -rescue and specified a new standard to +rescue and specified a new standard to package and distribute Python modules. Under PEP 517: a ``pyproject.toml`` file is used to specify what program to use - for generating distribution. + for generating distribution. - Then, two functions provided by the program, ``build_wheel(directory: str)`` - and ``build_sdist(directory: str)`` create the distribution bundle at the - specified ``directory``. The program is free to use its own configuration - script or extend the ``.toml`` file. + Then, two functions provided by the program, ``build_wheel(directory: str)`` + and ``build_sdist(directory: str)`` create the distribution bundle at the + specified ``directory``. The program is free to use its own configuration + script or extend the ``.toml`` file. Lastly, ``pip install *.whl`` or ``pip install *.tar.gz`` does the actual installation. If ``*.whl`` is available, ``pip`` will go ahead and copy the files into ``site-packages`` directory. If not, ``pip`` will look at - ``pyproject.toml`` and decide what program to use to 'build from source' + ``pyproject.toml`` and decide what program to use to 'build from source' (the default is ``setuptools``) With this standard, switching between packaging tools becomes a lot easier. ``build_meta`` @@ -48,8 +48,8 @@ scripts, a ``pyproject.toml`` file and a ``setup.cfg`` file:: setup.cfg meowpkg/__init__.py -The pyproject.toml file is required to specify the build system (i.e. what is -being used to package your scripts and install from source). To use it with +The pyproject.toml file is required to specify the build system (i.e. what is +being used to package your scripts and install from source). To use it with setuptools, the content would be:: [build-system] @@ -67,7 +67,7 @@ specify the package information:: name = meowpkg version = 0.0.1 description = a package that meows - + [options] packages = find: @@ -77,7 +77,7 @@ Now generate the distribution. To build the package, use $ pip install -q build $ python -m build -And now it's done! The ``.whl`` file and ``.tar.gz`` can then be distributed +And now it's done! The ``.whl`` file and ``.tar.gz`` can then be distributed and installed:: dist/ diff --git a/docs/deprecated/distutils-legacy.rst b/docs/deprecated/distutils-legacy.rst index 94104fe8..148dc259 100644 --- a/docs/deprecated/distutils-legacy.rst +++ b/docs/deprecated/distutils-legacy.rst @@ -5,7 +5,7 @@ Setuptools and the PyPA have a `stated goal `_ scheme, but it also supports legacy versions. There are, however, a few special things to watch out for, in order to ensure that setuptools and diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst index 4c62c6df..28d4ac33 100644 --- a/docs/userguide/quickstart.rst +++ b/docs/userguide/quickstart.rst @@ -96,7 +96,7 @@ to specify to properly package your project. 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, +the ``packages`` keyword in ``setup.cfg``. However, for very large projects, it can be a big burden to keep the package list updated. ``setuptools`` therefore provides two convenient tools to ease the burden: :literal:`find:\ ` and :literal:`find_namespace:\ `. To use it in your project: @@ -189,9 +189,9 @@ Development mode .. tip:: - Prior to :ref:`pip v21.1 `, a ``setup.py`` script was - required to be compatible with development mode. With late - versions of pip, any project may be installed in this mode. + Prior to :ref:`pip v21.1 `, a ``setup.py`` script was + required to be compatible with development mode. With late + versions of pip, any project may be installed in this mode. ``setuptools`` allows you to install a package without copying any files to your interpreter directory (e.g. the ``site-packages`` directory). -- cgit v1.2.1 From 4d36a45c4bf1a700105a6b5c34719a50ef2fdb81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E4=B9=9D=E9=BC=8E?= <109224573@qq.com> Date: Wed, 26 Jan 2022 16:49:59 +0800 Subject: Use super() --- distutils/_msvccompiler.py | 2 +- distutils/bcppcompiler.py | 2 +- distutils/command/bdist_msi.py | 2 +- distutils/command/check.py | 2 +- distutils/cygwinccompiler.py | 4 ++-- distutils/msvc9compiler.py | 2 +- distutils/msvccompiler.py | 2 +- distutils/tests/test_config.py | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/distutils/_msvccompiler.py b/distutils/_msvccompiler.py index c41ea9ae..f2f801c5 100644 --- a/distutils/_msvccompiler.py +++ b/distutils/_msvccompiler.py @@ -203,7 +203,7 @@ class MSVCCompiler(CCompiler) : def __init__(self, verbose=0, dry_run=0, force=0): - CCompiler.__init__ (self, verbose, dry_run, force) + super().__init__(verbose, dry_run, force) # target platform (.plat_name is consistent with 'bdist') self.plat_name = None self.initialized = False diff --git a/distutils/bcppcompiler.py b/distutils/bcppcompiler.py index 071fea5d..2eb6d2e9 100644 --- a/distutils/bcppcompiler.py +++ b/distutils/bcppcompiler.py @@ -55,7 +55,7 @@ class BCPPCompiler(CCompiler) : dry_run=0, force=0): - CCompiler.__init__ (self, verbose, dry_run, force) + super().__init__(verbose, dry_run, force) # These executables are assumed to all be in the path. # Borland doesn't seem to use any special registry settings to diff --git a/distutils/command/bdist_msi.py b/distutils/command/bdist_msi.py index 0863a188..15259532 100644 --- a/distutils/command/bdist_msi.py +++ b/distutils/command/bdist_msi.py @@ -27,7 +27,7 @@ class PyDialog(Dialog): def __init__(self, *args, **kw): """Dialog(database, name, x, y, w, h, attributes, title, first, default, cancel, bitmap=true)""" - Dialog.__init__(self, *args) + super().__init__(*args) ruler = self.h - 36 bmwidth = 152*ruler/328 #if kw.get("bitmap", True): diff --git a/distutils/command/check.py b/distutils/command/check.py index ada25006..525540b6 100644 --- a/distutils/command/check.py +++ b/distutils/command/check.py @@ -17,7 +17,7 @@ try: def __init__(self, source, report_level, halt_level, stream=None, debug=0, encoding='ascii', error_handler='replace'): self.messages = [] - Reporter.__init__(self, source, report_level, halt_level, stream, + super().__init__(source, report_level, halt_level, stream, debug, encoding, error_handler) def system_message(self, level, message, *children, **kwargs): diff --git a/distutils/cygwinccompiler.py b/distutils/cygwinccompiler.py index fd082f6d..c5c86d8f 100644 --- a/distutils/cygwinccompiler.py +++ b/distutils/cygwinccompiler.py @@ -108,7 +108,7 @@ class CygwinCCompiler(UnixCCompiler): def __init__(self, verbose=0, dry_run=0, force=0): - UnixCCompiler.__init__(self, verbose, dry_run, force) + super().__init__(verbose, dry_run, force) status, details = check_config_h() self.debug_print("Python's GCC status: %s (details: %s)" % @@ -268,7 +268,7 @@ class Mingw32CCompiler(CygwinCCompiler): def __init__(self, verbose=0, dry_run=0, force=0): - CygwinCCompiler.__init__ (self, verbose, dry_run, force) + super().__init__ (verbose, dry_run, force) shared_option = "-shared" diff --git a/distutils/msvc9compiler.py b/distutils/msvc9compiler.py index 14d13775..6b627383 100644 --- a/distutils/msvc9compiler.py +++ b/distutils/msvc9compiler.py @@ -324,7 +324,7 @@ class MSVCCompiler(CCompiler) : exe_extension = '.exe' def __init__(self, verbose=0, dry_run=0, force=0): - CCompiler.__init__ (self, verbose, dry_run, force) + super().__init__(verbose, dry_run, force) self.__version = VERSION self.__root = r"Software\Microsoft\VisualStudio" # self.__macros = MACROS diff --git a/distutils/msvccompiler.py b/distutils/msvccompiler.py index 2d447b85..e1367b89 100644 --- a/distutils/msvccompiler.py +++ b/distutils/msvccompiler.py @@ -228,7 +228,7 @@ class MSVCCompiler(CCompiler) : exe_extension = '.exe' def __init__(self, verbose=0, dry_run=0, force=0): - CCompiler.__init__ (self, verbose, dry_run, force) + super().__init__(verbose, dry_run, force) self.__version = get_build_version() self.__arch = get_build_architecture() if self.__arch == "Intel": diff --git a/distutils/tests/test_config.py b/distutils/tests/test_config.py index 8ab70efb..27bd9d44 100644 --- a/distutils/tests/test_config.py +++ b/distutils/tests/test_config.py @@ -66,7 +66,7 @@ class BasePyPIRCCommandTestCase(support.TempdirManager, class command(PyPIRCCommand): def __init__(self, dist): - PyPIRCCommand.__init__(self, dist) + super().__init__(dist) def initialize_options(self): pass finalize_options = initialize_options -- cgit v1.2.1 From a7c6d64f76091d1cfd8297ba813fe4e901d00ce4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E4=B9=9D=E9=BC=8E?= <109224573@qq.com> Date: Wed, 26 Jan 2022 16:58:29 +0800 Subject: Use super() --- pkg_resources/__init__.py | 4 ++-- setuptools/__init__.py | 4 ++-- setuptools/command/easy_install.py | 2 +- setuptools/extension.py | 2 +- setuptools/package_index.py | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index 9cc6b0a4..9933aad8 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -1581,7 +1581,7 @@ class EggProvider(NullProvider): """Provider based on a virtual filesystem""" def __init__(self, module): - NullProvider.__init__(self, module) + super().__init__(module) self._setup_prefix() def _setup_prefix(self): @@ -1701,7 +1701,7 @@ class ZipProvider(EggProvider): _zip_manifests = MemoizedZipManifests() def __init__(self, module): - EggProvider.__init__(self, module) + super().__init__(module) self.zip_pre = self.loader.archive + os.sep def _zipinfo_name(self, fspath): diff --git a/setuptools/__init__.py b/setuptools/__init__.py index 43d1c96e..06991b65 100644 --- a/setuptools/__init__.py +++ b/setuptools/__init__.py @@ -132,7 +132,7 @@ def _install_setup_requires(attrs): def __init__(self, attrs): _incl = 'dependency_links', 'setup_requires' filtered = {k: attrs[k] for k in set(_incl) & set(attrs)} - distutils.core.Distribution.__init__(self, filtered) + super().__init__(filtered) def finalize_options(self): """ @@ -171,7 +171,7 @@ class Command(_Command): Construct the command for dist, updating vars(self) with any keyword parameters. """ - _Command.__init__(self, dist) + super().__init__(dist) vars(self).update(kw) def _ensure_stringlike(self, option, what, default=None): diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index 514719de..5fab0fdb 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -1577,7 +1577,7 @@ class PthDistributions(Environment): self.sitedirs = list(map(normalize_path, sitedirs)) self.basedir = normalize_path(os.path.dirname(self.filename)) self._load() - Environment.__init__(self, [], None, None) + super().__init__([], None, None) for path in yield_lines(self.paths): list(map(self.add, find_distributions(path, True))) diff --git a/setuptools/extension.py b/setuptools/extension.py index 1820722a..f696c9c1 100644 --- a/setuptools/extension.py +++ b/setuptools/extension.py @@ -34,7 +34,7 @@ class Extension(_Extension): # The *args is needed for compatibility as calls may use positional # arguments. py_limited_api may be set only via keyword. self.py_limited_api = kw.pop("py_limited_api", False) - _Extension.__init__(self, name, sources, *args, **kw) + super().__init__(name, sources, *args, **kw) def _convert_pyx_sources_to_lang(self): """ diff --git a/setuptools/package_index.py b/setuptools/package_index.py index 270e7f3c..051e523a 100644 --- a/setuptools/package_index.py +++ b/setuptools/package_index.py @@ -285,7 +285,7 @@ class PackageIndex(Environment): self, index_url="https://pypi.org/simple/", hosts=('*',), ca_bundle=None, verify_ssl=True, *args, **kw ): - Environment.__init__(self, *args, **kw) + super().__init__(*args, **kw) self.index_url = index_url + "/" [:not index_url.endswith('/')] self.scanned_urls = {} self.fetched_urls = {} @@ -1002,7 +1002,7 @@ class PyPIConfig(configparser.RawConfigParser): Load from ~/.pypirc """ defaults = dict.fromkeys(['username', 'password', 'repository'], '') - configparser.RawConfigParser.__init__(self, defaults) + super().__init__(defaults) rc = os.path.join(os.path.expanduser('~'), '.pypirc') if os.path.exists(rc): -- cgit v1.2.1 From 0c94d6a5f192891f789c7249491642061509d2ac Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 26 Jan 2022 10:07:35 +0000 Subject: Fix codecov config The previous `.codecov.yml` file had an invalid configuration: ```bash $ curl --data-binary @.codecov.yml https://codecov.io/validate Error at ['coverage', 'status', 'project', 'threshold']: must be of ['dict', 'boolean'] type ``` According to the [documentation](https://docs.codecov.com/docs/commit-status#section-project-status), `coverage.status.project` does not seem to accept configuration parameters directly. Instead it requires a mapping between project names (or `"default"` when all the projects are target) and the configuration parameters. This change fixes that by introducing a new nesting level with the `default` key between `project` and `default`. This passes the validation: ```bash % curl --data-binary @.codecov.yml https://codecov.io/validate Valid! { "comment": false, "coverage": { "status": { "project": { "default": { "threshold": 0.5 } } } } } ``` --- .codecov.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.codecov.yml b/.codecov.yml index 7510dfc6..51b248ba 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -2,4 +2,5 @@ comment: false coverage: status: project: - threshold: 0.5% + default: + threshold: 0.5% -- cgit v1.2.1 From 4645c9bd406282279f270de125967bfc18594cc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=AD=E4=B9=9D=E9=BC=8E?= <109224573@qq.com> Date: Wed, 26 Jan 2022 18:32:43 +0800 Subject: Create 3054.misc.rst --- changelog.d/3054.misc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/3054.misc.rst diff --git a/changelog.d/3054.misc.rst b/changelog.d/3054.misc.rst new file mode 100644 index 00000000..7166f837 --- /dev/null +++ b/changelog.d/3054.misc.rst @@ -0,0 +1 @@ +Used Py3 syntax ``super().__init__()`` -- by :user:`imba-tjd` -- cgit v1.2.1 From f7d30a9529378cf69054b5176249e5457aaf640a Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Thu, 27 Jan 2022 23:00:56 +0100 Subject: Stop mentioning `wheel` in the context of PEP 517 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This dependency is exposed automatically by setuptools and the users do not need to declare it explicitly — it will be installed by PEP 517 front-ends automatically, when building wheels. --- docs/build_meta.rst | 5 +++-- docs/userguide/dependency_management.rst | 2 +- docs/userguide/quickstart.rst | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/build_meta.rst b/docs/build_meta.rst index 9c77f9f3..a14a5843 100644 --- a/docs/build_meta.rst +++ b/docs/build_meta.rst @@ -53,12 +53,13 @@ being used to package your scripts and install from source). To use it with setuptools, the content would be:: [build-system] - requires = ["setuptools", "wheel"] + requires = ["setuptools"] build-backend = "setuptools.build_meta" The ``setuptools`` package implements the ``build_sdist`` command and the ``wheel`` package implements the ``build_wheel`` -command; both are required to be compliant with PEP 517. +command; the latter is a dependency of the former +exposed via :pep:`517` hooks. Use ``setuptools``' :ref:`declarative config ` to specify the package information:: diff --git a/docs/userguide/dependency_management.rst b/docs/userguide/dependency_management.rst index 9c29dbd5..ea2fc556 100644 --- a/docs/userguide/dependency_management.rst +++ b/docs/userguide/dependency_management.rst @@ -28,7 +28,7 @@ other two types of dependency keyword, this one is specified in your .. code-block:: ini [build-system] - requires = ["setuptools", "wheel"] + requires = ["setuptools"] #... .. note:: diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst index 28d4ac33..203d6204 100644 --- a/docs/userguide/quickstart.rst +++ b/docs/userguide/quickstart.rst @@ -32,7 +32,7 @@ package your project: .. code-block:: toml [build-system] - requires = ["setuptools", "wheel"] + requires = ["setuptools"] build-backend = "setuptools.build_meta" Then, you will need a ``setup.cfg`` or ``setup.py`` to specify your package -- cgit v1.2.1 From ba81886c363bdc9e7dda5e087fe412c374da174f Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Thu, 27 Jan 2022 23:05:54 +0100 Subject: Add a change note for PR #3056 --- changelog.d/3056.docs.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelog.d/3056.docs.rst diff --git a/changelog.d/3056.docs.rst b/changelog.d/3056.docs.rst new file mode 100644 index 00000000..c3de4e99 --- /dev/null +++ b/changelog.d/3056.docs.rst @@ -0,0 +1,2 @@ +The documentation has stopped suggesting to add ``wheel`` to +:pep:`517` requirements -- by :user:`webknjaz` -- cgit v1.2.1 From c2794005b9dd2c11df96dd444902e221db4cc30b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 28 Jan 2022 16:29:54 +0100 Subject: Don't include Home-page with UNKNOWN value --- changelog.d/3057.change.rst | 1 + setuptools/dist.py | 11 +++-------- 2 files changed, 4 insertions(+), 8 deletions(-) create mode 100644 changelog.d/3057.change.rst diff --git a/changelog.d/3057.change.rst b/changelog.d/3057.change.rst new file mode 100644 index 00000000..1e18efc0 --- /dev/null +++ b/changelog.d/3057.change.rst @@ -0,0 +1 @@ +Don't include optional ``Home-page`` in metadata if no ``url`` is specified. -- by :user:`cdce8p` diff --git a/setuptools/dist.py b/setuptools/dist.py index 37a10d1d..0ca166e2 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -113,13 +113,9 @@ def read_pkg_file(self, file): self.author_email = _read_field_from_msg(msg, 'author-email') self.maintainer_email = None self.url = _read_field_from_msg(msg, 'home-page') + self.download_url = _read_field_from_msg(msg, 'download-url') self.license = _read_field_unescaped_from_msg(msg, 'license') - if 'download-url' in msg: - self.download_url = _read_field_from_msg(msg, 'download-url') - else: - self.download_url = None - self.long_description = _read_field_unescaped_from_msg(msg, 'description') if ( self.long_description is None and @@ -171,9 +167,10 @@ def write_pkg_file(self, file): # noqa: C901 # is too complex (14) # FIXME write_field('Name', self.get_name()) write_field('Version', self.get_version()) write_field('Summary', single_line(self.get_description())) - write_field('Home-page', self.get_url()) optional_fields = ( + ('Home-page', 'url'), + ('Download-URL', 'download_url'), ('Author', 'author'), ('Author-email', 'author_email'), ('Maintainer', 'maintainer'), @@ -187,8 +184,6 @@ def write_pkg_file(self, file): # noqa: C901 # is too complex (14) # FIXME license = rfc822_escape(self.get_license()) write_field('License', license) - if self.download_url: - write_field('Download-URL', self.download_url) for project_url in self.project_urls.items(): write_field('Project-URL', '%s, %s' % project_url) -- cgit v1.2.1 From a62a5479a8ba4b9ff952e19e72e4a0d0fd1a37ce Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 29 Jan 2022 15:49:29 -0500 Subject: Only validate name --- setuptools/dist.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index 84c06f93..c2d711a4 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -467,7 +467,7 @@ class Distribution(_Distribution): self._finalize_requires() def _validate_metadata(self): - required = ["name", "version"] + required = ["name"] missing = [] for req_attr in required: @@ -488,7 +488,6 @@ class Distribution(_Distribution): self._validate_metadata() super().run_commands() - def _set_metadata_defaults(self, attrs): """ Fill-in missing metadata fields not supported by distutils. -- cgit v1.2.1 From 9b7111a9a62b2c59bca43d1f9c2e0600f7feba33 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 29 Jan 2022 16:39:59 -0500 Subject: Add test capturing expectation when name is not supplied. --- setuptools/tests/test_dist.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/setuptools/tests/test_dist.py b/setuptools/tests/test_dist.py index c4279f0b..4980f2c3 100644 --- a/setuptools/tests/test_dist.py +++ b/setuptools/tests/test_dist.py @@ -374,3 +374,8 @@ def test_check_specifier(): ) def test_rfc822_unescape(content, result): assert (result or content) == rfc822_unescape(rfc822_escape(content)) + + +def test_metadata_name(): + with pytest.raises(DistutilsSetupError, match='missing.*name'): + Distribution()._validate_metadata() -- cgit v1.2.1 From 650ff7f37915bdbcc50585c87c6eeb5626cb7595 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 29 Jan 2022 16:41:58 -0500 Subject: Remove hook in run_commands; a different hook point may be better. --- setuptools/dist.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index c2d711a4..d1d37cf6 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -484,10 +484,6 @@ class Distribution(_Distribution): "Required package metadata is missing: please supply the %s." % message ) - def run_commands(self): - self._validate_metadata() - super().run_commands() - def _set_metadata_defaults(self, attrs): """ Fill-in missing metadata fields not supported by distutils. -- cgit v1.2.1 From 0b29d4e464f9282a908fbd89a63f966727a9ad15 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 29 Jan 2022 16:57:50 -0500 Subject: Refactor to calculate missing as a difference of sets. Simply emit all missing keys as repr() of that difference. --- setuptools/dist.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index d1d37cf6..61c1130b 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -467,22 +467,17 @@ class Distribution(_Distribution): self._finalize_requires() def _validate_metadata(self): - required = ["name"] - missing = [] - - for req_attr in required: - if getattr(self.metadata, req_attr) is None: - missing.append(req_attr) + required = {"name"} + provided = { + key + for key in vars(self.metadata) + if getattr(self.metadata, key, None) is not None + } + missing = required - provided if missing: - if len(missing) == 1: - message = "%s attribute" % missing[0] - else: - message = "%s and %s attributes" % (", ".join(missing[:-1]), - missing[-1]) - raise DistutilsSetupError( - "Required package metadata is missing: please supply the %s." % message - ) + msg = f"Required package metadata is missing: {missing}" + raise DistutilsSetupError(msg) def _set_metadata_defaults(self, attrs): """ -- cgit v1.2.1 From 4d660833d040b20d2923fb1077c9cc5525943568 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 29 Jan 2022 17:09:36 -0500 Subject: Remove name from distributions where the name is irrelevant to the tests. --- setuptools/tests/test_bdist_egg.py | 3 +-- setuptools/tests/test_build_py.py | 3 --- setuptools/tests/test_easy_install.py | 2 +- setuptools/tests/test_sphinx_upload_docs.py | 1 - setuptools/tests/test_test.py | 3 +-- setuptools/tests/test_upload_docs.py | 2 +- 6 files changed, 4 insertions(+), 10 deletions(-) diff --git a/setuptools/tests/test_bdist_egg.py b/setuptools/tests/test_bdist_egg.py index fb5b90b1..67f788cc 100644 --- a/setuptools/tests/test_bdist_egg.py +++ b/setuptools/tests/test_bdist_egg.py @@ -13,7 +13,7 @@ from . import contexts SETUP_PY = """\ from setuptools import setup -setup(name='foo', py_modules=['hi']) +setup(py_modules=['hi']) """ @@ -52,7 +52,6 @@ class Test: dist = Distribution(dict( script_name='setup.py', script_args=['bdist_egg', '--exclude-source-files'], - name='foo', py_modules=['hi'], )) with contexts.quiet(): diff --git a/setuptools/tests/test_build_py.py b/setuptools/tests/test_build_py.py index 78a31ac4..19c8b780 100644 --- a/setuptools/tests/test_build_py.py +++ b/setuptools/tests/test_build_py.py @@ -18,7 +18,6 @@ def test_directories_in_package_data_glob(tmpdir_cwd): script_name='setup.py', script_args=['build_py'], packages=[''], - name='foo', package_data={'': ['path/*']}, )) os.makedirs('path/subpath') @@ -40,7 +39,6 @@ def test_read_only(tmpdir_cwd): script_args=['build_py'], packages=['pkg'], package_data={'pkg': ['data.dat']}, - name='pkg', )) os.makedirs('pkg') open('pkg/__init__.py', 'w').close() @@ -70,7 +68,6 @@ def test_executable_data(tmpdir_cwd): script_args=['build_py'], packages=['pkg'], package_data={'pkg': ['run-me']}, - name='pkg', )) os.makedirs('pkg') open('pkg/__init__.py', 'w').close() diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py index 6840d03b..1e77a649 100644 --- a/setuptools/tests/test_easy_install.py +++ b/setuptools/tests/test_easy_install.py @@ -62,7 +62,7 @@ class FakeDist: SETUP_PY = DALS(""" from setuptools import setup - setup(name='foo') + setup() """) diff --git a/setuptools/tests/test_sphinx_upload_docs.py b/setuptools/tests/test_sphinx_upload_docs.py index cc5b8293..f24077fd 100644 --- a/setuptools/tests/test_sphinx_upload_docs.py +++ b/setuptools/tests/test_sphinx_upload_docs.py @@ -25,7 +25,6 @@ def sphinx_doc_sample_project(tmpdir_cwd): class TestSphinxUploadDocs: def test_sphinx_doc(self): params = dict( - name='foo', packages=['test'], ) dist = Distribution(params) diff --git a/setuptools/tests/test_test.py b/setuptools/tests/test_test.py index 6bce8e20..d0a49461 100644 --- a/setuptools/tests/test_test.py +++ b/setuptools/tests/test_test.py @@ -13,7 +13,7 @@ SETUP_PY = DALS( """ from setuptools import setup - setup(name='foo', + setup( packages=['name', 'name.space', 'name.space.tests'], namespace_packages=['name'], test_suite='name.space.tests.test_suite', @@ -77,7 +77,6 @@ def quiet_log(): @pytest.mark.usefixtures('tmpdir_cwd', 'quiet_log') def test_tests_are_run_once(capfd): params = dict( - name='foo', packages=['dummy'], ) with open('setup.py', 'wt') as f: diff --git a/setuptools/tests/test_upload_docs.py b/setuptools/tests/test_upload_docs.py index 55978aad..68977a5d 100644 --- a/setuptools/tests/test_upload_docs.py +++ b/setuptools/tests/test_upload_docs.py @@ -18,7 +18,7 @@ def sample_project(tmpdir_cwd): 'setup.py': DALS(""" from setuptools import setup - setup(name='foo') + setup() """), 'build': { 'index.html': 'Hello world.', -- cgit v1.2.1 From 253ecb0179707487e94472dae041ebbaabbb4bc8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 29 Jan 2022 21:09:26 -0500 Subject: Only resolve 'parent' directory when xdist is in use. Fixes #3059. --- setuptools/tests/contexts.py | 22 ++++++++++++++++++---- setuptools/tests/fixtures.py | 6 ++++-- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/setuptools/tests/contexts.py b/setuptools/tests/contexts.py index 5316e599..58948824 100644 --- a/setuptools/tests/contexts.py +++ b/setuptools/tests/contexts.py @@ -99,12 +99,26 @@ def suppress_exceptions(*excs): pass +def multiproc(request): + """ + Return True if running under xdist and multiple + workers are used. + """ + try: + worker_id = request.getfixturevalue('worker_id') + except Exception: + return False + return worker_id != 'master' + + @contextlib.contextmanager -def session_locked_tmp_dir(tmp_path_factory, name): +def session_locked_tmp_dir(request, tmp_path_factory, name): """Uses a file lock to guarantee only one worker can access a temp dir""" - root_tmp_dir = tmp_path_factory.getbasetemp().parent - # ^-- get the temp directory shared by all workers - locked_dir = root_tmp_dir / name + # get the temp directory shared by all workers + base = tmp_path_factory.getbasetemp() + shared_dir = base.parent if multiproc(request) else base + + locked_dir = shared_dir / name with FileLock(locked_dir.with_suffix(".lock")): # ^-- prevent multiple workers to access the directory at once locked_dir.mkdir(exist_ok=True, parents=True) diff --git a/setuptools/tests/fixtures.py b/setuptools/tests/fixtures.py index 9b91d7d7..7599e655 100644 --- a/setuptools/tests/fixtures.py +++ b/setuptools/tests/fixtures.py @@ -64,7 +64,8 @@ def sample_project(tmp_path): @pytest.fixture(scope="session") def setuptools_sdist(tmp_path_factory, request): - with contexts.session_locked_tmp_dir(tmp_path_factory, "sdist_build") as tmp: + with contexts.session_locked_tmp_dir( + request, tmp_path_factory, "sdist_build") as tmp: dist = next(tmp.glob("*.tar.gz"), None) if dist: return dist @@ -78,7 +79,8 @@ def setuptools_sdist(tmp_path_factory, request): @pytest.fixture(scope="session") def setuptools_wheel(tmp_path_factory, request): - with contexts.session_locked_tmp_dir(tmp_path_factory, "wheel_build") as tmp: + with contexts.session_locked_tmp_dir( + request, tmp_path_factory, "wheel_build") as tmp: dist = next(tmp.glob("*.whl"), None) if dist: return dist -- cgit v1.2.1 From fb4a1f79f497920c3aa5b95cdfba134cc72e1dfd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 23 Jan 2022 22:06:36 -0500 Subject: Vendor jaraco.text to supply yield_lines, drop_comment, and join_continuation. --- pkg_resources/__init__.py | 82 +- .../jaraco.context-4.1.1.dist-info/INSTALLER | 1 + .../_vendor/jaraco.context-4.1.1.dist-info/LICENSE | 19 + .../jaraco.context-4.1.1.dist-info/METADATA | 52 + .../_vendor/jaraco.context-4.1.1.dist-info/RECORD | 8 + .../_vendor/jaraco.context-4.1.1.dist-info/WHEEL | 5 + .../jaraco.context-4.1.1.dist-info/top_level.txt | 1 + .../jaraco.functools-3.5.0.dist-info/INSTALLER | 1 + .../jaraco.functools-3.5.0.dist-info/LICENSE | 19 + .../jaraco.functools-3.5.0.dist-info/METADATA | 58 + .../jaraco.functools-3.5.0.dist-info/RECORD | 8 + .../_vendor/jaraco.functools-3.5.0.dist-info/WHEEL | 5 + .../jaraco.functools-3.5.0.dist-info/top_level.txt | 1 + .../_vendor/jaraco.text-3.7.0.dist-info/INSTALLER | 1 + .../_vendor/jaraco.text-3.7.0.dist-info/LICENSE | 19 + .../_vendor/jaraco.text-3.7.0.dist-info/METADATA | 55 + .../_vendor/jaraco.text-3.7.0.dist-info/RECORD | 10 + .../_vendor/jaraco.text-3.7.0.dist-info/REQUESTED | 0 .../_vendor/jaraco.text-3.7.0.dist-info/WHEEL | 5 + .../jaraco.text-3.7.0.dist-info/top_level.txt | 1 + pkg_resources/_vendor/jaraco/context.py | 213 + pkg_resources/_vendor/jaraco/functools.py | 525 +++ pkg_resources/_vendor/jaraco/text/Lorem ipsum.txt | 2 + pkg_resources/_vendor/jaraco/text/__init__.py | 600 +++ .../more_itertools-8.12.0.dist-info/INSTALLER | 1 + .../more_itertools-8.12.0.dist-info/LICENSE | 19 + .../more_itertools-8.12.0.dist-info/METADATA | 521 +++ .../_vendor/more_itertools-8.12.0.dist-info/RECORD | 16 + .../_vendor/more_itertools-8.12.0.dist-info/WHEEL | 5 + .../more_itertools-8.12.0.dist-info/top_level.txt | 1 + pkg_resources/_vendor/more_itertools/__init__.py | 4 + pkg_resources/_vendor/more_itertools/__init__.pyi | 2 + pkg_resources/_vendor/more_itertools/more.py | 4317 ++++++++++++++++++++ pkg_resources/_vendor/more_itertools/more.pyi | 664 +++ pkg_resources/_vendor/more_itertools/py.typed | 0 pkg_resources/_vendor/more_itertools/recipes.py | 698 ++++ pkg_resources/_vendor/more_itertools/recipes.pyi | 112 + pkg_resources/_vendor/vendored.txt | 1 + pkg_resources/extern/__init__.py | 2 +- 39 files changed, 7977 insertions(+), 77 deletions(-) create mode 100644 pkg_resources/_vendor/jaraco.context-4.1.1.dist-info/INSTALLER create mode 100644 pkg_resources/_vendor/jaraco.context-4.1.1.dist-info/LICENSE create mode 100644 pkg_resources/_vendor/jaraco.context-4.1.1.dist-info/METADATA create mode 100644 pkg_resources/_vendor/jaraco.context-4.1.1.dist-info/RECORD create mode 100644 pkg_resources/_vendor/jaraco.context-4.1.1.dist-info/WHEEL create mode 100644 pkg_resources/_vendor/jaraco.context-4.1.1.dist-info/top_level.txt create mode 100644 pkg_resources/_vendor/jaraco.functools-3.5.0.dist-info/INSTALLER create mode 100644 pkg_resources/_vendor/jaraco.functools-3.5.0.dist-info/LICENSE create mode 100644 pkg_resources/_vendor/jaraco.functools-3.5.0.dist-info/METADATA create mode 100644 pkg_resources/_vendor/jaraco.functools-3.5.0.dist-info/RECORD create mode 100644 pkg_resources/_vendor/jaraco.functools-3.5.0.dist-info/WHEEL create mode 100644 pkg_resources/_vendor/jaraco.functools-3.5.0.dist-info/top_level.txt create mode 100644 pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/INSTALLER create mode 100644 pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/LICENSE create mode 100644 pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/METADATA create mode 100644 pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/RECORD create mode 100644 pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/REQUESTED create mode 100644 pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/WHEEL create mode 100644 pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/top_level.txt create mode 100644 pkg_resources/_vendor/jaraco/context.py create mode 100644 pkg_resources/_vendor/jaraco/functools.py create mode 100644 pkg_resources/_vendor/jaraco/text/Lorem ipsum.txt create mode 100644 pkg_resources/_vendor/jaraco/text/__init__.py create mode 100644 pkg_resources/_vendor/more_itertools-8.12.0.dist-info/INSTALLER create mode 100644 pkg_resources/_vendor/more_itertools-8.12.0.dist-info/LICENSE create mode 100644 pkg_resources/_vendor/more_itertools-8.12.0.dist-info/METADATA create mode 100644 pkg_resources/_vendor/more_itertools-8.12.0.dist-info/RECORD create mode 100644 pkg_resources/_vendor/more_itertools-8.12.0.dist-info/WHEEL create mode 100644 pkg_resources/_vendor/more_itertools-8.12.0.dist-info/top_level.txt create mode 100644 pkg_resources/_vendor/more_itertools/__init__.py create mode 100644 pkg_resources/_vendor/more_itertools/__init__.pyi create mode 100644 pkg_resources/_vendor/more_itertools/more.py create mode 100644 pkg_resources/_vendor/more_itertools/more.pyi create mode 100644 pkg_resources/_vendor/more_itertools/py.typed create mode 100644 pkg_resources/_vendor/more_itertools/recipes.py create mode 100644 pkg_resources/_vendor/more_itertools/recipes.pyi diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index 93db52d2..852476e2 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -71,6 +71,12 @@ try: except ImportError: importlib_machinery = None +from pkg_resources.extern.jaraco.text import ( + yield_lines, + drop_comment, + join_continuation, +) + from pkg_resources.extern import appdirs from pkg_resources.extern import packaging __import__('pkg_resources.extern.packaging.version') @@ -2398,34 +2404,6 @@ def _set_parent_ns(packageName): setattr(sys.modules[parent], name, sys.modules[packageName]) -def _nonblank(str): - return str and not str.startswith('#') - - -@functools.singledispatch -def yield_lines(iterable): - r""" - Yield valid lines of a string or iterable. - - >>> list(yield_lines('')) - [] - >>> list(yield_lines(['foo', 'bar'])) - ['foo', 'bar'] - >>> list(yield_lines('foo\nbar')) - ['foo', 'bar'] - >>> list(yield_lines('\nfoo\n#bar\nbaz #comment')) - ['foo', 'baz #comment'] - >>> list(yield_lines(['foo\nbar', 'baz', 'bing\n\n\n'])) - ['foo', 'bar', 'baz', 'bing'] - """ - return itertools.chain.from_iterable(map(yield_lines, iterable)) - - -@yield_lines.register(str) -def _(text): - return filter(_nonblank, map(str.strip, text.splitlines())) - - MODULE = re.compile(r"\w+(\.\w+)*$").match EGG_NAME = re.compile( r""" @@ -3092,54 +3070,6 @@ def issue_warning(*args, **kw): warnings.warn(stacklevel=level + 1, *args, **kw) -def drop_comment(line): - """ - Drop comments. - - >>> drop_comment('foo # bar') - 'foo' - - A hash without a space may be in a URL. - - >>> drop_comment('http://example.com/foo#bar') - 'http://example.com/foo#bar' - """ - return line.partition(' #')[0] - - -def join_continuation(lines): - r""" - Join lines continued by a trailing backslash. - - >>> list(join_continuation(['foo \\', 'bar', 'baz'])) - ['foobar', 'baz'] - >>> list(join_continuation(['foo \\', 'bar', 'baz'])) - ['foobar', 'baz'] - >>> list(join_continuation(['foo \\', 'bar \\', 'baz'])) - ['foobarbaz'] - - Not sure why, but... - The character preceeding the backslash is also elided. - - >>> list(join_continuation(['goo\\', 'dly'])) - ['godly'] - - A terrible idea, but... - If no line is available to continue, suppress the lines. - - >>> list(join_continuation(['foo', 'bar\\', 'baz\\'])) - ['foo'] - """ - lines = iter(lines) - for item in lines: - while item.endswith('\\'): - try: - item = item[:-2].strip() + next(lines) - except StopIteration: - return - yield item - - def parse_requirements(strs): """ Yield ``Requirement`` objects for each specification in `strs`. diff --git a/pkg_resources/_vendor/jaraco.context-4.1.1.dist-info/INSTALLER b/pkg_resources/_vendor/jaraco.context-4.1.1.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/pkg_resources/_vendor/jaraco.context-4.1.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/pkg_resources/_vendor/jaraco.context-4.1.1.dist-info/LICENSE b/pkg_resources/_vendor/jaraco.context-4.1.1.dist-info/LICENSE new file mode 100644 index 00000000..353924be --- /dev/null +++ b/pkg_resources/_vendor/jaraco.context-4.1.1.dist-info/LICENSE @@ -0,0 +1,19 @@ +Copyright Jason R. Coombs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/pkg_resources/_vendor/jaraco.context-4.1.1.dist-info/METADATA b/pkg_resources/_vendor/jaraco.context-4.1.1.dist-info/METADATA new file mode 100644 index 00000000..908711b7 --- /dev/null +++ b/pkg_resources/_vendor/jaraco.context-4.1.1.dist-info/METADATA @@ -0,0 +1,52 @@ +Metadata-Version: 2.1 +Name: jaraco.context +Version: 4.1.1 +Summary: Context managers by jaraco +Home-page: https://github.com/jaraco/jaraco.context +Author: Jason R. Coombs +Author-email: jaraco@jaraco.com +License: UNKNOWN +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Requires-Python: >=3.6 +License-File: LICENSE +Provides-Extra: docs +Requires-Dist: sphinx ; extra == 'docs' +Requires-Dist: jaraco.packaging (>=8.2) ; extra == 'docs' +Requires-Dist: rst.linker (>=1.9) ; extra == 'docs' +Provides-Extra: testing +Requires-Dist: pytest (>=6) ; extra == 'testing' +Requires-Dist: pytest-checkdocs (>=2.4) ; extra == 'testing' +Requires-Dist: pytest-flake8 ; extra == 'testing' +Requires-Dist: pytest-cov ; extra == 'testing' +Requires-Dist: pytest-enabler (>=1.0.1) ; extra == 'testing' +Requires-Dist: pytest-black (>=0.3.7) ; (platform_python_implementation != "PyPy") and extra == 'testing' +Requires-Dist: pytest-mypy ; (platform_python_implementation != "PyPy") and extra == 'testing' + +.. image:: https://img.shields.io/pypi/v/jaraco.context.svg + :target: `PyPI link`_ + +.. image:: https://img.shields.io/pypi/pyversions/jaraco.context.svg + :target: `PyPI link`_ + +.. _PyPI link: https://pypi.org/project/jaraco.context + +.. image:: https://github.com/jaraco/jaraco.context/workflows/tests/badge.svg + :target: https://github.com/jaraco/jaraco.context/actions?query=workflow%3A%22tests%22 + :alt: tests + +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + :alt: Code style: Black + +.. image:: https://readthedocs.org/projects/jaracocontext/badge/?version=latest + :target: https://jaracocontext.readthedocs.io/en/latest/?badge=latest + +.. image:: https://img.shields.io/badge/skeleton-2021-informational + :target: https://blog.jaraco.com/skeleton + + diff --git a/pkg_resources/_vendor/jaraco.context-4.1.1.dist-info/RECORD b/pkg_resources/_vendor/jaraco.context-4.1.1.dist-info/RECORD new file mode 100644 index 00000000..f40d48c7 --- /dev/null +++ b/pkg_resources/_vendor/jaraco.context-4.1.1.dist-info/RECORD @@ -0,0 +1,8 @@ +jaraco.context-4.1.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +jaraco.context-4.1.1.dist-info/LICENSE,sha256=2z8CRrH5J48VhFuZ_sR4uLUG63ZIeZNyL4xuJUKF-vg,1050 +jaraco.context-4.1.1.dist-info/METADATA,sha256=bvqDGCk6Z7TkohUqr5XZm19SbF9mVxrtXjN6uF_BAMQ,2031 +jaraco.context-4.1.1.dist-info/RECORD,, +jaraco.context-4.1.1.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92 +jaraco.context-4.1.1.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7 +jaraco/__pycache__/context.cpython-310.pyc,, +jaraco/context.py,sha256=7X1tpCLc5EN45iWGzGcsH0Unx62REIkvtRvglj0SiUA,5420 diff --git a/pkg_resources/_vendor/jaraco.context-4.1.1.dist-info/WHEEL b/pkg_resources/_vendor/jaraco.context-4.1.1.dist-info/WHEEL new file mode 100644 index 00000000..5bad85fd --- /dev/null +++ b/pkg_resources/_vendor/jaraco.context-4.1.1.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/pkg_resources/_vendor/jaraco.context-4.1.1.dist-info/top_level.txt b/pkg_resources/_vendor/jaraco.context-4.1.1.dist-info/top_level.txt new file mode 100644 index 00000000..f6205a5f --- /dev/null +++ b/pkg_resources/_vendor/jaraco.context-4.1.1.dist-info/top_level.txt @@ -0,0 +1 @@ +jaraco diff --git a/pkg_resources/_vendor/jaraco.functools-3.5.0.dist-info/INSTALLER b/pkg_resources/_vendor/jaraco.functools-3.5.0.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/pkg_resources/_vendor/jaraco.functools-3.5.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/pkg_resources/_vendor/jaraco.functools-3.5.0.dist-info/LICENSE b/pkg_resources/_vendor/jaraco.functools-3.5.0.dist-info/LICENSE new file mode 100644 index 00000000..353924be --- /dev/null +++ b/pkg_resources/_vendor/jaraco.functools-3.5.0.dist-info/LICENSE @@ -0,0 +1,19 @@ +Copyright Jason R. Coombs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/pkg_resources/_vendor/jaraco.functools-3.5.0.dist-info/METADATA b/pkg_resources/_vendor/jaraco.functools-3.5.0.dist-info/METADATA new file mode 100644 index 00000000..12dfbdd0 --- /dev/null +++ b/pkg_resources/_vendor/jaraco.functools-3.5.0.dist-info/METADATA @@ -0,0 +1,58 @@ +Metadata-Version: 2.1 +Name: jaraco.functools +Version: 3.5.0 +Summary: Functools like those found in stdlib +Home-page: https://github.com/jaraco/jaraco.functools +Author: Jason R. Coombs +Author-email: jaraco@jaraco.com +License: UNKNOWN +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Requires-Python: >=3.7 +License-File: LICENSE +Requires-Dist: more-itertools +Provides-Extra: docs +Requires-Dist: sphinx ; extra == 'docs' +Requires-Dist: jaraco.packaging (>=8.2) ; extra == 'docs' +Requires-Dist: rst.linker (>=1.9) ; extra == 'docs' +Provides-Extra: testing +Requires-Dist: pytest (>=6) ; extra == 'testing' +Requires-Dist: pytest-checkdocs (>=2.4) ; extra == 'testing' +Requires-Dist: pytest-flake8 ; extra == 'testing' +Requires-Dist: pytest-cov ; extra == 'testing' +Requires-Dist: pytest-enabler (>=1.0.1) ; extra == 'testing' +Requires-Dist: jaraco.classes ; extra == 'testing' +Requires-Dist: pytest-black (>=0.3.7) ; (platform_python_implementation != "PyPy") and extra == 'testing' +Requires-Dist: pytest-mypy ; (platform_python_implementation != "PyPy") and extra == 'testing' + +.. image:: https://img.shields.io/pypi/v/jaraco.functools.svg + :target: `PyPI link`_ + +.. image:: https://img.shields.io/pypi/pyversions/jaraco.functools.svg + +.. image:: https://img.shields.io/travis/jaraco/jaraco.functools/master.svg + :target: `PyPI link`_ + +.. _PyPI link: https://pypi.org/project/jaraco.functools + +.. image:: https://github.com/jaraco/jaraco.functools/workflows/tests/badge.svg + :target: https://github.com/jaraco/jaraco.functools/actions?query=workflow%3A%22tests%22 + :alt: tests + +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + :alt: Code style: Black + +.. image:: https://readthedocs.org/projects/jaracofunctools/badge/?version=latest + :target: https://jaracofunctools.readthedocs.io/en/latest/?badge=latest + +.. image:: https://img.shields.io/badge/skeleton-2021-informational + :target: https://blog.jaraco.com/skeleton + +Additional functools in the spirit of stdlib's functools. + + diff --git a/pkg_resources/_vendor/jaraco.functools-3.5.0.dist-info/RECORD b/pkg_resources/_vendor/jaraco.functools-3.5.0.dist-info/RECORD new file mode 100644 index 00000000..fbda3d1f --- /dev/null +++ b/pkg_resources/_vendor/jaraco.functools-3.5.0.dist-info/RECORD @@ -0,0 +1,8 @@ +jaraco.functools-3.5.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +jaraco.functools-3.5.0.dist-info/LICENSE,sha256=2z8CRrH5J48VhFuZ_sR4uLUG63ZIeZNyL4xuJUKF-vg,1050 +jaraco.functools-3.5.0.dist-info/METADATA,sha256=cE9C7u9bo_GjLAuw4nML67a25kUaPDiHn4j03lG4jd0,2276 +jaraco.functools-3.5.0.dist-info/RECORD,, +jaraco.functools-3.5.0.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92 +jaraco.functools-3.5.0.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7 +jaraco/__pycache__/functools.cpython-310.pyc,, +jaraco/functools.py,sha256=PtEHbXZstgVJrwje4GvJOsz5pEbgslOcgEn2EJNpr2c,13494 diff --git a/pkg_resources/_vendor/jaraco.functools-3.5.0.dist-info/WHEEL b/pkg_resources/_vendor/jaraco.functools-3.5.0.dist-info/WHEEL new file mode 100644 index 00000000..5bad85fd --- /dev/null +++ b/pkg_resources/_vendor/jaraco.functools-3.5.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/pkg_resources/_vendor/jaraco.functools-3.5.0.dist-info/top_level.txt b/pkg_resources/_vendor/jaraco.functools-3.5.0.dist-info/top_level.txt new file mode 100644 index 00000000..f6205a5f --- /dev/null +++ b/pkg_resources/_vendor/jaraco.functools-3.5.0.dist-info/top_level.txt @@ -0,0 +1 @@ +jaraco diff --git a/pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/INSTALLER b/pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/LICENSE b/pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/LICENSE new file mode 100644 index 00000000..353924be --- /dev/null +++ b/pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/LICENSE @@ -0,0 +1,19 @@ +Copyright Jason R. Coombs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/METADATA b/pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/METADATA new file mode 100644 index 00000000..615a50a4 --- /dev/null +++ b/pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/METADATA @@ -0,0 +1,55 @@ +Metadata-Version: 2.1 +Name: jaraco.text +Version: 3.7.0 +Summary: Module for text manipulation +Home-page: https://github.com/jaraco/jaraco.text +Author: Jason R. Coombs +Author-email: jaraco@jaraco.com +License: UNKNOWN +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Requires-Python: >=3.6 +License-File: LICENSE +Requires-Dist: jaraco.functools +Requires-Dist: jaraco.context (>=4.1) +Requires-Dist: importlib-resources ; python_version < "3.9" +Provides-Extra: docs +Requires-Dist: sphinx ; extra == 'docs' +Requires-Dist: jaraco.packaging (>=8.2) ; extra == 'docs' +Requires-Dist: rst.linker (>=1.9) ; extra == 'docs' +Provides-Extra: testing +Requires-Dist: pytest (>=6) ; extra == 'testing' +Requires-Dist: pytest-checkdocs (>=2.4) ; extra == 'testing' +Requires-Dist: pytest-flake8 ; extra == 'testing' +Requires-Dist: pytest-cov ; extra == 'testing' +Requires-Dist: pytest-enabler (>=1.0.1) ; extra == 'testing' +Requires-Dist: pytest-black (>=0.3.7) ; (platform_python_implementation != "PyPy") and extra == 'testing' +Requires-Dist: pytest-mypy ; (platform_python_implementation != "PyPy") and extra == 'testing' + +.. image:: https://img.shields.io/pypi/v/jaraco.text.svg + :target: `PyPI link`_ + +.. image:: https://img.shields.io/pypi/pyversions/jaraco.text.svg + :target: `PyPI link`_ + +.. _PyPI link: https://pypi.org/project/jaraco.text + +.. image:: https://github.com/jaraco/jaraco.text/workflows/tests/badge.svg + :target: https://github.com/jaraco/jaraco.text/actions?query=workflow%3A%22tests%22 + :alt: tests + +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + :alt: Code style: Black + +.. image:: https://readthedocs.org/projects/jaracotext/badge/?version=latest + :target: https://jaracotext.readthedocs.io/en/latest/?badge=latest + +.. image:: https://img.shields.io/badge/skeleton-2021-informational + :target: https://blog.jaraco.com/skeleton + + diff --git a/pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/RECORD b/pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/RECORD new file mode 100644 index 00000000..916ad7d3 --- /dev/null +++ b/pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/RECORD @@ -0,0 +1,10 @@ +jaraco.text-3.7.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +jaraco.text-3.7.0.dist-info/LICENSE,sha256=2z8CRrH5J48VhFuZ_sR4uLUG63ZIeZNyL4xuJUKF-vg,1050 +jaraco.text-3.7.0.dist-info/METADATA,sha256=5mcR1dY0cJNrM-VIkAFkpjOgvgzmq6nM1GfD0gwTIhs,2136 +jaraco.text-3.7.0.dist-info/RECORD,, +jaraco.text-3.7.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +jaraco.text-3.7.0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92 +jaraco.text-3.7.0.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7 +jaraco/text/Lorem ipsum.txt,sha256=N_7c_79zxOufBY9HZ3yzMgOkNv-TkOTTio4BydrSjgs,1335 +jaraco/text/__init__.py,sha256=I56MW2ZFwPrYXIxzqxMBe2A1t-T4uZBgEgAKe9-JoqM,15538 +jaraco/text/__pycache__/__init__.cpython-310.pyc,, diff --git a/pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/REQUESTED b/pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/REQUESTED new file mode 100644 index 00000000..e69de29b diff --git a/pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/WHEEL b/pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/WHEEL new file mode 100644 index 00000000..becc9a66 --- /dev/null +++ b/pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.1) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/top_level.txt b/pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/top_level.txt new file mode 100644 index 00000000..f6205a5f --- /dev/null +++ b/pkg_resources/_vendor/jaraco.text-3.7.0.dist-info/top_level.txt @@ -0,0 +1 @@ +jaraco diff --git a/pkg_resources/_vendor/jaraco/context.py b/pkg_resources/_vendor/jaraco/context.py new file mode 100644 index 00000000..87a4e3dc --- /dev/null +++ b/pkg_resources/_vendor/jaraco/context.py @@ -0,0 +1,213 @@ +import os +import subprocess +import contextlib +import functools +import tempfile +import shutil +import operator + + +@contextlib.contextmanager +def pushd(dir): + orig = os.getcwd() + os.chdir(dir) + try: + yield dir + finally: + os.chdir(orig) + + +@contextlib.contextmanager +def tarball_context(url, target_dir=None, runner=None, pushd=pushd): + """ + Get a tarball, extract it, change to that directory, yield, then + clean up. + `runner` is the function to invoke commands. + `pushd` is a context manager for changing the directory. + """ + if target_dir is None: + target_dir = os.path.basename(url).replace('.tar.gz', '').replace('.tgz', '') + if runner is None: + runner = functools.partial(subprocess.check_call, shell=True) + # In the tar command, use --strip-components=1 to strip the first path and + # then + # use -C to cause the files to be extracted to {target_dir}. This ensures + # that we always know where the files were extracted. + runner('mkdir {target_dir}'.format(**vars())) + try: + getter = 'wget {url} -O -' + extract = 'tar x{compression} --strip-components=1 -C {target_dir}' + cmd = ' | '.join((getter, extract)) + runner(cmd.format(compression=infer_compression(url), **vars())) + with pushd(target_dir): + yield target_dir + finally: + runner('rm -Rf {target_dir}'.format(**vars())) + + +def infer_compression(url): + """ + Given a URL or filename, infer the compression code for tar. + """ + # cheat and just assume it's the last two characters + compression_indicator = url[-2:] + mapping = dict(gz='z', bz='j', xz='J') + # Assume 'z' (gzip) if no match + return mapping.get(compression_indicator, 'z') + + +@contextlib.contextmanager +def temp_dir(remover=shutil.rmtree): + """ + Create a temporary directory context. Pass a custom remover + to override the removal behavior. + """ + temp_dir = tempfile.mkdtemp() + try: + yield temp_dir + finally: + remover(temp_dir) + + +@contextlib.contextmanager +def repo_context(url, branch=None, quiet=True, dest_ctx=temp_dir): + """ + Check out the repo indicated by url. + + If dest_ctx is supplied, it should be a context manager + to yield the target directory for the check out. + """ + exe = 'git' if 'git' in url else 'hg' + with dest_ctx() as repo_dir: + cmd = [exe, 'clone', url, repo_dir] + if branch: + cmd.extend(['--branch', branch]) + devnull = open(os.path.devnull, 'w') + stdout = devnull if quiet else None + subprocess.check_call(cmd, stdout=stdout) + yield repo_dir + + +@contextlib.contextmanager +def null(): + yield + + +class ExceptionTrap: + """ + A context manager that will catch certain exceptions and provide an + indication they occurred. + + >>> with ExceptionTrap() as trap: + ... raise Exception() + >>> bool(trap) + True + + >>> with ExceptionTrap() as trap: + ... pass + >>> bool(trap) + False + + >>> with ExceptionTrap(ValueError) as trap: + ... raise ValueError("1 + 1 is not 3") + >>> bool(trap) + True + + >>> with ExceptionTrap(ValueError) as trap: + ... raise Exception() + Traceback (most recent call last): + ... + Exception + + >>> bool(trap) + False + """ + + exc_info = None, None, None + + def __init__(self, exceptions=(Exception,)): + self.exceptions = exceptions + + def __enter__(self): + return self + + @property + def type(self): + return self.exc_info[0] + + @property + def value(self): + return self.exc_info[1] + + @property + def tb(self): + return self.exc_info[2] + + def __exit__(self, *exc_info): + type = exc_info[0] + matches = type and issubclass(type, self.exceptions) + if matches: + self.exc_info = exc_info + return matches + + def __bool__(self): + return bool(self.type) + + def raises(self, func, *, _test=bool): + """ + Wrap func and replace the result with the truth + value of the trap (True if an exception occurred). + + First, give the decorator an alias to support Python 3.8 + Syntax. + + >>> raises = ExceptionTrap(ValueError).raises + + Now decorate a function that always fails. + + >>> @raises + ... def fail(): + ... raise ValueError('failed') + >>> fail() + True + """ + + @functools.wraps(func) + def wrapper(*args, **kwargs): + with ExceptionTrap(self.exceptions) as trap: + func(*args, **kwargs) + return _test(trap) + + return wrapper + + def passes(self, func): + """ + Wrap func and replace the result with the truth + value of the trap (True if no exception). + + First, give the decorator an alias to support Python 3.8 + Syntax. + + >>> passes = ExceptionTrap(ValueError).passes + + Now decorate a function that always fails. + + >>> @passes + ... def fail(): + ... raise ValueError('failed') + + >>> fail() + False + """ + return self.raises(func, _test=operator.not_) + + +class suppress(contextlib.suppress, contextlib.ContextDecorator): + """ + A version of contextlib.suppress with decorator support. + + >>> @suppress(KeyError) + ... def key_error(): + ... {}[''] + >>> key_error() + """ diff --git a/pkg_resources/_vendor/jaraco/functools.py b/pkg_resources/_vendor/jaraco/functools.py new file mode 100644 index 00000000..fcdbb4f9 --- /dev/null +++ b/pkg_resources/_vendor/jaraco/functools.py @@ -0,0 +1,525 @@ +import functools +import time +import inspect +import collections +import types +import itertools + +import more_itertools + +from typing import Callable, TypeVar + + +CallableT = TypeVar("CallableT", bound=Callable[..., object]) + + +def compose(*funcs): + """ + Compose any number of unary functions into a single unary function. + + >>> import textwrap + >>> expected = str.strip(textwrap.dedent(compose.__doc__)) + >>> strip_and_dedent = compose(str.strip, textwrap.dedent) + >>> strip_and_dedent(compose.__doc__) == expected + True + + Compose also allows the innermost function to take arbitrary arguments. + + >>> round_three = lambda x: round(x, ndigits=3) + >>> f = compose(round_three, int.__truediv__) + >>> [f(3*x, x+1) for x in range(1,10)] + [1.5, 2.0, 2.25, 2.4, 2.5, 2.571, 2.625, 2.667, 2.7] + """ + + def compose_two(f1, f2): + return lambda *args, **kwargs: f1(f2(*args, **kwargs)) + + return functools.reduce(compose_two, funcs) + + +def method_caller(method_name, *args, **kwargs): + """ + Return a function that will call a named method on the + target object with optional positional and keyword + arguments. + + >>> lower = method_caller('lower') + >>> lower('MyString') + 'mystring' + """ + + def call_method(target): + func = getattr(target, method_name) + return func(*args, **kwargs) + + return call_method + + +def once(func): + """ + Decorate func so it's only ever called the first time. + + This decorator can ensure that an expensive or non-idempotent function + will not be expensive on subsequent calls and is idempotent. + + >>> add_three = once(lambda a: a+3) + >>> add_three(3) + 6 + >>> add_three(9) + 6 + >>> add_three('12') + 6 + + To reset the stored value, simply clear the property ``saved_result``. + + >>> del add_three.saved_result + >>> add_three(9) + 12 + >>> add_three(8) + 12 + + Or invoke 'reset()' on it. + + >>> add_three.reset() + >>> add_three(-3) + 0 + >>> add_three(0) + 0 + """ + + @functools.wraps(func) + def wrapper(*args, **kwargs): + if not hasattr(wrapper, 'saved_result'): + wrapper.saved_result = func(*args, **kwargs) + return wrapper.saved_result + + wrapper.reset = lambda: vars(wrapper).__delitem__('saved_result') + return wrapper + + +def method_cache( + method: CallableT, + cache_wrapper: Callable[ + [CallableT], CallableT + ] = functools.lru_cache(), # type: ignore[assignment] +) -> CallableT: + """ + Wrap lru_cache to support storing the cache data in the object instances. + + Abstracts the common paradigm where the method explicitly saves an + underscore-prefixed protected property on first call and returns that + subsequently. + + >>> class MyClass: + ... calls = 0 + ... + ... @method_cache + ... def method(self, value): + ... self.calls += 1 + ... return value + + >>> a = MyClass() + >>> a.method(3) + 3 + >>> for x in range(75): + ... res = a.method(x) + >>> a.calls + 75 + + Note that the apparent behavior will be exactly like that of lru_cache + except that the cache is stored on each instance, so values in one + instance will not flush values from another, and when an instance is + deleted, so are the cached values for that instance. + + >>> b = MyClass() + >>> for x in range(35): + ... res = b.method(x) + >>> b.calls + 35 + >>> a.method(0) + 0 + >>> a.calls + 75 + + Note that if method had been decorated with ``functools.lru_cache()``, + a.calls would have been 76 (due to the cached value of 0 having been + flushed by the 'b' instance). + + Clear the cache with ``.cache_clear()`` + + >>> a.method.cache_clear() + + Same for a method that hasn't yet been called. + + >>> c = MyClass() + >>> c.method.cache_clear() + + Another cache wrapper may be supplied: + + >>> cache = functools.lru_cache(maxsize=2) + >>> MyClass.method2 = method_cache(lambda self: 3, cache_wrapper=cache) + >>> a = MyClass() + >>> a.method2() + 3 + + Caution - do not subsequently wrap the method with another decorator, such + as ``@property``, which changes the semantics of the function. + + See also + http://code.activestate.com/recipes/577452-a-memoize-decorator-for-instance-methods/ + for another implementation and additional justification. + """ + + def wrapper(self: object, *args: object, **kwargs: object) -> object: + # it's the first call, replace the method with a cached, bound method + bound_method: CallableT = types.MethodType( # type: ignore[assignment] + method, self + ) + cached_method = cache_wrapper(bound_method) + setattr(self, method.__name__, cached_method) + return cached_method(*args, **kwargs) + + # Support cache clear even before cache has been created. + wrapper.cache_clear = lambda: None # type: ignore[attr-defined] + + return ( # type: ignore[return-value] + _special_method_cache(method, cache_wrapper) or wrapper + ) + + +def _special_method_cache(method, cache_wrapper): + """ + Because Python treats special methods differently, it's not + possible to use instance attributes to implement the cached + methods. + + Instead, install the wrapper method under a different name + and return a simple proxy to that wrapper. + + https://github.com/jaraco/jaraco.functools/issues/5 + """ + name = method.__name__ + special_names = '__getattr__', '__getitem__' + if name not in special_names: + return + + wrapper_name = '__cached' + name + + def proxy(self, *args, **kwargs): + if wrapper_name not in vars(self): + bound = types.MethodType(method, self) + cache = cache_wrapper(bound) + setattr(self, wrapper_name, cache) + else: + cache = getattr(self, wrapper_name) + return cache(*args, **kwargs) + + return proxy + + +def apply(transform): + """ + Decorate a function with a transform function that is + invoked on results returned from the decorated function. + + >>> @apply(reversed) + ... def get_numbers(start): + ... "doc for get_numbers" + ... return range(start, start+3) + >>> list(get_numbers(4)) + [6, 5, 4] + >>> get_numbers.__doc__ + 'doc for get_numbers' + """ + + def wrap(func): + return functools.wraps(func)(compose(transform, func)) + + return wrap + + +def result_invoke(action): + r""" + Decorate a function with an action function that is + invoked on the results returned from the decorated + function (for its side-effect), then return the original + result. + + >>> @result_invoke(print) + ... def add_two(a, b): + ... return a + b + >>> x = add_two(2, 3) + 5 + >>> x + 5 + """ + + def wrap(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + action(result) + return result + + return wrapper + + return wrap + + +def call_aside(f, *args, **kwargs): + """ + Call a function for its side effect after initialization. + + >>> @call_aside + ... def func(): print("called") + called + >>> func() + called + + Use functools.partial to pass parameters to the initial call + + >>> @functools.partial(call_aside, name='bingo') + ... def func(name): print("called with", name) + called with bingo + """ + f(*args, **kwargs) + return f + + +class Throttler: + """ + Rate-limit a function (or other callable) + """ + + def __init__(self, func, max_rate=float('Inf')): + if isinstance(func, Throttler): + func = func.func + self.func = func + self.max_rate = max_rate + self.reset() + + def reset(self): + self.last_called = 0 + + def __call__(self, *args, **kwargs): + self._wait() + return self.func(*args, **kwargs) + + def _wait(self): + "ensure at least 1/max_rate seconds from last call" + elapsed = time.time() - self.last_called + must_wait = 1 / self.max_rate - elapsed + time.sleep(max(0, must_wait)) + self.last_called = time.time() + + def __get__(self, obj, type=None): + return first_invoke(self._wait, functools.partial(self.func, obj)) + + +def first_invoke(func1, func2): + """ + Return a function that when invoked will invoke func1 without + any parameters (for its side-effect) and then invoke func2 + with whatever parameters were passed, returning its result. + """ + + def wrapper(*args, **kwargs): + func1() + return func2(*args, **kwargs) + + return wrapper + + +def retry_call(func, cleanup=lambda: None, retries=0, trap=()): + """ + Given a callable func, trap the indicated exceptions + for up to 'retries' times, invoking cleanup on the + exception. On the final attempt, allow any exceptions + to propagate. + """ + attempts = itertools.count() if retries == float('inf') else range(retries) + for attempt in attempts: + try: + return func() + except trap: + cleanup() + + return func() + + +def retry(*r_args, **r_kwargs): + """ + Decorator wrapper for retry_call. Accepts arguments to retry_call + except func and then returns a decorator for the decorated function. + + Ex: + + >>> @retry(retries=3) + ... def my_func(a, b): + ... "this is my funk" + ... print(a, b) + >>> my_func.__doc__ + 'this is my funk' + """ + + def decorate(func): + @functools.wraps(func) + def wrapper(*f_args, **f_kwargs): + bound = functools.partial(func, *f_args, **f_kwargs) + return retry_call(bound, *r_args, **r_kwargs) + + return wrapper + + return decorate + + +def print_yielded(func): + """ + Convert a generator into a function that prints all yielded elements + + >>> @print_yielded + ... def x(): + ... yield 3; yield None + >>> x() + 3 + None + """ + print_all = functools.partial(map, print) + print_results = compose(more_itertools.consume, print_all, func) + return functools.wraps(func)(print_results) + + +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 + + +def assign_params(func, namespace): + """ + Assign parameters from namespace where func solicits. + + >>> def func(x, y=3): + ... print(x, y) + >>> assigned = assign_params(func, dict(x=2, z=4)) + >>> assigned() + 2 3 + + The usual errors are raised if a function doesn't receive + its required parameters: + + >>> assigned = assign_params(func, dict(y=3, z=4)) + >>> assigned() + Traceback (most recent call last): + TypeError: func() ...argument... + + It even works on methods: + + >>> class Handler: + ... def meth(self, arg): + ... print(arg) + >>> assign_params(Handler().meth, dict(arg='crystal', foo='clear'))() + crystal + """ + sig = inspect.signature(func) + params = sig.parameters.keys() + call_ns = {k: namespace[k] for k in params if k in namespace} + return functools.partial(func, **call_ns) + + +def save_method_args(method): + """ + Wrap a method such that when it is called, the args and kwargs are + saved on the method. + + >>> class MyClass: + ... @save_method_args + ... def method(self, a, b): + ... print(a, b) + >>> my_ob = MyClass() + >>> my_ob.method(1, 2) + 1 2 + >>> my_ob._saved_method.args + (1, 2) + >>> my_ob._saved_method.kwargs + {} + >>> my_ob.method(a=3, b='foo') + 3 foo + >>> my_ob._saved_method.args + () + >>> my_ob._saved_method.kwargs == dict(a=3, b='foo') + True + + The arguments are stored on the instance, allowing for + different instance to save different args. + + >>> your_ob = MyClass() + >>> your_ob.method({str('x'): 3}, b=[4]) + {'x': 3} [4] + >>> your_ob._saved_method.args + ({'x': 3},) + >>> my_ob._saved_method.args + () + """ + args_and_kwargs = collections.namedtuple('args_and_kwargs', 'args kwargs') + + @functools.wraps(method) + def wrapper(self, *args, **kwargs): + attr_name = '_saved_' + method.__name__ + attr = args_and_kwargs(args, kwargs) + setattr(self, attr_name, attr) + return method(self, *args, **kwargs) + + return wrapper + + +def except_(*exceptions, replace=None, use=None): + """ + Replace the indicated exceptions, if raised, with the indicated + literal replacement or evaluated expression (if present). + + >>> safe_int = except_(ValueError)(int) + >>> safe_int('five') + >>> safe_int('5') + 5 + + Specify a literal replacement with ``replace``. + + >>> safe_int_r = except_(ValueError, replace=0)(int) + >>> safe_int_r('five') + 0 + + Provide an expression to ``use`` to pass through particular parameters. + + >>> safe_int_pt = except_(ValueError, use='args[0]')(int) + >>> safe_int_pt('five') + 'five' + + """ + + def decorate(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except exceptions: + try: + return eval(use) + except TypeError: + return replace + + return wrapper + + return decorate diff --git a/pkg_resources/_vendor/jaraco/text/Lorem ipsum.txt b/pkg_resources/_vendor/jaraco/text/Lorem ipsum.txt new file mode 100644 index 00000000..986f944b --- /dev/null +++ b/pkg_resources/_vendor/jaraco/text/Lorem ipsum.txt @@ -0,0 +1,2 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra, est eros bibendum elit, nec luctus magna felis sollicitudin mauris. Integer in mauris eu nibh euismod gravida. Duis ac tellus et risus vulputate vehicula. Donec lobortis risus a elit. Etiam tempor. Ut ullamcorper, ligula eu tempor congue, eros est euismod turpis, id tincidunt sapien risus a quam. Maecenas fermentum consequat mi. Donec fermentum. Pellentesque malesuada nulla a mi. Duis sapien sem, aliquet nec, commodo eget, consequat quis, neque. Aliquam faucibus, elit ut dictum aliquet, felis nisl adipiscing sapien, sed malesuada diam lacus eget erat. Cras mollis scelerisque nunc. Nullam arcu. Aliquam consequat. Curabitur augue lorem, dapibus quis, laoreet et, pretium ac, nisi. Aenean magna nisl, mollis quis, molestie eu, feugiat in, orci. In hac habitasse platea dictumst. diff --git a/pkg_resources/_vendor/jaraco/text/__init__.py b/pkg_resources/_vendor/jaraco/text/__init__.py new file mode 100644 index 00000000..5f75519a --- /dev/null +++ b/pkg_resources/_vendor/jaraco/text/__init__.py @@ -0,0 +1,600 @@ +import re +import itertools +import textwrap +import functools + +try: + from importlib.resources import files # type: ignore +except ImportError: # pragma: nocover + from importlib_resources import files # type: ignore + +from jaraco.functools import compose, method_cache +from jaraco.context import ExceptionTrap + + +def substitution(old, new): + """ + Return a function that will perform a substitution on a string + """ + return lambda s: s.replace(old, new) + + +def multi_substitution(*substitutions): + """ + Take a sequence of pairs specifying substitutions, and create + a function that performs those substitutions. + + >>> multi_substitution(('foo', 'bar'), ('bar', 'baz'))('foo') + 'baz' + """ + substitutions = itertools.starmap(substitution, substitutions) + # compose function applies last function first, so reverse the + # substitutions to get the expected order. + substitutions = reversed(tuple(substitutions)) + return compose(*substitutions) + + +class FoldedCase(str): + """ + A case insensitive string class; behaves just like str + except compares equal when the only variation is case. + + >>> s = FoldedCase('hello world') + + >>> s == 'Hello World' + True + + >>> 'Hello World' == s + True + + >>> s != 'Hello World' + False + + >>> s.index('O') + 4 + + >>> s.split('O') + ['hell', ' w', 'rld'] + + >>> sorted(map(FoldedCase, ['GAMMA', 'alpha', 'Beta'])) + ['alpha', 'Beta', 'GAMMA'] + + Sequence membership is straightforward. + + >>> "Hello World" in [s] + True + >>> s in ["Hello World"] + True + + You may test for set inclusion, but candidate and elements + must both be folded. + + >>> FoldedCase("Hello World") in {s} + True + >>> s in {FoldedCase("Hello World")} + True + + String inclusion works as long as the FoldedCase object + is on the right. + + >>> "hello" in FoldedCase("Hello World") + True + + But not if the FoldedCase object is on the left: + + >>> FoldedCase('hello') in 'Hello World' + False + + In that case, use ``in_``: + + >>> FoldedCase('hello').in_('Hello World') + True + + >>> FoldedCase('hello') > FoldedCase('Hello') + False + """ + + def __lt__(self, other): + return self.lower() < other.lower() + + def __gt__(self, other): + return self.lower() > other.lower() + + def __eq__(self, other): + return self.lower() == other.lower() + + def __ne__(self, other): + return self.lower() != other.lower() + + def __hash__(self): + return hash(self.lower()) + + def __contains__(self, other): + return super().lower().__contains__(other.lower()) + + def in_(self, other): + "Does self appear in other?" + return self in FoldedCase(other) + + # cache lower since it's likely to be called frequently. + @method_cache + def lower(self): + return super().lower() + + def index(self, sub): + return self.lower().index(sub.lower()) + + def split(self, splitter=' ', maxsplit=0): + pattern = re.compile(re.escape(splitter), re.I) + return pattern.split(self, maxsplit) + + +# Python 3.8 compatibility +_unicode_trap = ExceptionTrap(UnicodeDecodeError) + + +@_unicode_trap.passes +def is_decodable(value): + r""" + Return True if the supplied value is decodable (using the default + encoding). + + >>> is_decodable(b'\xff') + False + >>> is_decodable(b'\x32') + True + """ + value.decode() + + +def is_binary(value): + r""" + Return True if the value appears to be binary (that is, it's a byte + string and isn't decodable). + + >>> is_binary(b'\xff') + True + >>> is_binary('\xff') + False + """ + return isinstance(value, bytes) and not is_decodable(value) + + +def trim(s): + r""" + Trim something like a docstring to remove the whitespace that + is common due to indentation and formatting. + + >>> trim("\n\tfoo = bar\n\t\tbar = baz\n") + 'foo = bar\n\tbar = baz' + """ + return textwrap.dedent(s).strip() + + +def wrap(s): + """ + Wrap lines of text, retaining existing newlines as + paragraph markers. + + >>> print(wrap(lorem_ipsum)) + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad + minim veniam, quis nostrud exercitation ullamco laboris nisi ut + aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in + culpa qui officia deserunt mollit anim id est laborum. + + Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam + varius, turpis et commodo pharetra, est eros bibendum elit, nec luctus + magna felis sollicitudin mauris. Integer in mauris eu nibh euismod + gravida. Duis ac tellus et risus vulputate vehicula. Donec lobortis + risus a elit. Etiam tempor. Ut ullamcorper, ligula eu tempor congue, + eros est euismod turpis, id tincidunt sapien risus a quam. Maecenas + fermentum consequat mi. Donec fermentum. Pellentesque malesuada nulla + a mi. Duis sapien sem, aliquet nec, commodo eget, consequat quis, + neque. Aliquam faucibus, elit ut dictum aliquet, felis nisl adipiscing + sapien, sed malesuada diam lacus eget erat. Cras mollis scelerisque + nunc. Nullam arcu. Aliquam consequat. Curabitur augue lorem, dapibus + quis, laoreet et, pretium ac, nisi. Aenean magna nisl, mollis quis, + molestie eu, feugiat in, orci. In hac habitasse platea dictumst. + """ + paragraphs = s.splitlines() + wrapped = ('\n'.join(textwrap.wrap(para)) for para in paragraphs) + return '\n\n'.join(wrapped) + + +def unwrap(s): + r""" + Given a multi-line string, return an unwrapped version. + + >>> wrapped = wrap(lorem_ipsum) + >>> wrapped.count('\n') + 20 + >>> unwrapped = unwrap(wrapped) + >>> unwrapped.count('\n') + 1 + >>> print(unwrapped) + Lorem ipsum dolor sit amet, consectetur adipiscing ... + Curabitur pretium tincidunt lacus. Nulla gravida orci ... + + """ + paragraphs = re.split(r'\n\n+', s) + cleaned = (para.replace('\n', ' ') for para in paragraphs) + return '\n'.join(cleaned) + + +lorem_ipsum: str = files(__name__).joinpath('Lorem ipsum.txt').read_text() + + +class Splitter(object): + """object that will split a string with the given arguments for each call + + >>> s = Splitter(',') + >>> s('hello, world, this is your, master calling') + ['hello', ' world', ' this is your', ' master calling'] + """ + + def __init__(self, *args): + self.args = args + + def __call__(self, s): + return s.split(*self.args) + + +def indent(string, prefix=' ' * 4): + """ + >>> indent('foo') + ' foo' + """ + return prefix + string + + +class WordSet(tuple): + """ + Given an identifier, return the words that identifier represents, + whether in camel case, underscore-separated, etc. + + >>> WordSet.parse("camelCase") + ('camel', 'Case') + + >>> WordSet.parse("under_sep") + ('under', 'sep') + + Acronyms should be retained + + >>> WordSet.parse("firstSNL") + ('first', 'SNL') + + >>> WordSet.parse("you_and_I") + ('you', 'and', 'I') + + >>> WordSet.parse("A simple test") + ('A', 'simple', 'test') + + Multiple caps should not interfere with the first cap of another word. + + >>> WordSet.parse("myABCClass") + ('my', 'ABC', 'Class') + + The result is a WordSet, so you can get the form you need. + + >>> WordSet.parse("myABCClass").underscore_separated() + 'my_ABC_Class' + + >>> WordSet.parse('a-command').camel_case() + 'ACommand' + + >>> WordSet.parse('someIdentifier').lowered().space_separated() + 'some identifier' + + Slices of the result should return another WordSet. + + >>> WordSet.parse('taken-out-of-context')[1:].underscore_separated() + 'out_of_context' + + >>> WordSet.from_class_name(WordSet()).lowered().space_separated() + 'word set' + + >>> example = WordSet.parse('figured it out') + >>> example.headless_camel_case() + 'figuredItOut' + >>> example.dash_separated() + 'figured-it-out' + + """ + + _pattern = re.compile('([A-Z]?[a-z]+)|([A-Z]+(?![a-z]))') + + def capitalized(self): + return WordSet(word.capitalize() for word in self) + + def lowered(self): + return WordSet(word.lower() for word in self) + + def camel_case(self): + return ''.join(self.capitalized()) + + def headless_camel_case(self): + words = iter(self) + first = next(words).lower() + new_words = itertools.chain((first,), WordSet(words).camel_case()) + return ''.join(new_words) + + def underscore_separated(self): + return '_'.join(self) + + def dash_separated(self): + return '-'.join(self) + + def space_separated(self): + return ' '.join(self) + + def trim_right(self, item): + """ + Remove the item from the end of the set. + + >>> WordSet.parse('foo bar').trim_right('foo') + ('foo', 'bar') + >>> WordSet.parse('foo bar').trim_right('bar') + ('foo',) + >>> WordSet.parse('').trim_right('bar') + () + """ + return self[:-1] if self and self[-1] == item else self + + def trim_left(self, item): + """ + Remove the item from the beginning of the set. + + >>> WordSet.parse('foo bar').trim_left('foo') + ('bar',) + >>> WordSet.parse('foo bar').trim_left('bar') + ('foo', 'bar') + >>> WordSet.parse('').trim_left('bar') + () + """ + return self[1:] if self and self[0] == item else self + + def trim(self, item): + """ + >>> WordSet.parse('foo bar').trim('foo') + ('bar',) + """ + return self.trim_left(item).trim_right(item) + + def __getitem__(self, item): + result = super(WordSet, self).__getitem__(item) + if isinstance(item, slice): + result = WordSet(result) + return result + + @classmethod + def parse(cls, identifier): + matches = cls._pattern.finditer(identifier) + return WordSet(match.group(0) for match in matches) + + @classmethod + def from_class_name(cls, subject): + return cls.parse(subject.__class__.__name__) + + +# for backward compatibility +words = WordSet.parse + + +def simple_html_strip(s): + r""" + Remove HTML from the string `s`. + + >>> str(simple_html_strip('')) + '' + + >>> print(simple_html_strip('A stormy day in paradise')) + A stormy day in paradise + + >>> print(simple_html_strip('Somebody tell the truth.')) + Somebody tell the truth. + + >>> print(simple_html_strip('What about
\nmultiple lines?')) + What about + multiple lines? + """ + html_stripper = re.compile('()|(<[^>]*>)|([^<]+)', re.DOTALL) + texts = (match.group(3) or '' for match in html_stripper.finditer(s)) + return ''.join(texts) + + +class SeparatedValues(str): + """ + A string separated by a separator. Overrides __iter__ for getting + the values. + + >>> list(SeparatedValues('a,b,c')) + ['a', 'b', 'c'] + + Whitespace is stripped and empty values are discarded. + + >>> list(SeparatedValues(' a, b , c, ')) + ['a', 'b', 'c'] + """ + + separator = ',' + + def __iter__(self): + parts = self.split(self.separator) + return filter(None, (part.strip() for part in parts)) + + +class Stripper: + r""" + Given a series of lines, find the common prefix and strip it from them. + + >>> lines = [ + ... 'abcdefg\n', + ... 'abc\n', + ... 'abcde\n', + ... ] + >>> res = Stripper.strip_prefix(lines) + >>> res.prefix + 'abc' + >>> list(res.lines) + ['defg\n', '\n', 'de\n'] + + If no prefix is common, nothing should be stripped. + + >>> lines = [ + ... 'abcd\n', + ... '1234\n', + ... ] + >>> res = Stripper.strip_prefix(lines) + >>> res.prefix = '' + >>> list(res.lines) + ['abcd\n', '1234\n'] + """ + + def __init__(self, prefix, lines): + self.prefix = prefix + self.lines = map(self, lines) + + @classmethod + def strip_prefix(cls, lines): + prefix_lines, lines = itertools.tee(lines) + prefix = functools.reduce(cls.common_prefix, prefix_lines) + return cls(prefix, lines) + + def __call__(self, line): + if not self.prefix: + return line + null, prefix, rest = line.partition(self.prefix) + return rest + + @staticmethod + def common_prefix(s1, s2): + """ + Return the common prefix of two lines. + """ + index = min(len(s1), len(s2)) + while s1[:index] != s2[:index]: + index -= 1 + return s1[:index] + + +def remove_prefix(text, prefix): + """ + Remove the prefix from the text if it exists. + + >>> remove_prefix('underwhelming performance', 'underwhelming ') + 'performance' + + >>> remove_prefix('something special', 'sample') + 'something special' + """ + null, prefix, rest = text.rpartition(prefix) + return rest + + +def remove_suffix(text, suffix): + """ + Remove the suffix from the text if it exists. + + >>> remove_suffix('name.git', '.git') + 'name' + + >>> remove_suffix('something special', 'sample') + 'something special' + """ + rest, suffix, null = text.partition(suffix) + return rest + + +def normalize_newlines(text): + r""" + Replace alternate newlines with the canonical newline. + + >>> normalize_newlines('Lorem Ipsum\u2029') + 'Lorem Ipsum\n' + >>> normalize_newlines('Lorem Ipsum\r\n') + 'Lorem Ipsum\n' + >>> normalize_newlines('Lorem Ipsum\x85') + 'Lorem Ipsum\n' + """ + newlines = ['\r\n', '\r', '\n', '\u0085', '\u2028', '\u2029'] + pattern = '|'.join(newlines) + return re.sub(pattern, '\n', text) + + +def _nonblank(str): + return str and not str.startswith('#') + + +@functools.singledispatch +def yield_lines(iterable): + r""" + Yield valid lines of a string or iterable. + + >>> list(yield_lines('')) + [] + >>> list(yield_lines(['foo', 'bar'])) + ['foo', 'bar'] + >>> list(yield_lines('foo\nbar')) + ['foo', 'bar'] + >>> list(yield_lines('\nfoo\n#bar\nbaz #comment')) + ['foo', 'baz #comment'] + >>> list(yield_lines(['foo\nbar', 'baz', 'bing\n\n\n'])) + ['foo', 'bar', 'baz', 'bing'] + """ + return itertools.chain.from_iterable(map(yield_lines, iterable)) + + +@yield_lines.register(str) +def _(text): + return filter(_nonblank, map(str.strip, text.splitlines())) + + +def drop_comment(line): + """ + Drop comments. + + >>> drop_comment('foo # bar') + 'foo' + + A hash without a space may be in a URL. + + >>> drop_comment('http://example.com/foo#bar') + 'http://example.com/foo#bar' + """ + return line.partition(' #')[0] + + +def join_continuation(lines): + r""" + Join lines continued by a trailing backslash. + + >>> list(join_continuation(['foo \\', 'bar', 'baz'])) + ['foobar', 'baz'] + >>> list(join_continuation(['foo \\', 'bar', 'baz'])) + ['foobar', 'baz'] + >>> list(join_continuation(['foo \\', 'bar \\', 'baz'])) + ['foobarbaz'] + + Not sure why, but... + The character preceeding the backslash is also elided. + + >>> list(join_continuation(['goo\\', 'dly'])) + ['godly'] + + A terrible idea, but... + If no line is available to continue, suppress the lines. + + >>> list(join_continuation(['foo', 'bar\\', 'baz\\'])) + ['foo'] + """ + lines = iter(lines) + for item in lines: + while item.endswith('\\'): + try: + item = item[:-2].strip() + next(lines) + except StopIteration: + return + yield item diff --git a/pkg_resources/_vendor/more_itertools-8.12.0.dist-info/INSTALLER b/pkg_resources/_vendor/more_itertools-8.12.0.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/pkg_resources/_vendor/more_itertools-8.12.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/pkg_resources/_vendor/more_itertools-8.12.0.dist-info/LICENSE b/pkg_resources/_vendor/more_itertools-8.12.0.dist-info/LICENSE new file mode 100644 index 00000000..0a523bec --- /dev/null +++ b/pkg_resources/_vendor/more_itertools-8.12.0.dist-info/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2012 Erik Rose + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pkg_resources/_vendor/more_itertools-8.12.0.dist-info/METADATA b/pkg_resources/_vendor/more_itertools-8.12.0.dist-info/METADATA new file mode 100644 index 00000000..9efacdd7 --- /dev/null +++ b/pkg_resources/_vendor/more_itertools-8.12.0.dist-info/METADATA @@ -0,0 +1,521 @@ +Metadata-Version: 2.1 +Name: more-itertools +Version: 8.12.0 +Summary: More routines for operating on iterables, beyond itertools +Home-page: https://github.com/more-itertools/more-itertools +Author: Erik Rose +Author-email: erikrose@grinchcentral.com +License: MIT +Keywords: itertools,iterator,iteration,filter,peek,peekable,collate,chunk,chunked +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Natural Language :: English +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Software Development :: Libraries +Requires-Python: >=3.5 +Description-Content-Type: text/x-rst +License-File: LICENSE + +============== +More Itertools +============== + +.. image:: https://readthedocs.org/projects/more-itertools/badge/?version=latest + :target: https://more-itertools.readthedocs.io/en/stable/ + +Python's ``itertools`` library is a gem - you can compose elegant solutions +for a variety of problems with the functions it provides. In ``more-itertools`` +we collect additional building blocks, recipes, and routines for working with +Python iterables. + ++------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Grouping | `chunked `_, | +| | `ichunked `_, | +| | `sliced `_, | +| | `distribute `_, | +| | `divide `_, | +| | `split_at `_, | +| | `split_before `_, | +| | `split_after `_, | +| | `split_into `_, | +| | `split_when `_, | +| | `bucket `_, | +| | `unzip `_, | +| | `grouper `_, | +| | `partition `_ | ++------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Lookahead and lookback | `spy `_, | +| | `peekable `_, | +| | `seekable `_ | ++------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Windowing | `windowed `_, | +| | `substrings `_, | +| | `substrings_indexes `_, | +| | `stagger `_, | +| | `windowed_complete `_, | +| | `pairwise `_, | +| | `triplewise `_, | +| | `sliding_window `_ | ++------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Augmenting | `count_cycle `_, | +| | `intersperse `_, | +| | `padded `_, | +| | `mark_ends `_, | +| | `repeat_last `_, | +| | `adjacent `_, | +| | `groupby_transform `_, | +| | `pad_none `_, | +| | `ncycles `_ | ++------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Combining | `collapse `_, | +| | `sort_together `_, | +| | `interleave `_, | +| | `interleave_longest `_, | +| | `interleave_evenly `_, | +| | `zip_offset `_, | +| | `zip_equal `_, | +| | `zip_broadcast `_, | +| | `dotproduct `_, | +| | `convolve `_, | +| | `flatten `_, | +| | `roundrobin `_, | +| | `prepend `_, | +| | `value_chain `_ | ++------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Summarizing | `ilen `_, | +| | `unique_to_each `_, | +| | `sample `_, | +| | `consecutive_groups `_, | +| | `run_length `_, | +| | `map_reduce `_, | +| | `exactly_n `_, | +| | `is_sorted `_, | +| | `all_equal `_, | +| | `all_unique `_, | +| | `minmax `_, | +| | `first_true `_, | +| | `quantify `_ | ++------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Selecting | `islice_extended `_, | +| | `first `_, | +| | `last `_, | +| | `one `_, | +| | `only `_, | +| | `strictly_n `_, | +| | `strip `_, | +| | `lstrip `_, | +| | `rstrip `_, | +| | `filter_except `_, | +| | `map_except `_, | +| | `nth_or_last `_, | +| | `unique_in_window `_, | +| | `before_and_after `_, | +| | `nth `_, | +| | `take `_, | +| | `tail `_, | +| | `unique_everseen `_, | +| | `unique_justseen `_, | +| | `duplicates_everseen `_, | +| | `duplicates_justseen `_ | ++------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Combinatorics | `distinct_permutations `_, | +| | `distinct_combinations `_, | +| | `circular_shifts `_, | +| | `partitions `_, | +| | `set_partitions `_, | +| | `product_index `_, | +| | `combination_index `_, | +| | `permutation_index `_, | +| | `powerset `_, | +| | `random_product `_, | +| | `random_permutation `_, | +| | `random_combination `_, | +| | `random_combination_with_replacement `_, | +| | `nth_product `_, | +| | `nth_permutation `_, | +| | `nth_combination `_ | ++------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Wrapping | `always_iterable `_, | +| | `always_reversible `_, | +| | `countable `_, | +| | `consumer `_, | +| | `with_iter `_, | +| | `iter_except `_ | ++------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Others | `locate `_, | +| | `rlocate `_, | +| | `replace `_, | +| | `numeric_range `_, | +| | `side_effect `_, | +| | `iterate `_, | +| | `difference `_, | +| | `make_decorator `_, | +| | `SequenceView `_, | +| | `time_limited `_, | +| | `consume `_, | +| | `tabulate `_, | +| | `repeatfunc `_ | ++------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + + +Getting started +=============== + +To get started, install the library with `pip `_: + +.. code-block:: shell + + pip install more-itertools + +The recipes from the `itertools docs `_ +are included in the top-level package: + +.. code-block:: python + + >>> from more_itertools import flatten + >>> iterable = [(0, 1), (2, 3)] + >>> list(flatten(iterable)) + [0, 1, 2, 3] + +Several new recipes are available as well: + +.. code-block:: python + + >>> from more_itertools import chunked + >>> iterable = [0, 1, 2, 3, 4, 5, 6, 7, 8] + >>> list(chunked(iterable, 3)) + [[0, 1, 2], [3, 4, 5], [6, 7, 8]] + + >>> from more_itertools import spy + >>> iterable = (x * x for x in range(1, 6)) + >>> head, iterable = spy(iterable, n=3) + >>> list(head) + [1, 4, 9] + >>> list(iterable) + [1, 4, 9, 16, 25] + + + +For the full listing of functions, see the `API documentation `_. + + +Links elsewhere +=============== + +Blog posts about ``more-itertools``: + +* `Yo, I heard you like decorators `__ +* `Tour of Python Itertools `__ (`Alternate `__) +* `Real-World Python More Itertools `_ + + +Development +=========== + +``more-itertools`` is maintained by `@erikrose `_ +and `@bbayles `_, with help from `many others `_. +If you have a problem or suggestion, please file a bug or pull request in this +repository. Thanks for contributing! + + +Version History +=============== + + + :noindex: + +8.12.0 +------ + +* Bug fixes + * Some documentation issues were fixed (thanks to Masynchin, spookylukey, astrojuanlu, and stephengmatthews) + * Python 3.5 support was temporarily restored (thanks to mattbonnell) + +8.11.0 +------ + +* New functions + * The before_and_after, sliding_window, and triplewise recipes from the Python 3.10 docs were added + * duplicates_everseen and duplicates_justseen (thanks to OrBin and DavidPratt512) + * minmax (thanks to Ricocotam, MSeifert04, and ruancomelli) + * strictly_n (thanks to hwalinga and NotWearingPants) + * unique_in_window + +* Changes to existing functions + * groupby_transform had its type stub improved (thanks to mjk4 and ruancomelli) + * is_sorted now accepts a ``strict`` parameter (thanks to Dutcho and ruancomelli) + * zip_broadcast was updated to fix a bug (thanks to kalekundert) + +8.10.0 +------ + +* Changes to existing functions + * The type stub for iter_except was improved (thanks to MarcinKonowalczyk) + +* Other changes: + * Type stubs now ship with the source release (thanks to saaketp) + * The Sphinx docs were improved (thanks to MarcinKonowalczyk) + +8.9.0 +----- + +* New functions + * interleave_evenly (thanks to mbugert) + * repeat_each (thanks to FinalSh4re) + * chunked_even (thanks to valtron) + * map_if (thanks to sassbalint) + * zip_broadcast (thanks to kalekundert) + +* Changes to existing functions + * The type stub for chunked was improved (thanks to PhilMacKay) + * The type stubs for zip_equal and `zip_offset` were improved (thanks to maffoo) + * Building Sphinx docs locally was improved (thanks to MarcinKonowalczyk) + +8.8.0 +----- + +* New functions + * countable (thanks to krzysieq) + +* Changes to existing functions + * split_before was updated to handle empy collections (thanks to TiunovNN) + * unique_everseen got a performance boost (thanks to Numerlor) + * The type hint for value_chain was corrected (thanks to vr2262) + +8.7.0 +----- + +* New functions + * convolve (from the Python itertools docs) + * product_index, combination_index, and permutation_index (thanks to N8Brooks) + * value_chain (thanks to jenstroeger) + +* Changes to existing functions + * distinct_combinations now uses a non-recursive algorithm (thanks to knutdrand) + * pad_none is now the preferred name for padnone, though the latter remains available. + * pairwise will now use the Python standard library implementation on Python 3.10+ + * sort_together now accepts a ``key`` argument (thanks to brianmaissy) + * seekable now has a ``peek`` method, and can indicate whether the iterator it's wrapping is exhausted (thanks to gsakkis) + * time_limited can now indicate whether its iterator has expired (thanks to roysmith) + * The implementation of unique_everseen was improved (thanks to plammens) + +* Other changes: + * Various documentation updates (thanks to cthoyt, Evantm, and cyphase) + +8.6.0 +----- + +* New itertools + * all_unique (thanks to brianmaissy) + * nth_product and nth_permutation (thanks to N8Brooks) + +* Changes to existing itertools + * chunked and sliced now accept a ``strict`` parameter (thanks to shlomif and jtwool) + +* Other changes + * Python 3.5 has reached its end of life and is no longer supported. + * Python 3.9 is officially supported. + * Various documentation fixes (thanks to timgates42) + +8.5.0 +----- + +* New itertools + * windowed_complete (thanks to MarcinKonowalczyk) + +* Changes to existing itertools: + * The is_sorted implementation was improved (thanks to cool-RR) + * The groupby_transform now accepts a ``reducefunc`` parameter. + * The last implementation was improved (thanks to brianmaissy) + +* Other changes + * Various documentation fixes (thanks to craigrosie, samuelstjean, PiCT0) + * The tests for distinct_combinations were improved (thanks to Minabsapi) + * Automated tests now run on GitHub Actions. All commits now check: + * That unit tests pass + * That the examples in docstrings work + * That test coverage remains high (using `coverage`) + * For linting errors (using `flake8`) + * For consistent style (using `black`) + * That the type stubs work (using `mypy`) + * That the docs build correctly (using `sphinx`) + * That packages build correctly (using `twine`) + +8.4.0 +----- + +* New itertools + * mark_ends (thanks to kalekundert) + * is_sorted + +* Changes to existing itertools: + * islice_extended can now be used with real slices (thanks to cool-RR) + * The implementations for filter_except and map_except were improved (thanks to SergBobrovsky) + +* Other changes + * Automated tests now enforce code style (using `black `__) + * The various signatures of islice_extended and numeric_range now appear in the docs (thanks to dsfulf) + * The test configuration for mypy was updated (thanks to blueyed) + + +8.3.0 +----- + +* New itertools + * zip_equal (thanks to frankier and alexmojaki) + +* Changes to existing itertools: + * split_at, split_before, split_after, and split_when all got a ``maxsplit`` paramter (thanks to jferard and ilai-deutel) + * split_at now accepts a ``keep_separator`` parameter (thanks to jferard) + * distinct_permutations can now generate ``r``-length permutations (thanks to SergBobrovsky and ilai-deutel) + * The windowed implementation was improved (thanks to SergBobrovsky) + * The spy implementation was improved (thanks to has2k1) + +* Other changes + * Type stubs are now tested with ``stubtest`` (thanks to ilai-deutel) + * Tests now run with ``python -m unittest`` instead of ``python setup.py test`` (thanks to jdufresne) + +8.2.0 +----- + +* Bug fixes + * The .pyi files for typing were updated. (thanks to blueyed and ilai-deutel) + +* Changes to existing itertools: + * numeric_range now behaves more like the built-in range. (thanks to jferard) + * bucket now allows for enumerating keys. (thanks to alexchandel) + * sliced now should now work for numpy arrays. (thanks to sswingle) + * seekable now has a ``maxlen`` parameter. + +8.1.0 +----- + +* Bug fixes + * partition works with ``pred=None`` again. (thanks to MSeifert04) + +* New itertools + * sample (thanks to tommyod) + * nth_or_last (thanks to d-ryzhikov) + +* Changes to existing itertools: + * The implementation for divide was improved. (thanks to jferard) + +8.0.2 +----- + +* Bug fixes + * The type stub files are now part of the wheel distribution (thanks to keisheiled) + +8.0.1 +----- + +* Bug fixes + * The type stub files now work for functions imported from the + root package (thanks to keisheiled) + +8.0.0 +----- + +* New itertools and other additions + * This library now ships type hints for use with mypy. + (thanks to ilai-deutel for the implementation, and to gabbard and fmagin for assistance) + * split_when (thanks to jferard) + * repeat_last (thanks to d-ryzhikov) + +* Changes to existing itertools: + * The implementation for set_partitions was improved. (thanks to jferard) + * partition was optimized for expensive predicates. (thanks to stevecj) + * unique_everseen and groupby_transform were re-factored. (thanks to SergBobrovsky) + * The implementation for difference was improved. (thanks to Jabbey92) + +* Other changes + * Python 3.4 has reached its end of life and is no longer supported. + * Python 3.8 is officially supported. (thanks to jdufresne) + * The ``collate`` function has been deprecated. + It raises a ``DeprecationWarning`` if used, and will be removed in a future release. + * one and only now provide more informative error messages. (thanks to gabbard) + * Unit tests were moved outside of the main package (thanks to jdufresne) + * Various documentation fixes (thanks to kriomant, gabbard, jdufresne) + + +7.2.0 +----- + +* New itertools + * distinct_combinations + * set_partitions (thanks to kbarrett) + * filter_except + * map_except + +7.1.0 +----- + +* New itertools + * ichunked (thanks davebelais and youtux) + * only (thanks jaraco) + +* Changes to existing itertools: + * numeric_range now supports ranges specified by + ``datetime.datetime`` and ``datetime.timedelta`` objects (thanks to MSeifert04 for tests). + * difference now supports an *initial* keyword argument. + + +* Other changes + * Various documentation fixes (thanks raimon49, pylang) + +7.0.0 +----- + +* New itertools: + * time_limited + * partitions (thanks to rominf and Saluev) + * substrings_indexes (thanks to rominf) + +* Changes to existing itertools: + * collapse now treats ``bytes`` objects the same as ``str`` objects. (thanks to Sweenpet) + +The major version update is due to the change in the default behavior of +collapse. It now treats ``bytes`` objects the same as ``str`` objects. +This aligns its behavior with always_iterable. + +.. code-block:: python + + >>> from more_itertools import collapse + >>> iterable = [[1, 2], b'345', [6]] + >>> print(list(collapse(iterable))) + [1, 2, b'345', 6] + +6.0.0 +----- + +* Major changes: + * Python 2.7 is no longer supported. The 5.0.0 release will be the last + version targeting Python 2.7. + * All future releases will target the active versions of Python 3. + As of 2019, those are Python 3.4 and above. + * The ``six`` library is no longer a dependency. + * The accumulate function is no longer part of this library. You + may import a better version from the standard ``itertools`` module. + +* Changes to existing itertools: + * The order of the parameters in grouper have changed to match + the latest recipe in the itertools documentation. Use of the old order + will be supported in this release, but emit a ``DeprecationWarning``. + The legacy behavior will be dropped in a future release. (thanks to jaraco) + * distinct_permutations was improved (thanks to jferard - see also `permutations with unique values `_ at StackOverflow.) + * An unused parameter was removed from substrings. (thanks to pylang) + +* Other changes: + * The docs for unique_everseen were improved. (thanks to jferard and MSeifert04) + * Several Python 2-isms were removed. (thanks to jaraco, MSeifert04, and hugovk) + + diff --git a/pkg_resources/_vendor/more_itertools-8.12.0.dist-info/RECORD b/pkg_resources/_vendor/more_itertools-8.12.0.dist-info/RECORD new file mode 100644 index 00000000..44847291 --- /dev/null +++ b/pkg_resources/_vendor/more_itertools-8.12.0.dist-info/RECORD @@ -0,0 +1,16 @@ +more_itertools-8.12.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +more_itertools-8.12.0.dist-info/LICENSE,sha256=CfHIyelBrz5YTVlkHqm4fYPAyw_QB-te85Gn4mQ8GkY,1053 +more_itertools-8.12.0.dist-info/METADATA,sha256=QCCEcisEPr7iSfBIKCukhP-FbG9ehMK8tDIliZ3FBDc,39405 +more_itertools-8.12.0.dist-info/RECORD,, +more_itertools-8.12.0.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92 +more_itertools-8.12.0.dist-info/top_level.txt,sha256=fAuqRXu9LPhxdB9ujJowcFOu1rZ8wzSpOW9_jlKis6M,15 +more_itertools/__init__.py,sha256=ZQYu_9H6stSG7viUgT32TFqslqcZwq82kWRZooKiI8Y,83 +more_itertools/__init__.pyi,sha256=5B3eTzON1BBuOLob1vCflyEb2lSd6usXQQ-Cv-hXkeA,43 +more_itertools/__pycache__/__init__.cpython-310.pyc,, +more_itertools/__pycache__/more.cpython-310.pyc,, +more_itertools/__pycache__/recipes.cpython-310.pyc,, +more_itertools/more.py,sha256=jSrvV9BK-XKa4x7MPPp9yWYRDtRgR5h7yryEqHMU4mg,132578 +more_itertools/more.pyi,sha256=kWOkRKx0V8ZwC1D2j0c0DUfy56dazzpmRcm5ZuY_aqo,20006 +more_itertools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +more_itertools/recipes.py,sha256=N6aCDwoIPvE-aiqpGU-nbFwqiM3X8MKRcxBM84naW88,18410 +more_itertools/recipes.pyi,sha256=Lx3vb0p_vY7rF8MQuguvOcVaS9qd1WRL8JO_qVo7hiY,3925 diff --git a/pkg_resources/_vendor/more_itertools-8.12.0.dist-info/WHEEL b/pkg_resources/_vendor/more_itertools-8.12.0.dist-info/WHEEL new file mode 100644 index 00000000..5bad85fd --- /dev/null +++ b/pkg_resources/_vendor/more_itertools-8.12.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/pkg_resources/_vendor/more_itertools-8.12.0.dist-info/top_level.txt b/pkg_resources/_vendor/more_itertools-8.12.0.dist-info/top_level.txt new file mode 100644 index 00000000..a5035bef --- /dev/null +++ b/pkg_resources/_vendor/more_itertools-8.12.0.dist-info/top_level.txt @@ -0,0 +1 @@ +more_itertools diff --git a/pkg_resources/_vendor/more_itertools/__init__.py b/pkg_resources/_vendor/more_itertools/__init__.py new file mode 100644 index 00000000..ea38bef1 --- /dev/null +++ b/pkg_resources/_vendor/more_itertools/__init__.py @@ -0,0 +1,4 @@ +from .more import * # noqa +from .recipes import * # noqa + +__version__ = '8.12.0' diff --git a/pkg_resources/_vendor/more_itertools/__init__.pyi b/pkg_resources/_vendor/more_itertools/__init__.pyi new file mode 100644 index 00000000..96f6e36c --- /dev/null +++ b/pkg_resources/_vendor/more_itertools/__init__.pyi @@ -0,0 +1,2 @@ +from .more import * +from .recipes import * diff --git a/pkg_resources/_vendor/more_itertools/more.py b/pkg_resources/_vendor/more_itertools/more.py new file mode 100644 index 00000000..630af973 --- /dev/null +++ b/pkg_resources/_vendor/more_itertools/more.py @@ -0,0 +1,4317 @@ +import warnings + +from collections import Counter, defaultdict, deque, abc +from collections.abc import Sequence +from concurrent.futures import ThreadPoolExecutor +from functools import partial, reduce, wraps +from heapq import merge, heapify, heapreplace, heappop +from itertools import ( + chain, + compress, + count, + cycle, + dropwhile, + groupby, + islice, + repeat, + starmap, + takewhile, + tee, + zip_longest, +) +from math import exp, factorial, floor, log +from queue import Empty, Queue +from random import random, randrange, uniform +from operator import itemgetter, mul, sub, gt, lt, ge, le +from sys import hexversion, maxsize +from time import monotonic + +from .recipes import ( + consume, + flatten, + pairwise, + powerset, + take, + unique_everseen, +) + +__all__ = [ + 'AbortThread', + 'SequenceView', + 'UnequalIterablesError', + 'adjacent', + 'all_unique', + 'always_iterable', + 'always_reversible', + 'bucket', + 'callback_iter', + 'chunked', + 'chunked_even', + 'circular_shifts', + 'collapse', + 'collate', + 'combination_index', + 'consecutive_groups', + 'consumer', + 'count_cycle', + 'countable', + 'difference', + 'distinct_combinations', + 'distinct_permutations', + 'distribute', + 'divide', + 'duplicates_everseen', + 'duplicates_justseen', + 'exactly_n', + 'filter_except', + 'first', + 'groupby_transform', + 'ichunked', + 'ilen', + 'interleave', + 'interleave_evenly', + 'interleave_longest', + 'intersperse', + 'is_sorted', + 'islice_extended', + 'iterate', + 'last', + 'locate', + 'lstrip', + 'make_decorator', + 'map_except', + 'map_if', + 'map_reduce', + 'mark_ends', + 'minmax', + 'nth_or_last', + 'nth_permutation', + 'nth_product', + 'numeric_range', + 'one', + 'only', + 'padded', + 'partitions', + 'peekable', + 'permutation_index', + 'product_index', + 'raise_', + 'repeat_each', + 'repeat_last', + 'replace', + 'rlocate', + 'rstrip', + 'run_length', + 'sample', + 'seekable', + 'set_partitions', + 'side_effect', + 'sliced', + 'sort_together', + 'split_after', + 'split_at', + 'split_before', + 'split_into', + 'split_when', + 'spy', + 'stagger', + 'strip', + 'strictly_n', + 'substrings', + 'substrings_indexes', + 'time_limited', + 'unique_in_window', + 'unique_to_each', + 'unzip', + 'value_chain', + 'windowed', + 'windowed_complete', + 'with_iter', + 'zip_broadcast', + 'zip_equal', + 'zip_offset', +] + + +_marker = object() + + +def chunked(iterable, n, strict=False): + """Break *iterable* into lists of length *n*: + + >>> list(chunked([1, 2, 3, 4, 5, 6], 3)) + [[1, 2, 3], [4, 5, 6]] + + By the default, the last yielded list will have fewer than *n* elements + if the length of *iterable* is not divisible by *n*: + + >>> list(chunked([1, 2, 3, 4, 5, 6, 7, 8], 3)) + [[1, 2, 3], [4, 5, 6], [7, 8]] + + To use a fill-in value instead, see the :func:`grouper` recipe. + + If the length of *iterable* is not divisible by *n* and *strict* is + ``True``, then ``ValueError`` will be raised before the last + list is yielded. + + """ + iterator = iter(partial(take, n, iter(iterable)), []) + if strict: + if n is None: + raise ValueError('n must not be None when using strict mode.') + + def ret(): + for chunk in iterator: + if len(chunk) != n: + raise ValueError('iterable is not divisible by n.') + yield chunk + + return iter(ret()) + else: + return iterator + + +def first(iterable, default=_marker): + """Return the first item of *iterable*, or *default* if *iterable* is + empty. + + >>> first([0, 1, 2, 3]) + 0 + >>> first([], 'some default') + 'some default' + + If *default* is not provided and there are no items in the iterable, + raise ``ValueError``. + + :func:`first` is useful when you have a generator of expensive-to-retrieve + values and want any arbitrary one. It is marginally shorter than + ``next(iter(iterable), default)``. + + """ + try: + return next(iter(iterable)) + except StopIteration as e: + if default is _marker: + raise ValueError( + 'first() was called on an empty iterable, and no ' + 'default value was provided.' + ) from e + return default + + +def last(iterable, default=_marker): + """Return the last item of *iterable*, or *default* if *iterable* is + empty. + + >>> last([0, 1, 2, 3]) + 3 + >>> last([], 'some default') + 'some default' + + If *default* is not provided and there are no items in the iterable, + raise ``ValueError``. + """ + try: + if isinstance(iterable, Sequence): + return iterable[-1] + # Work around https://bugs.python.org/issue38525 + elif hasattr(iterable, '__reversed__') and (hexversion != 0x030800F0): + return next(reversed(iterable)) + else: + return deque(iterable, maxlen=1)[-1] + except (IndexError, TypeError, StopIteration): + if default is _marker: + raise ValueError( + 'last() was called on an empty iterable, and no default was ' + 'provided.' + ) + return default + + +def nth_or_last(iterable, n, default=_marker): + """Return the nth or the last item of *iterable*, + or *default* if *iterable* is empty. + + >>> nth_or_last([0, 1, 2, 3], 2) + 2 + >>> nth_or_last([0, 1], 2) + 1 + >>> nth_or_last([], 0, 'some default') + 'some default' + + If *default* is not provided and there are no items in the iterable, + raise ``ValueError``. + """ + return last(islice(iterable, n + 1), default=default) + + +class peekable: + """Wrap an iterator to allow lookahead and prepending elements. + + Call :meth:`peek` on the result to get the value that will be returned + by :func:`next`. This won't advance the iterator: + + >>> p = peekable(['a', 'b']) + >>> p.peek() + 'a' + >>> next(p) + 'a' + + Pass :meth:`peek` a default value to return that instead of raising + ``StopIteration`` when the iterator is exhausted. + + >>> p = peekable([]) + >>> p.peek('hi') + 'hi' + + peekables also offer a :meth:`prepend` method, which "inserts" items + at the head of the iterable: + + >>> p = peekable([1, 2, 3]) + >>> p.prepend(10, 11, 12) + >>> next(p) + 10 + >>> p.peek() + 11 + >>> list(p) + [11, 12, 1, 2, 3] + + peekables can be indexed. Index 0 is the item that will be returned by + :func:`next`, index 1 is the item after that, and so on: + The values up to the given index will be cached. + + >>> p = peekable(['a', 'b', 'c', 'd']) + >>> p[0] + 'a' + >>> p[1] + 'b' + >>> next(p) + 'a' + + Negative indexes are supported, but be aware that they will cache the + remaining items in the source iterator, which may require significant + storage. + + To check whether a peekable is exhausted, check its truth value: + + >>> p = peekable(['a', 'b']) + >>> if p: # peekable has items + ... list(p) + ['a', 'b'] + >>> if not p: # peekable is exhausted + ... list(p) + [] + + """ + + def __init__(self, iterable): + self._it = iter(iterable) + self._cache = deque() + + def __iter__(self): + return self + + def __bool__(self): + try: + self.peek() + except StopIteration: + return False + return True + + def peek(self, default=_marker): + """Return the item that will be next returned from ``next()``. + + Return ``default`` if there are no items left. If ``default`` is not + provided, raise ``StopIteration``. + + """ + if not self._cache: + try: + self._cache.append(next(self._it)) + except StopIteration: + if default is _marker: + raise + return default + return self._cache[0] + + def prepend(self, *items): + """Stack up items to be the next ones returned from ``next()`` or + ``self.peek()``. The items will be returned in + first in, first out order:: + + >>> p = peekable([1, 2, 3]) + >>> p.prepend(10, 11, 12) + >>> next(p) + 10 + >>> list(p) + [11, 12, 1, 2, 3] + + It is possible, by prepending items, to "resurrect" a peekable that + previously raised ``StopIteration``. + + >>> p = peekable([]) + >>> next(p) + Traceback (most recent call last): + ... + StopIteration + >>> p.prepend(1) + >>> next(p) + 1 + >>> next(p) + Traceback (most recent call last): + ... + StopIteration + + """ + self._cache.extendleft(reversed(items)) + + def __next__(self): + if self._cache: + return self._cache.popleft() + + return next(self._it) + + def _get_slice(self, index): + # Normalize the slice's arguments + step = 1 if (index.step is None) else index.step + if step > 0: + start = 0 if (index.start is None) else index.start + stop = maxsize if (index.stop is None) else index.stop + elif step < 0: + start = -1 if (index.start is None) else index.start + stop = (-maxsize - 1) if (index.stop is None) else index.stop + else: + raise ValueError('slice step cannot be zero') + + # If either the start or stop index is negative, we'll need to cache + # the rest of the iterable in order to slice from the right side. + if (start < 0) or (stop < 0): + self._cache.extend(self._it) + # Otherwise we'll need to find the rightmost index and cache to that + # point. + else: + n = min(max(start, stop) + 1, maxsize) + cache_len = len(self._cache) + if n >= cache_len: + self._cache.extend(islice(self._it, n - cache_len)) + + return list(self._cache)[index] + + def __getitem__(self, index): + if isinstance(index, slice): + return self._get_slice(index) + + cache_len = len(self._cache) + if index < 0: + self._cache.extend(self._it) + elif index >= cache_len: + self._cache.extend(islice(self._it, index + 1 - cache_len)) + + return self._cache[index] + + +def collate(*iterables, **kwargs): + """Return a sorted merge of the items from each of several already-sorted + *iterables*. + + >>> list(collate('ACDZ', 'AZ', 'JKL')) + ['A', 'A', 'C', 'D', 'J', 'K', 'L', 'Z', 'Z'] + + Works lazily, keeping only the next value from each iterable in memory. Use + :func:`collate` to, for example, perform a n-way mergesort of items that + don't fit in memory. + + If a *key* function is specified, the iterables will be sorted according + to its result: + + >>> key = lambda s: int(s) # Sort by numeric value, not by string + >>> list(collate(['1', '10'], ['2', '11'], key=key)) + ['1', '2', '10', '11'] + + + If the *iterables* are sorted in descending order, set *reverse* to + ``True``: + + >>> list(collate([5, 3, 1], [4, 2, 0], reverse=True)) + [5, 4, 3, 2, 1, 0] + + If the elements of the passed-in iterables are out of order, you might get + unexpected results. + + On Python 3.5+, this function is an alias for :func:`heapq.merge`. + + """ + warnings.warn( + "collate is no longer part of more_itertools, use heapq.merge", + DeprecationWarning, + ) + return merge(*iterables, **kwargs) + + +def consumer(func): + """Decorator that automatically advances a PEP-342-style "reverse iterator" + to its first yield point so you don't have to call ``next()`` on it + manually. + + >>> @consumer + ... def tally(): + ... i = 0 + ... while True: + ... print('Thing number %s is %s.' % (i, (yield))) + ... i += 1 + ... + >>> t = tally() + >>> t.send('red') + Thing number 0 is red. + >>> t.send('fish') + Thing number 1 is fish. + + Without the decorator, you would have to call ``next(t)`` before + ``t.send()`` could be used. + + """ + + @wraps(func) + def wrapper(*args, **kwargs): + gen = func(*args, **kwargs) + next(gen) + return gen + + return wrapper + + +def ilen(iterable): + """Return the number of items in *iterable*. + + >>> ilen(x for x in range(1000000) if x % 3 == 0) + 333334 + + This consumes the iterable, so handle with care. + + """ + # This approach was selected because benchmarks showed it's likely the + # fastest of the known implementations at the time of writing. + # See GitHub tracker: #236, #230. + counter = count() + deque(zip(iterable, counter), maxlen=0) + return next(counter) + + +def iterate(func, start): + """Return ``start``, ``func(start)``, ``func(func(start))``, ... + + >>> from itertools import islice + >>> list(islice(iterate(lambda x: 2*x, 1), 10)) + [1, 2, 4, 8, 16, 32, 64, 128, 256, 512] + + """ + while True: + yield start + start = func(start) + + +def with_iter(context_manager): + """Wrap an iterable in a ``with`` statement, so it closes once exhausted. + + For example, this will close the file when the iterator is exhausted:: + + upper_lines = (line.upper() for line in with_iter(open('foo'))) + + Any context manager which returns an iterable is a candidate for + ``with_iter``. + + """ + with context_manager as iterable: + yield from iterable + + +def one(iterable, too_short=None, too_long=None): + """Return the first item from *iterable*, which is expected to contain only + that item. Raise an exception if *iterable* is empty or has more than one + item. + + :func:`one` is useful for ensuring that an iterable contains only one item. + For example, it can be used to retrieve the result of a database query + that is expected to return a single row. + + If *iterable* is empty, ``ValueError`` will be raised. You may specify a + different exception with the *too_short* keyword: + + >>> it = [] + >>> one(it) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ValueError: too many items in iterable (expected 1)' + >>> too_short = IndexError('too few items') + >>> one(it, too_short=too_short) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + IndexError: too few items + + Similarly, if *iterable* contains more than one item, ``ValueError`` will + be raised. You may specify a different exception with the *too_long* + keyword: + + >>> it = ['too', 'many'] + >>> one(it) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ValueError: Expected exactly one item in iterable, but got 'too', + 'many', and perhaps more. + >>> too_long = RuntimeError + >>> one(it, too_long=too_long) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + RuntimeError + + Note that :func:`one` attempts to advance *iterable* twice to ensure there + is only one item. See :func:`spy` or :func:`peekable` to check iterable + contents less destructively. + + """ + it = iter(iterable) + + try: + first_value = next(it) + except StopIteration as e: + raise ( + too_short or ValueError('too few items in iterable (expected 1)') + ) from e + + try: + second_value = next(it) + except StopIteration: + pass + else: + msg = ( + 'Expected exactly one item in iterable, but got {!r}, {!r}, ' + 'and perhaps more.'.format(first_value, second_value) + ) + raise too_long or ValueError(msg) + + return first_value + + +def raise_(exception, *args): + raise exception(*args) + + +def strictly_n(iterable, n, too_short=None, too_long=None): + """Validate that *iterable* has exactly *n* items and return them if + it does. If it has fewer than *n* items, call function *too_short* + with those items. If it has more than *n* items, call function + *too_long* with the first ``n + 1`` items. + + >>> iterable = ['a', 'b', 'c', 'd'] + >>> n = 4 + >>> list(strictly_n(iterable, n)) + ['a', 'b', 'c', 'd'] + + By default, *too_short* and *too_long* are functions that raise + ``ValueError``. + + >>> list(strictly_n('ab', 3)) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ValueError: too few items in iterable (got 2) + + >>> list(strictly_n('abc', 2)) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ValueError: too many items in iterable (got at least 3) + + You can instead supply functions that do something else. + *too_short* will be called with the number of items in *iterable*. + *too_long* will be called with `n + 1`. + + >>> def too_short(item_count): + ... raise RuntimeError + >>> it = strictly_n('abcd', 6, too_short=too_short) + >>> list(it) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + RuntimeError + + >>> def too_long(item_count): + ... print('The boss is going to hear about this') + >>> it = strictly_n('abcdef', 4, too_long=too_long) + >>> list(it) + The boss is going to hear about this + ['a', 'b', 'c', 'd'] + + """ + if too_short is None: + too_short = lambda item_count: raise_( + ValueError, + 'Too few items in iterable (got {})'.format(item_count), + ) + + if too_long is None: + too_long = lambda item_count: raise_( + ValueError, + 'Too many items in iterable (got at least {})'.format(item_count), + ) + + it = iter(iterable) + for i in range(n): + try: + item = next(it) + except StopIteration: + too_short(i) + return + else: + yield item + + try: + next(it) + except StopIteration: + pass + else: + too_long(n + 1) + + +def distinct_permutations(iterable, r=None): + """Yield successive distinct permutations of the elements in *iterable*. + + >>> sorted(distinct_permutations([1, 0, 1])) + [(0, 1, 1), (1, 0, 1), (1, 1, 0)] + + Equivalent to ``set(permutations(iterable))``, except duplicates are not + generated and thrown away. For larger input sequences this is much more + efficient. + + Duplicate permutations arise when there are duplicated elements in the + input iterable. The number of items returned is + `n! / (x_1! * x_2! * ... * x_n!)`, where `n` is the total number of + items input, and each `x_i` is the count of a distinct item in the input + sequence. + + If *r* is given, only the *r*-length permutations are yielded. + + >>> sorted(distinct_permutations([1, 0, 1], r=2)) + [(0, 1), (1, 0), (1, 1)] + >>> sorted(distinct_permutations(range(3), r=2)) + [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)] + + """ + # Algorithm: https://w.wiki/Qai + def _full(A): + while True: + # Yield the permutation we have + yield tuple(A) + + # Find the largest index i such that A[i] < A[i + 1] + for i in range(size - 2, -1, -1): + if A[i] < A[i + 1]: + break + # If no such index exists, this permutation is the last one + else: + return + + # Find the largest index j greater than j such that A[i] < A[j] + for j in range(size - 1, i, -1): + if A[i] < A[j]: + break + + # Swap the value of A[i] with that of A[j], then reverse the + # sequence from A[i + 1] to form the new permutation + A[i], A[j] = A[j], A[i] + A[i + 1 :] = A[: i - size : -1] # A[i + 1:][::-1] + + # Algorithm: modified from the above + def _partial(A, r): + # Split A into the first r items and the last r items + head, tail = A[:r], A[r:] + right_head_indexes = range(r - 1, -1, -1) + left_tail_indexes = range(len(tail)) + + while True: + # Yield the permutation we have + yield tuple(head) + + # Starting from the right, find the first index of the head with + # value smaller than the maximum value of the tail - call it i. + pivot = tail[-1] + for i in right_head_indexes: + if head[i] < pivot: + break + pivot = head[i] + else: + return + + # Starting from the left, find the first value of the tail + # with a value greater than head[i] and swap. + for j in left_tail_indexes: + if tail[j] > head[i]: + head[i], tail[j] = tail[j], head[i] + break + # If we didn't find one, start from the right and find the first + # index of the head with a value greater than head[i] and swap. + else: + for j in right_head_indexes: + if head[j] > head[i]: + head[i], head[j] = head[j], head[i] + break + + # Reverse head[i + 1:] and swap it with tail[:r - (i + 1)] + tail += head[: i - r : -1] # head[i + 1:][::-1] + i += 1 + head[i:], tail[:] = tail[: r - i], tail[r - i :] + + items = sorted(iterable) + + size = len(items) + if r is None: + r = size + + if 0 < r <= size: + return _full(items) if (r == size) else _partial(items, r) + + return iter(() if r else ((),)) + + +def intersperse(e, iterable, n=1): + """Intersperse filler element *e* among the items in *iterable*, leaving + *n* items between each filler element. + + >>> list(intersperse('!', [1, 2, 3, 4, 5])) + [1, '!', 2, '!', 3, '!', 4, '!', 5] + + >>> list(intersperse(None, [1, 2, 3, 4, 5], n=2)) + [1, 2, None, 3, 4, None, 5] + + """ + if n == 0: + raise ValueError('n must be > 0') + elif n == 1: + # interleave(repeat(e), iterable) -> e, x_0, e, x_1, e, x_2... + # islice(..., 1, None) -> x_0, e, x_1, e, x_2... + return islice(interleave(repeat(e), iterable), 1, None) + else: + # interleave(filler, chunks) -> [e], [x_0, x_1], [e], [x_2, x_3]... + # islice(..., 1, None) -> [x_0, x_1], [e], [x_2, x_3]... + # flatten(...) -> x_0, x_1, e, x_2, x_3... + filler = repeat([e]) + chunks = chunked(iterable, n) + return flatten(islice(interleave(filler, chunks), 1, None)) + + +def unique_to_each(*iterables): + """Return the elements from each of the input iterables that aren't in the + other input iterables. + + For example, suppose you have a set of packages, each with a set of + dependencies:: + + {'pkg_1': {'A', 'B'}, 'pkg_2': {'B', 'C'}, 'pkg_3': {'B', 'D'}} + + If you remove one package, which dependencies can also be removed? + + If ``pkg_1`` is removed, then ``A`` is no longer necessary - it is not + associated with ``pkg_2`` or ``pkg_3``. Similarly, ``C`` is only needed for + ``pkg_2``, and ``D`` is only needed for ``pkg_3``:: + + >>> unique_to_each({'A', 'B'}, {'B', 'C'}, {'B', 'D'}) + [['A'], ['C'], ['D']] + + If there are duplicates in one input iterable that aren't in the others + they will be duplicated in the output. Input order is preserved:: + + >>> unique_to_each("mississippi", "missouri") + [['p', 'p'], ['o', 'u', 'r']] + + It is assumed that the elements of each iterable are hashable. + + """ + pool = [list(it) for it in iterables] + counts = Counter(chain.from_iterable(map(set, pool))) + uniques = {element for element in counts if counts[element] == 1} + return [list(filter(uniques.__contains__, it)) for it in pool] + + +def windowed(seq, n, fillvalue=None, step=1): + """Return a sliding window of width *n* over the given iterable. + + >>> all_windows = windowed([1, 2, 3, 4, 5], 3) + >>> list(all_windows) + [(1, 2, 3), (2, 3, 4), (3, 4, 5)] + + When the window is larger than the iterable, *fillvalue* is used in place + of missing values: + + >>> list(windowed([1, 2, 3], 4)) + [(1, 2, 3, None)] + + Each window will advance in increments of *step*: + + >>> list(windowed([1, 2, 3, 4, 5, 6], 3, fillvalue='!', step=2)) + [(1, 2, 3), (3, 4, 5), (5, 6, '!')] + + To slide into the iterable's items, use :func:`chain` to add filler items + to the left: + + >>> iterable = [1, 2, 3, 4] + >>> n = 3 + >>> padding = [None] * (n - 1) + >>> list(windowed(chain(padding, iterable), 3)) + [(None, None, 1), (None, 1, 2), (1, 2, 3), (2, 3, 4)] + """ + if n < 0: + raise ValueError('n must be >= 0') + if n == 0: + yield tuple() + return + if step < 1: + raise ValueError('step must be >= 1') + + window = deque(maxlen=n) + i = n + for _ in map(window.append, seq): + i -= 1 + if not i: + i = step + yield tuple(window) + + size = len(window) + if size < n: + yield tuple(chain(window, repeat(fillvalue, n - size))) + elif 0 < i < min(step, n): + window += (fillvalue,) * i + yield tuple(window) + + +def substrings(iterable): + """Yield all of the substrings of *iterable*. + + >>> [''.join(s) for s in substrings('more')] + ['m', 'o', 'r', 'e', 'mo', 'or', 're', 'mor', 'ore', 'more'] + + Note that non-string iterables can also be subdivided. + + >>> list(substrings([0, 1, 2])) + [(0,), (1,), (2,), (0, 1), (1, 2), (0, 1, 2)] + + """ + # The length-1 substrings + seq = [] + for item in iter(iterable): + seq.append(item) + yield (item,) + seq = tuple(seq) + item_count = len(seq) + + # And the rest + for n in range(2, item_count + 1): + for i in range(item_count - n + 1): + yield seq[i : i + n] + + +def substrings_indexes(seq, reverse=False): + """Yield all substrings and their positions in *seq* + + The items yielded will be a tuple of the form ``(substr, i, j)``, where + ``substr == seq[i:j]``. + + This function only works for iterables that support slicing, such as + ``str`` objects. + + >>> for item in substrings_indexes('more'): + ... print(item) + ('m', 0, 1) + ('o', 1, 2) + ('r', 2, 3) + ('e', 3, 4) + ('mo', 0, 2) + ('or', 1, 3) + ('re', 2, 4) + ('mor', 0, 3) + ('ore', 1, 4) + ('more', 0, 4) + + Set *reverse* to ``True`` to yield the same items in the opposite order. + + + """ + r = range(1, len(seq) + 1) + if reverse: + r = reversed(r) + return ( + (seq[i : i + L], i, i + L) for L in r for i in range(len(seq) - L + 1) + ) + + +class bucket: + """Wrap *iterable* and return an object that buckets it iterable into + child iterables based on a *key* function. + + >>> iterable = ['a1', 'b1', 'c1', 'a2', 'b2', 'c2', 'b3'] + >>> s = bucket(iterable, key=lambda x: x[0]) # Bucket by 1st character + >>> sorted(list(s)) # Get the keys + ['a', 'b', 'c'] + >>> a_iterable = s['a'] + >>> next(a_iterable) + 'a1' + >>> next(a_iterable) + 'a2' + >>> list(s['b']) + ['b1', 'b2', 'b3'] + + The original iterable will be advanced and its items will be cached until + they are used by the child iterables. This may require significant storage. + + By default, attempting to select a bucket to which no items belong will + exhaust the iterable and cache all values. + If you specify a *validator* function, selected buckets will instead be + checked against it. + + >>> from itertools import count + >>> it = count(1, 2) # Infinite sequence of odd numbers + >>> key = lambda x: x % 10 # Bucket by last digit + >>> validator = lambda x: x in {1, 3, 5, 7, 9} # Odd digits only + >>> s = bucket(it, key=key, validator=validator) + >>> 2 in s + False + >>> list(s[2]) + [] + + """ + + def __init__(self, iterable, key, validator=None): + self._it = iter(iterable) + self._key = key + self._cache = defaultdict(deque) + self._validator = validator or (lambda x: True) + + def __contains__(self, value): + if not self._validator(value): + return False + + try: + item = next(self[value]) + except StopIteration: + return False + else: + self._cache[value].appendleft(item) + + return True + + def _get_values(self, value): + """ + Helper to yield items from the parent iterator that match *value*. + Items that don't match are stored in the local cache as they + are encountered. + """ + while True: + # If we've cached some items that match the target value, emit + # the first one and evict it from the cache. + if self._cache[value]: + yield self._cache[value].popleft() + # Otherwise we need to advance the parent iterator to search for + # a matching item, caching the rest. + else: + while True: + try: + item = next(self._it) + except StopIteration: + return + item_value = self._key(item) + if item_value == value: + yield item + break + elif self._validator(item_value): + self._cache[item_value].append(item) + + def __iter__(self): + for item in self._it: + item_value = self._key(item) + if self._validator(item_value): + self._cache[item_value].append(item) + + yield from self._cache.keys() + + def __getitem__(self, value): + if not self._validator(value): + return iter(()) + + return self._get_values(value) + + +def spy(iterable, n=1): + """Return a 2-tuple with a list containing the first *n* elements of + *iterable*, and an iterator with the same items as *iterable*. + This allows you to "look ahead" at the items in the iterable without + advancing it. + + There is one item in the list by default: + + >>> iterable = 'abcdefg' + >>> head, iterable = spy(iterable) + >>> head + ['a'] + >>> list(iterable) + ['a', 'b', 'c', 'd', 'e', 'f', 'g'] + + You may use unpacking to retrieve items instead of lists: + + >>> (head,), iterable = spy('abcdefg') + >>> head + 'a' + >>> (first, second), iterable = spy('abcdefg', 2) + >>> first + 'a' + >>> second + 'b' + + The number of items requested can be larger than the number of items in + the iterable: + + >>> iterable = [1, 2, 3, 4, 5] + >>> head, iterable = spy(iterable, 10) + >>> head + [1, 2, 3, 4, 5] + >>> list(iterable) + [1, 2, 3, 4, 5] + + """ + it = iter(iterable) + head = take(n, it) + + return head.copy(), chain(head, it) + + +def interleave(*iterables): + """Return a new iterable yielding from each iterable in turn, + until the shortest is exhausted. + + >>> list(interleave([1, 2, 3], [4, 5], [6, 7, 8])) + [1, 4, 6, 2, 5, 7] + + For a version that doesn't terminate after the shortest iterable is + exhausted, see :func:`interleave_longest`. + + """ + return chain.from_iterable(zip(*iterables)) + + +def interleave_longest(*iterables): + """Return a new iterable yielding from each iterable in turn, + skipping any that are exhausted. + + >>> list(interleave_longest([1, 2, 3], [4, 5], [6, 7, 8])) + [1, 4, 6, 2, 5, 7, 3, 8] + + This function produces the same output as :func:`roundrobin`, but may + perform better for some inputs (in particular when the number of iterables + is large). + + """ + i = chain.from_iterable(zip_longest(*iterables, fillvalue=_marker)) + return (x for x in i if x is not _marker) + + +def interleave_evenly(iterables, lengths=None): + """ + Interleave multiple iterables so that their elements are evenly distributed + throughout the output sequence. + + >>> iterables = [1, 2, 3, 4, 5], ['a', 'b'] + >>> list(interleave_evenly(iterables)) + [1, 2, 'a', 3, 4, 'b', 5] + + >>> iterables = [[1, 2, 3], [4, 5], [6, 7, 8]] + >>> list(interleave_evenly(iterables)) + [1, 6, 4, 2, 7, 3, 8, 5] + + This function requires iterables of known length. Iterables without + ``__len__()`` can be used by manually specifying lengths with *lengths*: + + >>> from itertools import combinations, repeat + >>> iterables = [combinations(range(4), 2), ['a', 'b', 'c']] + >>> lengths = [4 * (4 - 1) // 2, 3] + >>> list(interleave_evenly(iterables, lengths=lengths)) + [(0, 1), (0, 2), 'a', (0, 3), (1, 2), 'b', (1, 3), (2, 3), 'c'] + + Based on Bresenham's algorithm. + """ + if lengths is None: + try: + lengths = [len(it) for it in iterables] + except TypeError: + raise ValueError( + 'Iterable lengths could not be determined automatically. ' + 'Specify them with the lengths keyword.' + ) + elif len(iterables) != len(lengths): + raise ValueError('Mismatching number of iterables and lengths.') + + dims = len(lengths) + + # sort iterables by length, descending + lengths_permute = sorted( + range(dims), key=lambda i: lengths[i], reverse=True + ) + lengths_desc = [lengths[i] for i in lengths_permute] + iters_desc = [iter(iterables[i]) for i in lengths_permute] + + # the longest iterable is the primary one (Bresenham: the longest + # distance along an axis) + delta_primary, deltas_secondary = lengths_desc[0], lengths_desc[1:] + iter_primary, iters_secondary = iters_desc[0], iters_desc[1:] + errors = [delta_primary // dims] * len(deltas_secondary) + + to_yield = sum(lengths) + while to_yield: + yield next(iter_primary) + to_yield -= 1 + # update errors for each secondary iterable + errors = [e - delta for e, delta in zip(errors, deltas_secondary)] + + # those iterables for which the error is negative are yielded + # ("diagonal step" in Bresenham) + for i, e in enumerate(errors): + if e < 0: + yield next(iters_secondary[i]) + to_yield -= 1 + errors[i] += delta_primary + + +def collapse(iterable, base_type=None, levels=None): + """Flatten an iterable with multiple levels of nesting (e.g., a list of + lists of tuples) into non-iterable types. + + >>> iterable = [(1, 2), ([3, 4], [[5], [6]])] + >>> list(collapse(iterable)) + [1, 2, 3, 4, 5, 6] + + Binary and text strings are not considered iterable and + will not be collapsed. + + To avoid collapsing other types, specify *base_type*: + + >>> iterable = ['ab', ('cd', 'ef'), ['gh', 'ij']] + >>> list(collapse(iterable, base_type=tuple)) + ['ab', ('cd', 'ef'), 'gh', 'ij'] + + Specify *levels* to stop flattening after a certain level: + + >>> iterable = [('a', ['b']), ('c', ['d'])] + >>> list(collapse(iterable)) # Fully flattened + ['a', 'b', 'c', 'd'] + >>> list(collapse(iterable, levels=1)) # Only one level flattened + ['a', ['b'], 'c', ['d']] + + """ + + def walk(node, level): + if ( + ((levels is not None) and (level > levels)) + or isinstance(node, (str, bytes)) + or ((base_type is not None) and isinstance(node, base_type)) + ): + yield node + return + + try: + tree = iter(node) + except TypeError: + yield node + return + else: + for child in tree: + yield from walk(child, level + 1) + + yield from walk(iterable, 0) + + +def side_effect(func, iterable, chunk_size=None, before=None, after=None): + """Invoke *func* on each item in *iterable* (or on each *chunk_size* group + of items) before yielding the item. + + `func` must be a function that takes a single argument. Its return value + will be discarded. + + *before* and *after* are optional functions that take no arguments. They + will be executed before iteration starts and after it ends, respectively. + + `side_effect` can be used for logging, updating progress bars, or anything + that is not functionally "pure." + + Emitting a status message: + + >>> from more_itertools import consume + >>> func = lambda item: print('Received {}'.format(item)) + >>> consume(side_effect(func, range(2))) + Received 0 + Received 1 + + Operating on chunks of items: + + >>> pair_sums = [] + >>> func = lambda chunk: pair_sums.append(sum(chunk)) + >>> list(side_effect(func, [0, 1, 2, 3, 4, 5], 2)) + [0, 1, 2, 3, 4, 5] + >>> list(pair_sums) + [1, 5, 9] + + Writing to a file-like object: + + >>> from io import StringIO + >>> from more_itertools import consume + >>> f = StringIO() + >>> func = lambda x: print(x, file=f) + >>> before = lambda: print(u'HEADER', file=f) + >>> after = f.close + >>> it = [u'a', u'b', u'c'] + >>> consume(side_effect(func, it, before=before, after=after)) + >>> f.closed + True + + """ + try: + if before is not None: + before() + + if chunk_size is None: + for item in iterable: + func(item) + yield item + else: + for chunk in chunked(iterable, chunk_size): + func(chunk) + yield from chunk + finally: + if after is not None: + after() + + +def sliced(seq, n, strict=False): + """Yield slices of length *n* from the sequence *seq*. + + >>> list(sliced((1, 2, 3, 4, 5, 6), 3)) + [(1, 2, 3), (4, 5, 6)] + + By the default, the last yielded slice will have fewer than *n* elements + if the length of *seq* is not divisible by *n*: + + >>> list(sliced((1, 2, 3, 4, 5, 6, 7, 8), 3)) + [(1, 2, 3), (4, 5, 6), (7, 8)] + + If the length of *seq* is not divisible by *n* and *strict* is + ``True``, then ``ValueError`` will be raised before the last + slice is yielded. + + This function will only work for iterables that support slicing. + For non-sliceable iterables, see :func:`chunked`. + + """ + iterator = takewhile(len, (seq[i : i + n] for i in count(0, n))) + if strict: + + def ret(): + for _slice in iterator: + if len(_slice) != n: + raise ValueError("seq is not divisible by n.") + yield _slice + + return iter(ret()) + else: + return iterator + + +def split_at(iterable, pred, maxsplit=-1, keep_separator=False): + """Yield lists of items from *iterable*, where each list is delimited by + an item where callable *pred* returns ``True``. + + >>> list(split_at('abcdcba', lambda x: x == 'b')) + [['a'], ['c', 'd', 'c'], ['a']] + + >>> list(split_at(range(10), lambda n: n % 2 == 1)) + [[0], [2], [4], [6], [8], []] + + At most *maxsplit* splits are done. If *maxsplit* is not specified or -1, + then there is no limit on the number of splits: + + >>> list(split_at(range(10), lambda n: n % 2 == 1, maxsplit=2)) + [[0], [2], [4, 5, 6, 7, 8, 9]] + + By default, the delimiting items are not included in the output. + The include them, set *keep_separator* to ``True``. + + >>> list(split_at('abcdcba', lambda x: x == 'b', keep_separator=True)) + [['a'], ['b'], ['c', 'd', 'c'], ['b'], ['a']] + + """ + if maxsplit == 0: + yield list(iterable) + return + + buf = [] + it = iter(iterable) + for item in it: + if pred(item): + yield buf + if keep_separator: + yield [item] + if maxsplit == 1: + yield list(it) + return + buf = [] + maxsplit -= 1 + else: + buf.append(item) + yield buf + + +def split_before(iterable, pred, maxsplit=-1): + """Yield lists of items from *iterable*, where each list ends just before + an item for which callable *pred* returns ``True``: + + >>> list(split_before('OneTwo', lambda s: s.isupper())) + [['O', 'n', 'e'], ['T', 'w', 'o']] + + >>> list(split_before(range(10), lambda n: n % 3 == 0)) + [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]] + + At most *maxsplit* splits are done. If *maxsplit* is not specified or -1, + then there is no limit on the number of splits: + + >>> list(split_before(range(10), lambda n: n % 3 == 0, maxsplit=2)) + [[0, 1, 2], [3, 4, 5], [6, 7, 8, 9]] + """ + if maxsplit == 0: + yield list(iterable) + return + + buf = [] + it = iter(iterable) + for item in it: + if pred(item) and buf: + yield buf + if maxsplit == 1: + yield [item] + list(it) + return + buf = [] + maxsplit -= 1 + buf.append(item) + if buf: + yield buf + + +def split_after(iterable, pred, maxsplit=-1): + """Yield lists of items from *iterable*, where each list ends with an + item where callable *pred* returns ``True``: + + >>> list(split_after('one1two2', lambda s: s.isdigit())) + [['o', 'n', 'e', '1'], ['t', 'w', 'o', '2']] + + >>> list(split_after(range(10), lambda n: n % 3 == 0)) + [[0], [1, 2, 3], [4, 5, 6], [7, 8, 9]] + + At most *maxsplit* splits are done. If *maxsplit* is not specified or -1, + then there is no limit on the number of splits: + + >>> list(split_after(range(10), lambda n: n % 3 == 0, maxsplit=2)) + [[0], [1, 2, 3], [4, 5, 6, 7, 8, 9]] + + """ + if maxsplit == 0: + yield list(iterable) + return + + buf = [] + it = iter(iterable) + for item in it: + buf.append(item) + if pred(item) and buf: + yield buf + if maxsplit == 1: + yield list(it) + return + buf = [] + maxsplit -= 1 + if buf: + yield buf + + +def split_when(iterable, pred, maxsplit=-1): + """Split *iterable* into pieces based on the output of *pred*. + *pred* should be a function that takes successive pairs of items and + returns ``True`` if the iterable should be split in between them. + + For example, to find runs of increasing numbers, split the iterable when + element ``i`` is larger than element ``i + 1``: + + >>> list(split_when([1, 2, 3, 3, 2, 5, 2, 4, 2], lambda x, y: x > y)) + [[1, 2, 3, 3], [2, 5], [2, 4], [2]] + + At most *maxsplit* splits are done. If *maxsplit* is not specified or -1, + then there is no limit on the number of splits: + + >>> list(split_when([1, 2, 3, 3, 2, 5, 2, 4, 2], + ... lambda x, y: x > y, maxsplit=2)) + [[1, 2, 3, 3], [2, 5], [2, 4, 2]] + + """ + if maxsplit == 0: + yield list(iterable) + return + + it = iter(iterable) + try: + cur_item = next(it) + except StopIteration: + return + + buf = [cur_item] + for next_item in it: + if pred(cur_item, next_item): + yield buf + if maxsplit == 1: + yield [next_item] + list(it) + return + buf = [] + maxsplit -= 1 + + buf.append(next_item) + cur_item = next_item + + yield buf + + +def split_into(iterable, sizes): + """Yield a list of sequential items from *iterable* of length 'n' for each + integer 'n' in *sizes*. + + >>> list(split_into([1,2,3,4,5,6], [1,2,3])) + [[1], [2, 3], [4, 5, 6]] + + If the sum of *sizes* is smaller than the length of *iterable*, then the + remaining items of *iterable* will not be returned. + + >>> list(split_into([1,2,3,4,5,6], [2,3])) + [[1, 2], [3, 4, 5]] + + If the sum of *sizes* is larger than the length of *iterable*, fewer items + will be returned in the iteration that overruns *iterable* and further + lists will be empty: + + >>> list(split_into([1,2,3,4], [1,2,3,4])) + [[1], [2, 3], [4], []] + + When a ``None`` object is encountered in *sizes*, the returned list will + contain items up to the end of *iterable* the same way that itertools.slice + does: + + >>> list(split_into([1,2,3,4,5,6,7,8,9,0], [2,3,None])) + [[1, 2], [3, 4, 5], [6, 7, 8, 9, 0]] + + :func:`split_into` can be useful for grouping a series of items where the + sizes of the groups are not uniform. An example would be where in a row + from a table, multiple columns represent elements of the same feature + (e.g. a point represented by x,y,z) but, the format is not the same for + all columns. + """ + # convert the iterable argument into an iterator so its contents can + # be consumed by islice in case it is a generator + it = iter(iterable) + + for size in sizes: + if size is None: + yield list(it) + return + else: + yield list(islice(it, size)) + + +def padded(iterable, fillvalue=None, n=None, next_multiple=False): + """Yield the elements from *iterable*, followed by *fillvalue*, such that + at least *n* items are emitted. + + >>> list(padded([1, 2, 3], '?', 5)) + [1, 2, 3, '?', '?'] + + If *next_multiple* is ``True``, *fillvalue* will be emitted until the + number of items emitted is a multiple of *n*:: + + >>> list(padded([1, 2, 3, 4], n=3, next_multiple=True)) + [1, 2, 3, 4, None, None] + + If *n* is ``None``, *fillvalue* will be emitted indefinitely. + + """ + it = iter(iterable) + if n is None: + yield from chain(it, repeat(fillvalue)) + elif n < 1: + raise ValueError('n must be at least 1') + else: + item_count = 0 + for item in it: + yield item + item_count += 1 + + remaining = (n - item_count) % n if next_multiple else n - item_count + for _ in range(remaining): + yield fillvalue + + +def repeat_each(iterable, n=2): + """Repeat each element in *iterable* *n* times. + + >>> list(repeat_each('ABC', 3)) + ['A', 'A', 'A', 'B', 'B', 'B', 'C', 'C', 'C'] + """ + return chain.from_iterable(map(repeat, iterable, repeat(n))) + + +def repeat_last(iterable, default=None): + """After the *iterable* is exhausted, keep yielding its last element. + + >>> list(islice(repeat_last(range(3)), 5)) + [0, 1, 2, 2, 2] + + If the iterable is empty, yield *default* forever:: + + >>> list(islice(repeat_last(range(0), 42), 5)) + [42, 42, 42, 42, 42] + + """ + item = _marker + for item in iterable: + yield item + final = default if item is _marker else item + yield from repeat(final) + + +def distribute(n, iterable): + """Distribute the items from *iterable* among *n* smaller iterables. + + >>> group_1, group_2 = distribute(2, [1, 2, 3, 4, 5, 6]) + >>> list(group_1) + [1, 3, 5] + >>> list(group_2) + [2, 4, 6] + + If the length of *iterable* is not evenly divisible by *n*, then the + length of the returned iterables will not be identical: + + >>> children = distribute(3, [1, 2, 3, 4, 5, 6, 7]) + >>> [list(c) for c in children] + [[1, 4, 7], [2, 5], [3, 6]] + + If the length of *iterable* is smaller than *n*, then the last returned + iterables will be empty: + + >>> children = distribute(5, [1, 2, 3]) + >>> [list(c) for c in children] + [[1], [2], [3], [], []] + + This function uses :func:`itertools.tee` and may require significant + storage. If you need the order items in the smaller iterables to match the + original iterable, see :func:`divide`. + + """ + if n < 1: + raise ValueError('n must be at least 1') + + children = tee(iterable, n) + return [islice(it, index, None, n) for index, it in enumerate(children)] + + +def stagger(iterable, offsets=(-1, 0, 1), longest=False, fillvalue=None): + """Yield tuples whose elements are offset from *iterable*. + The amount by which the `i`-th item in each tuple is offset is given by + the `i`-th item in *offsets*. + + >>> list(stagger([0, 1, 2, 3])) + [(None, 0, 1), (0, 1, 2), (1, 2, 3)] + >>> list(stagger(range(8), offsets=(0, 2, 4))) + [(0, 2, 4), (1, 3, 5), (2, 4, 6), (3, 5, 7)] + + By default, the sequence will end when the final element of a tuple is the + last item in the iterable. To continue until the first element of a tuple + is the last item in the iterable, set *longest* to ``True``:: + + >>> list(stagger([0, 1, 2, 3], longest=True)) + [(None, 0, 1), (0, 1, 2), (1, 2, 3), (2, 3, None), (3, None, None)] + + By default, ``None`` will be used to replace offsets beyond the end of the + sequence. Specify *fillvalue* to use some other value. + + """ + children = tee(iterable, len(offsets)) + + return zip_offset( + *children, offsets=offsets, longest=longest, fillvalue=fillvalue + ) + + +class UnequalIterablesError(ValueError): + def __init__(self, details=None): + msg = 'Iterables have different lengths' + if details is not None: + msg += (': index 0 has length {}; index {} has length {}').format( + *details + ) + + super().__init__(msg) + + +def _zip_equal_generator(iterables): + for combo in zip_longest(*iterables, fillvalue=_marker): + for val in combo: + if val is _marker: + raise UnequalIterablesError() + yield combo + + +def _zip_equal(*iterables): + # Check whether the iterables are all the same size. + try: + first_size = len(iterables[0]) + for i, it in enumerate(iterables[1:], 1): + size = len(it) + if size != first_size: + break + else: + # If we didn't break out, we can use the built-in zip. + return zip(*iterables) + + # If we did break out, there was a mismatch. + raise UnequalIterablesError(details=(first_size, i, size)) + # If any one of the iterables didn't have a length, start reading + # them until one runs out. + except TypeError: + return _zip_equal_generator(iterables) + + +def zip_equal(*iterables): + """``zip`` the input *iterables* together, but raise + ``UnequalIterablesError`` if they aren't all the same length. + + >>> it_1 = range(3) + >>> it_2 = iter('abc') + >>> list(zip_equal(it_1, it_2)) + [(0, 'a'), (1, 'b'), (2, 'c')] + + >>> it_1 = range(3) + >>> it_2 = iter('abcd') + >>> list(zip_equal(it_1, it_2)) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + more_itertools.more.UnequalIterablesError: Iterables have different + lengths + + """ + if hexversion >= 0x30A00A6: + warnings.warn( + ( + 'zip_equal will be removed in a future version of ' + 'more-itertools. Use the builtin zip function with ' + 'strict=True instead.' + ), + DeprecationWarning, + ) + + return _zip_equal(*iterables) + + +def zip_offset(*iterables, offsets, longest=False, fillvalue=None): + """``zip`` the input *iterables* together, but offset the `i`-th iterable + by the `i`-th item in *offsets*. + + >>> list(zip_offset('0123', 'abcdef', offsets=(0, 1))) + [('0', 'b'), ('1', 'c'), ('2', 'd'), ('3', 'e')] + + This can be used as a lightweight alternative to SciPy or pandas to analyze + data sets in which some series have a lead or lag relationship. + + By default, the sequence will end when the shortest iterable is exhausted. + To continue until the longest iterable is exhausted, set *longest* to + ``True``. + + >>> list(zip_offset('0123', 'abcdef', offsets=(0, 1), longest=True)) + [('0', 'b'), ('1', 'c'), ('2', 'd'), ('3', 'e'), (None, 'f')] + + By default, ``None`` will be used to replace offsets beyond the end of the + sequence. Specify *fillvalue* to use some other value. + + """ + if len(iterables) != len(offsets): + raise ValueError("Number of iterables and offsets didn't match") + + staggered = [] + for it, n in zip(iterables, offsets): + if n < 0: + staggered.append(chain(repeat(fillvalue, -n), it)) + elif n > 0: + staggered.append(islice(it, n, None)) + else: + staggered.append(it) + + if longest: + return zip_longest(*staggered, fillvalue=fillvalue) + + return zip(*staggered) + + +def sort_together(iterables, key_list=(0,), key=None, reverse=False): + """Return the input iterables sorted together, with *key_list* as the + priority for sorting. All iterables are trimmed to the length of the + shortest one. + + This can be used like the sorting function in a spreadsheet. If each + iterable represents a column of data, the key list determines which + columns are used for sorting. + + By default, all iterables are sorted using the ``0``-th iterable:: + + >>> iterables = [(4, 3, 2, 1), ('a', 'b', 'c', 'd')] + >>> sort_together(iterables) + [(1, 2, 3, 4), ('d', 'c', 'b', 'a')] + + Set a different key list to sort according to another iterable. + Specifying multiple keys dictates how ties are broken:: + + >>> iterables = [(3, 1, 2), (0, 1, 0), ('c', 'b', 'a')] + >>> sort_together(iterables, key_list=(1, 2)) + [(2, 3, 1), (0, 0, 1), ('a', 'c', 'b')] + + To sort by a function of the elements of the iterable, pass a *key* + function. Its arguments are the elements of the iterables corresponding to + the key list:: + + >>> names = ('a', 'b', 'c') + >>> lengths = (1, 2, 3) + >>> widths = (5, 2, 1) + >>> def area(length, width): + ... return length * width + >>> sort_together([names, lengths, widths], key_list=(1, 2), key=area) + [('c', 'b', 'a'), (3, 2, 1), (1, 2, 5)] + + Set *reverse* to ``True`` to sort in descending order. + + >>> sort_together([(1, 2, 3), ('c', 'b', 'a')], reverse=True) + [(3, 2, 1), ('a', 'b', 'c')] + + """ + if key is None: + # if there is no key function, the key argument to sorted is an + # itemgetter + key_argument = itemgetter(*key_list) + else: + # if there is a key function, call it with the items at the offsets + # specified by the key function as arguments + key_list = list(key_list) + if len(key_list) == 1: + # if key_list contains a single item, pass the item at that offset + # as the only argument to the key function + key_offset = key_list[0] + key_argument = lambda zipped_items: key(zipped_items[key_offset]) + else: + # if key_list contains multiple items, use itemgetter to return a + # tuple of items, which we pass as *args to the key function + get_key_items = itemgetter(*key_list) + key_argument = lambda zipped_items: key( + *get_key_items(zipped_items) + ) + + return list( + zip(*sorted(zip(*iterables), key=key_argument, reverse=reverse)) + ) + + +def unzip(iterable): + """The inverse of :func:`zip`, this function disaggregates the elements + of the zipped *iterable*. + + The ``i``-th iterable contains the ``i``-th element from each element + of the zipped iterable. The first element is used to to determine the + length of the remaining elements. + + >>> iterable = [('a', 1), ('b', 2), ('c', 3), ('d', 4)] + >>> letters, numbers = unzip(iterable) + >>> list(letters) + ['a', 'b', 'c', 'd'] + >>> list(numbers) + [1, 2, 3, 4] + + This is similar to using ``zip(*iterable)``, but it avoids reading + *iterable* into memory. Note, however, that this function uses + :func:`itertools.tee` and thus may require significant storage. + + """ + head, iterable = spy(iter(iterable)) + if not head: + # empty iterable, e.g. zip([], [], []) + return () + # spy returns a one-length iterable as head + head = head[0] + iterables = tee(iterable, len(head)) + + def itemgetter(i): + def getter(obj): + try: + return obj[i] + except IndexError: + # basically if we have an iterable like + # iter([(1, 2, 3), (4, 5), (6,)]) + # the second unzipped iterable would fail at the third tuple + # since it would try to access tup[1] + # same with the third unzipped iterable and the second tuple + # to support these "improperly zipped" iterables, + # we create a custom itemgetter + # which just stops the unzipped iterables + # at first length mismatch + raise StopIteration + + return getter + + return tuple(map(itemgetter(i), it) for i, it in enumerate(iterables)) + + +def divide(n, iterable): + """Divide the elements from *iterable* into *n* parts, maintaining + order. + + >>> group_1, group_2 = divide(2, [1, 2, 3, 4, 5, 6]) + >>> list(group_1) + [1, 2, 3] + >>> list(group_2) + [4, 5, 6] + + If the length of *iterable* is not evenly divisible by *n*, then the + length of the returned iterables will not be identical: + + >>> children = divide(3, [1, 2, 3, 4, 5, 6, 7]) + >>> [list(c) for c in children] + [[1, 2, 3], [4, 5], [6, 7]] + + If the length of the iterable is smaller than n, then the last returned + iterables will be empty: + + >>> children = divide(5, [1, 2, 3]) + >>> [list(c) for c in children] + [[1], [2], [3], [], []] + + This function will exhaust the iterable before returning and may require + significant storage. If order is not important, see :func:`distribute`, + which does not first pull the iterable into memory. + + """ + if n < 1: + raise ValueError('n must be at least 1') + + try: + iterable[:0] + except TypeError: + seq = tuple(iterable) + else: + seq = iterable + + q, r = divmod(len(seq), n) + + ret = [] + stop = 0 + for i in range(1, n + 1): + start = stop + stop += q + 1 if i <= r else q + ret.append(iter(seq[start:stop])) + + return ret + + +def always_iterable(obj, base_type=(str, bytes)): + """If *obj* is iterable, return an iterator over its items:: + + >>> obj = (1, 2, 3) + >>> list(always_iterable(obj)) + [1, 2, 3] + + If *obj* is not iterable, return a one-item iterable containing *obj*:: + + >>> obj = 1 + >>> list(always_iterable(obj)) + [1] + + If *obj* is ``None``, return an empty iterable: + + >>> obj = None + >>> list(always_iterable(None)) + [] + + By default, binary and text strings are not considered iterable:: + + >>> obj = 'foo' + >>> list(always_iterable(obj)) + ['foo'] + + If *base_type* is set, objects for which ``isinstance(obj, base_type)`` + returns ``True`` won't be considered iterable. + + >>> obj = {'a': 1} + >>> list(always_iterable(obj)) # Iterate over the dict's keys + ['a'] + >>> list(always_iterable(obj, base_type=dict)) # Treat dicts as a unit + [{'a': 1}] + + Set *base_type* to ``None`` to avoid any special handling and treat objects + Python considers iterable as iterable: + + >>> obj = 'foo' + >>> list(always_iterable(obj, base_type=None)) + ['f', 'o', 'o'] + """ + if obj is None: + return iter(()) + + if (base_type is not None) and isinstance(obj, base_type): + return iter((obj,)) + + try: + return iter(obj) + except TypeError: + return iter((obj,)) + + +def adjacent(predicate, iterable, distance=1): + """Return an iterable over `(bool, item)` tuples where the `item` is + drawn from *iterable* and the `bool` indicates whether + that item satisfies the *predicate* or is adjacent to an item that does. + + For example, to find whether items are adjacent to a ``3``:: + + >>> list(adjacent(lambda x: x == 3, range(6))) + [(False, 0), (False, 1), (True, 2), (True, 3), (True, 4), (False, 5)] + + Set *distance* to change what counts as adjacent. For example, to find + whether items are two places away from a ``3``: + + >>> list(adjacent(lambda x: x == 3, range(6), distance=2)) + [(False, 0), (True, 1), (True, 2), (True, 3), (True, 4), (True, 5)] + + This is useful for contextualizing the results of a search function. + For example, a code comparison tool might want to identify lines that + have changed, but also surrounding lines to give the viewer of the diff + context. + + The predicate function will only be called once for each item in the + iterable. + + See also :func:`groupby_transform`, which can be used with this function + to group ranges of items with the same `bool` value. + + """ + # Allow distance=0 mainly for testing that it reproduces results with map() + if distance < 0: + raise ValueError('distance must be at least 0') + + i1, i2 = tee(iterable) + padding = [False] * distance + selected = chain(padding, map(predicate, i1), padding) + adjacent_to_selected = map(any, windowed(selected, 2 * distance + 1)) + return zip(adjacent_to_selected, i2) + + +def groupby_transform(iterable, keyfunc=None, valuefunc=None, reducefunc=None): + """An extension of :func:`itertools.groupby` that can apply transformations + to the grouped data. + + * *keyfunc* is a function computing a key value for each item in *iterable* + * *valuefunc* is a function that transforms the individual items from + *iterable* after grouping + * *reducefunc* is a function that transforms each group of items + + >>> iterable = 'aAAbBBcCC' + >>> keyfunc = lambda k: k.upper() + >>> valuefunc = lambda v: v.lower() + >>> reducefunc = lambda g: ''.join(g) + >>> list(groupby_transform(iterable, keyfunc, valuefunc, reducefunc)) + [('A', 'aaa'), ('B', 'bbb'), ('C', 'ccc')] + + Each optional argument defaults to an identity function if not specified. + + :func:`groupby_transform` is useful when grouping elements of an iterable + using a separate iterable as the key. To do this, :func:`zip` the iterables + and pass a *keyfunc* that extracts the first element and a *valuefunc* + that extracts the second element:: + + >>> from operator import itemgetter + >>> keys = [0, 0, 1, 1, 1, 2, 2, 2, 3] + >>> values = 'abcdefghi' + >>> iterable = zip(keys, values) + >>> grouper = groupby_transform(iterable, itemgetter(0), itemgetter(1)) + >>> [(k, ''.join(g)) for k, g in grouper] + [(0, 'ab'), (1, 'cde'), (2, 'fgh'), (3, 'i')] + + Note that the order of items in the iterable is significant. + Only adjacent items are grouped together, so if you don't want any + duplicate groups, you should sort the iterable by the key function. + + """ + ret = groupby(iterable, keyfunc) + if valuefunc: + ret = ((k, map(valuefunc, g)) for k, g in ret) + if reducefunc: + ret = ((k, reducefunc(g)) for k, g in ret) + + return ret + + +class numeric_range(abc.Sequence, abc.Hashable): + """An extension of the built-in ``range()`` function whose arguments can + be any orderable numeric type. + + With only *stop* specified, *start* defaults to ``0`` and *step* + defaults to ``1``. The output items will match the type of *stop*: + + >>> list(numeric_range(3.5)) + [0.0, 1.0, 2.0, 3.0] + + With only *start* and *stop* specified, *step* defaults to ``1``. The + output items will match the type of *start*: + + >>> from decimal import Decimal + >>> start = Decimal('2.1') + >>> stop = Decimal('5.1') + >>> list(numeric_range(start, stop)) + [Decimal('2.1'), Decimal('3.1'), Decimal('4.1')] + + With *start*, *stop*, and *step* specified the output items will match + the type of ``start + step``: + + >>> from fractions import Fraction + >>> start = Fraction(1, 2) # Start at 1/2 + >>> stop = Fraction(5, 2) # End at 5/2 + >>> step = Fraction(1, 2) # Count by 1/2 + >>> list(numeric_range(start, stop, step)) + [Fraction(1, 2), Fraction(1, 1), Fraction(3, 2), Fraction(2, 1)] + + If *step* is zero, ``ValueError`` is raised. Negative steps are supported: + + >>> list(numeric_range(3, -1, -1.0)) + [3.0, 2.0, 1.0, 0.0] + + Be aware of the limitations of floating point numbers; the representation + of the yielded numbers may be surprising. + + ``datetime.datetime`` objects can be used for *start* and *stop*, if *step* + is a ``datetime.timedelta`` object: + + >>> import datetime + >>> start = datetime.datetime(2019, 1, 1) + >>> stop = datetime.datetime(2019, 1, 3) + >>> step = datetime.timedelta(days=1) + >>> items = iter(numeric_range(start, stop, step)) + >>> next(items) + datetime.datetime(2019, 1, 1, 0, 0) + >>> next(items) + datetime.datetime(2019, 1, 2, 0, 0) + + """ + + _EMPTY_HASH = hash(range(0, 0)) + + def __init__(self, *args): + argc = len(args) + if argc == 1: + (self._stop,) = args + self._start = type(self._stop)(0) + self._step = type(self._stop - self._start)(1) + elif argc == 2: + self._start, self._stop = args + self._step = type(self._stop - self._start)(1) + elif argc == 3: + self._start, self._stop, self._step = args + elif argc == 0: + raise TypeError( + 'numeric_range expected at least ' + '1 argument, got {}'.format(argc) + ) + else: + raise TypeError( + 'numeric_range expected at most ' + '3 arguments, got {}'.format(argc) + ) + + self._zero = type(self._step)(0) + if self._step == self._zero: + raise ValueError('numeric_range() arg 3 must not be zero') + self._growing = self._step > self._zero + self._init_len() + + def __bool__(self): + if self._growing: + return self._start < self._stop + else: + return self._start > self._stop + + def __contains__(self, elem): + if self._growing: + if self._start <= elem < self._stop: + return (elem - self._start) % self._step == self._zero + else: + if self._start >= elem > self._stop: + return (self._start - elem) % (-self._step) == self._zero + + return False + + def __eq__(self, other): + if isinstance(other, numeric_range): + empty_self = not bool(self) + empty_other = not bool(other) + if empty_self or empty_other: + return empty_self and empty_other # True if both empty + else: + return ( + self._start == other._start + and self._step == other._step + and self._get_by_index(-1) == other._get_by_index(-1) + ) + else: + return False + + def __getitem__(self, key): + if isinstance(key, int): + return self._get_by_index(key) + elif isinstance(key, slice): + step = self._step if key.step is None else key.step * self._step + + if key.start is None or key.start <= -self._len: + start = self._start + elif key.start >= self._len: + start = self._stop + else: # -self._len < key.start < self._len + start = self._get_by_index(key.start) + + if key.stop is None or key.stop >= self._len: + stop = self._stop + elif key.stop <= -self._len: + stop = self._start + else: # -self._len < key.stop < self._len + stop = self._get_by_index(key.stop) + + return numeric_range(start, stop, step) + else: + raise TypeError( + 'numeric range indices must be ' + 'integers or slices, not {}'.format(type(key).__name__) + ) + + def __hash__(self): + if self: + return hash((self._start, self._get_by_index(-1), self._step)) + else: + return self._EMPTY_HASH + + def __iter__(self): + values = (self._start + (n * self._step) for n in count()) + if self._growing: + return takewhile(partial(gt, self._stop), values) + else: + return takewhile(partial(lt, self._stop), values) + + def __len__(self): + return self._len + + def _init_len(self): + if self._growing: + start = self._start + stop = self._stop + step = self._step + else: + start = self._stop + stop = self._start + step = -self._step + distance = stop - start + if distance <= self._zero: + self._len = 0 + else: # distance > 0 and step > 0: regular euclidean division + q, r = divmod(distance, step) + self._len = int(q) + int(r != self._zero) + + def __reduce__(self): + return numeric_range, (self._start, self._stop, self._step) + + def __repr__(self): + if self._step == 1: + return "numeric_range({}, {})".format( + repr(self._start), repr(self._stop) + ) + else: + return "numeric_range({}, {}, {})".format( + repr(self._start), repr(self._stop), repr(self._step) + ) + + def __reversed__(self): + return iter( + numeric_range( + self._get_by_index(-1), self._start - self._step, -self._step + ) + ) + + def count(self, value): + return int(value in self) + + def index(self, value): + if self._growing: + if self._start <= value < self._stop: + q, r = divmod(value - self._start, self._step) + if r == self._zero: + return int(q) + else: + if self._start >= value > self._stop: + q, r = divmod(self._start - value, -self._step) + if r == self._zero: + return int(q) + + raise ValueError("{} is not in numeric range".format(value)) + + def _get_by_index(self, i): + if i < 0: + i += self._len + if i < 0 or i >= self._len: + raise IndexError("numeric range object index out of range") + return self._start + i * self._step + + +def count_cycle(iterable, n=None): + """Cycle through the items from *iterable* up to *n* times, yielding + the number of completed cycles along with each item. If *n* is omitted the + process repeats indefinitely. + + >>> list(count_cycle('AB', 3)) + [(0, 'A'), (0, 'B'), (1, 'A'), (1, 'B'), (2, 'A'), (2, 'B')] + + """ + iterable = tuple(iterable) + if not iterable: + return iter(()) + counter = count() if n is None else range(n) + return ((i, item) for i in counter for item in iterable) + + +def mark_ends(iterable): + """Yield 3-tuples of the form ``(is_first, is_last, item)``. + + >>> list(mark_ends('ABC')) + [(True, False, 'A'), (False, False, 'B'), (False, True, 'C')] + + Use this when looping over an iterable to take special action on its first + and/or last items: + + >>> iterable = ['Header', 100, 200, 'Footer'] + >>> total = 0 + >>> for is_first, is_last, item in mark_ends(iterable): + ... if is_first: + ... continue # Skip the header + ... if is_last: + ... continue # Skip the footer + ... total += item + >>> print(total) + 300 + """ + it = iter(iterable) + + try: + b = next(it) + except StopIteration: + return + + try: + for i in count(): + a = b + b = next(it) + yield i == 0, False, a + + except StopIteration: + yield i == 0, True, a + + +def locate(iterable, pred=bool, window_size=None): + """Yield the index of each item in *iterable* for which *pred* returns + ``True``. + + *pred* defaults to :func:`bool`, which will select truthy items: + + >>> list(locate([0, 1, 1, 0, 1, 0, 0])) + [1, 2, 4] + + Set *pred* to a custom function to, e.g., find the indexes for a particular + item. + + >>> list(locate(['a', 'b', 'c', 'b'], lambda x: x == 'b')) + [1, 3] + + If *window_size* is given, then the *pred* function will be called with + that many items. This enables searching for sub-sequences: + + >>> iterable = [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3] + >>> pred = lambda *args: args == (1, 2, 3) + >>> list(locate(iterable, pred=pred, window_size=3)) + [1, 5, 9] + + Use with :func:`seekable` to find indexes and then retrieve the associated + items: + + >>> from itertools import count + >>> from more_itertools import seekable + >>> source = (3 * n + 1 if (n % 2) else n // 2 for n in count()) + >>> it = seekable(source) + >>> pred = lambda x: x > 100 + >>> indexes = locate(it, pred=pred) + >>> i = next(indexes) + >>> it.seek(i) + >>> next(it) + 106 + + """ + if window_size is None: + return compress(count(), map(pred, iterable)) + + if window_size < 1: + raise ValueError('window size must be at least 1') + + it = windowed(iterable, window_size, fillvalue=_marker) + return compress(count(), starmap(pred, it)) + + +def lstrip(iterable, pred): + """Yield the items from *iterable*, but strip any from the beginning + for which *pred* returns ``True``. + + For example, to remove a set of items from the start of an iterable: + + >>> iterable = (None, False, None, 1, 2, None, 3, False, None) + >>> pred = lambda x: x in {None, False, ''} + >>> list(lstrip(iterable, pred)) + [1, 2, None, 3, False, None] + + This function is analogous to to :func:`str.lstrip`, and is essentially + an wrapper for :func:`itertools.dropwhile`. + + """ + return dropwhile(pred, iterable) + + +def rstrip(iterable, pred): + """Yield the items from *iterable*, but strip any from the end + for which *pred* returns ``True``. + + For example, to remove a set of items from the end of an iterable: + + >>> iterable = (None, False, None, 1, 2, None, 3, False, None) + >>> pred = lambda x: x in {None, False, ''} + >>> list(rstrip(iterable, pred)) + [None, False, None, 1, 2, None, 3] + + This function is analogous to :func:`str.rstrip`. + + """ + cache = [] + cache_append = cache.append + cache_clear = cache.clear + for x in iterable: + if pred(x): + cache_append(x) + else: + yield from cache + cache_clear() + yield x + + +def strip(iterable, pred): + """Yield the items from *iterable*, but strip any from the + beginning and end for which *pred* returns ``True``. + + For example, to remove a set of items from both ends of an iterable: + + >>> iterable = (None, False, None, 1, 2, None, 3, False, None) + >>> pred = lambda x: x in {None, False, ''} + >>> list(strip(iterable, pred)) + [1, 2, None, 3] + + This function is analogous to :func:`str.strip`. + + """ + return rstrip(lstrip(iterable, pred), pred) + + +class islice_extended: + """An extension of :func:`itertools.islice` that supports negative values + for *stop*, *start*, and *step*. + + >>> iterable = iter('abcdefgh') + >>> list(islice_extended(iterable, -4, -1)) + ['e', 'f', 'g'] + + Slices with negative values require some caching of *iterable*, but this + function takes care to minimize the amount of memory required. + + For example, you can use a negative step with an infinite iterator: + + >>> from itertools import count + >>> list(islice_extended(count(), 110, 99, -2)) + [110, 108, 106, 104, 102, 100] + + You can also use slice notation directly: + + >>> iterable = map(str, count()) + >>> it = islice_extended(iterable)[10:20:2] + >>> list(it) + ['10', '12', '14', '16', '18'] + + """ + + def __init__(self, iterable, *args): + it = iter(iterable) + if args: + self._iterable = _islice_helper(it, slice(*args)) + else: + self._iterable = it + + def __iter__(self): + return self + + def __next__(self): + return next(self._iterable) + + def __getitem__(self, key): + if isinstance(key, slice): + return islice_extended(_islice_helper(self._iterable, key)) + + raise TypeError('islice_extended.__getitem__ argument must be a slice') + + +def _islice_helper(it, s): + start = s.start + stop = s.stop + if s.step == 0: + raise ValueError('step argument must be a non-zero integer or None.') + step = s.step or 1 + + if step > 0: + start = 0 if (start is None) else start + + if start < 0: + # Consume all but the last -start items + cache = deque(enumerate(it, 1), maxlen=-start) + len_iter = cache[-1][0] if cache else 0 + + # Adjust start to be positive + i = max(len_iter + start, 0) + + # Adjust stop to be positive + if stop is None: + j = len_iter + elif stop >= 0: + j = min(stop, len_iter) + else: + j = max(len_iter + stop, 0) + + # Slice the cache + n = j - i + if n <= 0: + return + + for index, item in islice(cache, 0, n, step): + yield item + elif (stop is not None) and (stop < 0): + # Advance to the start position + next(islice(it, start, start), None) + + # When stop is negative, we have to carry -stop items while + # iterating + cache = deque(islice(it, -stop), maxlen=-stop) + + for index, item in enumerate(it): + cached_item = cache.popleft() + if index % step == 0: + yield cached_item + cache.append(item) + else: + # When both start and stop are positive we have the normal case + yield from islice(it, start, stop, step) + else: + start = -1 if (start is None) else start + + if (stop is not None) and (stop < 0): + # Consume all but the last items + n = -stop - 1 + cache = deque(enumerate(it, 1), maxlen=n) + len_iter = cache[-1][0] if cache else 0 + + # If start and stop are both negative they are comparable and + # we can just slice. Otherwise we can adjust start to be negative + # and then slice. + if start < 0: + i, j = start, stop + else: + i, j = min(start - len_iter, -1), None + + for index, item in list(cache)[i:j:step]: + yield item + else: + # Advance to the stop position + if stop is not None: + m = stop + 1 + next(islice(it, m, m), None) + + # stop is positive, so if start is negative they are not comparable + # and we need the rest of the items. + if start < 0: + i = start + n = None + # stop is None and start is positive, so we just need items up to + # the start index. + elif stop is None: + i = None + n = start + 1 + # Both stop and start are positive, so they are comparable. + else: + i = None + n = start - stop + if n <= 0: + return + + cache = list(islice(it, n)) + + yield from cache[i::step] + + +def always_reversible(iterable): + """An extension of :func:`reversed` that supports all iterables, not + just those which implement the ``Reversible`` or ``Sequence`` protocols. + + >>> print(*always_reversible(x for x in range(3))) + 2 1 0 + + If the iterable is already reversible, this function returns the + result of :func:`reversed()`. If the iterable is not reversible, + this function will cache the remaining items in the iterable and + yield them in reverse order, which may require significant storage. + """ + try: + return reversed(iterable) + except TypeError: + return reversed(list(iterable)) + + +def consecutive_groups(iterable, ordering=lambda x: x): + """Yield groups of consecutive items using :func:`itertools.groupby`. + The *ordering* function determines whether two items are adjacent by + returning their position. + + By default, the ordering function is the identity function. This is + suitable for finding runs of numbers: + + >>> iterable = [1, 10, 11, 12, 20, 30, 31, 32, 33, 40] + >>> for group in consecutive_groups(iterable): + ... print(list(group)) + [1] + [10, 11, 12] + [20] + [30, 31, 32, 33] + [40] + + For finding runs of adjacent letters, try using the :meth:`index` method + of a string of letters: + + >>> from string import ascii_lowercase + >>> iterable = 'abcdfgilmnop' + >>> ordering = ascii_lowercase.index + >>> for group in consecutive_groups(iterable, ordering): + ... print(list(group)) + ['a', 'b', 'c', 'd'] + ['f', 'g'] + ['i'] + ['l', 'm', 'n', 'o', 'p'] + + Each group of consecutive items is an iterator that shares it source with + *iterable*. When an an output group is advanced, the previous group is + no longer available unless its elements are copied (e.g., into a ``list``). + + >>> iterable = [1, 2, 11, 12, 21, 22] + >>> saved_groups = [] + >>> for group in consecutive_groups(iterable): + ... saved_groups.append(list(group)) # Copy group elements + >>> saved_groups + [[1, 2], [11, 12], [21, 22]] + + """ + for k, g in groupby( + enumerate(iterable), key=lambda x: x[0] - ordering(x[1]) + ): + yield map(itemgetter(1), g) + + +def difference(iterable, func=sub, *, initial=None): + """This function is the inverse of :func:`itertools.accumulate`. By default + it will compute the first difference of *iterable* using + :func:`operator.sub`: + + >>> from itertools import accumulate + >>> iterable = accumulate([0, 1, 2, 3, 4]) # produces 0, 1, 3, 6, 10 + >>> list(difference(iterable)) + [0, 1, 2, 3, 4] + + *func* defaults to :func:`operator.sub`, but other functions can be + specified. They will be applied as follows:: + + A, B, C, D, ... --> A, func(B, A), func(C, B), func(D, C), ... + + For example, to do progressive division: + + >>> iterable = [1, 2, 6, 24, 120] + >>> func = lambda x, y: x // y + >>> list(difference(iterable, func)) + [1, 2, 3, 4, 5] + + If the *initial* keyword is set, the first element will be skipped when + computing successive differences. + + >>> it = [10, 11, 13, 16] # from accumulate([1, 2, 3], initial=10) + >>> list(difference(it, initial=10)) + [1, 2, 3] + + """ + a, b = tee(iterable) + try: + first = [next(b)] + except StopIteration: + return iter([]) + + if initial is not None: + first = [] + + return chain(first, starmap(func, zip(b, a))) + + +class SequenceView(Sequence): + """Return a read-only view of the sequence object *target*. + + :class:`SequenceView` objects are analogous to Python's built-in + "dictionary view" types. They provide a dynamic view of a sequence's items, + meaning that when the sequence updates, so does the view. + + >>> seq = ['0', '1', '2'] + >>> view = SequenceView(seq) + >>> view + SequenceView(['0', '1', '2']) + >>> seq.append('3') + >>> view + SequenceView(['0', '1', '2', '3']) + + Sequence views support indexing, slicing, and length queries. They act + like the underlying sequence, except they don't allow assignment: + + >>> view[1] + '1' + >>> view[1:-1] + ['1', '2'] + >>> len(view) + 4 + + Sequence views are useful as an alternative to copying, as they don't + require (much) extra storage. + + """ + + def __init__(self, target): + if not isinstance(target, Sequence): + raise TypeError + self._target = target + + def __getitem__(self, index): + return self._target[index] + + def __len__(self): + return len(self._target) + + def __repr__(self): + return '{}({})'.format(self.__class__.__name__, repr(self._target)) + + +class seekable: + """Wrap an iterator to allow for seeking backward and forward. This + progressively caches the items in the source iterable so they can be + re-visited. + + Call :meth:`seek` with an index to seek to that position in the source + iterable. + + To "reset" an iterator, seek to ``0``: + + >>> from itertools import count + >>> it = seekable((str(n) for n in count())) + >>> next(it), next(it), next(it) + ('0', '1', '2') + >>> it.seek(0) + >>> next(it), next(it), next(it) + ('0', '1', '2') + >>> next(it) + '3' + + You can also seek forward: + + >>> it = seekable((str(n) for n in range(20))) + >>> it.seek(10) + >>> next(it) + '10' + >>> it.seek(20) # Seeking past the end of the source isn't a problem + >>> list(it) + [] + >>> it.seek(0) # Resetting works even after hitting the end + >>> next(it), next(it), next(it) + ('0', '1', '2') + + Call :meth:`peek` to look ahead one item without advancing the iterator: + + >>> it = seekable('1234') + >>> it.peek() + '1' + >>> list(it) + ['1', '2', '3', '4'] + >>> it.peek(default='empty') + 'empty' + + Before the iterator is at its end, calling :func:`bool` on it will return + ``True``. After it will return ``False``: + + >>> it = seekable('5678') + >>> bool(it) + True + >>> list(it) + ['5', '6', '7', '8'] + >>> bool(it) + False + + You may view the contents of the cache with the :meth:`elements` method. + That returns a :class:`SequenceView`, a view that updates automatically: + + >>> it = seekable((str(n) for n in range(10))) + >>> next(it), next(it), next(it) + ('0', '1', '2') + >>> elements = it.elements() + >>> elements + SequenceView(['0', '1', '2']) + >>> next(it) + '3' + >>> elements + SequenceView(['0', '1', '2', '3']) + + By default, the cache grows as the source iterable progresses, so beware of + wrapping very large or infinite iterables. Supply *maxlen* to limit the + size of the cache (this of course limits how far back you can seek). + + >>> from itertools import count + >>> it = seekable((str(n) for n in count()), maxlen=2) + >>> next(it), next(it), next(it), next(it) + ('0', '1', '2', '3') + >>> list(it.elements()) + ['2', '3'] + >>> it.seek(0) + >>> next(it), next(it), next(it), next(it) + ('2', '3', '4', '5') + >>> next(it) + '6' + + """ + + def __init__(self, iterable, maxlen=None): + self._source = iter(iterable) + if maxlen is None: + self._cache = [] + else: + self._cache = deque([], maxlen) + self._index = None + + def __iter__(self): + return self + + def __next__(self): + if self._index is not None: + try: + item = self._cache[self._index] + except IndexError: + self._index = None + else: + self._index += 1 + return item + + item = next(self._source) + self._cache.append(item) + return item + + def __bool__(self): + try: + self.peek() + except StopIteration: + return False + return True + + def peek(self, default=_marker): + try: + peeked = next(self) + except StopIteration: + if default is _marker: + raise + return default + if self._index is None: + self._index = len(self._cache) + self._index -= 1 + return peeked + + def elements(self): + return SequenceView(self._cache) + + def seek(self, index): + self._index = index + remainder = index - len(self._cache) + if remainder > 0: + consume(self, remainder) + + +class run_length: + """ + :func:`run_length.encode` compresses an iterable with run-length encoding. + It yields groups of repeated items with the count of how many times they + were repeated: + + >>> uncompressed = 'abbcccdddd' + >>> list(run_length.encode(uncompressed)) + [('a', 1), ('b', 2), ('c', 3), ('d', 4)] + + :func:`run_length.decode` decompresses an iterable that was previously + compressed with run-length encoding. It yields the items of the + decompressed iterable: + + >>> compressed = [('a', 1), ('b', 2), ('c', 3), ('d', 4)] + >>> list(run_length.decode(compressed)) + ['a', 'b', 'b', 'c', 'c', 'c', 'd', 'd', 'd', 'd'] + + """ + + @staticmethod + def encode(iterable): + return ((k, ilen(g)) for k, g in groupby(iterable)) + + @staticmethod + def decode(iterable): + return chain.from_iterable(repeat(k, n) for k, n in iterable) + + +def exactly_n(iterable, n, predicate=bool): + """Return ``True`` if exactly ``n`` items in the iterable are ``True`` + according to the *predicate* function. + + >>> exactly_n([True, True, False], 2) + True + >>> exactly_n([True, True, False], 1) + False + >>> exactly_n([0, 1, 2, 3, 4, 5], 3, lambda x: x < 3) + True + + The iterable will be advanced until ``n + 1`` truthy items are encountered, + so avoid calling it on infinite iterables. + + """ + return len(take(n + 1, filter(predicate, iterable))) == n + + +def circular_shifts(iterable): + """Return a list of circular shifts of *iterable*. + + >>> circular_shifts(range(4)) + [(0, 1, 2, 3), (1, 2, 3, 0), (2, 3, 0, 1), (3, 0, 1, 2)] + """ + lst = list(iterable) + return take(len(lst), windowed(cycle(lst), len(lst))) + + +def make_decorator(wrapping_func, result_index=0): + """Return a decorator version of *wrapping_func*, which is a function that + modifies an iterable. *result_index* is the position in that function's + signature where the iterable goes. + + This lets you use itertools on the "production end," i.e. at function + definition. This can augment what the function returns without changing the + function's code. + + For example, to produce a decorator version of :func:`chunked`: + + >>> from more_itertools import chunked + >>> chunker = make_decorator(chunked, result_index=0) + >>> @chunker(3) + ... def iter_range(n): + ... return iter(range(n)) + ... + >>> list(iter_range(9)) + [[0, 1, 2], [3, 4, 5], [6, 7, 8]] + + To only allow truthy items to be returned: + + >>> truth_serum = make_decorator(filter, result_index=1) + >>> @truth_serum(bool) + ... def boolean_test(): + ... return [0, 1, '', ' ', False, True] + ... + >>> list(boolean_test()) + [1, ' ', True] + + The :func:`peekable` and :func:`seekable` wrappers make for practical + decorators: + + >>> from more_itertools import peekable + >>> peekable_function = make_decorator(peekable) + >>> @peekable_function() + ... def str_range(*args): + ... return (str(x) for x in range(*args)) + ... + >>> it = str_range(1, 20, 2) + >>> next(it), next(it), next(it) + ('1', '3', '5') + >>> it.peek() + '7' + >>> next(it) + '7' + + """ + # See https://sites.google.com/site/bbayles/index/decorator_factory for + # notes on how this works. + def decorator(*wrapping_args, **wrapping_kwargs): + def outer_wrapper(f): + def inner_wrapper(*args, **kwargs): + result = f(*args, **kwargs) + wrapping_args_ = list(wrapping_args) + wrapping_args_.insert(result_index, result) + return wrapping_func(*wrapping_args_, **wrapping_kwargs) + + return inner_wrapper + + return outer_wrapper + + return decorator + + +def map_reduce(iterable, keyfunc, valuefunc=None, reducefunc=None): + """Return a dictionary that maps the items in *iterable* to categories + defined by *keyfunc*, transforms them with *valuefunc*, and + then summarizes them by category with *reducefunc*. + + *valuefunc* defaults to the identity function if it is unspecified. + If *reducefunc* is unspecified, no summarization takes place: + + >>> keyfunc = lambda x: x.upper() + >>> result = map_reduce('abbccc', keyfunc) + >>> sorted(result.items()) + [('A', ['a']), ('B', ['b', 'b']), ('C', ['c', 'c', 'c'])] + + Specifying *valuefunc* transforms the categorized items: + + >>> keyfunc = lambda x: x.upper() + >>> valuefunc = lambda x: 1 + >>> result = map_reduce('abbccc', keyfunc, valuefunc) + >>> sorted(result.items()) + [('A', [1]), ('B', [1, 1]), ('C', [1, 1, 1])] + + Specifying *reducefunc* summarizes the categorized items: + + >>> keyfunc = lambda x: x.upper() + >>> valuefunc = lambda x: 1 + >>> reducefunc = sum + >>> result = map_reduce('abbccc', keyfunc, valuefunc, reducefunc) + >>> sorted(result.items()) + [('A', 1), ('B', 2), ('C', 3)] + + You may want to filter the input iterable before applying the map/reduce + procedure: + + >>> all_items = range(30) + >>> items = [x for x in all_items if 10 <= x <= 20] # Filter + >>> keyfunc = lambda x: x % 2 # Evens map to 0; odds to 1 + >>> categories = map_reduce(items, keyfunc=keyfunc) + >>> sorted(categories.items()) + [(0, [10, 12, 14, 16, 18, 20]), (1, [11, 13, 15, 17, 19])] + >>> summaries = map_reduce(items, keyfunc=keyfunc, reducefunc=sum) + >>> sorted(summaries.items()) + [(0, 90), (1, 75)] + + Note that all items in the iterable are gathered into a list before the + summarization step, which may require significant storage. + + The returned object is a :obj:`collections.defaultdict` with the + ``default_factory`` set to ``None``, such that it behaves like a normal + dictionary. + + """ + valuefunc = (lambda x: x) if (valuefunc is None) else valuefunc + + ret = defaultdict(list) + for item in iterable: + key = keyfunc(item) + value = valuefunc(item) + ret[key].append(value) + + if reducefunc is not None: + for key, value_list in ret.items(): + ret[key] = reducefunc(value_list) + + ret.default_factory = None + return ret + + +def rlocate(iterable, pred=bool, window_size=None): + """Yield the index of each item in *iterable* for which *pred* returns + ``True``, starting from the right and moving left. + + *pred* defaults to :func:`bool`, which will select truthy items: + + >>> list(rlocate([0, 1, 1, 0, 1, 0, 0])) # Truthy at 1, 2, and 4 + [4, 2, 1] + + Set *pred* to a custom function to, e.g., find the indexes for a particular + item: + + >>> iterable = iter('abcb') + >>> pred = lambda x: x == 'b' + >>> list(rlocate(iterable, pred)) + [3, 1] + + If *window_size* is given, then the *pred* function will be called with + that many items. This enables searching for sub-sequences: + + >>> iterable = [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3] + >>> pred = lambda *args: args == (1, 2, 3) + >>> list(rlocate(iterable, pred=pred, window_size=3)) + [9, 5, 1] + + Beware, this function won't return anything for infinite iterables. + If *iterable* is reversible, ``rlocate`` will reverse it and search from + the right. Otherwise, it will search from the left and return the results + in reverse order. + + See :func:`locate` to for other example applications. + + """ + if window_size is None: + try: + len_iter = len(iterable) + return (len_iter - i - 1 for i in locate(reversed(iterable), pred)) + except TypeError: + pass + + return reversed(list(locate(iterable, pred, window_size))) + + +def replace(iterable, pred, substitutes, count=None, window_size=1): + """Yield the items from *iterable*, replacing the items for which *pred* + returns ``True`` with the items from the iterable *substitutes*. + + >>> iterable = [1, 1, 0, 1, 1, 0, 1, 1] + >>> pred = lambda x: x == 0 + >>> substitutes = (2, 3) + >>> list(replace(iterable, pred, substitutes)) + [1, 1, 2, 3, 1, 1, 2, 3, 1, 1] + + If *count* is given, the number of replacements will be limited: + + >>> iterable = [1, 1, 0, 1, 1, 0, 1, 1, 0] + >>> pred = lambda x: x == 0 + >>> substitutes = [None] + >>> list(replace(iterable, pred, substitutes, count=2)) + [1, 1, None, 1, 1, None, 1, 1, 0] + + Use *window_size* to control the number of items passed as arguments to + *pred*. This allows for locating and replacing subsequences. + + >>> iterable = [0, 1, 2, 5, 0, 1, 2, 5] + >>> window_size = 3 + >>> pred = lambda *args: args == (0, 1, 2) # 3 items passed to pred + >>> substitutes = [3, 4] # Splice in these items + >>> list(replace(iterable, pred, substitutes, window_size=window_size)) + [3, 4, 5, 3, 4, 5] + + """ + if window_size < 1: + raise ValueError('window_size must be at least 1') + + # Save the substitutes iterable, since it's used more than once + substitutes = tuple(substitutes) + + # Add padding such that the number of windows matches the length of the + # iterable + it = chain(iterable, [_marker] * (window_size - 1)) + windows = windowed(it, window_size) + + n = 0 + for w in windows: + # If the current window matches our predicate (and we haven't hit + # our maximum number of replacements), splice in the substitutes + # and then consume the following windows that overlap with this one. + # For example, if the iterable is (0, 1, 2, 3, 4...) + # and the window size is 2, we have (0, 1), (1, 2), (2, 3)... + # If the predicate matches on (0, 1), we need to zap (0, 1) and (1, 2) + if pred(*w): + if (count is None) or (n < count): + n += 1 + yield from substitutes + consume(windows, window_size - 1) + continue + + # If there was no match (or we've reached the replacement limit), + # yield the first item from the window. + if w and (w[0] is not _marker): + yield w[0] + + +def partitions(iterable): + """Yield all possible order-preserving partitions of *iterable*. + + >>> iterable = 'abc' + >>> for part in partitions(iterable): + ... print([''.join(p) for p in part]) + ['abc'] + ['a', 'bc'] + ['ab', 'c'] + ['a', 'b', 'c'] + + This is unrelated to :func:`partition`. + + """ + sequence = list(iterable) + n = len(sequence) + for i in powerset(range(1, n)): + yield [sequence[i:j] for i, j in zip((0,) + i, i + (n,))] + + +def set_partitions(iterable, k=None): + """ + Yield the set partitions of *iterable* into *k* parts. Set partitions are + not order-preserving. + + >>> iterable = 'abc' + >>> for part in set_partitions(iterable, 2): + ... print([''.join(p) for p in part]) + ['a', 'bc'] + ['ab', 'c'] + ['b', 'ac'] + + + If *k* is not given, every set partition is generated. + + >>> iterable = 'abc' + >>> for part in set_partitions(iterable): + ... print([''.join(p) for p in part]) + ['abc'] + ['a', 'bc'] + ['ab', 'c'] + ['b', 'ac'] + ['a', 'b', 'c'] + + """ + L = list(iterable) + n = len(L) + if k is not None: + if k < 1: + raise ValueError( + "Can't partition in a negative or zero number of groups" + ) + elif k > n: + return + + def set_partitions_helper(L, k): + n = len(L) + if k == 1: + yield [L] + elif n == k: + yield [[s] for s in L] + else: + e, *M = L + for p in set_partitions_helper(M, k - 1): + yield [[e], *p] + for p in set_partitions_helper(M, k): + for i in range(len(p)): + yield p[:i] + [[e] + p[i]] + p[i + 1 :] + + if k is None: + for k in range(1, n + 1): + yield from set_partitions_helper(L, k) + else: + yield from set_partitions_helper(L, k) + + +class time_limited: + """ + Yield items from *iterable* until *limit_seconds* have passed. + If the time limit expires before all items have been yielded, the + ``timed_out`` parameter will be set to ``True``. + + >>> from time import sleep + >>> def generator(): + ... yield 1 + ... yield 2 + ... sleep(0.2) + ... yield 3 + >>> iterable = time_limited(0.1, generator()) + >>> list(iterable) + [1, 2] + >>> iterable.timed_out + True + + Note that the time is checked before each item is yielded, and iteration + stops if the time elapsed is greater than *limit_seconds*. If your time + limit is 1 second, but it takes 2 seconds to generate the first item from + the iterable, the function will run for 2 seconds and not yield anything. + + """ + + def __init__(self, limit_seconds, iterable): + if limit_seconds < 0: + raise ValueError('limit_seconds must be positive') + self.limit_seconds = limit_seconds + self._iterable = iter(iterable) + self._start_time = monotonic() + self.timed_out = False + + def __iter__(self): + return self + + def __next__(self): + item = next(self._iterable) + if monotonic() - self._start_time > self.limit_seconds: + self.timed_out = True + raise StopIteration + + return item + + +def only(iterable, default=None, too_long=None): + """If *iterable* has only one item, return it. + If it has zero items, return *default*. + If it has more than one item, raise the exception given by *too_long*, + which is ``ValueError`` by default. + + >>> only([], default='missing') + 'missing' + >>> only([1]) + 1 + >>> only([1, 2]) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ValueError: Expected exactly one item in iterable, but got 1, 2, + and perhaps more.' + >>> only([1, 2], too_long=TypeError) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + TypeError + + Note that :func:`only` attempts to advance *iterable* twice to ensure there + is only one item. See :func:`spy` or :func:`peekable` to check + iterable contents less destructively. + """ + it = iter(iterable) + first_value = next(it, default) + + try: + second_value = next(it) + except StopIteration: + pass + else: + msg = ( + 'Expected exactly one item in iterable, but got {!r}, {!r}, ' + 'and perhaps more.'.format(first_value, second_value) + ) + raise too_long or ValueError(msg) + + return first_value + + +def ichunked(iterable, n): + """Break *iterable* into sub-iterables with *n* elements each. + :func:`ichunked` is like :func:`chunked`, but it yields iterables + instead of lists. + + If the sub-iterables are read in order, the elements of *iterable* + won't be stored in memory. + If they are read out of order, :func:`itertools.tee` is used to cache + elements as necessary. + + >>> from itertools import count + >>> all_chunks = ichunked(count(), 4) + >>> c_1, c_2, c_3 = next(all_chunks), next(all_chunks), next(all_chunks) + >>> list(c_2) # c_1's elements have been cached; c_3's haven't been + [4, 5, 6, 7] + >>> list(c_1) + [0, 1, 2, 3] + >>> list(c_3) + [8, 9, 10, 11] + + """ + source = iter(iterable) + + while True: + # Check to see whether we're at the end of the source iterable + item = next(source, _marker) + if item is _marker: + return + + # Clone the source and yield an n-length slice + source, it = tee(chain([item], source)) + yield islice(it, n) + + # Advance the source iterable + consume(source, n) + + +def distinct_combinations(iterable, r): + """Yield the distinct combinations of *r* items taken from *iterable*. + + >>> list(distinct_combinations([0, 0, 1], 2)) + [(0, 0), (0, 1)] + + Equivalent to ``set(combinations(iterable))``, except duplicates are not + generated and thrown away. For larger input sequences this is much more + efficient. + + """ + if r < 0: + raise ValueError('r must be non-negative') + elif r == 0: + yield () + return + pool = tuple(iterable) + generators = [unique_everseen(enumerate(pool), key=itemgetter(1))] + current_combo = [None] * r + level = 0 + while generators: + try: + cur_idx, p = next(generators[-1]) + except StopIteration: + generators.pop() + level -= 1 + continue + current_combo[level] = p + if level + 1 == r: + yield tuple(current_combo) + else: + generators.append( + unique_everseen( + enumerate(pool[cur_idx + 1 :], cur_idx + 1), + key=itemgetter(1), + ) + ) + level += 1 + + +def filter_except(validator, iterable, *exceptions): + """Yield the items from *iterable* for which the *validator* function does + not raise one of the specified *exceptions*. + + *validator* is called for each item in *iterable*. + It should be a function that accepts one argument and raises an exception + if that item is not valid. + + >>> iterable = ['1', '2', 'three', '4', None] + >>> list(filter_except(int, iterable, ValueError, TypeError)) + ['1', '2', '4'] + + If an exception other than one given by *exceptions* is raised by + *validator*, it is raised like normal. + """ + for item in iterable: + try: + validator(item) + except exceptions: + pass + else: + yield item + + +def map_except(function, iterable, *exceptions): + """Transform each item from *iterable* with *function* and yield the + result, unless *function* raises one of the specified *exceptions*. + + *function* is called to transform each item in *iterable*. + It should accept one argument. + + >>> iterable = ['1', '2', 'three', '4', None] + >>> list(map_except(int, iterable, ValueError, TypeError)) + [1, 2, 4] + + If an exception other than one given by *exceptions* is raised by + *function*, it is raised like normal. + """ + for item in iterable: + try: + yield function(item) + except exceptions: + pass + + +def map_if(iterable, pred, func, func_else=lambda x: x): + """Evaluate each item from *iterable* using *pred*. If the result is + equivalent to ``True``, transform the item with *func* and yield it. + Otherwise, transform the item with *func_else* and yield it. + + *pred*, *func*, and *func_else* should each be functions that accept + one argument. By default, *func_else* is the identity function. + + >>> from math import sqrt + >>> iterable = list(range(-5, 5)) + >>> iterable + [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4] + >>> list(map_if(iterable, lambda x: x > 3, lambda x: 'toobig')) + [-5, -4, -3, -2, -1, 0, 1, 2, 3, 'toobig'] + >>> list(map_if(iterable, lambda x: x >= 0, + ... lambda x: f'{sqrt(x):.2f}', lambda x: None)) + [None, None, None, None, None, '0.00', '1.00', '1.41', '1.73', '2.00'] + """ + for item in iterable: + yield func(item) if pred(item) else func_else(item) + + +def _sample_unweighted(iterable, k): + # Implementation of "Algorithm L" from the 1994 paper by Kim-Hung Li: + # "Reservoir-Sampling Algorithms of Time Complexity O(n(1+log(N/n)))". + + # Fill up the reservoir (collection of samples) with the first `k` samples + reservoir = take(k, iterable) + + # Generate random number that's the largest in a sample of k U(0,1) numbers + # Largest order statistic: https://en.wikipedia.org/wiki/Order_statistic + W = exp(log(random()) / k) + + # The number of elements to skip before changing the reservoir is a random + # number with a geometric distribution. Sample it using random() and logs. + next_index = k + floor(log(random()) / log(1 - W)) + + for index, element in enumerate(iterable, k): + + if index == next_index: + reservoir[randrange(k)] = element + # The new W is the largest in a sample of k U(0, `old_W`) numbers + W *= exp(log(random()) / k) + next_index += floor(log(random()) / log(1 - W)) + 1 + + return reservoir + + +def _sample_weighted(iterable, k, weights): + # Implementation of "A-ExpJ" from the 2006 paper by Efraimidis et al. : + # "Weighted random sampling with a reservoir". + + # Log-transform for numerical stability for weights that are small/large + weight_keys = (log(random()) / weight for weight in weights) + + # Fill up the reservoir (collection of samples) with the first `k` + # weight-keys and elements, then heapify the list. + reservoir = take(k, zip(weight_keys, iterable)) + heapify(reservoir) + + # The number of jumps before changing the reservoir is a random variable + # with an exponential distribution. Sample it using random() and logs. + smallest_weight_key, _ = reservoir[0] + weights_to_skip = log(random()) / smallest_weight_key + + for weight, element in zip(weights, iterable): + if weight >= weights_to_skip: + # The notation here is consistent with the paper, but we store + # the weight-keys in log-space for better numerical stability. + smallest_weight_key, _ = reservoir[0] + t_w = exp(weight * smallest_weight_key) + r_2 = uniform(t_w, 1) # generate U(t_w, 1) + weight_key = log(r_2) / weight + heapreplace(reservoir, (weight_key, element)) + smallest_weight_key, _ = reservoir[0] + weights_to_skip = log(random()) / smallest_weight_key + else: + weights_to_skip -= weight + + # Equivalent to [element for weight_key, element in sorted(reservoir)] + return [heappop(reservoir)[1] for _ in range(k)] + + +def sample(iterable, k, weights=None): + """Return a *k*-length list of elements chosen (without replacement) + from the *iterable*. Like :func:`random.sample`, but works on iterables + of unknown length. + + >>> iterable = range(100) + >>> sample(iterable, 5) # doctest: +SKIP + [81, 60, 96, 16, 4] + + An iterable with *weights* may also be given: + + >>> iterable = range(100) + >>> weights = (i * i + 1 for i in range(100)) + >>> sampled = sample(iterable, 5, weights=weights) # doctest: +SKIP + [79, 67, 74, 66, 78] + + The algorithm can also be used to generate weighted random permutations. + The relative weight of each item determines the probability that it + appears late in the permutation. + + >>> data = "abcdefgh" + >>> weights = range(1, len(data) + 1) + >>> sample(data, k=len(data), weights=weights) # doctest: +SKIP + ['c', 'a', 'b', 'e', 'g', 'd', 'h', 'f'] + """ + if k == 0: + return [] + + iterable = iter(iterable) + if weights is None: + return _sample_unweighted(iterable, k) + else: + weights = iter(weights) + return _sample_weighted(iterable, k, weights) + + +def is_sorted(iterable, key=None, reverse=False, strict=False): + """Returns ``True`` if the items of iterable are in sorted order, and + ``False`` otherwise. *key* and *reverse* have the same meaning that they do + in the built-in :func:`sorted` function. + + >>> is_sorted(['1', '2', '3', '4', '5'], key=int) + True + >>> is_sorted([5, 4, 3, 1, 2], reverse=True) + False + + If *strict*, tests for strict sorting, that is, returns ``False`` if equal + elements are found: + + >>> is_sorted([1, 2, 2]) + True + >>> is_sorted([1, 2, 2], strict=True) + False + + The function returns ``False`` after encountering the first out-of-order + item. If there are no out-of-order items, the iterable is exhausted. + """ + + compare = (le if reverse else ge) if strict else (lt if reverse else gt) + it = iterable if key is None else map(key, iterable) + return not any(starmap(compare, pairwise(it))) + + +class AbortThread(BaseException): + pass + + +class callback_iter: + """Convert a function that uses callbacks to an iterator. + + Let *func* be a function that takes a `callback` keyword argument. + For example: + + >>> def func(callback=None): + ... for i, c in [(1, 'a'), (2, 'b'), (3, 'c')]: + ... if callback: + ... callback(i, c) + ... return 4 + + + Use ``with callback_iter(func)`` to get an iterator over the parameters + that are delivered to the callback. + + >>> with callback_iter(func) as it: + ... for args, kwargs in it: + ... print(args) + (1, 'a') + (2, 'b') + (3, 'c') + + The function will be called in a background thread. The ``done`` property + indicates whether it has completed execution. + + >>> it.done + True + + If it completes successfully, its return value will be available + in the ``result`` property. + + >>> it.result + 4 + + Notes: + + * If the function uses some keyword argument besides ``callback``, supply + *callback_kwd*. + * If it finished executing, but raised an exception, accessing the + ``result`` property will raise the same exception. + * If it hasn't finished executing, accessing the ``result`` + property from within the ``with`` block will raise ``RuntimeError``. + * If it hasn't finished executing, accessing the ``result`` property from + outside the ``with`` block will raise a + ``more_itertools.AbortThread`` exception. + * Provide *wait_seconds* to adjust how frequently the it is polled for + output. + + """ + + def __init__(self, func, callback_kwd='callback', wait_seconds=0.1): + self._func = func + self._callback_kwd = callback_kwd + self._aborted = False + self._future = None + self._wait_seconds = wait_seconds + self._executor = ThreadPoolExecutor(max_workers=1) + self._iterator = self._reader() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self._aborted = True + self._executor.shutdown() + + def __iter__(self): + return self + + def __next__(self): + return next(self._iterator) + + @property + def done(self): + if self._future is None: + return False + return self._future.done() + + @property + def result(self): + if not self.done: + raise RuntimeError('Function has not yet completed') + + return self._future.result() + + def _reader(self): + q = Queue() + + def callback(*args, **kwargs): + if self._aborted: + raise AbortThread('canceled by user') + + q.put((args, kwargs)) + + self._future = self._executor.submit( + self._func, **{self._callback_kwd: callback} + ) + + while True: + try: + item = q.get(timeout=self._wait_seconds) + except Empty: + pass + else: + q.task_done() + yield item + + if self._future.done(): + break + + remaining = [] + while True: + try: + item = q.get_nowait() + except Empty: + break + else: + q.task_done() + remaining.append(item) + q.join() + yield from remaining + + +def windowed_complete(iterable, n): + """ + Yield ``(beginning, middle, end)`` tuples, where: + + * Each ``middle`` has *n* items from *iterable* + * Each ``beginning`` has the items before the ones in ``middle`` + * Each ``end`` has the items after the ones in ``middle`` + + >>> iterable = range(7) + >>> n = 3 + >>> for beginning, middle, end in windowed_complete(iterable, n): + ... print(beginning, middle, end) + () (0, 1, 2) (3, 4, 5, 6) + (0,) (1, 2, 3) (4, 5, 6) + (0, 1) (2, 3, 4) (5, 6) + (0, 1, 2) (3, 4, 5) (6,) + (0, 1, 2, 3) (4, 5, 6) () + + Note that *n* must be at least 0 and most equal to the length of + *iterable*. + + This function will exhaust the iterable and may require significant + storage. + """ + if n < 0: + raise ValueError('n must be >= 0') + + seq = tuple(iterable) + size = len(seq) + + if n > size: + raise ValueError('n must be <= len(seq)') + + for i in range(size - n + 1): + beginning = seq[:i] + middle = seq[i : i + n] + end = seq[i + n :] + yield beginning, middle, end + + +def all_unique(iterable, key=None): + """ + Returns ``True`` if all the elements of *iterable* are unique (no two + elements are equal). + + >>> all_unique('ABCB') + False + + If a *key* function is specified, it will be used to make comparisons. + + >>> all_unique('ABCb') + True + >>> all_unique('ABCb', str.lower) + False + + The function returns as soon as the first non-unique element is + encountered. Iterables with a mix of hashable and unhashable items can + be used, but the function will be slower for unhashable items. + """ + seenset = set() + seenset_add = seenset.add + seenlist = [] + seenlist_add = seenlist.append + for element in map(key, iterable) if key else iterable: + try: + if element in seenset: + return False + seenset_add(element) + except TypeError: + if element in seenlist: + return False + seenlist_add(element) + return True + + +def nth_product(index, *args): + """Equivalent to ``list(product(*args))[index]``. + + The products of *args* can be ordered lexicographically. + :func:`nth_product` computes the product at sort position *index* without + computing the previous products. + + >>> nth_product(8, range(2), range(2), range(2), range(2)) + (1, 0, 0, 0) + + ``IndexError`` will be raised if the given *index* is invalid. + """ + pools = list(map(tuple, reversed(args))) + ns = list(map(len, pools)) + + c = reduce(mul, ns) + + if index < 0: + index += c + + if not 0 <= index < c: + raise IndexError + + result = [] + for pool, n in zip(pools, ns): + result.append(pool[index % n]) + index //= n + + return tuple(reversed(result)) + + +def nth_permutation(iterable, r, index): + """Equivalent to ``list(permutations(iterable, r))[index]``` + + The subsequences of *iterable* that are of length *r* where order is + important can be ordered lexicographically. :func:`nth_permutation` + computes the subsequence at sort position *index* directly, without + computing the previous subsequences. + + >>> nth_permutation('ghijk', 2, 5) + ('h', 'i') + + ``ValueError`` will be raised If *r* is negative or greater than the length + of *iterable*. + ``IndexError`` will be raised if the given *index* is invalid. + """ + pool = list(iterable) + n = len(pool) + + if r is None or r == n: + r, c = n, factorial(n) + elif not 0 <= r < n: + raise ValueError + else: + c = factorial(n) // factorial(n - r) + + if index < 0: + index += c + + if not 0 <= index < c: + raise IndexError + + if c == 0: + return tuple() + + result = [0] * r + q = index * factorial(n) // c if r < n else index + for d in range(1, n + 1): + q, i = divmod(q, d) + if 0 <= n - d < r: + result[n - d] = i + if q == 0: + break + + return tuple(map(pool.pop, result)) + + +def value_chain(*args): + """Yield all arguments passed to the function in the same order in which + they were passed. If an argument itself is iterable then iterate over its + values. + + >>> list(value_chain(1, 2, 3, [4, 5, 6])) + [1, 2, 3, 4, 5, 6] + + Binary and text strings are not considered iterable and are emitted + as-is: + + >>> list(value_chain('12', '34', ['56', '78'])) + ['12', '34', '56', '78'] + + + Multiple levels of nesting are not flattened. + + """ + for value in args: + if isinstance(value, (str, bytes)): + yield value + continue + try: + yield from value + except TypeError: + yield value + + +def product_index(element, *args): + """Equivalent to ``list(product(*args)).index(element)`` + + The products of *args* can be ordered lexicographically. + :func:`product_index` computes the first index of *element* without + computing the previous products. + + >>> product_index([8, 2], range(10), range(5)) + 42 + + ``ValueError`` will be raised if the given *element* isn't in the product + of *args*. + """ + index = 0 + + for x, pool in zip_longest(element, args, fillvalue=_marker): + if x is _marker or pool is _marker: + raise ValueError('element is not a product of args') + + pool = tuple(pool) + index = index * len(pool) + pool.index(x) + + return index + + +def combination_index(element, iterable): + """Equivalent to ``list(combinations(iterable, r)).index(element)`` + + The subsequences of *iterable* that are of length *r* can be ordered + lexicographically. :func:`combination_index` computes the index of the + first *element*, without computing the previous combinations. + + >>> combination_index('adf', 'abcdefg') + 10 + + ``ValueError`` will be raised if the given *element* isn't one of the + combinations of *iterable*. + """ + element = enumerate(element) + k, y = next(element, (None, None)) + if k is None: + return 0 + + indexes = [] + pool = enumerate(iterable) + for n, x in pool: + if x == y: + indexes.append(n) + tmp, y = next(element, (None, None)) + if tmp is None: + break + else: + k = tmp + else: + raise ValueError('element is not a combination of iterable') + + n, _ = last(pool, default=(n, None)) + + # Python versiosn below 3.8 don't have math.comb + index = 1 + for i, j in enumerate(reversed(indexes), start=1): + j = n - j + if i <= j: + index += factorial(j) // (factorial(i) * factorial(j - i)) + + return factorial(n + 1) // (factorial(k + 1) * factorial(n - k)) - index + + +def permutation_index(element, iterable): + """Equivalent to ``list(permutations(iterable, r)).index(element)``` + + The subsequences of *iterable* that are of length *r* where order is + important can be ordered lexicographically. :func:`permutation_index` + computes the index of the first *element* directly, without computing + the previous permutations. + + >>> permutation_index([1, 3, 2], range(5)) + 19 + + ``ValueError`` will be raised if the given *element* isn't one of the + permutations of *iterable*. + """ + index = 0 + pool = list(iterable) + for i, x in zip(range(len(pool), -1, -1), element): + r = pool.index(x) + index = index * i + r + del pool[r] + + return index + + +class countable: + """Wrap *iterable* and keep a count of how many items have been consumed. + + The ``items_seen`` attribute starts at ``0`` and increments as the iterable + is consumed: + + >>> iterable = map(str, range(10)) + >>> it = countable(iterable) + >>> it.items_seen + 0 + >>> next(it), next(it) + ('0', '1') + >>> list(it) + ['2', '3', '4', '5', '6', '7', '8', '9'] + >>> it.items_seen + 10 + """ + + def __init__(self, iterable): + self._it = iter(iterable) + self.items_seen = 0 + + def __iter__(self): + return self + + def __next__(self): + item = next(self._it) + self.items_seen += 1 + + return item + + +def chunked_even(iterable, n): + """Break *iterable* into lists of approximately length *n*. + Items are distributed such the lengths of the lists differ by at most + 1 item. + + >>> iterable = [1, 2, 3, 4, 5, 6, 7] + >>> n = 3 + >>> list(chunked_even(iterable, n)) # List lengths: 3, 2, 2 + [[1, 2, 3], [4, 5], [6, 7]] + >>> list(chunked(iterable, n)) # List lengths: 3, 3, 1 + [[1, 2, 3], [4, 5, 6], [7]] + + """ + + len_method = getattr(iterable, '__len__', None) + + if len_method is None: + return _chunked_even_online(iterable, n) + else: + return _chunked_even_finite(iterable, len_method(), n) + + +def _chunked_even_online(iterable, n): + buffer = [] + maxbuf = n + (n - 2) * (n - 1) + for x in iterable: + buffer.append(x) + if len(buffer) == maxbuf: + yield buffer[:n] + buffer = buffer[n:] + yield from _chunked_even_finite(buffer, len(buffer), n) + + +def _chunked_even_finite(iterable, N, n): + if N < 1: + return + + # Lists are either size `full_size <= n` or `partial_size = full_size - 1` + q, r = divmod(N, n) + num_lists = q + (1 if r > 0 else 0) + q, r = divmod(N, num_lists) + full_size = q + (1 if r > 0 else 0) + partial_size = full_size - 1 + num_full = N - partial_size * num_lists + num_partial = num_lists - num_full + + buffer = [] + iterator = iter(iterable) + + # Yield num_full lists of full_size + for x in iterator: + buffer.append(x) + if len(buffer) == full_size: + yield buffer + buffer = [] + num_full -= 1 + if num_full <= 0: + break + + # Yield num_partial lists of partial_size + for x in iterator: + buffer.append(x) + if len(buffer) == partial_size: + yield buffer + buffer = [] + num_partial -= 1 + + +def zip_broadcast(*objects, scalar_types=(str, bytes), strict=False): + """A version of :func:`zip` that "broadcasts" any scalar + (i.e., non-iterable) items into output tuples. + + >>> iterable_1 = [1, 2, 3] + >>> iterable_2 = ['a', 'b', 'c'] + >>> scalar = '_' + >>> list(zip_broadcast(iterable_1, iterable_2, scalar)) + [(1, 'a', '_'), (2, 'b', '_'), (3, 'c', '_')] + + The *scalar_types* keyword argument determines what types are considered + scalar. It is set to ``(str, bytes)`` by default. Set it to ``None`` to + treat strings and byte strings as iterable: + + >>> list(zip_broadcast('abc', 0, 'xyz', scalar_types=None)) + [('a', 0, 'x'), ('b', 0, 'y'), ('c', 0, 'z')] + + If the *strict* keyword argument is ``True``, then + ``UnequalIterablesError`` will be raised if any of the iterables have + different lengthss. + """ + + def is_scalar(obj): + if scalar_types and isinstance(obj, scalar_types): + return True + try: + iter(obj) + except TypeError: + return True + else: + return False + + size = len(objects) + if not size: + return + + iterables, iterable_positions = [], [] + scalars, scalar_positions = [], [] + for i, obj in enumerate(objects): + if is_scalar(obj): + scalars.append(obj) + scalar_positions.append(i) + else: + iterables.append(iter(obj)) + iterable_positions.append(i) + + if len(scalars) == size: + yield tuple(objects) + return + + zipper = _zip_equal if strict else zip + for item in zipper(*iterables): + new_item = [None] * size + + for i, elem in zip(iterable_positions, item): + new_item[i] = elem + + for i, elem in zip(scalar_positions, scalars): + new_item[i] = elem + + yield tuple(new_item) + + +def unique_in_window(iterable, n, key=None): + """Yield the items from *iterable* that haven't been seen recently. + *n* is the size of the lookback window. + + >>> iterable = [0, 1, 0, 2, 3, 0] + >>> n = 3 + >>> list(unique_in_window(iterable, n)) + [0, 1, 2, 3, 0] + + The *key* function, if provided, will be used to determine uniqueness: + + >>> list(unique_in_window('abAcda', 3, key=lambda x: x.lower())) + ['a', 'b', 'c', 'd', 'a'] + + The items in *iterable* must be hashable. + + """ + if n <= 0: + raise ValueError('n must be greater than 0') + + window = deque(maxlen=n) + uniques = set() + use_key = key is not None + + for item in iterable: + k = key(item) if use_key else item + if k in uniques: + continue + + if len(uniques) == n: + uniques.discard(window[0]) + + uniques.add(k) + window.append(k) + + yield item + + +def duplicates_everseen(iterable, key=None): + """Yield duplicate elements after their first appearance. + + >>> list(duplicates_everseen('mississippi')) + ['s', 'i', 's', 's', 'i', 'p', 'i'] + >>> list(duplicates_everseen('AaaBbbCccAaa', str.lower)) + ['a', 'a', 'b', 'b', 'c', 'c', 'A', 'a', 'a'] + + This function is analagous to :func:`unique_everseen` and is subject to + the same performance considerations. + + """ + seen_set = set() + seen_list = [] + use_key = key is not None + + for element in iterable: + k = key(element) if use_key else element + try: + if k not in seen_set: + seen_set.add(k) + else: + yield element + except TypeError: + if k not in seen_list: + seen_list.append(k) + else: + yield element + + +def duplicates_justseen(iterable, key=None): + """Yields serially-duplicate elements after their first appearance. + + >>> list(duplicates_justseen('mississippi')) + ['s', 's', 'p'] + >>> list(duplicates_justseen('AaaBbbCccAaa', str.lower)) + ['a', 'a', 'b', 'b', 'c', 'c', 'a', 'a'] + + This function is analagous to :func:`unique_justseen`. + + """ + return flatten( + map( + lambda group_tuple: islice_extended(group_tuple[1])[1:], + groupby(iterable, key), + ) + ) + + +def minmax(iterable_or_value, *others, key=None, default=_marker): + """Returns both the smallest and largest items in an iterable + or the largest of two or more arguments. + + >>> minmax([3, 1, 5]) + (1, 5) + + >>> minmax(4, 2, 6) + (2, 6) + + If a *key* function is provided, it will be used to transform the input + items for comparison. + + >>> minmax([5, 30], key=str) # '30' sorts before '5' + (30, 5) + + If a *default* value is provided, it will be returned if there are no + input items. + + >>> minmax([], default=(0, 0)) + (0, 0) + + Otherwise ``ValueError`` is raised. + + This function is based on the + `recipe `__ by + Raymond Hettinger and takes care to minimize the number of comparisons + performed. + """ + iterable = (iterable_or_value, *others) if others else iterable_or_value + + it = iter(iterable) + + try: + lo = hi = next(it) + except StopIteration as e: + if default is _marker: + raise ValueError( + '`minmax()` argument is an empty iterable. ' + 'Provide a `default` value to suppress this error.' + ) from e + return default + + # Different branches depending on the presence of key. This saves a lot + # of unimportant copies which would slow the "key=None" branch + # significantly down. + if key is None: + for x, y in zip_longest(it, it, fillvalue=lo): + if y < x: + x, y = y, x + if x < lo: + lo = x + if hi < y: + hi = y + + else: + lo_key = hi_key = key(lo) + + for x, y in zip_longest(it, it, fillvalue=lo): + + x_key, y_key = key(x), key(y) + + if y_key < x_key: + x, y, x_key, y_key = y, x, y_key, x_key + if x_key < lo_key: + lo, lo_key = x, x_key + if hi_key < y_key: + hi, hi_key = y, y_key + + return lo, hi diff --git a/pkg_resources/_vendor/more_itertools/more.pyi b/pkg_resources/_vendor/more_itertools/more.pyi new file mode 100644 index 00000000..fe7d4bdd --- /dev/null +++ b/pkg_resources/_vendor/more_itertools/more.pyi @@ -0,0 +1,664 @@ +"""Stubs for more_itertools.more""" + +from typing import ( + Any, + Callable, + Container, + Dict, + Generic, + Hashable, + Iterable, + Iterator, + List, + Optional, + Reversible, + Sequence, + Sized, + Tuple, + Union, + TypeVar, + type_check_only, +) +from types import TracebackType +from typing_extensions import ContextManager, Protocol, Type, overload + +# Type and type variable definitions +_T = TypeVar('_T') +_T1 = TypeVar('_T1') +_T2 = TypeVar('_T2') +_U = TypeVar('_U') +_V = TypeVar('_V') +_W = TypeVar('_W') +_T_co = TypeVar('_T_co', covariant=True) +_GenFn = TypeVar('_GenFn', bound=Callable[..., Iterator[object]]) +_Raisable = Union[BaseException, 'Type[BaseException]'] + +@type_check_only +class _SizedIterable(Protocol[_T_co], Sized, Iterable[_T_co]): ... + +@type_check_only +class _SizedReversible(Protocol[_T_co], Sized, Reversible[_T_co]): ... + +def chunked( + iterable: Iterable[_T], n: Optional[int], strict: bool = ... +) -> Iterator[List[_T]]: ... +@overload +def first(iterable: Iterable[_T]) -> _T: ... +@overload +def first(iterable: Iterable[_T], default: _U) -> Union[_T, _U]: ... +@overload +def last(iterable: Iterable[_T]) -> _T: ... +@overload +def last(iterable: Iterable[_T], default: _U) -> Union[_T, _U]: ... +@overload +def nth_or_last(iterable: Iterable[_T], n: int) -> _T: ... +@overload +def nth_or_last( + iterable: Iterable[_T], n: int, default: _U +) -> Union[_T, _U]: ... + +class peekable(Generic[_T], Iterator[_T]): + def __init__(self, iterable: Iterable[_T]) -> None: ... + def __iter__(self) -> peekable[_T]: ... + def __bool__(self) -> bool: ... + @overload + def peek(self) -> _T: ... + @overload + def peek(self, default: _U) -> Union[_T, _U]: ... + def prepend(self, *items: _T) -> None: ... + def __next__(self) -> _T: ... + @overload + def __getitem__(self, index: int) -> _T: ... + @overload + def __getitem__(self, index: slice) -> List[_T]: ... + +def collate(*iterables: Iterable[_T], **kwargs: Any) -> Iterable[_T]: ... +def consumer(func: _GenFn) -> _GenFn: ... +def ilen(iterable: Iterable[object]) -> int: ... +def iterate(func: Callable[[_T], _T], start: _T) -> Iterator[_T]: ... +def with_iter( + context_manager: ContextManager[Iterable[_T]], +) -> Iterator[_T]: ... +def one( + iterable: Iterable[_T], + too_short: Optional[_Raisable] = ..., + too_long: Optional[_Raisable] = ..., +) -> _T: ... +def raise_(exception: _Raisable, *args: Any) -> None: ... +def strictly_n( + iterable: Iterable[_T], + n: int, + too_short: Optional[_GenFn] = ..., + too_long: Optional[_GenFn] = ..., +) -> List[_T]: ... +def distinct_permutations( + iterable: Iterable[_T], r: Optional[int] = ... +) -> Iterator[Tuple[_T, ...]]: ... +def intersperse( + e: _U, iterable: Iterable[_T], n: int = ... +) -> Iterator[Union[_T, _U]]: ... +def unique_to_each(*iterables: Iterable[_T]) -> List[List[_T]]: ... +@overload +def windowed( + seq: Iterable[_T], n: int, *, step: int = ... +) -> Iterator[Tuple[Optional[_T], ...]]: ... +@overload +def windowed( + seq: Iterable[_T], n: int, fillvalue: _U, step: int = ... +) -> Iterator[Tuple[Union[_T, _U], ...]]: ... +def substrings(iterable: Iterable[_T]) -> Iterator[Tuple[_T, ...]]: ... +def substrings_indexes( + seq: Sequence[_T], reverse: bool = ... +) -> Iterator[Tuple[Sequence[_T], int, int]]: ... + +class bucket(Generic[_T, _U], Container[_U]): + def __init__( + self, + iterable: Iterable[_T], + key: Callable[[_T], _U], + validator: Optional[Callable[[object], object]] = ..., + ) -> None: ... + def __contains__(self, value: object) -> bool: ... + def __iter__(self) -> Iterator[_U]: ... + def __getitem__(self, value: object) -> Iterator[_T]: ... + +def spy( + iterable: Iterable[_T], n: int = ... +) -> Tuple[List[_T], Iterator[_T]]: ... +def interleave(*iterables: Iterable[_T]) -> Iterator[_T]: ... +def interleave_longest(*iterables: Iterable[_T]) -> Iterator[_T]: ... +def interleave_evenly( + iterables: List[Iterable[_T]], lengths: Optional[List[int]] = ... +) -> Iterator[_T]: ... +def collapse( + iterable: Iterable[Any], + base_type: Optional[type] = ..., + levels: Optional[int] = ..., +) -> Iterator[Any]: ... +@overload +def side_effect( + func: Callable[[_T], object], + iterable: Iterable[_T], + chunk_size: None = ..., + before: Optional[Callable[[], object]] = ..., + after: Optional[Callable[[], object]] = ..., +) -> Iterator[_T]: ... +@overload +def side_effect( + func: Callable[[List[_T]], object], + iterable: Iterable[_T], + chunk_size: int, + before: Optional[Callable[[], object]] = ..., + after: Optional[Callable[[], object]] = ..., +) -> Iterator[_T]: ... +def sliced( + seq: Sequence[_T], n: int, strict: bool = ... +) -> Iterator[Sequence[_T]]: ... +def split_at( + iterable: Iterable[_T], + pred: Callable[[_T], object], + maxsplit: int = ..., + keep_separator: bool = ..., +) -> Iterator[List[_T]]: ... +def split_before( + iterable: Iterable[_T], pred: Callable[[_T], object], maxsplit: int = ... +) -> Iterator[List[_T]]: ... +def split_after( + iterable: Iterable[_T], pred: Callable[[_T], object], maxsplit: int = ... +) -> Iterator[List[_T]]: ... +def split_when( + iterable: Iterable[_T], + pred: Callable[[_T, _T], object], + maxsplit: int = ..., +) -> Iterator[List[_T]]: ... +def split_into( + iterable: Iterable[_T], sizes: Iterable[Optional[int]] +) -> Iterator[List[_T]]: ... +@overload +def padded( + iterable: Iterable[_T], + *, + n: Optional[int] = ..., + next_multiple: bool = ... +) -> Iterator[Optional[_T]]: ... +@overload +def padded( + iterable: Iterable[_T], + fillvalue: _U, + n: Optional[int] = ..., + next_multiple: bool = ..., +) -> Iterator[Union[_T, _U]]: ... +@overload +def repeat_last(iterable: Iterable[_T]) -> Iterator[_T]: ... +@overload +def repeat_last( + iterable: Iterable[_T], default: _U +) -> Iterator[Union[_T, _U]]: ... +def distribute(n: int, iterable: Iterable[_T]) -> List[Iterator[_T]]: ... +@overload +def stagger( + iterable: Iterable[_T], + offsets: _SizedIterable[int] = ..., + longest: bool = ..., +) -> Iterator[Tuple[Optional[_T], ...]]: ... +@overload +def stagger( + iterable: Iterable[_T], + offsets: _SizedIterable[int] = ..., + longest: bool = ..., + fillvalue: _U = ..., +) -> Iterator[Tuple[Union[_T, _U], ...]]: ... + +class UnequalIterablesError(ValueError): + def __init__( + self, details: Optional[Tuple[int, int, int]] = ... + ) -> None: ... + +@overload +def zip_equal(__iter1: Iterable[_T1]) -> Iterator[Tuple[_T1]]: ... +@overload +def zip_equal( + __iter1: Iterable[_T1], __iter2: Iterable[_T2] +) -> Iterator[Tuple[_T1, _T2]]: ... +@overload +def zip_equal( + __iter1: Iterable[_T], + __iter2: Iterable[_T], + __iter3: Iterable[_T], + *iterables: Iterable[_T] +) -> Iterator[Tuple[_T, ...]]: ... +@overload +def zip_offset( + __iter1: Iterable[_T1], + *, + offsets: _SizedIterable[int], + longest: bool = ..., + fillvalue: None = None +) -> Iterator[Tuple[Optional[_T1]]]: ... +@overload +def zip_offset( + __iter1: Iterable[_T1], + __iter2: Iterable[_T2], + *, + offsets: _SizedIterable[int], + longest: bool = ..., + fillvalue: None = None +) -> Iterator[Tuple[Optional[_T1], Optional[_T2]]]: ... +@overload +def zip_offset( + __iter1: Iterable[_T], + __iter2: Iterable[_T], + __iter3: Iterable[_T], + *iterables: Iterable[_T], + offsets: _SizedIterable[int], + longest: bool = ..., + fillvalue: None = None +) -> Iterator[Tuple[Optional[_T], ...]]: ... +@overload +def zip_offset( + __iter1: Iterable[_T1], + *, + offsets: _SizedIterable[int], + longest: bool = ..., + fillvalue: _U, +) -> Iterator[Tuple[Union[_T1, _U]]]: ... +@overload +def zip_offset( + __iter1: Iterable[_T1], + __iter2: Iterable[_T2], + *, + offsets: _SizedIterable[int], + longest: bool = ..., + fillvalue: _U, +) -> Iterator[Tuple[Union[_T1, _U], Union[_T2, _U]]]: ... +@overload +def zip_offset( + __iter1: Iterable[_T], + __iter2: Iterable[_T], + __iter3: Iterable[_T], + *iterables: Iterable[_T], + offsets: _SizedIterable[int], + longest: bool = ..., + fillvalue: _U, +) -> Iterator[Tuple[Union[_T, _U], ...]]: ... +def sort_together( + iterables: Iterable[Iterable[_T]], + key_list: Iterable[int] = ..., + key: Optional[Callable[..., Any]] = ..., + reverse: bool = ..., +) -> List[Tuple[_T, ...]]: ... +def unzip(iterable: Iterable[Sequence[_T]]) -> Tuple[Iterator[_T], ...]: ... +def divide(n: int, iterable: Iterable[_T]) -> List[Iterator[_T]]: ... +def always_iterable( + obj: object, + base_type: Union[ + type, Tuple[Union[type, Tuple[Any, ...]], ...], None + ] = ..., +) -> Iterator[Any]: ... +def adjacent( + predicate: Callable[[_T], bool], + iterable: Iterable[_T], + distance: int = ..., +) -> Iterator[Tuple[bool, _T]]: ... +@overload +def groupby_transform( + iterable: Iterable[_T], + keyfunc: None = None, + valuefunc: None = None, + reducefunc: None = None, +) -> Iterator[Tuple[_T, Iterator[_T]]]: ... +@overload +def groupby_transform( + iterable: Iterable[_T], + keyfunc: Callable[[_T], _U], + valuefunc: None, + reducefunc: None, +) -> Iterator[Tuple[_U, Iterator[_T]]]: ... +@overload +def groupby_transform( + iterable: Iterable[_T], + keyfunc: None, + valuefunc: Callable[[_T], _V], + reducefunc: None, +) -> Iterable[Tuple[_T, Iterable[_V]]]: ... +@overload +def groupby_transform( + iterable: Iterable[_T], + keyfunc: Callable[[_T], _U], + valuefunc: Callable[[_T], _V], + reducefunc: None, +) -> Iterable[Tuple[_U, Iterator[_V]]]: ... +@overload +def groupby_transform( + iterable: Iterable[_T], + keyfunc: None, + valuefunc: None, + reducefunc: Callable[[Iterator[_T]], _W], +) -> Iterable[Tuple[_T, _W]]: ... +@overload +def groupby_transform( + iterable: Iterable[_T], + keyfunc: Callable[[_T], _U], + valuefunc: None, + reducefunc: Callable[[Iterator[_T]], _W], +) -> Iterable[Tuple[_U, _W]]: ... +@overload +def groupby_transform( + iterable: Iterable[_T], + keyfunc: None, + valuefunc: Callable[[_T], _V], + reducefunc: Callable[[Iterable[_V]], _W], +) -> Iterable[Tuple[_T, _W]]: ... +@overload +def groupby_transform( + iterable: Iterable[_T], + keyfunc: Callable[[_T], _U], + valuefunc: Callable[[_T], _V], + reducefunc: Callable[[Iterable[_V]], _W], +) -> Iterable[Tuple[_U, _W]]: ... + +class numeric_range(Generic[_T, _U], Sequence[_T], Hashable, Reversible[_T]): + @overload + def __init__(self, __stop: _T) -> None: ... + @overload + def __init__(self, __start: _T, __stop: _T) -> None: ... + @overload + def __init__(self, __start: _T, __stop: _T, __step: _U) -> None: ... + def __bool__(self) -> bool: ... + def __contains__(self, elem: object) -> bool: ... + def __eq__(self, other: object) -> bool: ... + @overload + def __getitem__(self, key: int) -> _T: ... + @overload + def __getitem__(self, key: slice) -> numeric_range[_T, _U]: ... + def __hash__(self) -> int: ... + def __iter__(self) -> Iterator[_T]: ... + def __len__(self) -> int: ... + def __reduce__( + self, + ) -> Tuple[Type[numeric_range[_T, _U]], Tuple[_T, _T, _U]]: ... + def __repr__(self) -> str: ... + def __reversed__(self) -> Iterator[_T]: ... + def count(self, value: _T) -> int: ... + def index(self, value: _T) -> int: ... # type: ignore + +def count_cycle( + iterable: Iterable[_T], n: Optional[int] = ... +) -> Iterable[Tuple[int, _T]]: ... +def mark_ends( + iterable: Iterable[_T], +) -> Iterable[Tuple[bool, bool, _T]]: ... +def locate( + iterable: Iterable[object], + pred: Callable[..., Any] = ..., + window_size: Optional[int] = ..., +) -> Iterator[int]: ... +def lstrip( + iterable: Iterable[_T], pred: Callable[[_T], object] +) -> Iterator[_T]: ... +def rstrip( + iterable: Iterable[_T], pred: Callable[[_T], object] +) -> Iterator[_T]: ... +def strip( + iterable: Iterable[_T], pred: Callable[[_T], object] +) -> Iterator[_T]: ... + +class islice_extended(Generic[_T], Iterator[_T]): + def __init__( + self, iterable: Iterable[_T], *args: Optional[int] + ) -> None: ... + def __iter__(self) -> islice_extended[_T]: ... + def __next__(self) -> _T: ... + def __getitem__(self, index: slice) -> islice_extended[_T]: ... + +def always_reversible(iterable: Iterable[_T]) -> Iterator[_T]: ... +def consecutive_groups( + iterable: Iterable[_T], ordering: Callable[[_T], int] = ... +) -> Iterator[Iterator[_T]]: ... +@overload +def difference( + iterable: Iterable[_T], + func: Callable[[_T, _T], _U] = ..., + *, + initial: None = ... +) -> Iterator[Union[_T, _U]]: ... +@overload +def difference( + iterable: Iterable[_T], func: Callable[[_T, _T], _U] = ..., *, initial: _U +) -> Iterator[_U]: ... + +class SequenceView(Generic[_T], Sequence[_T]): + def __init__(self, target: Sequence[_T]) -> None: ... + @overload + def __getitem__(self, index: int) -> _T: ... + @overload + def __getitem__(self, index: slice) -> Sequence[_T]: ... + def __len__(self) -> int: ... + +class seekable(Generic[_T], Iterator[_T]): + def __init__( + self, iterable: Iterable[_T], maxlen: Optional[int] = ... + ) -> None: ... + def __iter__(self) -> seekable[_T]: ... + def __next__(self) -> _T: ... + def __bool__(self) -> bool: ... + @overload + def peek(self) -> _T: ... + @overload + def peek(self, default: _U) -> Union[_T, _U]: ... + def elements(self) -> SequenceView[_T]: ... + def seek(self, index: int) -> None: ... + +class run_length: + @staticmethod + def encode(iterable: Iterable[_T]) -> Iterator[Tuple[_T, int]]: ... + @staticmethod + def decode(iterable: Iterable[Tuple[_T, int]]) -> Iterator[_T]: ... + +def exactly_n( + iterable: Iterable[_T], n: int, predicate: Callable[[_T], object] = ... +) -> bool: ... +def circular_shifts(iterable: Iterable[_T]) -> List[Tuple[_T, ...]]: ... +def make_decorator( + wrapping_func: Callable[..., _U], result_index: int = ... +) -> Callable[..., Callable[[Callable[..., Any]], Callable[..., _U]]]: ... +@overload +def map_reduce( + iterable: Iterable[_T], + keyfunc: Callable[[_T], _U], + valuefunc: None = ..., + reducefunc: None = ..., +) -> Dict[_U, List[_T]]: ... +@overload +def map_reduce( + iterable: Iterable[_T], + keyfunc: Callable[[_T], _U], + valuefunc: Callable[[_T], _V], + reducefunc: None = ..., +) -> Dict[_U, List[_V]]: ... +@overload +def map_reduce( + iterable: Iterable[_T], + keyfunc: Callable[[_T], _U], + valuefunc: None = ..., + reducefunc: Callable[[List[_T]], _W] = ..., +) -> Dict[_U, _W]: ... +@overload +def map_reduce( + iterable: Iterable[_T], + keyfunc: Callable[[_T], _U], + valuefunc: Callable[[_T], _V], + reducefunc: Callable[[List[_V]], _W], +) -> Dict[_U, _W]: ... +def rlocate( + iterable: Iterable[_T], + pred: Callable[..., object] = ..., + window_size: Optional[int] = ..., +) -> Iterator[int]: ... +def replace( + iterable: Iterable[_T], + pred: Callable[..., object], + substitutes: Iterable[_U], + count: Optional[int] = ..., + window_size: int = ..., +) -> Iterator[Union[_T, _U]]: ... +def partitions(iterable: Iterable[_T]) -> Iterator[List[List[_T]]]: ... +def set_partitions( + iterable: Iterable[_T], k: Optional[int] = ... +) -> Iterator[List[List[_T]]]: ... + +class time_limited(Generic[_T], Iterator[_T]): + def __init__( + self, limit_seconds: float, iterable: Iterable[_T] + ) -> None: ... + def __iter__(self) -> islice_extended[_T]: ... + def __next__(self) -> _T: ... + +@overload +def only( + iterable: Iterable[_T], *, too_long: Optional[_Raisable] = ... +) -> Optional[_T]: ... +@overload +def only( + iterable: Iterable[_T], default: _U, too_long: Optional[_Raisable] = ... +) -> Union[_T, _U]: ... +def ichunked(iterable: Iterable[_T], n: int) -> Iterator[Iterator[_T]]: ... +def distinct_combinations( + iterable: Iterable[_T], r: int +) -> Iterator[Tuple[_T, ...]]: ... +def filter_except( + validator: Callable[[Any], object], + iterable: Iterable[_T], + *exceptions: Type[BaseException] +) -> Iterator[_T]: ... +def map_except( + function: Callable[[Any], _U], + iterable: Iterable[_T], + *exceptions: Type[BaseException] +) -> Iterator[_U]: ... +def map_if( + iterable: Iterable[Any], + pred: Callable[[Any], bool], + func: Callable[[Any], Any], + func_else: Optional[Callable[[Any], Any]] = ..., +) -> Iterator[Any]: ... +def sample( + iterable: Iterable[_T], + k: int, + weights: Optional[Iterable[float]] = ..., +) -> List[_T]: ... +def is_sorted( + iterable: Iterable[_T], + key: Optional[Callable[[_T], _U]] = ..., + reverse: bool = False, + strict: bool = False, +) -> bool: ... + +class AbortThread(BaseException): + pass + +class callback_iter(Generic[_T], Iterator[_T]): + def __init__( + self, + func: Callable[..., Any], + callback_kwd: str = ..., + wait_seconds: float = ..., + ) -> None: ... + def __enter__(self) -> callback_iter[_T]: ... + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> Optional[bool]: ... + def __iter__(self) -> callback_iter[_T]: ... + def __next__(self) -> _T: ... + def _reader(self) -> Iterator[_T]: ... + @property + def done(self) -> bool: ... + @property + def result(self) -> Any: ... + +def windowed_complete( + iterable: Iterable[_T], n: int +) -> Iterator[Tuple[_T, ...]]: ... +def all_unique( + iterable: Iterable[_T], key: Optional[Callable[[_T], _U]] = ... +) -> bool: ... +def nth_product(index: int, *args: Iterable[_T]) -> Tuple[_T, ...]: ... +def nth_permutation( + iterable: Iterable[_T], r: int, index: int +) -> Tuple[_T, ...]: ... +def value_chain(*args: Union[_T, Iterable[_T]]) -> Iterable[_T]: ... +def product_index(element: Iterable[_T], *args: Iterable[_T]) -> int: ... +def combination_index( + element: Iterable[_T], iterable: Iterable[_T] +) -> int: ... +def permutation_index( + element: Iterable[_T], iterable: Iterable[_T] +) -> int: ... +def repeat_each(iterable: Iterable[_T], n: int = ...) -> Iterator[_T]: ... + +class countable(Generic[_T], Iterator[_T]): + def __init__(self, iterable: Iterable[_T]) -> None: ... + def __iter__(self) -> countable[_T]: ... + def __next__(self) -> _T: ... + +def chunked_even(iterable: Iterable[_T], n: int) -> Iterator[List[_T]]: ... +def zip_broadcast( + *objects: Union[_T, Iterable[_T]], + scalar_types: Union[ + type, Tuple[Union[type, Tuple[Any, ...]], ...], None + ] = ..., + strict: bool = ... +) -> Iterable[Tuple[_T, ...]]: ... +def unique_in_window( + iterable: Iterable[_T], n: int, key: Optional[Callable[[_T], _U]] = ... +) -> Iterator[_T]: ... +def duplicates_everseen( + iterable: Iterable[_T], key: Optional[Callable[[_T], _U]] = ... +) -> Iterator[_T]: ... +def duplicates_justseen( + iterable: Iterable[_T], key: Optional[Callable[[_T], _U]] = ... +) -> Iterator[_T]: ... + +class _SupportsLessThan(Protocol): + def __lt__(self, __other: Any) -> bool: ... + +_SupportsLessThanT = TypeVar("_SupportsLessThanT", bound=_SupportsLessThan) + +@overload +def minmax( + iterable_or_value: Iterable[_SupportsLessThanT], *, key: None = None +) -> Tuple[_SupportsLessThanT, _SupportsLessThanT]: ... +@overload +def minmax( + iterable_or_value: Iterable[_T], *, key: Callable[[_T], _SupportsLessThan] +) -> Tuple[_T, _T]: ... +@overload +def minmax( + iterable_or_value: Iterable[_SupportsLessThanT], + *, + key: None = None, + default: _U +) -> Union[_U, Tuple[_SupportsLessThanT, _SupportsLessThanT]]: ... +@overload +def minmax( + iterable_or_value: Iterable[_T], + *, + key: Callable[[_T], _SupportsLessThan], + default: _U, +) -> Union[_U, Tuple[_T, _T]]: ... +@overload +def minmax( + iterable_or_value: _SupportsLessThanT, + __other: _SupportsLessThanT, + *others: _SupportsLessThanT +) -> Tuple[_SupportsLessThanT, _SupportsLessThanT]: ... +@overload +def minmax( + iterable_or_value: _T, + __other: _T, + *others: _T, + key: Callable[[_T], _SupportsLessThan] +) -> Tuple[_T, _T]: ... diff --git a/pkg_resources/_vendor/more_itertools/py.typed b/pkg_resources/_vendor/more_itertools/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/pkg_resources/_vendor/more_itertools/recipes.py b/pkg_resources/_vendor/more_itertools/recipes.py new file mode 100644 index 00000000..a2596423 --- /dev/null +++ b/pkg_resources/_vendor/more_itertools/recipes.py @@ -0,0 +1,698 @@ +"""Imported from the recipes section of the itertools documentation. + +All functions taken from the recipes section of the itertools library docs +[1]_. +Some backward-compatible usability improvements have been made. + +.. [1] http://docs.python.org/library/itertools.html#recipes + +""" +import warnings +from collections import deque +from itertools import ( + chain, + combinations, + count, + cycle, + groupby, + islice, + repeat, + starmap, + tee, + zip_longest, +) +import operator +from random import randrange, sample, choice + +__all__ = [ + 'all_equal', + 'before_and_after', + 'consume', + 'convolve', + 'dotproduct', + 'first_true', + 'flatten', + 'grouper', + 'iter_except', + 'ncycles', + 'nth', + 'nth_combination', + 'padnone', + 'pad_none', + 'pairwise', + 'partition', + 'powerset', + 'prepend', + 'quantify', + 'random_combination_with_replacement', + 'random_combination', + 'random_permutation', + 'random_product', + 'repeatfunc', + 'roundrobin', + 'sliding_window', + 'tabulate', + 'tail', + 'take', + 'triplewise', + 'unique_everseen', + 'unique_justseen', +] + + +def take(n, iterable): + """Return first *n* items of the iterable as a list. + + >>> take(3, range(10)) + [0, 1, 2] + + If there are fewer than *n* items in the iterable, all of them are + returned. + + >>> take(10, range(3)) + [0, 1, 2] + + """ + return list(islice(iterable, n)) + + +def tabulate(function, start=0): + """Return an iterator over the results of ``func(start)``, + ``func(start + 1)``, ``func(start + 2)``... + + *func* should be a function that accepts one integer argument. + + If *start* is not specified it defaults to 0. It will be incremented each + time the iterator is advanced. + + >>> square = lambda x: x ** 2 + >>> iterator = tabulate(square, -3) + >>> take(4, iterator) + [9, 4, 1, 0] + + """ + return map(function, count(start)) + + +def tail(n, iterable): + """Return an iterator over the last *n* items of *iterable*. + + >>> t = tail(3, 'ABCDEFG') + >>> list(t) + ['E', 'F', 'G'] + + """ + return iter(deque(iterable, maxlen=n)) + + +def consume(iterator, n=None): + """Advance *iterable* by *n* steps. If *n* is ``None``, consume it + entirely. + + Efficiently exhausts an iterator without returning values. Defaults to + consuming the whole iterator, but an optional second argument may be + provided to limit consumption. + + >>> i = (x for x in range(10)) + >>> next(i) + 0 + >>> consume(i, 3) + >>> next(i) + 4 + >>> consume(i) + >>> next(i) + Traceback (most recent call last): + File "", line 1, in + StopIteration + + If the iterator has fewer items remaining than the provided limit, the + whole iterator will be consumed. + + >>> i = (x for x in range(3)) + >>> consume(i, 5) + >>> next(i) + Traceback (most recent call last): + File "", line 1, in + StopIteration + + """ + # Use functions that consume iterators at C speed. + if n is None: + # feed the entire iterator into a zero-length deque + deque(iterator, maxlen=0) + else: + # advance to the empty slice starting at position n + next(islice(iterator, n, n), None) + + +def nth(iterable, n, default=None): + """Returns the nth item or a default value. + + >>> l = range(10) + >>> nth(l, 3) + 3 + >>> nth(l, 20, "zebra") + 'zebra' + + """ + return next(islice(iterable, n, None), default) + + +def all_equal(iterable): + """ + Returns ``True`` if all the elements are equal to each other. + + >>> all_equal('aaaa') + True + >>> all_equal('aaab') + False + + """ + g = groupby(iterable) + return next(g, True) and not next(g, False) + + +def quantify(iterable, pred=bool): + """Return the how many times the predicate is true. + + >>> quantify([True, False, True]) + 2 + + """ + return sum(map(pred, iterable)) + + +def pad_none(iterable): + """Returns the sequence of elements and then returns ``None`` indefinitely. + + >>> take(5, pad_none(range(3))) + [0, 1, 2, None, None] + + Useful for emulating the behavior of the built-in :func:`map` function. + + See also :func:`padded`. + + """ + return chain(iterable, repeat(None)) + + +padnone = pad_none + + +def ncycles(iterable, n): + """Returns the sequence elements *n* times + + >>> list(ncycles(["a", "b"], 3)) + ['a', 'b', 'a', 'b', 'a', 'b'] + + """ + return chain.from_iterable(repeat(tuple(iterable), n)) + + +def dotproduct(vec1, vec2): + """Returns the dot product of the two iterables. + + >>> dotproduct([10, 10], [20, 20]) + 400 + + """ + return sum(map(operator.mul, vec1, vec2)) + + +def flatten(listOfLists): + """Return an iterator flattening one level of nesting in a list of lists. + + >>> list(flatten([[0, 1], [2, 3]])) + [0, 1, 2, 3] + + See also :func:`collapse`, which can flatten multiple levels of nesting. + + """ + return chain.from_iterable(listOfLists) + + +def repeatfunc(func, times=None, *args): + """Call *func* with *args* repeatedly, returning an iterable over the + results. + + If *times* is specified, the iterable will terminate after that many + repetitions: + + >>> from operator import add + >>> times = 4 + >>> args = 3, 5 + >>> list(repeatfunc(add, times, *args)) + [8, 8, 8, 8] + + If *times* is ``None`` the iterable will not terminate: + + >>> from random import randrange + >>> times = None + >>> args = 1, 11 + >>> take(6, repeatfunc(randrange, times, *args)) # doctest:+SKIP + [2, 4, 8, 1, 8, 4] + + """ + if times is None: + return starmap(func, repeat(args)) + return starmap(func, repeat(args, times)) + + +def _pairwise(iterable): + """Returns an iterator of paired items, overlapping, from the original + + >>> take(4, pairwise(count())) + [(0, 1), (1, 2), (2, 3), (3, 4)] + + On Python 3.10 and above, this is an alias for :func:`itertools.pairwise`. + + """ + a, b = tee(iterable) + next(b, None) + yield from zip(a, b) + + +try: + from itertools import pairwise as itertools_pairwise +except ImportError: + pairwise = _pairwise +else: + + def pairwise(iterable): + yield from itertools_pairwise(iterable) + + pairwise.__doc__ = _pairwise.__doc__ + + +def grouper(iterable, n, fillvalue=None): + """Collect data into fixed-length chunks or blocks. + + >>> list(grouper('ABCDEFG', 3, 'x')) + [('A', 'B', 'C'), ('D', 'E', 'F'), ('G', 'x', 'x')] + + """ + if isinstance(iterable, int): + warnings.warn( + "grouper expects iterable as first parameter", DeprecationWarning + ) + n, iterable = iterable, n + args = [iter(iterable)] * n + return zip_longest(fillvalue=fillvalue, *args) + + +def roundrobin(*iterables): + """Yields an item from each iterable, alternating between them. + + >>> list(roundrobin('ABC', 'D', 'EF')) + ['A', 'D', 'E', 'B', 'F', 'C'] + + This function produces the same output as :func:`interleave_longest`, but + may perform better for some inputs (in particular when the number of + iterables is small). + + """ + # Recipe credited to George Sakkis + pending = len(iterables) + nexts = cycle(iter(it).__next__ for it in iterables) + while pending: + try: + for next in nexts: + yield next() + except StopIteration: + pending -= 1 + nexts = cycle(islice(nexts, pending)) + + +def partition(pred, iterable): + """ + Returns a 2-tuple of iterables derived from the input iterable. + The first yields the items that have ``pred(item) == False``. + The second yields the items that have ``pred(item) == True``. + + >>> is_odd = lambda x: x % 2 != 0 + >>> iterable = range(10) + >>> even_items, odd_items = partition(is_odd, iterable) + >>> list(even_items), list(odd_items) + ([0, 2, 4, 6, 8], [1, 3, 5, 7, 9]) + + If *pred* is None, :func:`bool` is used. + + >>> iterable = [0, 1, False, True, '', ' '] + >>> false_items, true_items = partition(None, iterable) + >>> list(false_items), list(true_items) + ([0, False, ''], [1, True, ' ']) + + """ + if pred is None: + pred = bool + + evaluations = ((pred(x), x) for x in iterable) + t1, t2 = tee(evaluations) + return ( + (x for (cond, x) in t1 if not cond), + (x for (cond, x) in t2 if cond), + ) + + +def powerset(iterable): + """Yields all possible subsets of the iterable. + + >>> list(powerset([1, 2, 3])) + [(), (1,), (2,), (3,), (1, 2), (1, 3), (2, 3), (1, 2, 3)] + + :func:`powerset` will operate on iterables that aren't :class:`set` + instances, so repeated elements in the input will produce repeated elements + in the output. Use :func:`unique_everseen` on the input to avoid generating + duplicates: + + >>> seq = [1, 1, 0] + >>> list(powerset(seq)) + [(), (1,), (1,), (0,), (1, 1), (1, 0), (1, 0), (1, 1, 0)] + >>> from more_itertools import unique_everseen + >>> list(powerset(unique_everseen(seq))) + [(), (1,), (0,), (1, 0)] + + """ + s = list(iterable) + return chain.from_iterable(combinations(s, r) for r in range(len(s) + 1)) + + +def unique_everseen(iterable, key=None): + """ + Yield unique elements, preserving order. + + >>> list(unique_everseen('AAAABBBCCDAABBB')) + ['A', 'B', 'C', 'D'] + >>> list(unique_everseen('ABBCcAD', str.lower)) + ['A', 'B', 'C', 'D'] + + Sequences with a mix of hashable and unhashable items can be used. + The function will be slower (i.e., `O(n^2)`) for unhashable items. + + Remember that ``list`` objects are unhashable - you can use the *key* + parameter to transform the list to a tuple (which is hashable) to + avoid a slowdown. + + >>> iterable = ([1, 2], [2, 3], [1, 2]) + >>> list(unique_everseen(iterable)) # Slow + [[1, 2], [2, 3]] + >>> list(unique_everseen(iterable, key=tuple)) # Faster + [[1, 2], [2, 3]] + + Similary, you may want to convert unhashable ``set`` objects with + ``key=frozenset``. For ``dict`` objects, + ``key=lambda x: frozenset(x.items())`` can be used. + + """ + seenset = set() + seenset_add = seenset.add + seenlist = [] + seenlist_add = seenlist.append + use_key = key is not None + + for element in iterable: + k = key(element) if use_key else element + try: + if k not in seenset: + seenset_add(k) + yield element + except TypeError: + if k not in seenlist: + seenlist_add(k) + yield element + + +def unique_justseen(iterable, key=None): + """Yields elements in order, ignoring serial duplicates + + >>> list(unique_justseen('AAAABBBCCDAABBB')) + ['A', 'B', 'C', 'D', 'A', 'B'] + >>> list(unique_justseen('ABBCcAD', str.lower)) + ['A', 'B', 'C', 'A', 'D'] + + """ + return map(next, map(operator.itemgetter(1), groupby(iterable, key))) + + +def iter_except(func, exception, first=None): + """Yields results from a function repeatedly until an exception is raised. + + Converts a call-until-exception interface to an iterator interface. + Like ``iter(func, sentinel)``, but uses an exception instead of a sentinel + to end the loop. + + >>> l = [0, 1, 2] + >>> list(iter_except(l.pop, IndexError)) + [2, 1, 0] + + Multiple exceptions can be specified as a stopping condition: + + >>> l = [1, 2, 3, '...', 4, 5, 6] + >>> list(iter_except(lambda: 1 + l.pop(), (IndexError, TypeError))) + [7, 6, 5] + >>> list(iter_except(lambda: 1 + l.pop(), (IndexError, TypeError))) + [4, 3, 2] + >>> list(iter_except(lambda: 1 + l.pop(), (IndexError, TypeError))) + [] + + """ + try: + if first is not None: + yield first() + while 1: + yield func() + except exception: + pass + + +def first_true(iterable, default=None, pred=None): + """ + Returns the first true value in the iterable. + + If no true value is found, returns *default* + + If *pred* is not None, returns the first item for which + ``pred(item) == True`` . + + >>> first_true(range(10)) + 1 + >>> first_true(range(10), pred=lambda x: x > 5) + 6 + >>> first_true(range(10), default='missing', pred=lambda x: x > 9) + 'missing' + + """ + return next(filter(pred, iterable), default) + + +def random_product(*args, repeat=1): + """Draw an item at random from each of the input iterables. + + >>> random_product('abc', range(4), 'XYZ') # doctest:+SKIP + ('c', 3, 'Z') + + If *repeat* is provided as a keyword argument, that many items will be + drawn from each iterable. + + >>> random_product('abcd', range(4), repeat=2) # doctest:+SKIP + ('a', 2, 'd', 3) + + This equivalent to taking a random selection from + ``itertools.product(*args, **kwarg)``. + + """ + pools = [tuple(pool) for pool in args] * repeat + return tuple(choice(pool) for pool in pools) + + +def random_permutation(iterable, r=None): + """Return a random *r* length permutation of the elements in *iterable*. + + If *r* is not specified or is ``None``, then *r* defaults to the length of + *iterable*. + + >>> random_permutation(range(5)) # doctest:+SKIP + (3, 4, 0, 1, 2) + + This equivalent to taking a random selection from + ``itertools.permutations(iterable, r)``. + + """ + pool = tuple(iterable) + r = len(pool) if r is None else r + return tuple(sample(pool, r)) + + +def random_combination(iterable, r): + """Return a random *r* length subsequence of the elements in *iterable*. + + >>> random_combination(range(5), 3) # doctest:+SKIP + (2, 3, 4) + + This equivalent to taking a random selection from + ``itertools.combinations(iterable, r)``. + + """ + pool = tuple(iterable) + n = len(pool) + indices = sorted(sample(range(n), r)) + return tuple(pool[i] for i in indices) + + +def random_combination_with_replacement(iterable, r): + """Return a random *r* length subsequence of elements in *iterable*, + allowing individual elements to be repeated. + + >>> random_combination_with_replacement(range(3), 5) # doctest:+SKIP + (0, 0, 1, 2, 2) + + This equivalent to taking a random selection from + ``itertools.combinations_with_replacement(iterable, r)``. + + """ + pool = tuple(iterable) + n = len(pool) + indices = sorted(randrange(n) for i in range(r)) + return tuple(pool[i] for i in indices) + + +def nth_combination(iterable, r, index): + """Equivalent to ``list(combinations(iterable, r))[index]``. + + The subsequences of *iterable* that are of length *r* can be ordered + lexicographically. :func:`nth_combination` computes the subsequence at + sort position *index* directly, without computing the previous + subsequences. + + >>> nth_combination(range(5), 3, 5) + (0, 3, 4) + + ``ValueError`` will be raised If *r* is negative or greater than the length + of *iterable*. + ``IndexError`` will be raised if the given *index* is invalid. + """ + pool = tuple(iterable) + n = len(pool) + if (r < 0) or (r > n): + raise ValueError + + c = 1 + k = min(r, n - r) + for i in range(1, k + 1): + c = c * (n - k + i) // i + + if index < 0: + index += c + + if (index < 0) or (index >= c): + raise IndexError + + result = [] + while r: + c, n, r = c * r // n, n - 1, r - 1 + while index >= c: + index -= c + c, n = c * (n - r) // n, n - 1 + result.append(pool[-1 - n]) + + return tuple(result) + + +def prepend(value, iterator): + """Yield *value*, followed by the elements in *iterator*. + + >>> value = '0' + >>> iterator = ['1', '2', '3'] + >>> list(prepend(value, iterator)) + ['0', '1', '2', '3'] + + To prepend multiple values, see :func:`itertools.chain` + or :func:`value_chain`. + + """ + return chain([value], iterator) + + +def convolve(signal, kernel): + """Convolve the iterable *signal* with the iterable *kernel*. + + >>> signal = (1, 2, 3, 4, 5) + >>> kernel = [3, 2, 1] + >>> list(convolve(signal, kernel)) + [3, 8, 14, 20, 26, 14, 5] + + Note: the input arguments are not interchangeable, as the *kernel* + is immediately consumed and stored. + + """ + kernel = tuple(kernel)[::-1] + n = len(kernel) + window = deque([0], maxlen=n) * n + for x in chain(signal, repeat(0, n - 1)): + window.append(x) + yield sum(map(operator.mul, kernel, window)) + + +def before_and_after(predicate, it): + """A variant of :func:`takewhile` that allows complete access to the + remainder of the iterator. + + >>> it = iter('ABCdEfGhI') + >>> all_upper, remainder = before_and_after(str.isupper, it) + >>> ''.join(all_upper) + 'ABC' + >>> ''.join(remainder) # takewhile() would lose the 'd' + 'dEfGhI' + + Note that the first iterator must be fully consumed before the second + iterator can generate valid results. + """ + it = iter(it) + transition = [] + + def true_iterator(): + for elem in it: + if predicate(elem): + yield elem + else: + transition.append(elem) + return + + def remainder_iterator(): + yield from transition + yield from it + + return true_iterator(), remainder_iterator() + + +def triplewise(iterable): + """Return overlapping triplets from *iterable*. + + >>> list(triplewise('ABCDE')) + [('A', 'B', 'C'), ('B', 'C', 'D'), ('C', 'D', 'E')] + + """ + for (a, _), (b, c) in pairwise(pairwise(iterable)): + yield a, b, c + + +def sliding_window(iterable, n): + """Return a sliding window of width *n* over *iterable*. + + >>> list(sliding_window(range(6), 4)) + [(0, 1, 2, 3), (1, 2, 3, 4), (2, 3, 4, 5)] + + If *iterable* has fewer than *n* items, then nothing is yielded: + + >>> list(sliding_window(range(3), 4)) + [] + + For a variant with more features, see :func:`windowed`. + """ + it = iter(iterable) + window = deque(islice(it, n), maxlen=n) + if len(window) == n: + yield tuple(window) + for x in it: + window.append(x) + yield tuple(window) diff --git a/pkg_resources/_vendor/more_itertools/recipes.pyi b/pkg_resources/_vendor/more_itertools/recipes.pyi new file mode 100644 index 00000000..4648a41b --- /dev/null +++ b/pkg_resources/_vendor/more_itertools/recipes.pyi @@ -0,0 +1,112 @@ +"""Stubs for more_itertools.recipes""" +from typing import ( + Any, + Callable, + Iterable, + Iterator, + List, + Optional, + Tuple, + TypeVar, + Union, +) +from typing_extensions import overload, Type + +# Type and type variable definitions +_T = TypeVar('_T') +_U = TypeVar('_U') + +def take(n: int, iterable: Iterable[_T]) -> List[_T]: ... +def tabulate( + function: Callable[[int], _T], start: int = ... +) -> Iterator[_T]: ... +def tail(n: int, iterable: Iterable[_T]) -> Iterator[_T]: ... +def consume(iterator: Iterable[object], n: Optional[int] = ...) -> None: ... +@overload +def nth(iterable: Iterable[_T], n: int) -> Optional[_T]: ... +@overload +def nth(iterable: Iterable[_T], n: int, default: _U) -> Union[_T, _U]: ... +def all_equal(iterable: Iterable[object]) -> bool: ... +def quantify( + iterable: Iterable[_T], pred: Callable[[_T], bool] = ... +) -> int: ... +def pad_none(iterable: Iterable[_T]) -> Iterator[Optional[_T]]: ... +def padnone(iterable: Iterable[_T]) -> Iterator[Optional[_T]]: ... +def ncycles(iterable: Iterable[_T], n: int) -> Iterator[_T]: ... +def dotproduct(vec1: Iterable[object], vec2: Iterable[object]) -> object: ... +def flatten(listOfLists: Iterable[Iterable[_T]]) -> Iterator[_T]: ... +def repeatfunc( + func: Callable[..., _U], times: Optional[int] = ..., *args: Any +) -> Iterator[_U]: ... +def pairwise(iterable: Iterable[_T]) -> Iterator[Tuple[_T, _T]]: ... +@overload +def grouper( + iterable: Iterable[_T], n: int +) -> Iterator[Tuple[Optional[_T], ...]]: ... +@overload +def grouper( + iterable: Iterable[_T], n: int, fillvalue: _U +) -> Iterator[Tuple[Union[_T, _U], ...]]: ... +@overload +def grouper( # Deprecated interface + iterable: int, n: Iterable[_T] +) -> Iterator[Tuple[Optional[_T], ...]]: ... +@overload +def grouper( # Deprecated interface + iterable: int, n: Iterable[_T], fillvalue: _U +) -> Iterator[Tuple[Union[_T, _U], ...]]: ... +def roundrobin(*iterables: Iterable[_T]) -> Iterator[_T]: ... +def partition( + pred: Optional[Callable[[_T], object]], iterable: Iterable[_T] +) -> Tuple[Iterator[_T], Iterator[_T]]: ... +def powerset(iterable: Iterable[_T]) -> Iterator[Tuple[_T, ...]]: ... +def unique_everseen( + iterable: Iterable[_T], key: Optional[Callable[[_T], _U]] = ... +) -> Iterator[_T]: ... +def unique_justseen( + iterable: Iterable[_T], key: Optional[Callable[[_T], object]] = ... +) -> Iterator[_T]: ... +@overload +def iter_except( + func: Callable[[], _T], + exception: Union[Type[BaseException], Tuple[Type[BaseException], ...]], + first: None = ..., +) -> Iterator[_T]: ... +@overload +def iter_except( + func: Callable[[], _T], + exception: Union[Type[BaseException], Tuple[Type[BaseException], ...]], + first: Callable[[], _U], +) -> Iterator[Union[_T, _U]]: ... +@overload +def first_true( + iterable: Iterable[_T], *, pred: Optional[Callable[[_T], object]] = ... +) -> Optional[_T]: ... +@overload +def first_true( + iterable: Iterable[_T], + default: _U, + pred: Optional[Callable[[_T], object]] = ..., +) -> Union[_T, _U]: ... +def random_product( + *args: Iterable[_T], repeat: int = ... +) -> Tuple[_T, ...]: ... +def random_permutation( + iterable: Iterable[_T], r: Optional[int] = ... +) -> Tuple[_T, ...]: ... +def random_combination(iterable: Iterable[_T], r: int) -> Tuple[_T, ...]: ... +def random_combination_with_replacement( + iterable: Iterable[_T], r: int +) -> Tuple[_T, ...]: ... +def nth_combination( + iterable: Iterable[_T], r: int, index: int +) -> Tuple[_T, ...]: ... +def prepend(value: _T, iterator: Iterable[_U]) -> Iterator[Union[_T, _U]]: ... +def convolve(signal: Iterable[_T], kernel: Iterable[_T]) -> Iterator[_T]: ... +def before_and_after( + predicate: Callable[[_T], bool], it: Iterable[_T] +) -> Tuple[Iterator[_T], Iterator[_T]]: ... +def triplewise(iterable: Iterable[_T]) -> Iterator[Tuple[_T, _T, _T]]: ... +def sliding_window( + iterable: Iterable[_T], n: int +) -> Iterator[Tuple[_T, ...]]: ... diff --git a/pkg_resources/_vendor/vendored.txt b/pkg_resources/_vendor/vendored.txt index 444ed25b..a5d6840a 100644 --- a/pkg_resources/_vendor/vendored.txt +++ b/pkg_resources/_vendor/vendored.txt @@ -1,3 +1,4 @@ packaging==21.2 pyparsing==2.2.1 appdirs==1.4.3 +jaraco.text==3.7.0 diff --git a/pkg_resources/extern/__init__.py b/pkg_resources/extern/__init__.py index fed59295..b1811743 100644 --- a/pkg_resources/extern/__init__.py +++ b/pkg_resources/extern/__init__.py @@ -69,5 +69,5 @@ class VendorImporter: sys.meta_path.append(self) -names = 'packaging', 'pyparsing', 'appdirs' +names = 'packaging', 'pyparsing', 'appdirs', 'jaraco' VendorImporter(__name__, names).install() -- cgit v1.2.1 From b4f1bf1cb8b3285d0620b27f316d7e83470f7d68 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 23 Jan 2022 22:36:13 -0500 Subject: Include all the dependencies needed to run on Python 3.7 and patch all of them to work in a vendored environment. --- .../importlib_resources-5.4.0.dist-info/INSTALLER | 1 + .../importlib_resources-5.4.0.dist-info/LICENSE | 13 + .../importlib_resources-5.4.0.dist-info/METADATA | 86 ++++++ .../importlib_resources-5.4.0.dist-info/RECORD | 75 +++++ .../importlib_resources-5.4.0.dist-info/REQUESTED | 0 .../importlib_resources-5.4.0.dist-info/WHEEL | 5 + .../top_level.txt | 1 + .../_vendor/importlib_resources/__init__.py | 36 +++ .../_vendor/importlib_resources/_adapters.py | 170 +++++++++++ .../_vendor/importlib_resources/_common.py | 104 +++++++ .../_vendor/importlib_resources/_compat.py | 98 ++++++ .../_vendor/importlib_resources/_itertools.py | 35 +++ .../_vendor/importlib_resources/_legacy.py | 121 ++++++++ pkg_resources/_vendor/importlib_resources/abc.py | 137 +++++++++ pkg_resources/_vendor/importlib_resources/py.typed | 0 .../_vendor/importlib_resources/readers.py | 122 ++++++++ .../_vendor/importlib_resources/simple.py | 116 ++++++++ .../_vendor/importlib_resources/tests/__init__.py | 0 .../_vendor/importlib_resources/tests/_compat.py | 19 ++ .../importlib_resources/tests/data01/__init__.py | 0 .../importlib_resources/tests/data01/binary.file | Bin 0 -> 4 bytes .../tests/data01/subdirectory/__init__.py | 0 .../tests/data01/subdirectory/binary.file | Bin 0 -> 4 bytes .../importlib_resources/tests/data01/utf-16.file | Bin 0 -> 44 bytes .../importlib_resources/tests/data01/utf-8.file | 1 + .../importlib_resources/tests/data02/__init__.py | 0 .../tests/data02/one/__init__.py | 0 .../tests/data02/one/resource1.txt | 1 + .../tests/data02/two/__init__.py | 0 .../tests/data02/two/resource2.txt | 1 + .../tests/namespacedata01/binary.file | Bin 0 -> 4 bytes .../tests/namespacedata01/utf-16.file | Bin 0 -> 44 bytes .../tests/namespacedata01/utf-8.file | 1 + .../tests/test_compatibilty_files.py | 102 +++++++ .../importlib_resources/tests/test_contents.py | 43 +++ .../importlib_resources/tests/test_files.py | 46 +++ .../_vendor/importlib_resources/tests/test_open.py | 81 +++++ .../_vendor/importlib_resources/tests/test_path.py | 64 ++++ .../_vendor/importlib_resources/tests/test_read.py | 76 +++++ .../importlib_resources/tests/test_reader.py | 128 ++++++++ .../importlib_resources/tests/test_resource.py | 252 ++++++++++++++++ .../importlib_resources/tests/update-zips.py | 53 ++++ .../_vendor/importlib_resources/tests/util.py | 178 +++++++++++ .../tests/zipdata01/__init__.py | 0 .../tests/zipdata01/ziptestdata.zip | Bin 0 -> 876 bytes .../tests/zipdata02/__init__.py | 0 .../tests/zipdata02/ziptestdata.zip | Bin 0 -> 698 bytes pkg_resources/_vendor/jaraco/functools.py | 2 +- pkg_resources/_vendor/jaraco/text/__init__.py | 6 +- pkg_resources/_vendor/vendored.txt | 4 + .../_vendor/zipp-3.7.0.dist-info/INSTALLER | 1 + pkg_resources/_vendor/zipp-3.7.0.dist-info/LICENSE | 19 ++ .../_vendor/zipp-3.7.0.dist-info/METADATA | 58 ++++ pkg_resources/_vendor/zipp-3.7.0.dist-info/RECORD | 9 + .../_vendor/zipp-3.7.0.dist-info/REQUESTED | 0 pkg_resources/_vendor/zipp-3.7.0.dist-info/WHEEL | 5 + .../_vendor/zipp-3.7.0.dist-info/top_level.txt | 1 + pkg_resources/_vendor/zipp.py | 329 +++++++++++++++++++++ pkg_resources/extern/__init__.py | 5 +- tools/vendored.py | 34 +++ 60 files changed, 2634 insertions(+), 5 deletions(-) create mode 100644 pkg_resources/_vendor/importlib_resources-5.4.0.dist-info/INSTALLER create mode 100644 pkg_resources/_vendor/importlib_resources-5.4.0.dist-info/LICENSE create mode 100644 pkg_resources/_vendor/importlib_resources-5.4.0.dist-info/METADATA create mode 100644 pkg_resources/_vendor/importlib_resources-5.4.0.dist-info/RECORD create mode 100644 pkg_resources/_vendor/importlib_resources-5.4.0.dist-info/REQUESTED create mode 100644 pkg_resources/_vendor/importlib_resources-5.4.0.dist-info/WHEEL create mode 100644 pkg_resources/_vendor/importlib_resources-5.4.0.dist-info/top_level.txt create mode 100644 pkg_resources/_vendor/importlib_resources/__init__.py create mode 100644 pkg_resources/_vendor/importlib_resources/_adapters.py create mode 100644 pkg_resources/_vendor/importlib_resources/_common.py create mode 100644 pkg_resources/_vendor/importlib_resources/_compat.py create mode 100644 pkg_resources/_vendor/importlib_resources/_itertools.py create mode 100644 pkg_resources/_vendor/importlib_resources/_legacy.py create mode 100644 pkg_resources/_vendor/importlib_resources/abc.py create mode 100644 pkg_resources/_vendor/importlib_resources/py.typed create mode 100644 pkg_resources/_vendor/importlib_resources/readers.py create mode 100644 pkg_resources/_vendor/importlib_resources/simple.py create mode 100644 pkg_resources/_vendor/importlib_resources/tests/__init__.py create mode 100644 pkg_resources/_vendor/importlib_resources/tests/_compat.py create mode 100644 pkg_resources/_vendor/importlib_resources/tests/data01/__init__.py create mode 100644 pkg_resources/_vendor/importlib_resources/tests/data01/binary.file create mode 100644 pkg_resources/_vendor/importlib_resources/tests/data01/subdirectory/__init__.py create mode 100644 pkg_resources/_vendor/importlib_resources/tests/data01/subdirectory/binary.file create mode 100644 pkg_resources/_vendor/importlib_resources/tests/data01/utf-16.file create mode 100644 pkg_resources/_vendor/importlib_resources/tests/data01/utf-8.file create mode 100644 pkg_resources/_vendor/importlib_resources/tests/data02/__init__.py create mode 100644 pkg_resources/_vendor/importlib_resources/tests/data02/one/__init__.py create mode 100644 pkg_resources/_vendor/importlib_resources/tests/data02/one/resource1.txt create mode 100644 pkg_resources/_vendor/importlib_resources/tests/data02/two/__init__.py create mode 100644 pkg_resources/_vendor/importlib_resources/tests/data02/two/resource2.txt create mode 100644 pkg_resources/_vendor/importlib_resources/tests/namespacedata01/binary.file create mode 100644 pkg_resources/_vendor/importlib_resources/tests/namespacedata01/utf-16.file create mode 100644 pkg_resources/_vendor/importlib_resources/tests/namespacedata01/utf-8.file create mode 100644 pkg_resources/_vendor/importlib_resources/tests/test_compatibilty_files.py create mode 100644 pkg_resources/_vendor/importlib_resources/tests/test_contents.py create mode 100644 pkg_resources/_vendor/importlib_resources/tests/test_files.py create mode 100644 pkg_resources/_vendor/importlib_resources/tests/test_open.py create mode 100644 pkg_resources/_vendor/importlib_resources/tests/test_path.py create mode 100644 pkg_resources/_vendor/importlib_resources/tests/test_read.py create mode 100644 pkg_resources/_vendor/importlib_resources/tests/test_reader.py create mode 100644 pkg_resources/_vendor/importlib_resources/tests/test_resource.py create mode 100644 pkg_resources/_vendor/importlib_resources/tests/update-zips.py create mode 100644 pkg_resources/_vendor/importlib_resources/tests/util.py create mode 100644 pkg_resources/_vendor/importlib_resources/tests/zipdata01/__init__.py create mode 100644 pkg_resources/_vendor/importlib_resources/tests/zipdata01/ziptestdata.zip create mode 100644 pkg_resources/_vendor/importlib_resources/tests/zipdata02/__init__.py create mode 100644 pkg_resources/_vendor/importlib_resources/tests/zipdata02/ziptestdata.zip create mode 100644 pkg_resources/_vendor/zipp-3.7.0.dist-info/INSTALLER create mode 100644 pkg_resources/_vendor/zipp-3.7.0.dist-info/LICENSE create mode 100644 pkg_resources/_vendor/zipp-3.7.0.dist-info/METADATA create mode 100644 pkg_resources/_vendor/zipp-3.7.0.dist-info/RECORD create mode 100644 pkg_resources/_vendor/zipp-3.7.0.dist-info/REQUESTED create mode 100644 pkg_resources/_vendor/zipp-3.7.0.dist-info/WHEEL create mode 100644 pkg_resources/_vendor/zipp-3.7.0.dist-info/top_level.txt create mode 100644 pkg_resources/_vendor/zipp.py diff --git a/pkg_resources/_vendor/importlib_resources-5.4.0.dist-info/INSTALLER b/pkg_resources/_vendor/importlib_resources-5.4.0.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources-5.4.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/pkg_resources/_vendor/importlib_resources-5.4.0.dist-info/LICENSE b/pkg_resources/_vendor/importlib_resources-5.4.0.dist-info/LICENSE new file mode 100644 index 00000000..378b991a --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources-5.4.0.dist-info/LICENSE @@ -0,0 +1,13 @@ +Copyright 2017-2019 Brett Cannon, Barry Warsaw + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pkg_resources/_vendor/importlib_resources-5.4.0.dist-info/METADATA b/pkg_resources/_vendor/importlib_resources-5.4.0.dist-info/METADATA new file mode 100644 index 00000000..cdb1e783 --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources-5.4.0.dist-info/METADATA @@ -0,0 +1,86 @@ +Metadata-Version: 2.1 +Name: importlib-resources +Version: 5.4.0 +Summary: Read resources from Python packages +Home-page: https://github.com/python/importlib_resources +Author: Barry Warsaw +Author-email: barry@python.org +License: UNKNOWN +Project-URL: Documentation, https://importlib-resources.readthedocs.io/ +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Requires-Python: >=3.6 +License-File: LICENSE +Requires-Dist: zipp (>=3.1.0) ; python_version < "3.10" +Provides-Extra: docs +Requires-Dist: sphinx ; extra == 'docs' +Requires-Dist: jaraco.packaging (>=8.2) ; extra == 'docs' +Requires-Dist: rst.linker (>=1.9) ; extra == 'docs' +Provides-Extra: testing +Requires-Dist: pytest (>=6) ; extra == 'testing' +Requires-Dist: pytest-checkdocs (>=2.4) ; extra == 'testing' +Requires-Dist: pytest-flake8 ; extra == 'testing' +Requires-Dist: pytest-cov ; extra == 'testing' +Requires-Dist: pytest-enabler (>=1.0.1) ; extra == 'testing' +Requires-Dist: pytest-black (>=0.3.7) ; (platform_python_implementation != "PyPy") and extra == 'testing' +Requires-Dist: pytest-mypy ; (platform_python_implementation != "PyPy") and extra == 'testing' + +.. image:: https://img.shields.io/pypi/v/importlib_resources.svg + :target: `PyPI link`_ + +.. image:: https://img.shields.io/pypi/pyversions/importlib_resources.svg + :target: `PyPI link`_ + +.. _PyPI link: https://pypi.org/project/importlib_resources + +.. image:: https://github.com/python/importlib_resources/workflows/tests/badge.svg + :target: https://github.com/python/importlib_resources/actions?query=workflow%3A%22tests%22 + :alt: tests + +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + :alt: Code style: Black + +.. image:: https://readthedocs.org/projects/importlib-resources/badge/?version=latest + :target: https://importlib-resources.readthedocs.io/en/latest/?badge=latest + +.. image:: https://img.shields.io/badge/skeleton-2021-informational + :target: https://blog.jaraco.com/skeleton + +``importlib_resources`` is a backport of Python standard library +`importlib.resources +`_ +module for older Pythons. + +The key goal of this module is to replace parts of `pkg_resources +`_ with a +solution in Python's stdlib that relies on well-defined APIs. This makes +reading resources included in packages easier, with more stable and consistent +semantics. + +Compatibility +============= + +New features are introduced in this third-party library and later merged +into CPython. The following table indicates which versions of this library +were contributed to different versions in the standard library: + +.. list-table:: + :header-rows: 1 + + * - importlib_resources + - stdlib + * - 5.2 + - 3.11 + * - 5.0 + - 3.10 + * - 1.3 + - 3.9 + * - 0.5 (?) + - 3.7 + + diff --git a/pkg_resources/_vendor/importlib_resources-5.4.0.dist-info/RECORD b/pkg_resources/_vendor/importlib_resources-5.4.0.dist-info/RECORD new file mode 100644 index 00000000..7a68a2f2 --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources-5.4.0.dist-info/RECORD @@ -0,0 +1,75 @@ +importlib_resources-5.4.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +importlib_resources-5.4.0.dist-info/LICENSE,sha256=uWRjFdYGataJX2ziXk048ItUglQmjng3GWBALaWA36U,568 +importlib_resources-5.4.0.dist-info/METADATA,sha256=i5jH25IbM0Ls6u6UzSSCOa0c8hpDvePxqgnQwh2T5Io,3135 +importlib_resources-5.4.0.dist-info/RECORD,, +importlib_resources-5.4.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +importlib_resources-5.4.0.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92 +importlib_resources-5.4.0.dist-info/top_level.txt,sha256=fHIjHU1GZwAjvcydpmUnUrTnbvdiWjG4OEVZK8by0TQ,20 +importlib_resources/__init__.py,sha256=zuA0lbRgtVVCcAztM0z5LuBiOCV9L_3qtI6mW2p5xAg,525 +importlib_resources/__pycache__/__init__.cpython-310.pyc,, +importlib_resources/__pycache__/_adapters.cpython-310.pyc,, +importlib_resources/__pycache__/_common.cpython-310.pyc,, +importlib_resources/__pycache__/_compat.cpython-310.pyc,, +importlib_resources/__pycache__/_itertools.cpython-310.pyc,, +importlib_resources/__pycache__/_legacy.cpython-310.pyc,, +importlib_resources/__pycache__/abc.cpython-310.pyc,, +importlib_resources/__pycache__/readers.cpython-310.pyc,, +importlib_resources/__pycache__/simple.cpython-310.pyc,, +importlib_resources/_adapters.py,sha256=o51tP2hpVtohP33gSYyAkGNpLfYDBqxxYsadyiRZi1E,4504 +importlib_resources/_common.py,sha256=iIxAaQhotSh6TLLUEfL_ynU2fzEeyHMz9JcL46mUhLg,2741 +importlib_resources/_compat.py,sha256=3LpkIfeN9x4oXjRea5TxZP5VYhPlzuVRhGe-hEv-S0s,2704 +importlib_resources/_itertools.py,sha256=WCdJ1Gs_kNFwKENyIG7TO0Y434IWCu0zjVVSsSbZwU8,884 +importlib_resources/_legacy.py,sha256=TMLkx6aEM6U8xIREPXqGZrMbUhTiPUuPl6ESD7RdYj4,3494 +importlib_resources/abc.py,sha256=MvTJJXajbl74s36Gyeesf76egtbFnh-TMtzQMVhFWXo,3886 +importlib_resources/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +importlib_resources/readers.py,sha256=_9QLGQ5AzrED3PY8S2Zf8V6yLR0-nqqYqtQmgleDJzY,3566 +importlib_resources/simple.py,sha256=xt0qhXbwt3bZ86zuaaKbTiE9A0mDbwu0saRjUq_pcY0,2836 +importlib_resources/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +importlib_resources/tests/__pycache__/__init__.cpython-310.pyc,, +importlib_resources/tests/__pycache__/_compat.cpython-310.pyc,, +importlib_resources/tests/__pycache__/test_compatibilty_files.cpython-310.pyc,, +importlib_resources/tests/__pycache__/test_contents.cpython-310.pyc,, +importlib_resources/tests/__pycache__/test_files.cpython-310.pyc,, +importlib_resources/tests/__pycache__/test_open.cpython-310.pyc,, +importlib_resources/tests/__pycache__/test_path.cpython-310.pyc,, +importlib_resources/tests/__pycache__/test_read.cpython-310.pyc,, +importlib_resources/tests/__pycache__/test_reader.cpython-310.pyc,, +importlib_resources/tests/__pycache__/test_resource.cpython-310.pyc,, +importlib_resources/tests/__pycache__/update-zips.cpython-310.pyc,, +importlib_resources/tests/__pycache__/util.cpython-310.pyc,, +importlib_resources/tests/_compat.py,sha256=QGI_4p0DXybypoYvw0kr3jfQqvls3p8u4wy4Wvf0Z_o,435 +importlib_resources/tests/data01/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +importlib_resources/tests/data01/__pycache__/__init__.cpython-310.pyc,, +importlib_resources/tests/data01/binary.file,sha256=BU7ewdAhH2JP7Qy8qdT5QAsOSRxDdCryxbCr6_DJkNg,4 +importlib_resources/tests/data01/subdirectory/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +importlib_resources/tests/data01/subdirectory/__pycache__/__init__.cpython-310.pyc,, +importlib_resources/tests/data01/subdirectory/binary.file,sha256=BU7ewdAhH2JP7Qy8qdT5QAsOSRxDdCryxbCr6_DJkNg,4 +importlib_resources/tests/data01/utf-16.file,sha256=t5q9qhxX0rYqItBOM8D3ylwG-RHrnOYteTLtQr6sF7g,44 +importlib_resources/tests/data01/utf-8.file,sha256=kwWgYG4yQ-ZF2X_WA66EjYPmxJRn-w8aSOiS9e8tKYY,20 +importlib_resources/tests/data02/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +importlib_resources/tests/data02/__pycache__/__init__.cpython-310.pyc,, +importlib_resources/tests/data02/one/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +importlib_resources/tests/data02/one/__pycache__/__init__.cpython-310.pyc,, +importlib_resources/tests/data02/one/resource1.txt,sha256=10flKac7c-XXFzJ3t-AB5MJjlBy__dSZvPE_dOm2q6U,13 +importlib_resources/tests/data02/two/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +importlib_resources/tests/data02/two/__pycache__/__init__.cpython-310.pyc,, +importlib_resources/tests/data02/two/resource2.txt,sha256=lt2jbN3TMn9QiFKM832X39bU_62UptDdUkoYzkvEbl0,13 +importlib_resources/tests/namespacedata01/binary.file,sha256=BU7ewdAhH2JP7Qy8qdT5QAsOSRxDdCryxbCr6_DJkNg,4 +importlib_resources/tests/namespacedata01/utf-16.file,sha256=t5q9qhxX0rYqItBOM8D3ylwG-RHrnOYteTLtQr6sF7g,44 +importlib_resources/tests/namespacedata01/utf-8.file,sha256=kwWgYG4yQ-ZF2X_WA66EjYPmxJRn-w8aSOiS9e8tKYY,20 +importlib_resources/tests/test_compatibilty_files.py,sha256=NWkbIsylI8Wz3Dwsxo1quT4ZI6ToXFA2mojCG6Dzuxw,3260 +importlib_resources/tests/test_contents.py,sha256=V1Xfk3lqTDdvUsZuV18Kndf0CT_tkM2oEIwk9Vv0rhg,968 +importlib_resources/tests/test_files.py,sha256=1Nqv6VM_MjfwrmtXYL1a1CMT0QhCxi3hNMqwXlfMQTg,1184 +importlib_resources/tests/test_open.py,sha256=pmEgdrSFdM83L6FxtR8U_RT9BfI3JZ4snGmM_ZZIegY,2565 +importlib_resources/tests/test_path.py,sha256=xvPteNA-UKavDhKgLgrQuXSxKWYH7Q4nSNDVfBX95Gs,2103 +importlib_resources/tests/test_read.py,sha256=EyYvpHJ_7F4LuX2EU_c5EerIBQfRhOFmiIR7LOc5Y5E,2408 +importlib_resources/tests/test_reader.py,sha256=hgXHquqAEnioemv20ZZcDlVaiOrcZKADO37_FkiQ00Y,4286 +importlib_resources/tests/test_resource.py,sha256=DqfLNc9kaN5obqxU8kn0sRUWMf9MygagrpfMV5-QfWg,8145 +importlib_resources/tests/update-zips.py,sha256=x3iJVqWnMM5qp4Oob2Pl3o6Yi03sUjEv_5Wf-UCg3ps,1415 +importlib_resources/tests/util.py,sha256=X1j-0C96pu3_tmtJuLhzfBfcfMenOphDLkxtCt5j7t4,5309 +importlib_resources/tests/zipdata01/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +importlib_resources/tests/zipdata01/__pycache__/__init__.cpython-310.pyc,, +importlib_resources/tests/zipdata01/ziptestdata.zip,sha256=z5Of4dsv3T0t-46B0MsVhxlhsPGMz28aUhJDWpj3_oY,876 +importlib_resources/tests/zipdata02/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +importlib_resources/tests/zipdata02/__pycache__/__init__.cpython-310.pyc,, +importlib_resources/tests/zipdata02/ziptestdata.zip,sha256=ydI-_j-xgQ7tDxqBp9cjOqXBGxUp6ZBbwVJu6Xj-nrY,698 diff --git a/pkg_resources/_vendor/importlib_resources-5.4.0.dist-info/REQUESTED b/pkg_resources/_vendor/importlib_resources-5.4.0.dist-info/REQUESTED new file mode 100644 index 00000000..e69de29b diff --git a/pkg_resources/_vendor/importlib_resources-5.4.0.dist-info/WHEEL b/pkg_resources/_vendor/importlib_resources-5.4.0.dist-info/WHEEL new file mode 100644 index 00000000..5bad85fd --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources-5.4.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/pkg_resources/_vendor/importlib_resources-5.4.0.dist-info/top_level.txt b/pkg_resources/_vendor/importlib_resources-5.4.0.dist-info/top_level.txt new file mode 100644 index 00000000..58ad1bd3 --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources-5.4.0.dist-info/top_level.txt @@ -0,0 +1 @@ +importlib_resources diff --git a/pkg_resources/_vendor/importlib_resources/__init__.py b/pkg_resources/_vendor/importlib_resources/__init__.py new file mode 100644 index 00000000..34e3a995 --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources/__init__.py @@ -0,0 +1,36 @@ +"""Read resources contained within a package.""" + +from ._common import ( + as_file, + files, + Package, +) + +from ._legacy import ( + contents, + open_binary, + read_binary, + open_text, + read_text, + is_resource, + path, + Resource, +) + +from .abc import ResourceReader + + +__all__ = [ + 'Package', + 'Resource', + 'ResourceReader', + 'as_file', + 'contents', + 'files', + 'is_resource', + 'open_binary', + 'open_text', + 'path', + 'read_binary', + 'read_text', +] diff --git a/pkg_resources/_vendor/importlib_resources/_adapters.py b/pkg_resources/_vendor/importlib_resources/_adapters.py new file mode 100644 index 00000000..ea363d86 --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources/_adapters.py @@ -0,0 +1,170 @@ +from contextlib import suppress +from io import TextIOWrapper + +from . import abc + + +class SpecLoaderAdapter: + """ + Adapt a package spec to adapt the underlying loader. + """ + + def __init__(self, spec, adapter=lambda spec: spec.loader): + self.spec = spec + self.loader = adapter(spec) + + def __getattr__(self, name): + return getattr(self.spec, name) + + +class TraversableResourcesLoader: + """ + Adapt a loader to provide TraversableResources. + """ + + def __init__(self, spec): + self.spec = spec + + def get_resource_reader(self, name): + return CompatibilityFiles(self.spec)._native() + + +def _io_wrapper(file, mode='r', *args, **kwargs): + if mode == 'r': + return TextIOWrapper(file, *args, **kwargs) + elif mode == 'rb': + return file + raise ValueError( + "Invalid mode value '{}', only 'r' and 'rb' are supported".format(mode) + ) + + +class CompatibilityFiles: + """ + Adapter for an existing or non-existent resource reader + to provide a compatibility .files(). + """ + + class SpecPath(abc.Traversable): + """ + Path tied to a module spec. + Can be read and exposes the resource reader children. + """ + + def __init__(self, spec, reader): + self._spec = spec + self._reader = reader + + def iterdir(self): + if not self._reader: + return iter(()) + return iter( + CompatibilityFiles.ChildPath(self._reader, path) + for path in self._reader.contents() + ) + + def is_file(self): + return False + + is_dir = is_file + + def joinpath(self, other): + if not self._reader: + return CompatibilityFiles.OrphanPath(other) + return CompatibilityFiles.ChildPath(self._reader, other) + + @property + def name(self): + return self._spec.name + + def open(self, mode='r', *args, **kwargs): + return _io_wrapper(self._reader.open_resource(None), mode, *args, **kwargs) + + class ChildPath(abc.Traversable): + """ + Path tied to a resource reader child. + Can be read but doesn't expose any meaningful children. + """ + + def __init__(self, reader, name): + self._reader = reader + self._name = name + + def iterdir(self): + return iter(()) + + def is_file(self): + return self._reader.is_resource(self.name) + + def is_dir(self): + return not self.is_file() + + def joinpath(self, other): + return CompatibilityFiles.OrphanPath(self.name, other) + + @property + def name(self): + return self._name + + def open(self, mode='r', *args, **kwargs): + return _io_wrapper( + self._reader.open_resource(self.name), mode, *args, **kwargs + ) + + class OrphanPath(abc.Traversable): + """ + Orphan path, not tied to a module spec or resource reader. + Can't be read and doesn't expose any meaningful children. + """ + + def __init__(self, *path_parts): + if len(path_parts) < 1: + raise ValueError('Need at least one path part to construct a path') + self._path = path_parts + + def iterdir(self): + return iter(()) + + def is_file(self): + return False + + is_dir = is_file + + def joinpath(self, other): + return CompatibilityFiles.OrphanPath(*self._path, other) + + @property + def name(self): + return self._path[-1] + + def open(self, mode='r', *args, **kwargs): + raise FileNotFoundError("Can't open orphan path") + + def __init__(self, spec): + self.spec = spec + + @property + def _reader(self): + with suppress(AttributeError): + return self.spec.loader.get_resource_reader(self.spec.name) + + def _native(self): + """ + Return the native reader if it supports files(). + """ + reader = self._reader + return reader if hasattr(reader, 'files') else self + + def __getattr__(self, attr): + return getattr(self._reader, attr) + + def files(self): + return CompatibilityFiles.SpecPath(self.spec, self._reader) + + +def wrap_spec(package): + """ + Construct a package spec with traversable compatibility + on the spec/loader/reader. + """ + return SpecLoaderAdapter(package.__spec__, TraversableResourcesLoader) diff --git a/pkg_resources/_vendor/importlib_resources/_common.py b/pkg_resources/_vendor/importlib_resources/_common.py new file mode 100644 index 00000000..a12e2c75 --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources/_common.py @@ -0,0 +1,104 @@ +import os +import pathlib +import tempfile +import functools +import contextlib +import types +import importlib + +from typing import Union, Optional +from .abc import ResourceReader, Traversable + +from ._compat import wrap_spec + +Package = Union[types.ModuleType, str] + + +def files(package): + # type: (Package) -> Traversable + """ + Get a Traversable resource from a package + """ + return from_package(get_package(package)) + + +def get_resource_reader(package): + # type: (types.ModuleType) -> Optional[ResourceReader] + """ + Return the package's loader if it's a ResourceReader. + """ + # We can't use + # a issubclass() check here because apparently abc.'s __subclasscheck__() + # hook wants to create a weak reference to the object, but + # zipimport.zipimporter does not support weak references, resulting in a + # TypeError. That seems terrible. + spec = package.__spec__ + reader = getattr(spec.loader, 'get_resource_reader', None) # type: ignore + if reader is None: + return None + return reader(spec.name) # type: ignore + + +def resolve(cand): + # type: (Package) -> types.ModuleType + return cand if isinstance(cand, types.ModuleType) else importlib.import_module(cand) + + +def get_package(package): + # type: (Package) -> types.ModuleType + """Take a package name or module object and return the module. + + Raise an exception if the resolved module is not a package. + """ + resolved = resolve(package) + if wrap_spec(resolved).submodule_search_locations is None: + raise TypeError(f'{package!r} is not a package') + return resolved + + +def from_package(package): + """ + Return a Traversable object for the given package. + + """ + spec = wrap_spec(package) + reader = spec.loader.get_resource_reader(spec.name) + return reader.files() + + +@contextlib.contextmanager +def _tempfile(reader, suffix=''): + # Not using tempfile.NamedTemporaryFile as it leads to deeper 'try' + # blocks due to the need to close the temporary file to work on Windows + # properly. + fd, raw_path = tempfile.mkstemp(suffix=suffix) + try: + try: + os.write(fd, reader()) + finally: + os.close(fd) + del reader + yield pathlib.Path(raw_path) + finally: + try: + os.remove(raw_path) + except FileNotFoundError: + pass + + +@functools.singledispatch +def as_file(path): + """ + Given a Traversable object, return that object as a + path on the local file system in a context manager. + """ + return _tempfile(path.read_bytes, suffix=path.name) + + +@as_file.register(pathlib.Path) +@contextlib.contextmanager +def _(path): + """ + Degenerate behavior for pathlib.Path objects. + """ + yield path diff --git a/pkg_resources/_vendor/importlib_resources/_compat.py b/pkg_resources/_vendor/importlib_resources/_compat.py new file mode 100644 index 00000000..cb9fc820 --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources/_compat.py @@ -0,0 +1,98 @@ +# flake8: noqa + +import abc +import sys +import pathlib +from contextlib import suppress + +if sys.version_info >= (3, 10): + from zipfile import Path as ZipPath # type: ignore +else: + from ..zipp import Path as ZipPath # type: ignore + + +try: + from typing import runtime_checkable # type: ignore +except ImportError: + + def runtime_checkable(cls): # type: ignore + return cls + + +try: + from typing import Protocol # type: ignore +except ImportError: + Protocol = abc.ABC # type: ignore + + +class TraversableResourcesLoader: + """ + Adapt loaders to provide TraversableResources and other + compatibility. + + Used primarily for Python 3.9 and earlier where the native + loaders do not yet implement TraversableResources. + """ + + def __init__(self, spec): + self.spec = spec + + @property + def path(self): + return self.spec.origin + + def get_resource_reader(self, name): + from . import readers, _adapters + + def _zip_reader(spec): + with suppress(AttributeError): + return readers.ZipReader(spec.loader, spec.name) + + def _namespace_reader(spec): + with suppress(AttributeError, ValueError): + return readers.NamespaceReader(spec.submodule_search_locations) + + def _available_reader(spec): + with suppress(AttributeError): + return spec.loader.get_resource_reader(spec.name) + + def _native_reader(spec): + reader = _available_reader(spec) + return reader if hasattr(reader, 'files') else None + + def _file_reader(spec): + try: + path = pathlib.Path(self.path) + except TypeError: + return None + if path.exists(): + return readers.FileReader(self) + + return ( + # native reader if it supplies 'files' + _native_reader(self.spec) + or + # local ZipReader if a zip module + _zip_reader(self.spec) + or + # local NamespaceReader if a namespace module + _namespace_reader(self.spec) + or + # local FileReader + _file_reader(self.spec) + # fallback - adapt the spec ResourceReader to TraversableReader + or _adapters.CompatibilityFiles(self.spec) + ) + + +def wrap_spec(package): + """ + Construct a package spec with traversable compatibility + on the spec/loader/reader. + + Supersedes _adapters.wrap_spec to use TraversableResourcesLoader + from above for older Python compatibility (<3.10). + """ + from . import _adapters + + return _adapters.SpecLoaderAdapter(package.__spec__, TraversableResourcesLoader) diff --git a/pkg_resources/_vendor/importlib_resources/_itertools.py b/pkg_resources/_vendor/importlib_resources/_itertools.py new file mode 100644 index 00000000..cce05582 --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources/_itertools.py @@ -0,0 +1,35 @@ +from itertools import filterfalse + +from typing import ( + Callable, + Iterable, + Iterator, + Optional, + Set, + TypeVar, + Union, +) + +# Type and type variable definitions +_T = TypeVar('_T') +_U = TypeVar('_U') + + +def unique_everseen( + iterable: Iterable[_T], key: Optional[Callable[[_T], _U]] = None +) -> Iterator[_T]: + "List unique elements, preserving order. Remember all elements ever seen." + # unique_everseen('AAAABBBCCDAABBB') --> A B C D + # unique_everseen('ABBCcAD', str.lower) --> A B C D + seen: Set[Union[_T, _U]] = set() + seen_add = seen.add + if key is None: + for element in filterfalse(seen.__contains__, iterable): + seen_add(element) + yield element + else: + for element in iterable: + k = key(element) + if k not in seen: + seen_add(k) + yield element diff --git a/pkg_resources/_vendor/importlib_resources/_legacy.py b/pkg_resources/_vendor/importlib_resources/_legacy.py new file mode 100644 index 00000000..1d5d3f1f --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources/_legacy.py @@ -0,0 +1,121 @@ +import functools +import os +import pathlib +import types +import warnings + +from typing import Union, Iterable, ContextManager, BinaryIO, TextIO, Any + +from . import _common + +Package = Union[types.ModuleType, str] +Resource = str + + +def deprecated(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + warnings.warn( + f"{func.__name__} is deprecated. Use files() instead. " + "Refer to https://importlib-resources.readthedocs.io" + "/en/latest/using.html#migrating-from-legacy for migration advice.", + DeprecationWarning, + stacklevel=2, + ) + return func(*args, **kwargs) + + return wrapper + + +def normalize_path(path): + # type: (Any) -> str + """Normalize a path by ensuring it is a string. + + If the resulting string contains path separators, an exception is raised. + """ + str_path = str(path) + parent, file_name = os.path.split(str_path) + if parent: + raise ValueError(f'{path!r} must be only a file name') + return file_name + + +@deprecated +def open_binary(package: Package, resource: Resource) -> BinaryIO: + """Return a file-like object opened for binary reading of the resource.""" + return (_common.files(package) / normalize_path(resource)).open('rb') + + +@deprecated +def read_binary(package: Package, resource: Resource) -> bytes: + """Return the binary contents of the resource.""" + return (_common.files(package) / normalize_path(resource)).read_bytes() + + +@deprecated +def open_text( + package: Package, + resource: Resource, + encoding: str = 'utf-8', + errors: str = 'strict', +) -> TextIO: + """Return a file-like object opened for text reading of the resource.""" + return (_common.files(package) / normalize_path(resource)).open( + 'r', encoding=encoding, errors=errors + ) + + +@deprecated +def read_text( + package: Package, + resource: Resource, + encoding: str = 'utf-8', + errors: str = 'strict', +) -> str: + """Return the decoded string of the resource. + + The decoding-related arguments have the same semantics as those of + bytes.decode(). + """ + with open_text(package, resource, encoding, errors) as fp: + return fp.read() + + +@deprecated +def contents(package: Package) -> Iterable[str]: + """Return an iterable of entries in `package`. + + Note that not all entries are resources. Specifically, directories are + not considered resources. Use `is_resource()` on each entry returned here + to check if it is a resource or not. + """ + return [path.name for path in _common.files(package).iterdir()] + + +@deprecated +def is_resource(package: Package, name: str) -> bool: + """True if `name` is a resource inside `package`. + + Directories are *not* resources. + """ + resource = normalize_path(name) + return any( + traversable.name == resource and traversable.is_file() + for traversable in _common.files(package).iterdir() + ) + + +@deprecated +def path( + package: Package, + resource: Resource, +) -> ContextManager[pathlib.Path]: + """A context manager providing a file path object to the resource. + + If the resource does not already exist on its own on the file system, + a temporary file will be created. If the file was created, the file + will be deleted upon exiting the context manager (no exception is + raised if the file was deleted prior to the context manager + exiting). + """ + return _common.as_file(_common.files(package) / normalize_path(resource)) diff --git a/pkg_resources/_vendor/importlib_resources/abc.py b/pkg_resources/_vendor/importlib_resources/abc.py new file mode 100644 index 00000000..d39dc1ad --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources/abc.py @@ -0,0 +1,137 @@ +import abc +from typing import BinaryIO, Iterable, Text + +from ._compat import runtime_checkable, Protocol + + +class ResourceReader(metaclass=abc.ABCMeta): + """Abstract base class for loaders to provide resource reading support.""" + + @abc.abstractmethod + def open_resource(self, resource: Text) -> BinaryIO: + """Return an opened, file-like object for binary reading. + + The 'resource' argument is expected to represent only a file name. + If the resource cannot be found, FileNotFoundError is raised. + """ + # This deliberately raises FileNotFoundError instead of + # NotImplementedError so that if this method is accidentally called, + # it'll still do the right thing. + raise FileNotFoundError + + @abc.abstractmethod + def resource_path(self, resource: Text) -> Text: + """Return the file system path to the specified resource. + + The 'resource' argument is expected to represent only a file name. + If the resource does not exist on the file system, raise + FileNotFoundError. + """ + # This deliberately raises FileNotFoundError instead of + # NotImplementedError so that if this method is accidentally called, + # it'll still do the right thing. + raise FileNotFoundError + + @abc.abstractmethod + def is_resource(self, path: Text) -> bool: + """Return True if the named 'path' is a resource. + + Files are resources, directories are not. + """ + raise FileNotFoundError + + @abc.abstractmethod + def contents(self) -> Iterable[str]: + """Return an iterable of entries in `package`.""" + raise FileNotFoundError + + +@runtime_checkable +class Traversable(Protocol): + """ + An object with a subset of pathlib.Path methods suitable for + traversing directories and opening files. + """ + + @abc.abstractmethod + def iterdir(self): + """ + Yield Traversable objects in self + """ + + def read_bytes(self): + """ + Read contents of self as bytes + """ + with self.open('rb') as strm: + return strm.read() + + def read_text(self, encoding=None): + """ + Read contents of self as text + """ + with self.open(encoding=encoding) as strm: + return strm.read() + + @abc.abstractmethod + def is_dir(self) -> bool: + """ + Return True if self is a directory + """ + + @abc.abstractmethod + def is_file(self) -> bool: + """ + Return True if self is a file + """ + + @abc.abstractmethod + def joinpath(self, child): + """ + Return Traversable child in self + """ + + def __truediv__(self, child): + """ + Return Traversable child in self + """ + return self.joinpath(child) + + @abc.abstractmethod + def open(self, mode='r', *args, **kwargs): + """ + mode may be 'r' or 'rb' to open as text or binary. Return a handle + suitable for reading (same as pathlib.Path.open). + + When opening as text, accepts encoding parameters such as those + accepted by io.TextIOWrapper. + """ + + @abc.abstractproperty + def name(self) -> str: + """ + The base name of this object without any parent references. + """ + + +class TraversableResources(ResourceReader): + """ + The required interface for providing traversable + resources. + """ + + @abc.abstractmethod + def files(self): + """Return a Traversable object for the loaded package.""" + + def open_resource(self, resource): + return self.files().joinpath(resource).open('rb') + + def resource_path(self, resource): + raise FileNotFoundError(resource) + + def is_resource(self, path): + return self.files().joinpath(path).is_file() + + def contents(self): + return (item.name for item in self.files().iterdir()) diff --git a/pkg_resources/_vendor/importlib_resources/py.typed b/pkg_resources/_vendor/importlib_resources/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/pkg_resources/_vendor/importlib_resources/readers.py b/pkg_resources/_vendor/importlib_resources/readers.py new file mode 100644 index 00000000..f1190ca4 --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources/readers.py @@ -0,0 +1,122 @@ +import collections +import pathlib +import operator + +from . import abc + +from ._itertools import unique_everseen +from ._compat import ZipPath + + +def remove_duplicates(items): + return iter(collections.OrderedDict.fromkeys(items)) + + +class FileReader(abc.TraversableResources): + def __init__(self, loader): + self.path = pathlib.Path(loader.path).parent + + def resource_path(self, resource): + """ + Return the file system path to prevent + `resources.path()` from creating a temporary + copy. + """ + return str(self.path.joinpath(resource)) + + def files(self): + return self.path + + +class ZipReader(abc.TraversableResources): + def __init__(self, loader, module): + _, _, name = module.rpartition('.') + self.prefix = loader.prefix.replace('\\', '/') + name + '/' + self.archive = loader.archive + + def open_resource(self, resource): + try: + return super().open_resource(resource) + except KeyError as exc: + raise FileNotFoundError(exc.args[0]) + + def is_resource(self, path): + # workaround for `zipfile.Path.is_file` returning true + # for non-existent paths. + target = self.files().joinpath(path) + return target.is_file() and target.exists() + + def files(self): + return ZipPath(self.archive, self.prefix) + + +class MultiplexedPath(abc.Traversable): + """ + Given a series of Traversable objects, implement a merged + version of the interface across all objects. Useful for + namespace packages which may be multihomed at a single + name. + """ + + def __init__(self, *paths): + self._paths = list(map(pathlib.Path, remove_duplicates(paths))) + if not self._paths: + message = 'MultiplexedPath must contain at least one path' + raise FileNotFoundError(message) + if not all(path.is_dir() for path in self._paths): + raise NotADirectoryError('MultiplexedPath only supports directories') + + def iterdir(self): + files = (file for path in self._paths for file in path.iterdir()) + return unique_everseen(files, key=operator.attrgetter('name')) + + def read_bytes(self): + raise FileNotFoundError(f'{self} is not a file') + + def read_text(self, *args, **kwargs): + raise FileNotFoundError(f'{self} is not a file') + + def is_dir(self): + return True + + def is_file(self): + return False + + def joinpath(self, child): + # first try to find child in current paths + for file in self.iterdir(): + if file.name == child: + return file + # if it does not exist, construct it with the first path + return self._paths[0] / child + + __truediv__ = joinpath + + def open(self, *args, **kwargs): + raise FileNotFoundError(f'{self} is not a file') + + @property + def name(self): + return self._paths[0].name + + def __repr__(self): + paths = ', '.join(f"'{path}'" for path in self._paths) + return f'MultiplexedPath({paths})' + + +class NamespaceReader(abc.TraversableResources): + def __init__(self, namespace_path): + if 'NamespacePath' not in str(namespace_path): + raise ValueError('Invalid path') + self.path = MultiplexedPath(*list(namespace_path)) + + def resource_path(self, resource): + """ + Return the file system path to prevent + `resources.path()` from creating a temporary + copy. + """ + return str(self.path.joinpath(resource)) + + def files(self): + return self.path diff --git a/pkg_resources/_vendor/importlib_resources/simple.py b/pkg_resources/_vendor/importlib_resources/simple.py new file mode 100644 index 00000000..da073cbd --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources/simple.py @@ -0,0 +1,116 @@ +""" +Interface adapters for low-level readers. +""" + +import abc +import io +import itertools +from typing import BinaryIO, List + +from .abc import Traversable, TraversableResources + + +class SimpleReader(abc.ABC): + """ + The minimum, low-level interface required from a resource + provider. + """ + + @abc.abstractproperty + def package(self): + # type: () -> str + """ + The name of the package for which this reader loads resources. + """ + + @abc.abstractmethod + def children(self): + # type: () -> List['SimpleReader'] + """ + Obtain an iterable of SimpleReader for available + child containers (e.g. directories). + """ + + @abc.abstractmethod + def resources(self): + # type: () -> List[str] + """ + Obtain available named resources for this virtual package. + """ + + @abc.abstractmethod + def open_binary(self, resource): + # type: (str) -> BinaryIO + """ + Obtain a File-like for a named resource. + """ + + @property + def name(self): + return self.package.split('.')[-1] + + +class ResourceHandle(Traversable): + """ + Handle to a named resource in a ResourceReader. + """ + + def __init__(self, parent, name): + # type: (ResourceContainer, str) -> None + self.parent = parent + self.name = name # type: ignore + + def is_file(self): + return True + + def is_dir(self): + return False + + def open(self, mode='r', *args, **kwargs): + stream = self.parent.reader.open_binary(self.name) + if 'b' not in mode: + stream = io.TextIOWrapper(*args, **kwargs) + return stream + + def joinpath(self, name): + raise RuntimeError("Cannot traverse into a resource") + + +class ResourceContainer(Traversable): + """ + Traversable container for a package's resources via its reader. + """ + + def __init__(self, reader): + # type: (SimpleReader) -> None + self.reader = reader + + def is_dir(self): + return True + + def is_file(self): + return False + + def iterdir(self): + files = (ResourceHandle(self, name) for name in self.reader.resources) + dirs = map(ResourceContainer, self.reader.children()) + return itertools.chain(files, dirs) + + def open(self, *args, **kwargs): + raise IsADirectoryError() + + def joinpath(self, name): + return next( + traversable for traversable in self.iterdir() if traversable.name == name + ) + + +class TraversableReader(TraversableResources, SimpleReader): + """ + A TraversableResources based on SimpleReader. Resource providers + may derive from this class to provide the TraversableResources + interface by supplying the SimpleReader interface. + """ + + def files(self): + return ResourceContainer(self) diff --git a/pkg_resources/_vendor/importlib_resources/tests/__init__.py b/pkg_resources/_vendor/importlib_resources/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pkg_resources/_vendor/importlib_resources/tests/_compat.py b/pkg_resources/_vendor/importlib_resources/tests/_compat.py new file mode 100644 index 00000000..4c99cffd --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources/tests/_compat.py @@ -0,0 +1,19 @@ +import os + + +try: + from test.support import import_helper # type: ignore +except ImportError: + # Python 3.9 and earlier + class import_helper: # type: ignore + from test.support import modules_setup, modules_cleanup + + +try: + # Python 3.10 + from test.support.os_helper import unlink +except ImportError: + from test.support import unlink as _unlink + + def unlink(target): + return _unlink(os.fspath(target)) diff --git a/pkg_resources/_vendor/importlib_resources/tests/data01/__init__.py b/pkg_resources/_vendor/importlib_resources/tests/data01/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pkg_resources/_vendor/importlib_resources/tests/data01/binary.file b/pkg_resources/_vendor/importlib_resources/tests/data01/binary.file new file mode 100644 index 00000000..eaf36c1d Binary files /dev/null and b/pkg_resources/_vendor/importlib_resources/tests/data01/binary.file differ diff --git a/pkg_resources/_vendor/importlib_resources/tests/data01/subdirectory/__init__.py b/pkg_resources/_vendor/importlib_resources/tests/data01/subdirectory/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pkg_resources/_vendor/importlib_resources/tests/data01/subdirectory/binary.file b/pkg_resources/_vendor/importlib_resources/tests/data01/subdirectory/binary.file new file mode 100644 index 00000000..eaf36c1d Binary files /dev/null and b/pkg_resources/_vendor/importlib_resources/tests/data01/subdirectory/binary.file differ diff --git a/pkg_resources/_vendor/importlib_resources/tests/data01/utf-16.file b/pkg_resources/_vendor/importlib_resources/tests/data01/utf-16.file new file mode 100644 index 00000000..2cb77229 Binary files /dev/null and b/pkg_resources/_vendor/importlib_resources/tests/data01/utf-16.file differ diff --git a/pkg_resources/_vendor/importlib_resources/tests/data01/utf-8.file b/pkg_resources/_vendor/importlib_resources/tests/data01/utf-8.file new file mode 100644 index 00000000..1c0132ad --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources/tests/data01/utf-8.file @@ -0,0 +1 @@ +Hello, UTF-8 world! diff --git a/pkg_resources/_vendor/importlib_resources/tests/data02/__init__.py b/pkg_resources/_vendor/importlib_resources/tests/data02/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pkg_resources/_vendor/importlib_resources/tests/data02/one/__init__.py b/pkg_resources/_vendor/importlib_resources/tests/data02/one/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pkg_resources/_vendor/importlib_resources/tests/data02/one/resource1.txt b/pkg_resources/_vendor/importlib_resources/tests/data02/one/resource1.txt new file mode 100644 index 00000000..61a813e4 --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources/tests/data02/one/resource1.txt @@ -0,0 +1 @@ +one resource diff --git a/pkg_resources/_vendor/importlib_resources/tests/data02/two/__init__.py b/pkg_resources/_vendor/importlib_resources/tests/data02/two/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pkg_resources/_vendor/importlib_resources/tests/data02/two/resource2.txt b/pkg_resources/_vendor/importlib_resources/tests/data02/two/resource2.txt new file mode 100644 index 00000000..a80ce46e --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources/tests/data02/two/resource2.txt @@ -0,0 +1 @@ +two resource diff --git a/pkg_resources/_vendor/importlib_resources/tests/namespacedata01/binary.file b/pkg_resources/_vendor/importlib_resources/tests/namespacedata01/binary.file new file mode 100644 index 00000000..eaf36c1d Binary files /dev/null and b/pkg_resources/_vendor/importlib_resources/tests/namespacedata01/binary.file differ diff --git a/pkg_resources/_vendor/importlib_resources/tests/namespacedata01/utf-16.file b/pkg_resources/_vendor/importlib_resources/tests/namespacedata01/utf-16.file new file mode 100644 index 00000000..2cb77229 Binary files /dev/null and b/pkg_resources/_vendor/importlib_resources/tests/namespacedata01/utf-16.file differ diff --git a/pkg_resources/_vendor/importlib_resources/tests/namespacedata01/utf-8.file b/pkg_resources/_vendor/importlib_resources/tests/namespacedata01/utf-8.file new file mode 100644 index 00000000..1c0132ad --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources/tests/namespacedata01/utf-8.file @@ -0,0 +1 @@ +Hello, UTF-8 world! diff --git a/pkg_resources/_vendor/importlib_resources/tests/test_compatibilty_files.py b/pkg_resources/_vendor/importlib_resources/tests/test_compatibilty_files.py new file mode 100644 index 00000000..d92c7c56 --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources/tests/test_compatibilty_files.py @@ -0,0 +1,102 @@ +import io +import unittest + +import importlib_resources as resources + +from importlib_resources._adapters import ( + CompatibilityFiles, + wrap_spec, +) + +from . import util + + +class CompatibilityFilesTests(unittest.TestCase): + @property + def package(self): + bytes_data = io.BytesIO(b'Hello, world!') + return util.create_package( + file=bytes_data, + path='some_path', + contents=('a', 'b', 'c'), + ) + + @property + def files(self): + return resources.files(self.package) + + def test_spec_path_iter(self): + self.assertEqual( + sorted(path.name for path in self.files.iterdir()), + ['a', 'b', 'c'], + ) + + def test_child_path_iter(self): + self.assertEqual(list((self.files / 'a').iterdir()), []) + + def test_orphan_path_iter(self): + self.assertEqual(list((self.files / 'a' / 'a').iterdir()), []) + self.assertEqual(list((self.files / 'a' / 'a' / 'a').iterdir()), []) + + def test_spec_path_is(self): + self.assertFalse(self.files.is_file()) + self.assertFalse(self.files.is_dir()) + + def test_child_path_is(self): + self.assertTrue((self.files / 'a').is_file()) + self.assertFalse((self.files / 'a').is_dir()) + + def test_orphan_path_is(self): + self.assertFalse((self.files / 'a' / 'a').is_file()) + self.assertFalse((self.files / 'a' / 'a').is_dir()) + self.assertFalse((self.files / 'a' / 'a' / 'a').is_file()) + self.assertFalse((self.files / 'a' / 'a' / 'a').is_dir()) + + def test_spec_path_name(self): + self.assertEqual(self.files.name, 'testingpackage') + + def test_child_path_name(self): + self.assertEqual((self.files / 'a').name, 'a') + + def test_orphan_path_name(self): + self.assertEqual((self.files / 'a' / 'b').name, 'b') + self.assertEqual((self.files / 'a' / 'b' / 'c').name, 'c') + + def test_spec_path_open(self): + self.assertEqual(self.files.read_bytes(), b'Hello, world!') + self.assertEqual(self.files.read_text(), 'Hello, world!') + + def test_child_path_open(self): + self.assertEqual((self.files / 'a').read_bytes(), b'Hello, world!') + self.assertEqual((self.files / 'a').read_text(), 'Hello, world!') + + def test_orphan_path_open(self): + with self.assertRaises(FileNotFoundError): + (self.files / 'a' / 'b').read_bytes() + with self.assertRaises(FileNotFoundError): + (self.files / 'a' / 'b' / 'c').read_bytes() + + def test_open_invalid_mode(self): + with self.assertRaises(ValueError): + self.files.open('0') + + def test_orphan_path_invalid(self): + with self.assertRaises(ValueError): + CompatibilityFiles.OrphanPath() + + def test_wrap_spec(self): + spec = wrap_spec(self.package) + self.assertIsInstance(spec.loader.get_resource_reader(None), CompatibilityFiles) + + +class CompatibilityFilesNoReaderTests(unittest.TestCase): + @property + def package(self): + return util.create_package_from_loader(None) + + @property + def files(self): + return resources.files(self.package) + + def test_spec_path_joinpath(self): + self.assertIsInstance(self.files / 'a', CompatibilityFiles.OrphanPath) diff --git a/pkg_resources/_vendor/importlib_resources/tests/test_contents.py b/pkg_resources/_vendor/importlib_resources/tests/test_contents.py new file mode 100644 index 00000000..525568e8 --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources/tests/test_contents.py @@ -0,0 +1,43 @@ +import unittest +import importlib_resources as resources + +from . import data01 +from . import util + + +class ContentsTests: + expected = { + '__init__.py', + 'binary.file', + 'subdirectory', + 'utf-16.file', + 'utf-8.file', + } + + def test_contents(self): + contents = {path.name for path in resources.files(self.data).iterdir()} + assert self.expected <= contents + + +class ContentsDiskTests(ContentsTests, unittest.TestCase): + def setUp(self): + self.data = data01 + + +class ContentsZipTests(ContentsTests, util.ZipSetup, unittest.TestCase): + pass + + +class ContentsNamespaceTests(ContentsTests, unittest.TestCase): + expected = { + # no __init__ because of namespace design + # no subdirectory as incidental difference in fixture + 'binary.file', + 'utf-16.file', + 'utf-8.file', + } + + def setUp(self): + from . import namespacedata01 + + self.data = namespacedata01 diff --git a/pkg_resources/_vendor/importlib_resources/tests/test_files.py b/pkg_resources/_vendor/importlib_resources/tests/test_files.py new file mode 100644 index 00000000..2676b49e --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources/tests/test_files.py @@ -0,0 +1,46 @@ +import typing +import unittest + +import importlib_resources as resources +from importlib_resources.abc import Traversable +from . import data01 +from . import util + + +class FilesTests: + def test_read_bytes(self): + files = resources.files(self.data) + actual = files.joinpath('utf-8.file').read_bytes() + assert actual == b'Hello, UTF-8 world!\n' + + def test_read_text(self): + files = resources.files(self.data) + actual = files.joinpath('utf-8.file').read_text(encoding='utf-8') + assert actual == 'Hello, UTF-8 world!\n' + + @unittest.skipUnless( + hasattr(typing, 'runtime_checkable'), + "Only suitable when typing supports runtime_checkable", + ) + def test_traversable(self): + assert isinstance(resources.files(self.data), Traversable) + + +class OpenDiskTests(FilesTests, unittest.TestCase): + def setUp(self): + self.data = data01 + + +class OpenZipTests(FilesTests, util.ZipSetup, unittest.TestCase): + pass + + +class OpenNamespaceTests(FilesTests, unittest.TestCase): + def setUp(self): + from . import namespacedata01 + + self.data = namespacedata01 + + +if __name__ == '__main__': + unittest.main() diff --git a/pkg_resources/_vendor/importlib_resources/tests/test_open.py b/pkg_resources/_vendor/importlib_resources/tests/test_open.py new file mode 100644 index 00000000..87b42c3d --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources/tests/test_open.py @@ -0,0 +1,81 @@ +import unittest + +import importlib_resources as resources +from . import data01 +from . import util + + +class CommonBinaryTests(util.CommonTests, unittest.TestCase): + def execute(self, package, path): + target = resources.files(package).joinpath(path) + with target.open('rb'): + pass + + +class CommonTextTests(util.CommonTests, unittest.TestCase): + def execute(self, package, path): + target = resources.files(package).joinpath(path) + with target.open(): + pass + + +class OpenTests: + def test_open_binary(self): + target = resources.files(self.data) / 'binary.file' + with target.open('rb') as fp: + result = fp.read() + self.assertEqual(result, b'\x00\x01\x02\x03') + + def test_open_text_default_encoding(self): + target = resources.files(self.data) / 'utf-8.file' + with target.open() as fp: + result = fp.read() + self.assertEqual(result, 'Hello, UTF-8 world!\n') + + def test_open_text_given_encoding(self): + target = resources.files(self.data) / 'utf-16.file' + with target.open(encoding='utf-16', errors='strict') as fp: + result = fp.read() + self.assertEqual(result, 'Hello, UTF-16 world!\n') + + def test_open_text_with_errors(self): + # Raises UnicodeError without the 'errors' argument. + target = resources.files(self.data) / 'utf-16.file' + with target.open(encoding='utf-8', errors='strict') as fp: + self.assertRaises(UnicodeError, fp.read) + with target.open(encoding='utf-8', errors='ignore') as fp: + result = fp.read() + self.assertEqual( + result, + 'H\x00e\x00l\x00l\x00o\x00,\x00 ' + '\x00U\x00T\x00F\x00-\x001\x006\x00 ' + '\x00w\x00o\x00r\x00l\x00d\x00!\x00\n\x00', + ) + + def test_open_binary_FileNotFoundError(self): + target = resources.files(self.data) / 'does-not-exist' + self.assertRaises(FileNotFoundError, target.open, 'rb') + + def test_open_text_FileNotFoundError(self): + target = resources.files(self.data) / 'does-not-exist' + self.assertRaises(FileNotFoundError, target.open) + + +class OpenDiskTests(OpenTests, unittest.TestCase): + def setUp(self): + self.data = data01 + + +class OpenDiskNamespaceTests(OpenTests, unittest.TestCase): + def setUp(self): + from . import namespacedata01 + + self.data = namespacedata01 + + +class OpenZipTests(OpenTests, util.ZipSetup, unittest.TestCase): + pass + + +if __name__ == '__main__': + unittest.main() diff --git a/pkg_resources/_vendor/importlib_resources/tests/test_path.py b/pkg_resources/_vendor/importlib_resources/tests/test_path.py new file mode 100644 index 00000000..4f4d3943 --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources/tests/test_path.py @@ -0,0 +1,64 @@ +import io +import unittest + +import importlib_resources as resources +from . import data01 +from . import util + + +class CommonTests(util.CommonTests, unittest.TestCase): + def execute(self, package, path): + with resources.as_file(resources.files(package).joinpath(path)): + pass + + +class PathTests: + def test_reading(self): + # Path should be readable. + # Test also implicitly verifies the returned object is a pathlib.Path + # instance. + target = resources.files(self.data) / 'utf-8.file' + with resources.as_file(target) as path: + self.assertTrue(path.name.endswith("utf-8.file"), repr(path)) + # pathlib.Path.read_text() was introduced in Python 3.5. + with path.open('r', encoding='utf-8') as file: + text = file.read() + self.assertEqual('Hello, UTF-8 world!\n', text) + + +class PathDiskTests(PathTests, unittest.TestCase): + data = data01 + + def test_natural_path(self): + """ + Guarantee the internal implementation detail that + file-system-backed resources do not get the tempdir + treatment. + """ + target = resources.files(self.data) / 'utf-8.file' + with resources.as_file(target) as path: + assert 'data' in str(path) + + +class PathMemoryTests(PathTests, unittest.TestCase): + def setUp(self): + file = io.BytesIO(b'Hello, UTF-8 world!\n') + self.addCleanup(file.close) + self.data = util.create_package( + file=file, path=FileNotFoundError("package exists only in memory") + ) + self.data.__spec__.origin = None + self.data.__spec__.has_location = False + + +class PathZipTests(PathTests, util.ZipSetup, unittest.TestCase): + def test_remove_in_context_manager(self): + # It is not an error if the file that was temporarily stashed on the + # file system is removed inside the `with` stanza. + target = resources.files(self.data) / 'utf-8.file' + with resources.as_file(target) as path: + path.unlink() + + +if __name__ == '__main__': + unittest.main() diff --git a/pkg_resources/_vendor/importlib_resources/tests/test_read.py b/pkg_resources/_vendor/importlib_resources/tests/test_read.py new file mode 100644 index 00000000..41dd6db5 --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources/tests/test_read.py @@ -0,0 +1,76 @@ +import unittest +import importlib_resources as resources + +from . import data01 +from . import util +from importlib import import_module + + +class CommonBinaryTests(util.CommonTests, unittest.TestCase): + def execute(self, package, path): + resources.files(package).joinpath(path).read_bytes() + + +class CommonTextTests(util.CommonTests, unittest.TestCase): + def execute(self, package, path): + resources.files(package).joinpath(path).read_text() + + +class ReadTests: + def test_read_bytes(self): + result = resources.files(self.data).joinpath('binary.file').read_bytes() + self.assertEqual(result, b'\0\1\2\3') + + def test_read_text_default_encoding(self): + result = resources.files(self.data).joinpath('utf-8.file').read_text() + self.assertEqual(result, 'Hello, UTF-8 world!\n') + + def test_read_text_given_encoding(self): + result = ( + resources.files(self.data) + .joinpath('utf-16.file') + .read_text(encoding='utf-16') + ) + self.assertEqual(result, 'Hello, UTF-16 world!\n') + + def test_read_text_with_errors(self): + # Raises UnicodeError without the 'errors' argument. + target = resources.files(self.data) / 'utf-16.file' + self.assertRaises(UnicodeError, target.read_text, encoding='utf-8') + result = target.read_text(encoding='utf-8', errors='ignore') + self.assertEqual( + result, + 'H\x00e\x00l\x00l\x00o\x00,\x00 ' + '\x00U\x00T\x00F\x00-\x001\x006\x00 ' + '\x00w\x00o\x00r\x00l\x00d\x00!\x00\n\x00', + ) + + +class ReadDiskTests(ReadTests, unittest.TestCase): + data = data01 + + +class ReadZipTests(ReadTests, util.ZipSetup, unittest.TestCase): + def test_read_submodule_resource(self): + submodule = import_module('ziptestdata.subdirectory') + result = resources.files(submodule).joinpath('binary.file').read_bytes() + self.assertEqual(result, b'\0\1\2\3') + + def test_read_submodule_resource_by_name(self): + result = ( + resources.files('ziptestdata.subdirectory') + .joinpath('binary.file') + .read_bytes() + ) + self.assertEqual(result, b'\0\1\2\3') + + +class ReadNamespaceTests(ReadTests, unittest.TestCase): + def setUp(self): + from . import namespacedata01 + + self.data = namespacedata01 + + +if __name__ == '__main__': + unittest.main() diff --git a/pkg_resources/_vendor/importlib_resources/tests/test_reader.py b/pkg_resources/_vendor/importlib_resources/tests/test_reader.py new file mode 100644 index 00000000..16841a50 --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources/tests/test_reader.py @@ -0,0 +1,128 @@ +import os.path +import sys +import pathlib +import unittest + +from importlib import import_module +from importlib_resources.readers import MultiplexedPath, NamespaceReader + + +class MultiplexedPathTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + path = pathlib.Path(__file__).parent / 'namespacedata01' + cls.folder = str(path) + + def test_init_no_paths(self): + with self.assertRaises(FileNotFoundError): + MultiplexedPath() + + def test_init_file(self): + with self.assertRaises(NotADirectoryError): + MultiplexedPath(os.path.join(self.folder, 'binary.file')) + + def test_iterdir(self): + contents = {path.name for path in MultiplexedPath(self.folder).iterdir()} + try: + contents.remove('__pycache__') + except (KeyError, ValueError): + pass + self.assertEqual(contents, {'binary.file', 'utf-16.file', 'utf-8.file'}) + + def test_iterdir_duplicate(self): + data01 = os.path.abspath(os.path.join(__file__, '..', 'data01')) + contents = { + path.name for path in MultiplexedPath(self.folder, data01).iterdir() + } + for remove in ('__pycache__', '__init__.pyc'): + try: + contents.remove(remove) + except (KeyError, ValueError): + pass + self.assertEqual( + contents, + {'__init__.py', 'binary.file', 'subdirectory', 'utf-16.file', 'utf-8.file'}, + ) + + def test_is_dir(self): + self.assertEqual(MultiplexedPath(self.folder).is_dir(), True) + + def test_is_file(self): + self.assertEqual(MultiplexedPath(self.folder).is_file(), False) + + def test_open_file(self): + path = MultiplexedPath(self.folder) + with self.assertRaises(FileNotFoundError): + path.read_bytes() + with self.assertRaises(FileNotFoundError): + path.read_text() + with self.assertRaises(FileNotFoundError): + path.open() + + def test_join_path(self): + prefix = os.path.abspath(os.path.join(__file__, '..')) + data01 = os.path.join(prefix, 'data01') + path = MultiplexedPath(self.folder, data01) + self.assertEqual( + str(path.joinpath('binary.file'))[len(prefix) + 1 :], + os.path.join('namespacedata01', 'binary.file'), + ) + self.assertEqual( + str(path.joinpath('subdirectory'))[len(prefix) + 1 :], + os.path.join('data01', 'subdirectory'), + ) + self.assertEqual( + str(path.joinpath('imaginary'))[len(prefix) + 1 :], + os.path.join('namespacedata01', 'imaginary'), + ) + + def test_repr(self): + self.assertEqual( + repr(MultiplexedPath(self.folder)), + f"MultiplexedPath('{self.folder}')", + ) + + def test_name(self): + self.assertEqual( + MultiplexedPath(self.folder).name, + os.path.basename(self.folder), + ) + + +class NamespaceReaderTest(unittest.TestCase): + site_dir = str(pathlib.Path(__file__).parent) + + @classmethod + def setUpClass(cls): + sys.path.append(cls.site_dir) + + @classmethod + def tearDownClass(cls): + sys.path.remove(cls.site_dir) + + def test_init_error(self): + with self.assertRaises(ValueError): + NamespaceReader(['path1', 'path2']) + + def test_resource_path(self): + namespacedata01 = import_module('namespacedata01') + reader = NamespaceReader(namespacedata01.__spec__.submodule_search_locations) + + root = os.path.abspath(os.path.join(__file__, '..', 'namespacedata01')) + self.assertEqual( + reader.resource_path('binary.file'), os.path.join(root, 'binary.file') + ) + self.assertEqual( + reader.resource_path('imaginary'), os.path.join(root, 'imaginary') + ) + + def test_files(self): + namespacedata01 = import_module('namespacedata01') + reader = NamespaceReader(namespacedata01.__spec__.submodule_search_locations) + root = os.path.abspath(os.path.join(__file__, '..', 'namespacedata01')) + self.assertIsInstance(reader.files(), MultiplexedPath) + self.assertEqual(repr(reader.files()), f"MultiplexedPath('{root}')") + + +if __name__ == '__main__': + unittest.main() diff --git a/pkg_resources/_vendor/importlib_resources/tests/test_resource.py b/pkg_resources/_vendor/importlib_resources/tests/test_resource.py new file mode 100644 index 00000000..5affd8b0 --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources/tests/test_resource.py @@ -0,0 +1,252 @@ +import sys +import unittest +import importlib_resources as resources +import uuid +import pathlib + +from . import data01 +from . import zipdata01, zipdata02 +from . import util +from importlib import import_module +from ._compat import import_helper, unlink + + +class ResourceTests: + # Subclasses are expected to set the `data` attribute. + + def test_is_file_exists(self): + target = resources.files(self.data) / 'binary.file' + self.assertTrue(target.is_file()) + + def test_is_file_missing(self): + target = resources.files(self.data) / 'not-a-file' + self.assertFalse(target.is_file()) + + def test_is_dir(self): + target = resources.files(self.data) / 'subdirectory' + self.assertFalse(target.is_file()) + self.assertTrue(target.is_dir()) + + +class ResourceDiskTests(ResourceTests, unittest.TestCase): + def setUp(self): + self.data = data01 + + +class ResourceZipTests(ResourceTests, util.ZipSetup, unittest.TestCase): + pass + + +def names(traversable): + return {item.name for item in traversable.iterdir()} + + +class ResourceLoaderTests(unittest.TestCase): + def test_resource_contents(self): + package = util.create_package( + file=data01, path=data01.__file__, contents=['A', 'B', 'C'] + ) + self.assertEqual(names(resources.files(package)), {'A', 'B', 'C'}) + + def test_is_file(self): + package = util.create_package( + file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F'] + ) + self.assertTrue(resources.files(package).joinpath('B').is_file()) + + def test_is_dir(self): + package = util.create_package( + file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F'] + ) + self.assertTrue(resources.files(package).joinpath('D').is_dir()) + + def test_resource_missing(self): + package = util.create_package( + file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F'] + ) + self.assertFalse(resources.files(package).joinpath('Z').is_file()) + + +class ResourceCornerCaseTests(unittest.TestCase): + def test_package_has_no_reader_fallback(self): + # Test odd ball packages which: + # 1. Do not have a ResourceReader as a loader + # 2. Are not on the file system + # 3. Are not in a zip file + module = util.create_package( + file=data01, path=data01.__file__, contents=['A', 'B', 'C'] + ) + # Give the module a dummy loader. + module.__loader__ = object() + # Give the module a dummy origin. + module.__file__ = '/path/which/shall/not/be/named' + module.__spec__.loader = module.__loader__ + module.__spec__.origin = module.__file__ + self.assertFalse(resources.files(module).joinpath('A').is_file()) + + +class ResourceFromZipsTest01(util.ZipSetupBase, unittest.TestCase): + ZIP_MODULE = zipdata01 # type: ignore + + def test_is_submodule_resource(self): + submodule = import_module('ziptestdata.subdirectory') + self.assertTrue(resources.files(submodule).joinpath('binary.file').is_file()) + + def test_read_submodule_resource_by_name(self): + self.assertTrue( + resources.files('ziptestdata.subdirectory') + .joinpath('binary.file') + .is_file() + ) + + def test_submodule_contents(self): + submodule = import_module('ziptestdata.subdirectory') + self.assertEqual( + names(resources.files(submodule)), {'__init__.py', 'binary.file'} + ) + + def test_submodule_contents_by_name(self): + self.assertEqual( + names(resources.files('ziptestdata.subdirectory')), + {'__init__.py', 'binary.file'}, + ) + + +class ResourceFromZipsTest02(util.ZipSetupBase, unittest.TestCase): + ZIP_MODULE = zipdata02 # type: ignore + + def test_unrelated_contents(self): + """ + Test thata zip with two unrelated subpackages return + distinct resources. Ref python/importlib_resources#44. + """ + self.assertEqual( + names(resources.files('ziptestdata.one')), + {'__init__.py', 'resource1.txt'}, + ) + self.assertEqual( + names(resources.files('ziptestdata.two')), + {'__init__.py', 'resource2.txt'}, + ) + + +class DeletingZipsTest(unittest.TestCase): + """Having accessed resources in a zip file should not keep an open + reference to the zip. + """ + + ZIP_MODULE = zipdata01 + + def setUp(self): + modules = import_helper.modules_setup() + self.addCleanup(import_helper.modules_cleanup, *modules) + + data_path = pathlib.Path(self.ZIP_MODULE.__file__) + data_dir = data_path.parent + self.source_zip_path = data_dir / 'ziptestdata.zip' + self.zip_path = pathlib.Path(f'{uuid.uuid4()}.zip').absolute() + self.zip_path.write_bytes(self.source_zip_path.read_bytes()) + sys.path.append(str(self.zip_path)) + self.data = import_module('ziptestdata') + + def tearDown(self): + try: + sys.path.remove(str(self.zip_path)) + except ValueError: + pass + + try: + del sys.path_importer_cache[str(self.zip_path)] + del sys.modules[self.data.__name__] + except KeyError: + pass + + try: + unlink(self.zip_path) + except OSError: + # If the test fails, this will probably fail too + pass + + def test_iterdir_does_not_keep_open(self): + c = [item.name for item in resources.files('ziptestdata').iterdir()] + self.zip_path.unlink() + del c + + def test_is_file_does_not_keep_open(self): + c = resources.files('ziptestdata').joinpath('binary.file').is_file() + self.zip_path.unlink() + del c + + def test_is_file_failure_does_not_keep_open(self): + c = resources.files('ziptestdata').joinpath('not-present').is_file() + self.zip_path.unlink() + del c + + @unittest.skip("Desired but not supported.") + def test_as_file_does_not_keep_open(self): # pragma: no cover + c = resources.as_file(resources.files('ziptestdata') / 'binary.file') + self.zip_path.unlink() + del c + + def test_entered_path_does_not_keep_open(self): + # This is what certifi does on import to make its bundle + # available for the process duration. + c = resources.as_file( + resources.files('ziptestdata') / 'binary.file' + ).__enter__() + self.zip_path.unlink() + del c + + def test_read_binary_does_not_keep_open(self): + c = resources.files('ziptestdata').joinpath('binary.file').read_bytes() + self.zip_path.unlink() + del c + + def test_read_text_does_not_keep_open(self): + c = resources.files('ziptestdata').joinpath('utf-8.file').read_text() + self.zip_path.unlink() + del c + + +class ResourceFromNamespaceTest01(unittest.TestCase): + site_dir = str(pathlib.Path(__file__).parent) + + @classmethod + def setUpClass(cls): + sys.path.append(cls.site_dir) + + @classmethod + def tearDownClass(cls): + sys.path.remove(cls.site_dir) + + def test_is_submodule_resource(self): + self.assertTrue( + resources.files(import_module('namespacedata01')) + .joinpath('binary.file') + .is_file() + ) + + def test_read_submodule_resource_by_name(self): + self.assertTrue( + resources.files('namespacedata01').joinpath('binary.file').is_file() + ) + + def test_submodule_contents(self): + contents = names(resources.files(import_module('namespacedata01'))) + try: + contents.remove('__pycache__') + except KeyError: + pass + self.assertEqual(contents, {'binary.file', 'utf-8.file', 'utf-16.file'}) + + def test_submodule_contents_by_name(self): + contents = names(resources.files('namespacedata01')) + try: + contents.remove('__pycache__') + except KeyError: + pass + self.assertEqual(contents, {'binary.file', 'utf-8.file', 'utf-16.file'}) + + +if __name__ == '__main__': + unittest.main() diff --git a/pkg_resources/_vendor/importlib_resources/tests/update-zips.py b/pkg_resources/_vendor/importlib_resources/tests/update-zips.py new file mode 100644 index 00000000..9ef0224c --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources/tests/update-zips.py @@ -0,0 +1,53 @@ +""" +Generate the zip test data files. + +Run to build the tests/zipdataNN/ziptestdata.zip files from +files in tests/dataNN. + +Replaces the file with the working copy, but does commit anything +to the source repo. +""" + +import contextlib +import os +import pathlib +import zipfile + + +def main(): + """ + >>> from unittest import mock + >>> monkeypatch = getfixture('monkeypatch') + >>> monkeypatch.setattr(zipfile, 'ZipFile', mock.MagicMock()) + >>> print(); main() # print workaround for bpo-32509 + + ...data01... -> ziptestdata/... + ... + ...data02... -> ziptestdata/... + ... + """ + suffixes = '01', '02' + tuple(map(generate, suffixes)) + + +def generate(suffix): + root = pathlib.Path(__file__).parent.relative_to(os.getcwd()) + zfpath = root / f'zipdata{suffix}/ziptestdata.zip' + with zipfile.ZipFile(zfpath, 'w') as zf: + for src, rel in walk(root / f'data{suffix}'): + dst = 'ziptestdata' / pathlib.PurePosixPath(rel.as_posix()) + print(src, '->', dst) + zf.write(src, dst) + + +def walk(datapath): + for dirpath, dirnames, filenames in os.walk(datapath): + with contextlib.suppress(KeyError): + dirnames.remove('__pycache__') + for filename in filenames: + res = pathlib.Path(dirpath) / filename + rel = res.relative_to(datapath) + yield res, rel + + +__name__ == '__main__' and main() diff --git a/pkg_resources/_vendor/importlib_resources/tests/util.py b/pkg_resources/_vendor/importlib_resources/tests/util.py new file mode 100644 index 00000000..c6d83e4b --- /dev/null +++ b/pkg_resources/_vendor/importlib_resources/tests/util.py @@ -0,0 +1,178 @@ +import abc +import importlib +import io +import sys +import types +from pathlib import Path, PurePath + +from . import data01 +from . import zipdata01 +from ..abc import ResourceReader +from ._compat import import_helper + + +from importlib.machinery import ModuleSpec + + +class Reader(ResourceReader): + def __init__(self, **kwargs): + vars(self).update(kwargs) + + def get_resource_reader(self, package): + return self + + def open_resource(self, path): + self._path = path + if isinstance(self.file, Exception): + raise self.file + return self.file + + def resource_path(self, path_): + self._path = path_ + if isinstance(self.path, Exception): + raise self.path + return self.path + + def is_resource(self, path_): + self._path = path_ + if isinstance(self.path, Exception): + raise self.path + + def part(entry): + return entry.split('/') + + return any( + len(parts) == 1 and parts[0] == path_ for parts in map(part, self._contents) + ) + + def contents(self): + if isinstance(self.path, Exception): + raise self.path + yield from self._contents + + +def create_package_from_loader(loader, is_package=True): + name = 'testingpackage' + module = types.ModuleType(name) + spec = ModuleSpec(name, loader, origin='does-not-exist', is_package=is_package) + module.__spec__ = spec + module.__loader__ = loader + return module + + +def create_package(file=None, path=None, is_package=True, contents=()): + return create_package_from_loader( + Reader(file=file, path=path, _contents=contents), + is_package, + ) + + +class CommonTests(metaclass=abc.ABCMeta): + """ + Tests shared by test_open, test_path, and test_read. + """ + + @abc.abstractmethod + def execute(self, package, path): + """ + Call the pertinent legacy API function (e.g. open_text, path) + on package and path. + """ + + def test_package_name(self): + # Passing in the package name should succeed. + self.execute(data01.__name__, 'utf-8.file') + + def test_package_object(self): + # Passing in the package itself should succeed. + self.execute(data01, 'utf-8.file') + + def test_string_path(self): + # Passing in a string for the path should succeed. + path = 'utf-8.file' + self.execute(data01, path) + + def test_pathlib_path(self): + # Passing in a pathlib.PurePath object for the path should succeed. + path = PurePath('utf-8.file') + self.execute(data01, path) + + def test_importing_module_as_side_effect(self): + # The anchor package can already be imported. + del sys.modules[data01.__name__] + self.execute(data01.__name__, 'utf-8.file') + + def test_non_package_by_name(self): + # The anchor package cannot be a module. + with self.assertRaises(TypeError): + self.execute(__name__, 'utf-8.file') + + def test_non_package_by_package(self): + # The anchor package cannot be a module. + with self.assertRaises(TypeError): + module = sys.modules['importlib_resources.tests.util'] + self.execute(module, 'utf-8.file') + + def test_missing_path(self): + # Attempting to open or read or request the path for a + # non-existent path should succeed if open_resource + # can return a viable data stream. + bytes_data = io.BytesIO(b'Hello, world!') + package = create_package(file=bytes_data, path=FileNotFoundError()) + self.execute(package, 'utf-8.file') + self.assertEqual(package.__loader__._path, 'utf-8.file') + + def test_extant_path(self): + # Attempting to open or read or request the path when the + # path does exist should still succeed. Does not assert + # anything about the result. + bytes_data = io.BytesIO(b'Hello, world!') + # any path that exists + path = __file__ + package = create_package(file=bytes_data, path=path) + self.execute(package, 'utf-8.file') + self.assertEqual(package.__loader__._path, 'utf-8.file') + + def test_useless_loader(self): + package = create_package(file=FileNotFoundError(), path=FileNotFoundError()) + with self.assertRaises(FileNotFoundError): + self.execute(package, 'utf-8.file') + + +class ZipSetupBase: + ZIP_MODULE = None + + @classmethod + def setUpClass(cls): + data_path = Path(cls.ZIP_MODULE.__file__) + data_dir = data_path.parent + cls._zip_path = str(data_dir / 'ziptestdata.zip') + sys.path.append(cls._zip_path) + cls.data = importlib.import_module('ziptestdata') + + @classmethod + def tearDownClass(cls): + try: + sys.path.remove(cls._zip_path) + except ValueError: + pass + + try: + del sys.path_importer_cache[cls._zip_path] + del sys.modules[cls.data.__name__] + except KeyError: + pass + + try: + del cls.data + del cls._zip_path + except AttributeError: + pass + + def setUp(self): + modules = import_helper.modules_setup() + self.addCleanup(import_helper.modules_cleanup, *modules) + + +class ZipSetup(ZipSetupBase): + ZIP_MODULE = zipdata01 # type: ignore diff --git a/pkg_resources/_vendor/importlib_resources/tests/zipdata01/__init__.py b/pkg_resources/_vendor/importlib_resources/tests/zipdata01/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pkg_resources/_vendor/importlib_resources/tests/zipdata01/ziptestdata.zip b/pkg_resources/_vendor/importlib_resources/tests/zipdata01/ziptestdata.zip new file mode 100644 index 00000000..9a3bb073 Binary files /dev/null and b/pkg_resources/_vendor/importlib_resources/tests/zipdata01/ziptestdata.zip differ diff --git a/pkg_resources/_vendor/importlib_resources/tests/zipdata02/__init__.py b/pkg_resources/_vendor/importlib_resources/tests/zipdata02/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pkg_resources/_vendor/importlib_resources/tests/zipdata02/ziptestdata.zip b/pkg_resources/_vendor/importlib_resources/tests/zipdata02/ziptestdata.zip new file mode 100644 index 00000000..d63ff512 Binary files /dev/null and b/pkg_resources/_vendor/importlib_resources/tests/zipdata02/ziptestdata.zip differ diff --git a/pkg_resources/_vendor/jaraco/functools.py b/pkg_resources/_vendor/jaraco/functools.py index fcdbb4f9..a3fea3a1 100644 --- a/pkg_resources/_vendor/jaraco/functools.py +++ b/pkg_resources/_vendor/jaraco/functools.py @@ -5,7 +5,7 @@ import collections import types import itertools -import more_itertools +import pkg_resources.extern.more_itertools from typing import Callable, TypeVar diff --git a/pkg_resources/_vendor/jaraco/text/__init__.py b/pkg_resources/_vendor/jaraco/text/__init__.py index 5f75519a..f39f2d93 100644 --- a/pkg_resources/_vendor/jaraco/text/__init__.py +++ b/pkg_resources/_vendor/jaraco/text/__init__.py @@ -6,10 +6,10 @@ import functools try: from importlib.resources import files # type: ignore except ImportError: # pragma: nocover - from importlib_resources import files # type: ignore + from pkg_resources.extern.importlib_resources import files # type: ignore -from jaraco.functools import compose, method_cache -from jaraco.context import ExceptionTrap +from pkg_resources.extern.jaraco.functools import compose, method_cache +from pkg_resources.extern.jaraco.context import ExceptionTrap def substitution(old, new): diff --git a/pkg_resources/_vendor/vendored.txt b/pkg_resources/_vendor/vendored.txt index a5d6840a..0128eb17 100644 --- a/pkg_resources/_vendor/vendored.txt +++ b/pkg_resources/_vendor/vendored.txt @@ -2,3 +2,7 @@ packaging==21.2 pyparsing==2.2.1 appdirs==1.4.3 jaraco.text==3.7.0 +# required for jaraco.text on older Pythons +importlib_resources==5.4.0 +# required for importlib_resources on older Pythons +zipp==3.7.0 diff --git a/pkg_resources/_vendor/zipp-3.7.0.dist-info/INSTALLER b/pkg_resources/_vendor/zipp-3.7.0.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/pkg_resources/_vendor/zipp-3.7.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/pkg_resources/_vendor/zipp-3.7.0.dist-info/LICENSE b/pkg_resources/_vendor/zipp-3.7.0.dist-info/LICENSE new file mode 100644 index 00000000..353924be --- /dev/null +++ b/pkg_resources/_vendor/zipp-3.7.0.dist-info/LICENSE @@ -0,0 +1,19 @@ +Copyright Jason R. Coombs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/pkg_resources/_vendor/zipp-3.7.0.dist-info/METADATA b/pkg_resources/_vendor/zipp-3.7.0.dist-info/METADATA new file mode 100644 index 00000000..b1308b5f --- /dev/null +++ b/pkg_resources/_vendor/zipp-3.7.0.dist-info/METADATA @@ -0,0 +1,58 @@ +Metadata-Version: 2.1 +Name: zipp +Version: 3.7.0 +Summary: Backport of pathlib-compatible object wrapper for zip files +Home-page: https://github.com/jaraco/zipp +Author: Jason R. Coombs +Author-email: jaraco@jaraco.com +License: UNKNOWN +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Requires-Python: >=3.7 +License-File: LICENSE +Provides-Extra: docs +Requires-Dist: sphinx ; extra == 'docs' +Requires-Dist: jaraco.packaging (>=8.2) ; extra == 'docs' +Requires-Dist: rst.linker (>=1.9) ; extra == 'docs' +Provides-Extra: testing +Requires-Dist: pytest (>=6) ; extra == 'testing' +Requires-Dist: pytest-checkdocs (>=2.4) ; extra == 'testing' +Requires-Dist: pytest-flake8 ; extra == 'testing' +Requires-Dist: pytest-cov ; extra == 'testing' +Requires-Dist: pytest-enabler (>=1.0.1) ; extra == 'testing' +Requires-Dist: jaraco.itertools ; extra == 'testing' +Requires-Dist: func-timeout ; extra == 'testing' +Requires-Dist: pytest-black (>=0.3.7) ; (platform_python_implementation != "PyPy") and extra == 'testing' +Requires-Dist: pytest-mypy ; (platform_python_implementation != "PyPy") and extra == 'testing' + +.. image:: https://img.shields.io/pypi/v/zipp.svg + :target: `PyPI link`_ + +.. image:: https://img.shields.io/pypi/pyversions/zipp.svg + :target: `PyPI link`_ + +.. _PyPI link: https://pypi.org/project/zipp + +.. image:: https://github.com/jaraco/zipp/workflows/tests/badge.svg + :target: https://github.com/jaraco/zipp/actions?query=workflow%3A%22tests%22 + :alt: tests + +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + :alt: Code style: Black + +.. .. image:: https://readthedocs.org/projects/skeleton/badge/?version=latest +.. :target: https://skeleton.readthedocs.io/en/latest/?badge=latest + +.. image:: https://img.shields.io/badge/skeleton-2021-informational + :target: https://blog.jaraco.com/skeleton + + +A pathlib-compatible Zipfile object wrapper. Official backport of the standard library +`Path object `_. + + diff --git a/pkg_resources/_vendor/zipp-3.7.0.dist-info/RECORD b/pkg_resources/_vendor/zipp-3.7.0.dist-info/RECORD new file mode 100644 index 00000000..38d0b21a --- /dev/null +++ b/pkg_resources/_vendor/zipp-3.7.0.dist-info/RECORD @@ -0,0 +1,9 @@ +__pycache__/zipp.cpython-310.pyc,, +zipp-3.7.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +zipp-3.7.0.dist-info/LICENSE,sha256=2z8CRrH5J48VhFuZ_sR4uLUG63ZIeZNyL4xuJUKF-vg,1050 +zipp-3.7.0.dist-info/METADATA,sha256=ZLzgaXTyZX_MxTU0lcGfhdPY4CjFrT_3vyQ2Fo49pl8,2261 +zipp-3.7.0.dist-info/RECORD,, +zipp-3.7.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +zipp-3.7.0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92 +zipp-3.7.0.dist-info/top_level.txt,sha256=iAbdoSHfaGqBfVb2XuR9JqSQHCoOsOtG6y9C_LSpqFw,5 +zipp.py,sha256=ajztOH-9I7KA_4wqDYygtHa6xUBVZgFpmZ8FE74HHHI,8425 diff --git a/pkg_resources/_vendor/zipp-3.7.0.dist-info/REQUESTED b/pkg_resources/_vendor/zipp-3.7.0.dist-info/REQUESTED new file mode 100644 index 00000000..e69de29b diff --git a/pkg_resources/_vendor/zipp-3.7.0.dist-info/WHEEL b/pkg_resources/_vendor/zipp-3.7.0.dist-info/WHEEL new file mode 100644 index 00000000..becc9a66 --- /dev/null +++ b/pkg_resources/_vendor/zipp-3.7.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.1) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/pkg_resources/_vendor/zipp-3.7.0.dist-info/top_level.txt b/pkg_resources/_vendor/zipp-3.7.0.dist-info/top_level.txt new file mode 100644 index 00000000..e82f676f --- /dev/null +++ b/pkg_resources/_vendor/zipp-3.7.0.dist-info/top_level.txt @@ -0,0 +1 @@ +zipp diff --git a/pkg_resources/_vendor/zipp.py b/pkg_resources/_vendor/zipp.py new file mode 100644 index 00000000..26b723c1 --- /dev/null +++ b/pkg_resources/_vendor/zipp.py @@ -0,0 +1,329 @@ +import io +import posixpath +import zipfile +import itertools +import contextlib +import sys +import pathlib + +if sys.version_info < (3, 7): + from collections import OrderedDict +else: + OrderedDict = dict + + +__all__ = ['Path'] + + +def _parents(path): + """ + Given a path with elements separated by + posixpath.sep, generate all parents of that path. + + >>> list(_parents('b/d')) + ['b'] + >>> list(_parents('/b/d/')) + ['/b'] + >>> list(_parents('b/d/f/')) + ['b/d', 'b'] + >>> list(_parents('b')) + [] + >>> list(_parents('')) + [] + """ + return itertools.islice(_ancestry(path), 1, None) + + +def _ancestry(path): + """ + Given a path with elements separated by + posixpath.sep, generate all elements of that path + + >>> list(_ancestry('b/d')) + ['b/d', 'b'] + >>> list(_ancestry('/b/d/')) + ['/b/d', '/b'] + >>> list(_ancestry('b/d/f/')) + ['b/d/f', 'b/d', 'b'] + >>> list(_ancestry('b')) + ['b'] + >>> list(_ancestry('')) + [] + """ + path = path.rstrip(posixpath.sep) + while path and path != posixpath.sep: + yield path + path, tail = posixpath.split(path) + + +_dedupe = OrderedDict.fromkeys +"""Deduplicate an iterable in original order""" + + +def _difference(minuend, subtrahend): + """ + Return items in minuend not in subtrahend, retaining order + with O(1) lookup. + """ + return itertools.filterfalse(set(subtrahend).__contains__, minuend) + + +class CompleteDirs(zipfile.ZipFile): + """ + A ZipFile subclass that ensures that implied directories + are always included in the namelist. + """ + + @staticmethod + def _implied_dirs(names): + parents = itertools.chain.from_iterable(map(_parents, names)) + as_dirs = (p + posixpath.sep for p in parents) + return _dedupe(_difference(as_dirs, names)) + + def namelist(self): + names = super(CompleteDirs, self).namelist() + return names + list(self._implied_dirs(names)) + + def _name_set(self): + return set(self.namelist()) + + def resolve_dir(self, name): + """ + If the name represents a directory, return that name + as a directory (with the trailing slash). + """ + names = self._name_set() + dirname = name + '/' + dir_match = name not in names and dirname in names + return dirname if dir_match else name + + @classmethod + def make(cls, source): + """ + Given a source (filename or zipfile), return an + appropriate CompleteDirs subclass. + """ + if isinstance(source, CompleteDirs): + return source + + if not isinstance(source, zipfile.ZipFile): + return cls(_pathlib_compat(source)) + + # Only allow for FastLookup when supplied zipfile is read-only + if 'r' not in source.mode: + cls = CompleteDirs + + source.__class__ = cls + return source + + +class FastLookup(CompleteDirs): + """ + ZipFile subclass to ensure implicit + dirs exist and are resolved rapidly. + """ + + def namelist(self): + with contextlib.suppress(AttributeError): + return self.__names + self.__names = super(FastLookup, self).namelist() + return self.__names + + def _name_set(self): + with contextlib.suppress(AttributeError): + return self.__lookup + self.__lookup = super(FastLookup, self)._name_set() + return self.__lookup + + +def _pathlib_compat(path): + """ + For path-like objects, convert to a filename for compatibility + on Python 3.6.1 and earlier. + """ + try: + return path.__fspath__() + except AttributeError: + return str(path) + + +class Path: + """ + A pathlib-compatible interface for zip files. + + Consider a zip file with this structure:: + + . + ├── a.txt + └── b + ├── c.txt + └── d + └── e.txt + + >>> data = io.BytesIO() + >>> zf = zipfile.ZipFile(data, 'w') + >>> zf.writestr('a.txt', 'content of a') + >>> zf.writestr('b/c.txt', 'content of c') + >>> zf.writestr('b/d/e.txt', 'content of e') + >>> zf.filename = 'mem/abcde.zip' + + Path accepts the zipfile object itself or a filename + + >>> root = Path(zf) + + From there, several path operations are available. + + Directory iteration (including the zip file itself): + + >>> a, b = root.iterdir() + >>> a + Path('mem/abcde.zip', 'a.txt') + >>> b + Path('mem/abcde.zip', 'b/') + + name property: + + >>> b.name + 'b' + + join with divide operator: + + >>> c = b / 'c.txt' + >>> c + Path('mem/abcde.zip', 'b/c.txt') + >>> c.name + 'c.txt' + + Read text: + + >>> c.read_text() + 'content of c' + + existence: + + >>> c.exists() + True + >>> (b / 'missing.txt').exists() + False + + Coercion to string: + + >>> import os + >>> str(c).replace(os.sep, posixpath.sep) + 'mem/abcde.zip/b/c.txt' + + At the root, ``name``, ``filename``, and ``parent`` + resolve to the zipfile. Note these attributes are not + valid and will raise a ``ValueError`` if the zipfile + has no filename. + + >>> root.name + 'abcde.zip' + >>> str(root.filename).replace(os.sep, posixpath.sep) + 'mem/abcde.zip' + >>> str(root.parent) + 'mem' + """ + + __repr = "{self.__class__.__name__}({self.root.filename!r}, {self.at!r})" + + def __init__(self, root, at=""): + """ + Construct a Path from a ZipFile or filename. + + Note: When the source is an existing ZipFile object, + its type (__class__) will be mutated to a + specialized type. If the caller wishes to retain the + original type, the caller should either create a + separate ZipFile object or pass a filename. + """ + self.root = FastLookup.make(root) + self.at = at + + def open(self, mode='r', *args, pwd=None, **kwargs): + """ + Open this entry as text or binary following the semantics + of ``pathlib.Path.open()`` by passing arguments through + to io.TextIOWrapper(). + """ + if self.is_dir(): + raise IsADirectoryError(self) + zip_mode = mode[0] + if not self.exists() and zip_mode == 'r': + raise FileNotFoundError(self) + stream = self.root.open(self.at, zip_mode, pwd=pwd) + if 'b' in mode: + if args or kwargs: + raise ValueError("encoding args invalid for binary operation") + return stream + return io.TextIOWrapper(stream, *args, **kwargs) + + @property + def name(self): + return pathlib.Path(self.at).name or self.filename.name + + @property + def suffix(self): + return pathlib.Path(self.at).suffix or self.filename.suffix + + @property + def suffixes(self): + return pathlib.Path(self.at).suffixes or self.filename.suffixes + + @property + def stem(self): + return pathlib.Path(self.at).stem or self.filename.stem + + @property + def filename(self): + return pathlib.Path(self.root.filename).joinpath(self.at) + + def read_text(self, *args, **kwargs): + with self.open('r', *args, **kwargs) as strm: + return strm.read() + + def read_bytes(self): + with self.open('rb') as strm: + return strm.read() + + def _is_child(self, path): + return posixpath.dirname(path.at.rstrip("/")) == self.at.rstrip("/") + + def _next(self, at): + return self.__class__(self.root, at) + + def is_dir(self): + return not self.at or self.at.endswith("/") + + def is_file(self): + return self.exists() and not self.is_dir() + + def exists(self): + return self.at in self.root._name_set() + + def iterdir(self): + if not self.is_dir(): + raise ValueError("Can't listdir a file") + subs = map(self._next, self.root.namelist()) + return filter(self._is_child, subs) + + def __str__(self): + return posixpath.join(self.root.filename, self.at) + + def __repr__(self): + return self.__repr.format(self=self) + + def joinpath(self, *other): + next = posixpath.join(self.at, *map(_pathlib_compat, other)) + return self._next(self.root.resolve_dir(next)) + + __truediv__ = joinpath + + @property + def parent(self): + if not self.at: + return self.filename.parent + parent_at = posixpath.dirname(self.at.rstrip('/')) + if parent_at: + parent_at += '/' + return self._next(parent_at) diff --git a/pkg_resources/extern/__init__.py b/pkg_resources/extern/__init__.py index b1811743..70897eea 100644 --- a/pkg_resources/extern/__init__.py +++ b/pkg_resources/extern/__init__.py @@ -69,5 +69,8 @@ class VendorImporter: sys.meta_path.append(self) -names = 'packaging', 'pyparsing', 'appdirs', 'jaraco' +names = ( + 'packaging', 'pyparsing', 'appdirs', 'jaraco', 'importlib_resources', + 'more_itertools', +) VendorImporter(__name__, names).install() diff --git a/tools/vendored.py b/tools/vendored.py index ee34dc0f..a5f3b9f1 100644 --- a/tools/vendored.py +++ b/tools/vendored.py @@ -29,6 +29,37 @@ def rewrite_packaging(pkg_files, new_root): file.write_text(text) +def rewrite_jaraco_text(pkg_files, new_root): + """ + Rewrite imports in jaraco.text to redirect to vendored copies. + """ + for file in pkg_files.glob('*.py'): + text = file.read_text() + text = re.sub(r' (jaraco\.)', rf' {new_root}.\1', text) + text = re.sub(r' (importlib_resources)', rf' {new_root}.\1', text) + file.write_text(text) + + +def rewrite_jaraco(pkg_files, new_root): + """ + Rewrite imports in jaraco.functools to redirect to vendored copies. + """ + for file in pkg_files.glob('*.py'): + text = file.read_text() + text = re.sub(r' (more_itertools)', rf' {new_root}.\1', text) + file.write_text(text) + + +def rewrite_importlib_resources(pkg_files, new_root): + """ + Rewrite imports in importlib_resources to redirect to vendored copies. + """ + for file in pkg_files.glob('*.py'): + text = file.read_text().replace('importlib_resources.abc', '.abc') + text = text.replace('zipp', '..zipp') + file.write_text(text) + + def clean(vendor): """ Remove all files out of the vendor directory except the meta @@ -58,6 +89,9 @@ def update_pkg_resources(): vendor = Path('pkg_resources/_vendor') install(vendor) rewrite_packaging(vendor / 'packaging', 'pkg_resources.extern') + rewrite_jaraco_text(vendor / 'jaraco/text', 'pkg_resources.extern') + rewrite_jaraco(vendor / 'jaraco', 'pkg_resources.extern') + rewrite_importlib_resources(vendor / 'importlib_resources', 'pkg_resources.extern') def update_setuptools(): -- cgit v1.2.1 From 13c8c7d27e3dc730ef2fe81c22a6b7202212739d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 28 Jan 2022 20:05:14 -0500 Subject: Ensure text file from vendored package is included. --- pkg_resources/_vendor/jaraco/__init__.py | 0 setup.py | 1 + tools/vendored.py | 3 +++ 3 files changed, 4 insertions(+) create mode 100644 pkg_resources/_vendor/jaraco/__init__.py diff --git a/pkg_resources/_vendor/jaraco/__init__.py b/pkg_resources/_vendor/jaraco/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/setup.py b/setup.py index 4cda3d38..0b85f8e7 100755 --- a/setup.py +++ b/setup.py @@ -13,6 +13,7 @@ here = os.path.dirname(__file__) package_data = dict( setuptools=['script (dev).tmpl', 'script.tmpl', 'site-patch.py'], ) +package_data.update({'pkg_resources._vendor.jaraco.text': ['*.txt']}) force_windows_specific_files = ( os.environ.get("SETUPTOOLS_INSTALL_WINDOWS_SPECIFIC_FILES", "1").lower() diff --git a/tools/vendored.py b/tools/vendored.py index a5f3b9f1..a921efae 100644 --- a/tools/vendored.py +++ b/tools/vendored.py @@ -44,6 +44,9 @@ def rewrite_jaraco(pkg_files, new_root): """ Rewrite imports in jaraco.functools to redirect to vendored copies. """ + # jaraco is a namespace package, but for data to be discovered, + # such as in jaraco.txt, it must be a regular package. + pkg_files.joinpath('__init__.py').write_text('') for file in pkg_files.glob('*.py'): text = file.read_text() text = re.sub(r' (more_itertools)', rf' {new_root}.\1', text) -- cgit v1.2.1 From 9de36beceab9ade61b4d300174133c54ac87f60b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 29 Jan 2022 13:27:10 -0500 Subject: Remove workaround to add __init__ for vendored jaraco package, seemingly unnecessary. --- pkg_resources/_vendor/jaraco/__init__.py | 0 tools/vendored.py | 3 --- 2 files changed, 3 deletions(-) delete mode 100644 pkg_resources/_vendor/jaraco/__init__.py diff --git a/pkg_resources/_vendor/jaraco/__init__.py b/pkg_resources/_vendor/jaraco/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tools/vendored.py b/tools/vendored.py index a921efae..a5f3b9f1 100644 --- a/tools/vendored.py +++ b/tools/vendored.py @@ -44,9 +44,6 @@ def rewrite_jaraco(pkg_files, new_root): """ Rewrite imports in jaraco.functools to redirect to vendored copies. """ - # jaraco is a namespace package, but for data to be discovered, - # such as in jaraco.txt, it must be a regular package. - pkg_files.joinpath('__init__.py').write_text('') for file in pkg_files.glob('*.py'): text = file.read_text() text = re.sub(r' (more_itertools)', rf' {new_root}.\1', text) -- cgit v1.2.1 From d16d759bb080761732cafb0e85bc804e6b902e46 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 30 Jan 2022 12:55:04 -0500 Subject: Refactor to limit indentation and share behavior. --- distutils/tests/test_install.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/distutils/tests/test_install.py b/distutils/tests/test_install.py index e8ef1caf..5dbc06b0 100644 --- a/distutils/tests/test_install.py +++ b/distutils/tests/test_install.py @@ -126,13 +126,12 @@ class InstallTestCase(support.TempdirManager, actual_headers = os.path.relpath(cmd.install_headers, self.user_base) if os.name == 'nt': - expect_headers = os.path.join( - os.path.relpath(os.path.dirname(self.old_user_site), self.old_user_base), - 'Include', - 'xx', - ) + site_path = os.path.relpath( + os.path.dirname(self.old_user_site), self.old_user_base) + include = os.path.join(site_path, 'Include') else: - expect_headers = os.path.join(sysconfig.get_python_inc(0, ''), 'xx') + include = sysconfig.get_python_inc(0, '') + expect_headers = os.path.join(include, 'xx') self.assertEqual(os.path.normcase(actual_headers), os.path.normcase(expect_headers)) -- cgit v1.2.1 From 917046dc70da8c6c5ba87571b0864826085e3659 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 30 Jan 2022 13:12:34 -0500 Subject: Only rely on py_version_nodot_plat where not present (Python 3.9 and earlier). --- distutils/command/install.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/distutils/command/install.py b/distutils/command/install.py index 0c280f1c..9fe65913 100644 --- a/distutils/command/install.py +++ b/distutils/command/install.py @@ -411,17 +411,19 @@ class install(Command): 'implementation_lower': _get_implementation().lower(), 'implementation': _get_implementation(), } - try: - local_vars['py_version_nodot_plat'] = sys.winver.replace('.', '') - except AttributeError: - local_vars['py_version_nodot_plat'] = '' + + # vars for compatibility on older Pythons + compat_vars = dict( + # Python 3.9 and earlier + py_version_nodot_plat=getattr(sys, 'winver', '').replace('.', ''), + ) if HAS_USER_SITE: local_vars['userbase'] = self.install_userbase local_vars['usersite'] = self.install_usersite self.config_vars = _collections.DictStack( - [sysconfig.get_config_vars(), local_vars]) + [compat_vars, sysconfig.get_config_vars(), local_vars]) self.expand_basedirs() -- cgit v1.2.1 From 04b796f1a64577eaa66265778f186b6734a12eb5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 30 Jan 2022 13:18:37 -0500 Subject: Add changelog --- changelog.d/3062.change.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/3062.change.rst diff --git a/changelog.d/3062.change.rst b/changelog.d/3062.change.rst new file mode 100644 index 00000000..cf3ff502 --- /dev/null +++ b/changelog.d/3062.change.rst @@ -0,0 +1 @@ +Merge with pypa/distutils@b53a824ec3 including improved support for lib directories on non-x64 Windows builds. -- cgit v1.2.1 From 44649c5a483a9c1cf2d80f6e9706d581cfc7437e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 30 Jan 2022 15:25:09 -0500 Subject: Add py_version_nodot_plat substitution support to easy_install. --- setuptools/command/easy_install.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index 5fab0fdb..e25090b8 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -260,6 +260,12 @@ class easy_install(Command): 'implementation': install._get_implementation(), }) + # pypa/distutils#113 Python 3.9 compat + self.config_vars.setdefault( + 'py_version_nodot_plat', + getattr(sys, 'windir', '').replace('.', ''), + ) + if site.ENABLE_USER_SITE: self.config_vars['userbase'] = self.install_userbase self.config_vars['usersite'] = self.install_usersite -- cgit v1.2.1 From cae41c6a5546bfa9a80bda5841505331e566cdc1 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Sat, 11 Dec 2021 11:36:25 -0800 Subject: .github/workflows/ci-sage.yml: New --- .github/workflows/ci-sage.yml | 155 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 .github/workflows/ci-sage.yml diff --git a/.github/workflows/ci-sage.yml b/.github/workflows/ci-sage.yml new file mode 100644 index 00000000..191564fa --- /dev/null +++ b/.github/workflows/ci-sage.yml @@ -0,0 +1,155 @@ +name: Run Sage CI for Linux + +## This GitHub Actions workflow provides: +## +## - portability testing, by building and testing this project on many platforms +## +## - continuous integration, by building and testing other software +## that depends on this project. +## +## It runs on every pull request and push of a tag to the GitHub repository. +## +## The testing can be monitored in the "Actions" tab of the GitHub repository. +## +## After all jobs have finished (or are canceled) and a short delay, +## tar files of all logs are made available as "build artifacts". +## +## This GitHub Actions workflow uses the portability testing framework +## of SageMath (https://www.sagemath.org/). For more information, see +## https://doc.sagemath.org/html/en/developer/portability_testing.html + +## The workflow consists of two jobs: +## +## - First, it builds a source distribution of the project +## and generates a script "update-pkgs.sh". It uploads them +## as a build artifact named upstream. +## +## - Second, it checks out a copy of the SageMath source tree. +## It downloads the upstream artifact and replaces the project's +## package in the SageMath distribution by the newly packaged one +## from the upstream artifact, by running the script "update-pkgs.sh". +## Then it builds a small portion of the Sage distribution. +## +## Many copies of the second step are run in parallel for each of the tested +## systems/configurations. + +#on: [push, pull_request] + +on: + pull_request: + types: [opened, synchronize] + push: + tags: + - '*' + workflow_dispatch: + # Allow to run manually + +env: + # Ubuntu packages to install so that the project's "setup.py sdist" can succeed + DIST_PREREQ: python3 + # Name of this project in the Sage distribution + SPKG: setuptools + # Sage distribution packages to build + TARGETS_PRE: build/make/Makefile + TARGETS: setuptools pyzmq + TARGETS_OPTIONAL: build/make/Makefile + # Standard setting: Test the current beta release of Sage: + SAGE_REPO: sagemath/sage + SAGE_REF: develop + # Temporarily test with the branch from sage ticket + # (this is a no-op after that ticket is merged) + #SAGE_TRAC_GIT: https://github.com/sagemath/sagetrac-mirror.git + #SAGE_TICKET: 32579 + REMOVE_PATCHES: "*" + +jobs: + + dist: + runs-on: ubuntu-latest + steps: + - name: Check out ${{ env.SPKG }} + uses: actions/checkout@v2 + with: + path: build/pkgs/${{ env.SPKG }}/src + - name: Install prerequisites + run: | + sudo DEBIAN_FRONTEND=noninteractive apt-get update + sudo DEBIAN_FRONTEND=noninteractive apt-get install $DIST_PREREQ + python3 -m pip install build + - name: Run make dist, prepare upstream artifact + run: | + (cd build/pkgs/${{ env.SPKG }}/src && python3 -m bootstrap && python3 -m build --sdist) \ + && mkdir -p upstream && cp build/pkgs/${{ env.SPKG }}/src/dist/*.tar.gz upstream/${{ env.SPKG }}-git.tar.gz \ + && echo "sage-package create ${{ env.SPKG }} --version git --tarball ${{ env.SPKG }}-git.tar.gz --type=standard" > upstream/update-pkgs.sh \ + && if [ -n "${{ env.REMOVE_PATCHES }}" ]; then echo "(cd ../build/pkgs/${{ env.SPKG }}/patches && rm -f ${{ env.REMOVE_PATCHES }}; :)" >> upstream/update-pkgs.sh; fi \ + && ls -l upstream/ + - uses: actions/upload-artifact@v2 + with: + path: upstream + name: upstream + + docker: + runs-on: ubuntu-latest + needs: [dist] + strategy: + fail-fast: false + max-parallel: 32 + matrix: + tox_system_factor: [ubuntu-trusty, ubuntu-xenial, ubuntu-bionic, ubuntu-focal, ubuntu-groovy, ubuntu-hirsute, ubuntu-impish, debian-jessie, debian-stretch, debian-buster, debian-bullseye, debian-sid, linuxmint-17, linuxmint-18, linuxmint-19, linuxmint-19.3, linuxmint-20.1, linuxmint-20.2, fedora-26, fedora-27, fedora-28, fedora-29, fedora-30, fedora-31, fedora-32, fedora-33, fedora-34, fedora-35, centos-7, centos-8, gentoo, gentoo-python3.7, archlinux-latest, opensuse-15, opensuse-15.3, opensuse-tumbleweed, slackware-14.2, conda-forge, ubuntu-bionic-i386, manylinux-2_24-i686, debian-buster-i386, centos-7-i386, raspbian-buster-armhf] + tox_packages_factor: [minimal, standard] + env: + TOX_ENV: docker-${{ matrix.tox_system_factor }}-${{ matrix.tox_packages_factor }} + LOGS_ARTIFACT_NAME: logs-commit-${{ github.sha }}-tox-docker-${{ matrix.tox_system_factor }}-${{ matrix.tox_packages_factor }} + DOCKER_TARGETS: configured with-targets with-targets-optional + steps: + - name: Check out SageMath + uses: actions/checkout@v2 + with: + repository: ${{ env.SAGE_REPO }} + ref: ${{ env.SAGE_REF }} + fetch-depth: 2000 + if: env.SAGE_REPO != '' + - name: Check out git-trac-command + uses: actions/checkout@v2 + with: + repository: sagemath/git-trac-command + path: git-trac-command + if: env.SAGE_TRAC_GIT != '' + - name: Check out SageMath from trac.sagemath.org + shell: bash {0} + run: | + git config --global user.email "ci-sage@example.com" + git config --global user.name "ci-sage workflow" + if [ ! -d .git ]; then git init; fi; git remote add trac ${{ env.SAGE_TRAC_GIT }} && x=1 && while [ $x -le 5 ]; do x=$(( $x + 1 )); sleep $(( $RANDOM % 60 + 1 )); if git-trac-command/git-trac fetch $SAGE_TICKET; then git merge FETCH_HEAD || echo "(ignored)"; exit 0; fi; sleep 40; done; exit 1 + if: env.SAGE_TRAC_GIT != '' + - uses: actions/download-artifact@v2 + with: + path: upstream + name: upstream + - name: Install test prerequisites + run: | + sudo DEBIAN_FRONTEND=noninteractive apt-get update + sudo DEBIAN_FRONTEND=noninteractive apt-get install tox python3-setuptools + - name: Update Sage packages from upstream artifact + run: | + (export PATH=$(pwd)/build/bin:$PATH; (cd upstream && bash -x update-pkgs.sh) && sed -i.bak '/upstream/d' .dockerignore && echo "/:toolchain:/i ADD upstream upstream" | sed -i.bak -f - build/bin/write-dockerfile.sh && git diff) + - name: Configure and build Sage distribution within a Docker container + run: | + set -o pipefail; EXTRA_DOCKER_BUILD_ARGS="--build-arg USE_MAKEFLAGS=\"-k V=0 SAGE_NUM_THREADS=3\"" tox -e $TOX_ENV -- $TARGETS 2>&1 | sed "/^configure: notice:/s|^|::warning file=artifacts/$LOGS_ARTIFACT_NAME/config.log::|;/^configure: warning:/s|^|::warning file=artifacts/$LOGS_ARTIFACT_NAME/config.log::|;/^configure: error:/s|^|::error file=artifacts/$LOGS_ARTIFACT_NAME/config.log::|;" + - name: Copy logs from the Docker image or build container + run: | + mkdir -p "artifacts/$LOGS_ARTIFACT_NAME" + cp -r .tox/$TOX_ENV/Dockerfile .tox/$TOX_ENV/log "artifacts/$LOGS_ARTIFACT_NAME" + if [ -f .tox/$TOX_ENV/Dockertags ]; then CONTAINERS=$(docker create $(tail -1 .tox/$TOX_ENV/Dockertags) /bin/bash || true); fi + if [ -n "$CONTAINERS" ]; then for CONTAINER in $CONTAINERS; do for ARTIFACT in /sage/logs; do docker cp $CONTAINER:$ARTIFACT artifacts/$LOGS_ARTIFACT_NAME && HAVE_LOG=1; done; if [ -n "$HAVE_LOG" ]; then break; fi; done; fi + if: always() + - uses: actions/upload-artifact@v2 + with: + path: artifacts + name: ${{ env.LOGS_ARTIFACT_NAME }} + if: always() + - name: Print out logs for immediate inspection + # and markup the output with GitHub Actions logging commands + run: | + .github/workflows/scan-logs.sh "artifacts/$LOGS_ARTIFACT_NAME" + if: always() -- cgit v1.2.1 From 96947f4e5c44f1972b1f1359b90dc1529f862fd4 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Sun, 30 Jan 2022 16:23:33 -0800 Subject: .github/workflows/ci-sage.yml: Remove flaky platforms, add new platforms --- .github/workflows/ci-sage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-sage.yml b/.github/workflows/ci-sage.yml index 191564fa..3df4617b 100644 --- a/.github/workflows/ci-sage.yml +++ b/.github/workflows/ci-sage.yml @@ -95,7 +95,7 @@ jobs: fail-fast: false max-parallel: 32 matrix: - tox_system_factor: [ubuntu-trusty, ubuntu-xenial, ubuntu-bionic, ubuntu-focal, ubuntu-groovy, ubuntu-hirsute, ubuntu-impish, debian-jessie, debian-stretch, debian-buster, debian-bullseye, debian-sid, linuxmint-17, linuxmint-18, linuxmint-19, linuxmint-19.3, linuxmint-20.1, linuxmint-20.2, fedora-26, fedora-27, fedora-28, fedora-29, fedora-30, fedora-31, fedora-32, fedora-33, fedora-34, fedora-35, centos-7, centos-8, gentoo, gentoo-python3.7, archlinux-latest, opensuse-15, opensuse-15.3, opensuse-tumbleweed, slackware-14.2, conda-forge, ubuntu-bionic-i386, manylinux-2_24-i686, debian-buster-i386, centos-7-i386, raspbian-buster-armhf] + tox_system_factor: [ubuntu-trusty, ubuntu-xenial, ubuntu-bionic, ubuntu-focal, ubuntu-hirsute, ubuntu-impish, ubuntu-jammy, debian-stretch, debian-buster, debian-bullseye, debian-bookworm, debian-sid, linuxmint-17, linuxmint-18, linuxmint-19, linuxmint-19.3, linuxmint-20.1, linuxmint-20.2, linuxmint-20.3, fedora-26, fedora-27, fedora-28, fedora-29, fedora-30, fedora-31, fedora-32, fedora-33, fedora-34, fedora-35, fedora-36, centos-7, centos-8, gentoo-python3.9, archlinux-latest, opensuse-15, opensuse-15.3, opensuse-tumbleweed, slackware-14.2, ubuntu-bionic-i386, manylinux-2_24-i686, debian-buster-i386, centos-7-i386] tox_packages_factor: [minimal, standard] env: TOX_ENV: docker-${{ matrix.tox_system_factor }}-${{ matrix.tox_packages_factor }} -- cgit v1.2.1 From b2ba0e39918d58bd6c6b15093ddcdc42605040ce Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 30 Jan 2022 19:53:24 -0500 Subject: =?UTF-8?q?Bump=20version:=2060.5.4=20=E2=86=92=2060.6.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 15 +++++++++++++++ changelog.d/3043.change.rst | 1 - changelog.d/3054.misc.rst | 1 - changelog.d/3057.change.rst | 1 - changelog.d/3062.change.rst | 1 - setup.cfg | 2 +- 7 files changed, 17 insertions(+), 6 deletions(-) delete mode 100644 changelog.d/3043.change.rst delete mode 100644 changelog.d/3054.misc.rst delete mode 100644 changelog.d/3057.change.rst delete mode 100644 changelog.d/3062.change.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 6534cde9..baf5b088 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 60.5.4 +current_version = 60.6.0 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index b6938ce7..a77f5395 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,18 @@ +v60.6.0 +------- + + +Changes +^^^^^^^ +* #3043: Merge with pypa/distutils@bb018f1ac3 including consolidated behavior in sysconfig.get_platform (pypa/distutils#104). +* #3057: Don't include optional ``Home-page`` in metadata if no ``url`` is specified. -- by :user:`cdce8p` +* #3062: Merge with pypa/distutils@b53a824ec3 including improved support for lib directories on non-x64 Windows builds. + +Misc +^^^^ +* #3054: Used Py3 syntax ``super().__init__()`` -- by :user:`imba-tjd` + + v60.5.4 ------- diff --git a/changelog.d/3043.change.rst b/changelog.d/3043.change.rst deleted file mode 100644 index d52705f9..00000000 --- a/changelog.d/3043.change.rst +++ /dev/null @@ -1 +0,0 @@ -Merge with pypa/distutils@bb018f1ac3 including consolidated behavior in sysconfig.get_platform (pypa/distutils#104). diff --git a/changelog.d/3054.misc.rst b/changelog.d/3054.misc.rst deleted file mode 100644 index 7166f837..00000000 --- a/changelog.d/3054.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Used Py3 syntax ``super().__init__()`` -- by :user:`imba-tjd` diff --git a/changelog.d/3057.change.rst b/changelog.d/3057.change.rst deleted file mode 100644 index 1e18efc0..00000000 --- a/changelog.d/3057.change.rst +++ /dev/null @@ -1 +0,0 @@ -Don't include optional ``Home-page`` in metadata if no ``url`` is specified. -- by :user:`cdce8p` diff --git a/changelog.d/3062.change.rst b/changelog.d/3062.change.rst deleted file mode 100644 index cf3ff502..00000000 --- a/changelog.d/3062.change.rst +++ /dev/null @@ -1 +0,0 @@ -Merge with pypa/distutils@b53a824ec3 including improved support for lib directories on non-x64 Windows builds. diff --git a/setup.cfg b/setup.cfg index 0dc90438..957c7ad2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 60.5.4 +version = 60.6.0 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From 0d5caf2c42cff3641466b4c9f0898ab235ac9fb7 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Sun, 30 Jan 2022 22:24:57 -0800 Subject: .github/workflows/ci-sage.yml: Remove fedora-36, not ready --- .github/workflows/ci-sage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-sage.yml b/.github/workflows/ci-sage.yml index 3df4617b..892d13df 100644 --- a/.github/workflows/ci-sage.yml +++ b/.github/workflows/ci-sage.yml @@ -95,7 +95,7 @@ jobs: fail-fast: false max-parallel: 32 matrix: - tox_system_factor: [ubuntu-trusty, ubuntu-xenial, ubuntu-bionic, ubuntu-focal, ubuntu-hirsute, ubuntu-impish, ubuntu-jammy, debian-stretch, debian-buster, debian-bullseye, debian-bookworm, debian-sid, linuxmint-17, linuxmint-18, linuxmint-19, linuxmint-19.3, linuxmint-20.1, linuxmint-20.2, linuxmint-20.3, fedora-26, fedora-27, fedora-28, fedora-29, fedora-30, fedora-31, fedora-32, fedora-33, fedora-34, fedora-35, fedora-36, centos-7, centos-8, gentoo-python3.9, archlinux-latest, opensuse-15, opensuse-15.3, opensuse-tumbleweed, slackware-14.2, ubuntu-bionic-i386, manylinux-2_24-i686, debian-buster-i386, centos-7-i386] + tox_system_factor: [ubuntu-trusty, ubuntu-xenial, ubuntu-bionic, ubuntu-focal, ubuntu-hirsute, ubuntu-impish, ubuntu-jammy, debian-stretch, debian-buster, debian-bullseye, debian-bookworm, debian-sid, linuxmint-17, linuxmint-18, linuxmint-19, linuxmint-19.3, linuxmint-20.1, linuxmint-20.2, linuxmint-20.3, fedora-26, fedora-27, fedora-28, fedora-29, fedora-30, fedora-31, fedora-32, fedora-33, fedora-34, fedora-35, centos-7, centos-8, gentoo-python3.9, archlinux-latest, opensuse-15, opensuse-15.3, opensuse-tumbleweed, slackware-14.2, ubuntu-bionic-i386, manylinux-2_24-i686, debian-buster-i386, centos-7-i386] tox_packages_factor: [minimal, standard] env: TOX_ENV: docker-${{ matrix.tox_system_factor }}-${{ matrix.tox_packages_factor }} -- cgit v1.2.1 From 4f9825dafa8d13a5f8b8bd8eb8bfc6414329cb18 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 1 Feb 2022 04:09:17 -0500 Subject: Update badge year --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index a3e1b740..c82c6429 100644 --- a/README.rst +++ b/README.rst @@ -17,5 +17,5 @@ .. .. image:: https://readthedocs.org/projects/skeleton/badge/?version=latest .. :target: https://skeleton.readthedocs.io/en/latest/?badge=latest -.. image:: https://img.shields.io/badge/skeleton-2021-informational +.. image:: https://img.shields.io/badge/skeleton-2022-informational :target: https://blog.jaraco.com/skeleton -- cgit v1.2.1 From 7e01b721c237ee4947e3b9d6e56bb03a028f3f6a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 1 Feb 2022 04:11:54 -0500 Subject: Remove setup.py, no longer needed. --- setup.py | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 setup.py diff --git a/setup.py b/setup.py deleted file mode 100644 index bac24a43..00000000 --- a/setup.py +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env python - -import setuptools - -if __name__ == "__main__": - setuptools.setup() -- cgit v1.2.1 From ee8ed624ac46a7769fa16179ecca9b6aca8c02a6 Mon Sep 17 00:00:00 2001 From: Dominic Davis-Foster Date: Tue, 1 Feb 2022 17:30:53 +0000 Subject: Skip non-string values from sysconfig.get_config_vars() --- setuptools/command/easy_install.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index e25090b8..bdacbbfc 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -1334,7 +1334,9 @@ class easy_install(Command): if not self.user: return home = convert_path(os.path.expanduser("~")) - for name, path in self.config_vars.items(): + for path in self.config_vars.values(): + if not isinstance(path, str): + continue if path.startswith(home) and not os.path.isdir(path): self.debug_print("os.makedirs('%s', 0o700)" % path) os.makedirs(path, 0o700) -- cgit v1.2.1 From 2e5372b783b3aa6ba2e254144bd8db82b5a03ee8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 1 Feb 2022 19:54:17 -0500 Subject: Update changelog. --- changelog.d/3061.change.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/3061.change.rst diff --git a/changelog.d/3061.change.rst b/changelog.d/3061.change.rst new file mode 100644 index 00000000..8dddbce6 --- /dev/null +++ b/changelog.d/3061.change.rst @@ -0,0 +1 @@ +Vendored jaraco.text and use line processing from that library in pkg_resources. -- cgit v1.2.1 From 9c4ed1a5970397812e0988f3d45a513f1bd9442a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 1 Feb 2022 20:05:05 -0500 Subject: Update changelog. --- changelog.d/3070.misc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/3070.misc.rst diff --git a/changelog.d/3070.misc.rst b/changelog.d/3070.misc.rst new file mode 100644 index 00000000..871b5d00 --- /dev/null +++ b/changelog.d/3070.misc.rst @@ -0,0 +1 @@ +Avoid AttributeError in easy_install.create_home_path when sysconfig.get_config_vars values are not strings. -- cgit v1.2.1 From a3bc3d44fb0ad669d75e674218078752de7bb24d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 1 Feb 2022 20:13:22 -0500 Subject: Create a function for only_strs to help document its purpose. --- setuptools/command/easy_install.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index bdacbbfc..ef1a9b23 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -1334,9 +1334,7 @@ class easy_install(Command): if not self.user: return home = convert_path(os.path.expanduser("~")) - for path in self.config_vars.values(): - if not isinstance(path, str): - continue + for path in only_strs(self.config_vars.values()): if path.startswith(home) and not os.path.isdir(path): self.debug_print("os.makedirs('%s', 0o700)" % path) os.makedirs(path, 0o700) @@ -2306,6 +2304,13 @@ def current_umask(): return tmp +def only_strs(values): + """ + Exclude non-str values. Ref #3063. + """ + return filter(lambda val: isinstance(val, str), values) + + class EasyInstallDeprecationWarning(SetuptoolsDeprecationWarning): """ Warning for EasyInstall deprecations, bypassing suppression. -- cgit v1.2.1 From 763cf01ef7df80a5d0b64ec69427336a9cb419b8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 1 Feb 2022 20:14:22 -0500 Subject: =?UTF-8?q?Bump=20version:=2060.6.0=20=E2=86=92=2060.7.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 13 +++++++++++++ changelog.d/3061.change.rst | 1 - changelog.d/3070.misc.rst | 1 - setup.cfg | 2 +- 5 files changed, 15 insertions(+), 4 deletions(-) delete mode 100644 changelog.d/3061.change.rst delete mode 100644 changelog.d/3070.misc.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index baf5b088..93ad49cb 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 60.6.0 +current_version = 60.7.0 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index a77f5395..f396875d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,16 @@ +v60.7.0 +------- + + +Changes +^^^^^^^ +* #3061: Vendored jaraco.text and use line processing from that library in pkg_resources. + +Misc +^^^^ +* #3070: Avoid AttributeError in easy_install.create_home_path when sysconfig.get_config_vars values are not strings. + + v60.6.0 ------- diff --git a/changelog.d/3061.change.rst b/changelog.d/3061.change.rst deleted file mode 100644 index 8dddbce6..00000000 --- a/changelog.d/3061.change.rst +++ /dev/null @@ -1 +0,0 @@ -Vendored jaraco.text and use line processing from that library in pkg_resources. diff --git a/changelog.d/3070.misc.rst b/changelog.d/3070.misc.rst deleted file mode 100644 index 871b5d00..00000000 --- a/changelog.d/3070.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Avoid AttributeError in easy_install.create_home_path when sysconfig.get_config_vars values are not strings. diff --git a/setup.cfg b/setup.cfg index 957c7ad2..f1a535e6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 60.6.0 +version = 60.7.0 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From e04aa8ac322a8cb92064ccf832d864674eddb964 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 2 Feb 2022 20:39:38 -0500 Subject: Remove 'lorem_ipsum' property from jaraco.text, bypassing the behavior on import and other issues. --- changelog.d/3072.misc.rst | 1 + pkg_resources/_vendor/jaraco/text/__init__.py | 1 - setup.py | 1 - tools/vendored.py | 2 ++ 4 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/3072.misc.rst diff --git a/changelog.d/3072.misc.rst b/changelog.d/3072.misc.rst new file mode 100644 index 00000000..362c9c30 --- /dev/null +++ b/changelog.d/3072.misc.rst @@ -0,0 +1 @@ +Remove lorem_ipsum from jaraco.text when vendored. diff --git a/pkg_resources/_vendor/jaraco/text/__init__.py b/pkg_resources/_vendor/jaraco/text/__init__.py index f39f2d93..c466378c 100644 --- a/pkg_resources/_vendor/jaraco/text/__init__.py +++ b/pkg_resources/_vendor/jaraco/text/__init__.py @@ -224,7 +224,6 @@ def unwrap(s): return '\n'.join(cleaned) -lorem_ipsum: str = files(__name__).joinpath('Lorem ipsum.txt').read_text() class Splitter(object): diff --git a/setup.py b/setup.py index 0b85f8e7..4cda3d38 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,6 @@ here = os.path.dirname(__file__) package_data = dict( setuptools=['script (dev).tmpl', 'script.tmpl', 'site-patch.py'], ) -package_data.update({'pkg_resources._vendor.jaraco.text': ['*.txt']}) force_windows_specific_files = ( os.environ.get("SETUPTOOLS_INSTALL_WINDOWS_SPECIFIC_FILES", "1").lower() diff --git a/tools/vendored.py b/tools/vendored.py index a5f3b9f1..7159928a 100644 --- a/tools/vendored.py +++ b/tools/vendored.py @@ -37,6 +37,8 @@ def rewrite_jaraco_text(pkg_files, new_root): text = file.read_text() text = re.sub(r' (jaraco\.)', rf' {new_root}.\1', text) text = re.sub(r' (importlib_resources)', rf' {new_root}.\1', text) + # suppress loading of lorem_ipsum; ref #3072 + text = re.sub(r'^lorem_ipsum.*\n$', '', text, flags=re.M) file.write_text(text) -- cgit v1.2.1 From 780cae233b51aa6b93b25e35538f496480bae537 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 2 Feb 2022 21:18:25 -0500 Subject: =?UTF-8?q?Bump=20version:=2060.7.0=20=E2=86=92=2060.7.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 9 +++++++++ changelog.d/3072.misc.rst | 1 - setup.cfg | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/3072.misc.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 93ad49cb..93a72b39 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 60.7.0 +current_version = 60.7.1 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index f396875d..e43db159 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,12 @@ +v60.7.1 +------- + + +Misc +^^^^ +* #3072: Remove lorem_ipsum from jaraco.text when vendored. + + v60.7.0 ------- diff --git a/changelog.d/3072.misc.rst b/changelog.d/3072.misc.rst deleted file mode 100644 index 362c9c30..00000000 --- a/changelog.d/3072.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Remove lorem_ipsum from jaraco.text when vendored. diff --git a/setup.cfg b/setup.cfg index f1a535e6..7fee29b2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 60.7.0 +version = 60.7.1 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From dafb5e26955e48d59b8021835785e5d98ccd4f53 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 3 Feb 2022 14:49:39 +0000 Subject: Move lingering 'docs' files into correct sections in changelog --- CHANGES.rst | 12 ++++++++++++ changelog.d/2897.docs.rst | 4 ---- changelog.d/3034.docs.rst | 4 ---- changelog.d/3056.docs.rst | 2 -- 4 files changed, 12 insertions(+), 10 deletions(-) delete mode 100644 changelog.d/2897.docs.rst delete mode 100644 changelog.d/3034.docs.rst delete mode 100644 changelog.d/3056.docs.rst diff --git a/CHANGES.rst b/CHANGES.rst index e43db159..3f8b182a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -30,6 +30,18 @@ Changes * #3057: Don't include optional ``Home-page`` in metadata if no ``url`` is specified. -- by :user:`cdce8p` * #3062: Merge with pypa/distutils@b53a824ec3 including improved support for lib directories on non-x64 Windows builds. +Documentation changes +^^^^^^^^^^^^^^^^^^^^^ +* #2897: Added documentation about wrapping ``setuptools.build_meta`` in a in-tree + custom backend. This is a :pep:`517`-compliant way of dynamically specifying + build dependencies (e.g. when platform, OS and other markers are not enough). + -- by :user:`abravalheri` +* #3034: Replaced occurrences of the defunct distutils-sig mailing list with pointers + to GitHub Discussions. + -- by :user:`ashemedai` +* #3056: The documentation has stopped suggesting to add ``wheel`` to + :pep:`517` requirements -- by :user:`webknjaz` + Misc ^^^^ * #3054: Used Py3 syntax ``super().__init__()`` -- by :user:`imba-tjd` diff --git a/changelog.d/2897.docs.rst b/changelog.d/2897.docs.rst deleted file mode 100644 index 763a39b8..00000000 --- a/changelog.d/2897.docs.rst +++ /dev/null @@ -1,4 +0,0 @@ -Added documentation about wrapping ``setuptools.build_meta`` in a in-tree -custom backend. This is a :pep:`517`-compliant way of dynamically specifying -build dependencies (e.g. when platform, OS and other markers are not enough) --- by :user:`abravalheri`. diff --git a/changelog.d/3034.docs.rst b/changelog.d/3034.docs.rst deleted file mode 100644 index 6106e0ff..00000000 --- a/changelog.d/3034.docs.rst +++ /dev/null @@ -1,4 +0,0 @@ -Replaced occurrences of the defunct distutils-sig mailing list with pointers -to GitHub Discussions. --- by :user:`ashemedai` - diff --git a/changelog.d/3056.docs.rst b/changelog.d/3056.docs.rst deleted file mode 100644 index c3de4e99..00000000 --- a/changelog.d/3056.docs.rst +++ /dev/null @@ -1,2 +0,0 @@ -The documentation has stopped suggesting to add ``wheel`` to -:pep:`517` requirements -- by :user:`webknjaz` -- cgit v1.2.1 From e2a632576cd79a6d38a3f6f9f9ac348642e35230 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 3 Feb 2022 15:00:09 +0000 Subject: Modify tools.finalize.check_changes to validate extensions The previous existing implementation would just check for the existence of a preconfigured substring (e.g. `changes`, `doc`, ...). However that can be problematic when the used extension contains one of the valid substrings but is not valid itself. For example `changelog.d/3034.docs.rst` contains the valid `doc` substring but is not a valid name itself. The correct would be `changelog.d/3034.doc.rst`. --- tools/finalize.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tools/finalize.py b/tools/finalize.py index 516a2fb5..e4f65543 100644 --- a/tools/finalize.py +++ b/tools/finalize.py @@ -79,11 +79,18 @@ def check_changes(): """ allowed = 'deprecation', 'breaking', 'change', 'doc', 'misc' except_ = 'README.rst', '.gitignore' - assert all( - any(key in file.name for key in allowed) + news_fragments = ( + file for file in pathlib.Path('changelog.d').iterdir() if file.name not in except_ ) + unrecognized = [ + str(file) + for file in news_fragments + if not any(f".{key}" in file.suffixes for key in allowed) + ] + if unrecognized: + raise ValueError(f"Some news fragments have invalid names: {unrecognized}") if __name__ == '__main__': -- cgit v1.2.1 From a53b404fce7f50874ff8cb7c40d2f2e744d125fb Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Fri, 4 Feb 2022 11:43:02 -0800 Subject: .github/workflows/ci-sage.yml: Use https://trac.sagemath.org/ticket/33288 for CI fixes --- .github/workflows/ci-sage.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-sage.yml b/.github/workflows/ci-sage.yml index 892d13df..ef1e95fc 100644 --- a/.github/workflows/ci-sage.yml +++ b/.github/workflows/ci-sage.yml @@ -56,10 +56,11 @@ env: # Standard setting: Test the current beta release of Sage: SAGE_REPO: sagemath/sage SAGE_REF: develop - # Temporarily test with the branch from sage ticket - # (this is a no-op after that ticket is merged) - #SAGE_TRAC_GIT: https://github.com/sagemath/sagetrac-mirror.git - #SAGE_TICKET: 32579 + # Test with the branch from https://trac.sagemath.org/ticket/33288 + # This may provide hotfixes for the CI that have not been merged into + # the sage develop branch yet. + SAGE_TRAC_GIT: https://github.com/sagemath/sagetrac-mirror.git + SAGE_TICKET: 33288 REMOVE_PATCHES: "*" jobs: -- cgit v1.2.1 From 68fb3883e155a7fd133474d17f0034b072d47745 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Fri, 4 Feb 2022 11:43:22 -0800 Subject: .github/workflows/ci-sage.yml: Replace centos-8 (EOL) by centos-stream-8, centos-stream-9 --- .github/workflows/ci-sage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-sage.yml b/.github/workflows/ci-sage.yml index ef1e95fc..573b269b 100644 --- a/.github/workflows/ci-sage.yml +++ b/.github/workflows/ci-sage.yml @@ -96,7 +96,7 @@ jobs: fail-fast: false max-parallel: 32 matrix: - tox_system_factor: [ubuntu-trusty, ubuntu-xenial, ubuntu-bionic, ubuntu-focal, ubuntu-hirsute, ubuntu-impish, ubuntu-jammy, debian-stretch, debian-buster, debian-bullseye, debian-bookworm, debian-sid, linuxmint-17, linuxmint-18, linuxmint-19, linuxmint-19.3, linuxmint-20.1, linuxmint-20.2, linuxmint-20.3, fedora-26, fedora-27, fedora-28, fedora-29, fedora-30, fedora-31, fedora-32, fedora-33, fedora-34, fedora-35, centos-7, centos-8, gentoo-python3.9, archlinux-latest, opensuse-15, opensuse-15.3, opensuse-tumbleweed, slackware-14.2, ubuntu-bionic-i386, manylinux-2_24-i686, debian-buster-i386, centos-7-i386] + tox_system_factor: [ubuntu-trusty, ubuntu-xenial, ubuntu-bionic, ubuntu-focal, ubuntu-hirsute, ubuntu-impish, ubuntu-jammy, debian-stretch, debian-buster, debian-bullseye, debian-bookworm, debian-sid, linuxmint-17, linuxmint-18, linuxmint-19, linuxmint-19.3, linuxmint-20.1, linuxmint-20.2, linuxmint-20.3, fedora-26, fedora-27, fedora-28, fedora-29, fedora-30, fedora-31, fedora-32, fedora-33, fedora-34, fedora-35, centos-7, centos-stream-8, centos-stream-9, gentoo-python3.9, archlinux-latest, opensuse-15, opensuse-15.3, opensuse-tumbleweed, slackware-14.2, ubuntu-bionic-i386, manylinux-2_24-i686, debian-buster-i386, centos-7-i386] tox_packages_factor: [minimal, standard] env: TOX_ENV: docker-${{ matrix.tox_system_factor }}-${{ matrix.tox_packages_factor }} -- cgit v1.2.1 From 8949d1a1169c9271ceb8aab3f1deea9d82e2fa0d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 4 Feb 2022 21:45:52 -0500 Subject: Add exclusions for pytest 7 deprecations in plugins. Fixes jaraco/skeleton#57. --- pytest.ini | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pytest.ini b/pytest.ini index ec965b24..52f19bea 100644 --- a/pytest.ini +++ b/pytest.ini @@ -5,3 +5,14 @@ doctest_optionflags=ALLOW_UNICODE ELLIPSIS 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.PytestRemovedIn8Warning + + # 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.PytestRemovedIn8Warning + + # dbader/pytest-mypy#131 + ignore:The \(fspath. py.path.local\) argument to MypyFile is deprecated.:pytest.PytestRemovedIn8Warning -- cgit v1.2.1 From 7a70ca5d78562b0973030a0e18a5552c4bb5011f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 3 Dec 2021 21:48:21 -0500 Subject: Add importlib_resources and importlib_metadata to vendored packages. --- setuptools/_vendor/importlib_metadata/__init__.py | 1054 ++++++++++++++++++++ setuptools/_vendor/importlib_metadata/_adapters.py | 68 ++ .../_vendor/importlib_metadata/_collections.py | 30 + setuptools/_vendor/importlib_metadata/_compat.py | 71 ++ .../_vendor/importlib_metadata/_functools.py | 104 ++ .../_vendor/importlib_metadata/_itertools.py | 73 ++ setuptools/_vendor/importlib_metadata/_meta.py | 48 + setuptools/_vendor/importlib_metadata/_text.py | 99 ++ setuptools/_vendor/importlib_metadata/py.typed | 0 setuptools/_vendor/importlib_resources/__init__.py | 36 + .../_vendor/importlib_resources/_adapters.py | 170 ++++ setuptools/_vendor/importlib_resources/_common.py | 104 ++ setuptools/_vendor/importlib_resources/_compat.py | 98 ++ .../_vendor/importlib_resources/_itertools.py | 35 + setuptools/_vendor/importlib_resources/_legacy.py | 121 +++ setuptools/_vendor/importlib_resources/abc.py | 137 +++ setuptools/_vendor/importlib_resources/py.typed | 0 setuptools/_vendor/importlib_resources/readers.py | 122 +++ setuptools/_vendor/importlib_resources/simple.py | 116 +++ .../_vendor/importlib_resources/tests/__init__.py | 0 .../_vendor/importlib_resources/tests/_compat.py | 19 + .../importlib_resources/tests/data01/__init__.py | 0 .../importlib_resources/tests/data01/binary.file | Bin 0 -> 4 bytes .../tests/data01/subdirectory/__init__.py | 0 .../tests/data01/subdirectory/binary.file | Bin 0 -> 4 bytes .../importlib_resources/tests/data01/utf-16.file | Bin 0 -> 44 bytes .../importlib_resources/tests/data01/utf-8.file | 1 + .../importlib_resources/tests/data02/__init__.py | 0 .../tests/data02/one/__init__.py | 0 .../tests/data02/one/resource1.txt | 1 + .../tests/data02/two/__init__.py | 0 .../tests/data02/two/resource2.txt | 1 + .../tests/namespacedata01/binary.file | Bin 0 -> 4 bytes .../tests/namespacedata01/utf-16.file | Bin 0 -> 44 bytes .../tests/namespacedata01/utf-8.file | 1 + .../tests/test_compatibilty_files.py | 102 ++ .../importlib_resources/tests/test_contents.py | 43 + .../importlib_resources/tests/test_files.py | 46 + .../_vendor/importlib_resources/tests/test_open.py | 81 ++ .../_vendor/importlib_resources/tests/test_path.py | 64 ++ .../_vendor/importlib_resources/tests/test_read.py | 76 ++ .../importlib_resources/tests/test_reader.py | 128 +++ .../importlib_resources/tests/test_resource.py | 252 +++++ .../importlib_resources/tests/update-zips.py | 53 + .../_vendor/importlib_resources/tests/util.py | 178 ++++ .../tests/zipdata01/__init__.py | 0 .../tests/zipdata01/ziptestdata.zip | Bin 0 -> 876 bytes .../tests/zipdata02/__init__.py | 0 .../tests/zipdata02/ziptestdata.zip | Bin 0 -> 698 bytes setuptools/_vendor/vendored.txt | 2 + setuptools/_vendor/zipp.py | 329 ++++++ setuptools/extern/__init__.py | 5 +- 52 files changed, 3867 insertions(+), 1 deletion(-) create mode 100644 setuptools/_vendor/importlib_metadata/__init__.py create mode 100644 setuptools/_vendor/importlib_metadata/_adapters.py create mode 100644 setuptools/_vendor/importlib_metadata/_collections.py create mode 100644 setuptools/_vendor/importlib_metadata/_compat.py create mode 100644 setuptools/_vendor/importlib_metadata/_functools.py create mode 100644 setuptools/_vendor/importlib_metadata/_itertools.py create mode 100644 setuptools/_vendor/importlib_metadata/_meta.py create mode 100644 setuptools/_vendor/importlib_metadata/_text.py create mode 100644 setuptools/_vendor/importlib_metadata/py.typed create mode 100644 setuptools/_vendor/importlib_resources/__init__.py create mode 100644 setuptools/_vendor/importlib_resources/_adapters.py create mode 100644 setuptools/_vendor/importlib_resources/_common.py create mode 100644 setuptools/_vendor/importlib_resources/_compat.py create mode 100644 setuptools/_vendor/importlib_resources/_itertools.py create mode 100644 setuptools/_vendor/importlib_resources/_legacy.py create mode 100644 setuptools/_vendor/importlib_resources/abc.py create mode 100644 setuptools/_vendor/importlib_resources/py.typed create mode 100644 setuptools/_vendor/importlib_resources/readers.py create mode 100644 setuptools/_vendor/importlib_resources/simple.py create mode 100644 setuptools/_vendor/importlib_resources/tests/__init__.py create mode 100644 setuptools/_vendor/importlib_resources/tests/_compat.py create mode 100644 setuptools/_vendor/importlib_resources/tests/data01/__init__.py create mode 100644 setuptools/_vendor/importlib_resources/tests/data01/binary.file create mode 100644 setuptools/_vendor/importlib_resources/tests/data01/subdirectory/__init__.py create mode 100644 setuptools/_vendor/importlib_resources/tests/data01/subdirectory/binary.file create mode 100644 setuptools/_vendor/importlib_resources/tests/data01/utf-16.file create mode 100644 setuptools/_vendor/importlib_resources/tests/data01/utf-8.file create mode 100644 setuptools/_vendor/importlib_resources/tests/data02/__init__.py create mode 100644 setuptools/_vendor/importlib_resources/tests/data02/one/__init__.py create mode 100644 setuptools/_vendor/importlib_resources/tests/data02/one/resource1.txt create mode 100644 setuptools/_vendor/importlib_resources/tests/data02/two/__init__.py create mode 100644 setuptools/_vendor/importlib_resources/tests/data02/two/resource2.txt create mode 100644 setuptools/_vendor/importlib_resources/tests/namespacedata01/binary.file create mode 100644 setuptools/_vendor/importlib_resources/tests/namespacedata01/utf-16.file create mode 100644 setuptools/_vendor/importlib_resources/tests/namespacedata01/utf-8.file create mode 100644 setuptools/_vendor/importlib_resources/tests/test_compatibilty_files.py create mode 100644 setuptools/_vendor/importlib_resources/tests/test_contents.py create mode 100644 setuptools/_vendor/importlib_resources/tests/test_files.py create mode 100644 setuptools/_vendor/importlib_resources/tests/test_open.py create mode 100644 setuptools/_vendor/importlib_resources/tests/test_path.py create mode 100644 setuptools/_vendor/importlib_resources/tests/test_read.py create mode 100644 setuptools/_vendor/importlib_resources/tests/test_reader.py create mode 100644 setuptools/_vendor/importlib_resources/tests/test_resource.py create mode 100644 setuptools/_vendor/importlib_resources/tests/update-zips.py create mode 100644 setuptools/_vendor/importlib_resources/tests/util.py create mode 100644 setuptools/_vendor/importlib_resources/tests/zipdata01/__init__.py create mode 100644 setuptools/_vendor/importlib_resources/tests/zipdata01/ziptestdata.zip create mode 100644 setuptools/_vendor/importlib_resources/tests/zipdata02/__init__.py create mode 100644 setuptools/_vendor/importlib_resources/tests/zipdata02/ziptestdata.zip create mode 100644 setuptools/_vendor/zipp.py diff --git a/setuptools/_vendor/importlib_metadata/__init__.py b/setuptools/_vendor/importlib_metadata/__init__.py new file mode 100644 index 00000000..a7379810 --- /dev/null +++ b/setuptools/_vendor/importlib_metadata/__init__.py @@ -0,0 +1,1054 @@ +import os +import re +import abc +import csv +import sys +import zipp +import email +import pathlib +import operator +import textwrap +import warnings +import functools +import itertools +import posixpath +import collections + +from . import _adapters, _meta +from ._collections import FreezableDefaultDict, Pair +from ._compat import ( + NullFinder, + install, + pypy_partial, +) +from ._functools import method_cache, pass_none +from ._itertools import always_iterable, unique_everseen +from ._meta import PackageMetadata, SimplePath + +from contextlib import suppress +from importlib import import_module +from importlib.abc import MetaPathFinder +from itertools import starmap +from typing import List, Mapping, Optional, Union + + +__all__ = [ + 'Distribution', + 'DistributionFinder', + 'PackageMetadata', + 'PackageNotFoundError', + 'distribution', + 'distributions', + 'entry_points', + 'files', + 'metadata', + 'packages_distributions', + 'requires', + 'version', +] + + +class PackageNotFoundError(ModuleNotFoundError): + """The package was not found.""" + + def __str__(self): + return f"No package metadata was found for {self.name}" + + @property + def name(self): + (name,) = self.args + return name + + +class Sectioned: + """ + A simple entry point config parser for performance + + >>> for item in Sectioned.read(Sectioned._sample): + ... print(item) + Pair(name='sec1', value='# comments ignored') + Pair(name='sec1', value='a = 1') + Pair(name='sec1', value='b = 2') + Pair(name='sec2', value='a = 2') + + >>> res = Sectioned.section_pairs(Sectioned._sample) + >>> item = next(res) + >>> item.name + 'sec1' + >>> item.value + Pair(name='a', value='1') + >>> item = next(res) + >>> item.value + Pair(name='b', value='2') + >>> item = next(res) + >>> item.name + 'sec2' + >>> item.value + Pair(name='a', value='2') + >>> list(res) + [] + """ + + _sample = textwrap.dedent( + """ + [sec1] + # comments ignored + a = 1 + b = 2 + + [sec2] + a = 2 + """ + ).lstrip() + + @classmethod + def section_pairs(cls, text): + return ( + section._replace(value=Pair.parse(section.value)) + for section in cls.read(text, filter_=cls.valid) + if section.name is not None + ) + + @staticmethod + def read(text, filter_=None): + lines = filter(filter_, map(str.strip, text.splitlines())) + name = None + for value in lines: + section_match = value.startswith('[') and value.endswith(']') + if section_match: + name = value.strip('[]') + continue + yield Pair(name, value) + + @staticmethod + def valid(line): + return line and not line.startswith('#') + + +class DeprecatedTuple: + """ + Provide subscript item access for backward compatibility. + + >>> recwarn = getfixture('recwarn') + >>> ep = EntryPoint(name='name', value='value', group='group') + >>> ep[:] + ('name', 'value', 'group') + >>> ep[0] + 'name' + >>> len(recwarn) + 1 + """ + + _warn = functools.partial( + warnings.warn, + "EntryPoint tuple interface is deprecated. Access members by name.", + DeprecationWarning, + stacklevel=pypy_partial(2), + ) + + def __getitem__(self, item): + self._warn() + return self._key()[item] + + +class EntryPoint(DeprecatedTuple): + """An entry point as defined by Python packaging conventions. + + See `the packaging docs on entry points + `_ + for more information. + """ + + pattern = re.compile( + r'(?P[\w.]+)\s*' + r'(:\s*(?P[\w.]+))?\s*' + r'(?P\[.*\])?\s*$' + ) + """ + A regular expression describing the syntax for an entry point, + which might look like: + + - module + - package.module + - package.module:attribute + - package.module:object.attribute + - package.module:attr [extra1, extra2] + + Other combinations are possible as well. + + The expression is lenient about whitespace around the ':', + following the attr, and following any extras. + """ + + dist: Optional['Distribution'] = None + + def __init__(self, name, value, group): + vars(self).update(name=name, value=value, group=group) + + def load(self): + """Load the entry point from its definition. If only a module + is indicated by the value, return that module. Otherwise, + return the named object. + """ + match = self.pattern.match(self.value) + module = import_module(match.group('module')) + attrs = filter(None, (match.group('attr') or '').split('.')) + return functools.reduce(getattr, attrs, module) + + @property + def module(self): + match = self.pattern.match(self.value) + return match.group('module') + + @property + def attr(self): + match = self.pattern.match(self.value) + return match.group('attr') + + @property + def extras(self): + match = self.pattern.match(self.value) + return list(re.finditer(r'\w+', match.group('extras') or '')) + + def _for(self, dist): + vars(self).update(dist=dist) + return self + + def __iter__(self): + """ + Supply iter so one may construct dicts of EntryPoints by name. + """ + msg = ( + "Construction of dict of EntryPoints is deprecated in " + "favor of EntryPoints." + ) + warnings.warn(msg, DeprecationWarning) + return iter((self.name, self)) + + def matches(self, **params): + attrs = (getattr(self, param) for param in params) + return all(map(operator.eq, params.values(), attrs)) + + def _key(self): + return self.name, self.value, self.group + + def __lt__(self, other): + return self._key() < other._key() + + def __eq__(self, other): + return self._key() == other._key() + + def __setattr__(self, name, value): + raise AttributeError("EntryPoint objects are immutable.") + + def __repr__(self): + return ( + f'EntryPoint(name={self.name!r}, value={self.value!r}, ' + f'group={self.group!r})' + ) + + def __hash__(self): + return hash(self._key()) + + +class DeprecatedList(list): + """ + Allow an otherwise immutable object to implement mutability + for compatibility. + + >>> recwarn = getfixture('recwarn') + >>> dl = DeprecatedList(range(3)) + >>> dl[0] = 1 + >>> dl.append(3) + >>> del dl[3] + >>> dl.reverse() + >>> dl.sort() + >>> dl.extend([4]) + >>> dl.pop(-1) + 4 + >>> dl.remove(1) + >>> dl += [5] + >>> dl + [6] + [1, 2, 5, 6] + >>> dl + (6,) + [1, 2, 5, 6] + >>> dl.insert(0, 0) + >>> dl + [0, 1, 2, 5] + >>> dl == [0, 1, 2, 5] + True + >>> dl == (0, 1, 2, 5) + True + >>> len(recwarn) + 1 + """ + + _warn = functools.partial( + warnings.warn, + "EntryPoints list interface is deprecated. Cast to list if needed.", + DeprecationWarning, + stacklevel=pypy_partial(2), + ) + + def _wrap_deprecated_method(method_name: str): # type: ignore + def wrapped(self, *args, **kwargs): + self._warn() + return getattr(super(), method_name)(*args, **kwargs) + + return wrapped + + for method_name in [ + '__setitem__', + '__delitem__', + 'append', + 'reverse', + 'extend', + 'pop', + 'remove', + '__iadd__', + 'insert', + 'sort', + ]: + locals()[method_name] = _wrap_deprecated_method(method_name) + + def __add__(self, other): + if not isinstance(other, tuple): + self._warn() + other = tuple(other) + return self.__class__(tuple(self) + other) + + def __eq__(self, other): + if not isinstance(other, tuple): + self._warn() + other = tuple(other) + + return tuple(self).__eq__(other) + + +class EntryPoints(DeprecatedList): + """ + An immutable collection of selectable EntryPoint objects. + """ + + __slots__ = () + + def __getitem__(self, name): # -> EntryPoint: + """ + Get the EntryPoint in self matching name. + """ + if isinstance(name, int): + warnings.warn( + "Accessing entry points by index is deprecated. " + "Cast to tuple if needed.", + DeprecationWarning, + stacklevel=2, + ) + return super().__getitem__(name) + try: + return next(iter(self.select(name=name))) + except StopIteration: + raise KeyError(name) + + def select(self, **params): + """ + Select entry points from self that match the + given parameters (typically group and/or name). + """ + return EntryPoints(ep for ep in self if ep.matches(**params)) + + @property + def names(self): + """ + Return the set of all names of all entry points. + """ + return {ep.name for ep in self} + + @property + def groups(self): + """ + Return the set of all groups of all entry points. + + For coverage while SelectableGroups is present. + >>> EntryPoints().groups + set() + """ + return {ep.group for ep in self} + + @classmethod + def _from_text_for(cls, text, dist): + return cls(ep._for(dist) for ep in cls._from_text(text)) + + @staticmethod + def _from_text(text): + return ( + EntryPoint(name=item.value.name, value=item.value.value, group=item.name) + for item in Sectioned.section_pairs(text or '') + ) + + +class Deprecated: + """ + Compatibility add-in for mapping to indicate that + mapping behavior is deprecated. + + >>> recwarn = getfixture('recwarn') + >>> class DeprecatedDict(Deprecated, dict): pass + >>> dd = DeprecatedDict(foo='bar') + >>> dd.get('baz', None) + >>> dd['foo'] + 'bar' + >>> list(dd) + ['foo'] + >>> list(dd.keys()) + ['foo'] + >>> 'foo' in dd + True + >>> list(dd.values()) + ['bar'] + >>> len(recwarn) + 1 + """ + + _warn = functools.partial( + warnings.warn, + "SelectableGroups dict interface is deprecated. Use select.", + DeprecationWarning, + stacklevel=pypy_partial(2), + ) + + def __getitem__(self, name): + self._warn() + return super().__getitem__(name) + + def get(self, name, default=None): + self._warn() + return super().get(name, default) + + def __iter__(self): + self._warn() + return super().__iter__() + + def __contains__(self, *args): + self._warn() + return super().__contains__(*args) + + def keys(self): + self._warn() + return super().keys() + + def values(self): + self._warn() + return super().values() + + +class SelectableGroups(Deprecated, dict): + """ + A backward- and forward-compatible result from + entry_points that fully implements the dict interface. + """ + + @classmethod + def load(cls, eps): + by_group = operator.attrgetter('group') + ordered = sorted(eps, key=by_group) + grouped = itertools.groupby(ordered, by_group) + return cls((group, EntryPoints(eps)) for group, eps in grouped) + + @property + def _all(self): + """ + Reconstruct a list of all entrypoints from the groups. + """ + groups = super(Deprecated, self).values() + return EntryPoints(itertools.chain.from_iterable(groups)) + + @property + def groups(self): + return self._all.groups + + @property + def names(self): + """ + for coverage: + >>> SelectableGroups().names + set() + """ + return self._all.names + + def select(self, **params): + if not params: + return self + return self._all.select(**params) + + +class PackagePath(pathlib.PurePosixPath): + """A reference to a path in a package""" + + def read_text(self, encoding='utf-8'): + with self.locate().open(encoding=encoding) as stream: + return stream.read() + + def read_binary(self): + with self.locate().open('rb') as stream: + return stream.read() + + def locate(self): + """Return a path-like object for this path""" + return self.dist.locate_file(self) + + +class FileHash: + def __init__(self, spec): + self.mode, _, self.value = spec.partition('=') + + def __repr__(self): + return f'' + + +class Distribution: + """A Python distribution package.""" + + @abc.abstractmethod + def read_text(self, filename): + """Attempt to load metadata file given by the name. + + :param filename: The name of the file in the distribution info. + :return: The text if found, otherwise None. + """ + + @abc.abstractmethod + def locate_file(self, path): + """ + Given a path to a file in this distribution, return a path + to it. + """ + + @classmethod + def from_name(cls, name): + """Return the Distribution for the given package name. + + :param name: The name of the distribution package to search for. + :return: The Distribution instance (or subclass thereof) for the named + package, if found. + :raises PackageNotFoundError: When the named package's distribution + metadata cannot be found. + """ + for resolver in cls._discover_resolvers(): + dists = resolver(DistributionFinder.Context(name=name)) + dist = next(iter(dists), None) + if dist is not None: + return dist + else: + raise PackageNotFoundError(name) + + @classmethod + def discover(cls, **kwargs): + """Return an iterable of Distribution objects for all packages. + + Pass a ``context`` or pass keyword arguments for constructing + a context. + + :context: A ``DistributionFinder.Context`` object. + :return: Iterable of Distribution objects for all packages. + """ + context = kwargs.pop('context', None) + if context and kwargs: + raise ValueError("cannot accept context and kwargs") + context = context or DistributionFinder.Context(**kwargs) + return itertools.chain.from_iterable( + resolver(context) for resolver in cls._discover_resolvers() + ) + + @staticmethod + def at(path): + """Return a Distribution for the indicated metadata path + + :param path: a string or path-like object + :return: a concrete Distribution instance for the path + """ + return PathDistribution(pathlib.Path(path)) + + @staticmethod + def _discover_resolvers(): + """Search the meta_path for resolvers.""" + declared = ( + getattr(finder, 'find_distributions', None) for finder in sys.meta_path + ) + return filter(None, declared) + + @classmethod + def _local(cls, root='.'): + from pep517 import build, meta + + system = build.compat_system(root) + builder = functools.partial( + meta.build, + source_dir=root, + system=system, + ) + return PathDistribution(zipp.Path(meta.build_as_zip(builder))) + + @property + def metadata(self) -> _meta.PackageMetadata: + """Return the parsed metadata for this Distribution. + + The returned object will have keys that name the various bits of + metadata. See PEP 566 for details. + """ + text = ( + self.read_text('METADATA') + or self.read_text('PKG-INFO') + # This last clause is here to support old egg-info files. Its + # effect is to just end up using the PathDistribution's self._path + # (which points to the egg-info file) attribute unchanged. + or self.read_text('') + ) + return _adapters.Message(email.message_from_string(text)) + + @property + def name(self): + """Return the 'Name' metadata for the distribution package.""" + return self.metadata['Name'] + + @property + def _normalized_name(self): + """Return a normalized version of the name.""" + return Prepared.normalize(self.name) + + @property + def version(self): + """Return the 'Version' metadata for the distribution package.""" + return self.metadata['Version'] + + @property + def entry_points(self): + return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self) + + @property + def files(self): + """Files in this distribution. + + :return: List of PackagePath for this distribution or None + + Result is `None` if the metadata file that enumerates files + (i.e. RECORD for dist-info or SOURCES.txt for egg-info) is + missing. + Result may be empty if the metadata exists but is empty. + """ + + def make_file(name, hash=None, size_str=None): + result = PackagePath(name) + result.hash = FileHash(hash) if hash else None + result.size = int(size_str) if size_str else None + result.dist = self + return result + + @pass_none + def make_files(lines): + return list(starmap(make_file, csv.reader(lines))) + + return make_files(self._read_files_distinfo() or self._read_files_egginfo()) + + def _read_files_distinfo(self): + """ + Read the lines of RECORD + """ + text = self.read_text('RECORD') + return text and text.splitlines() + + def _read_files_egginfo(self): + """ + SOURCES.txt might contain literal commas, so wrap each line + in quotes. + """ + text = self.read_text('SOURCES.txt') + return text and map('"{}"'.format, text.splitlines()) + + @property + def requires(self): + """Generated requirements specified for this Distribution""" + reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs() + return reqs and list(reqs) + + def _read_dist_info_reqs(self): + return self.metadata.get_all('Requires-Dist') + + def _read_egg_info_reqs(self): + source = self.read_text('requires.txt') + return source and self._deps_from_requires_text(source) + + @classmethod + def _deps_from_requires_text(cls, source): + return cls._convert_egg_info_reqs_to_simple_reqs(Sectioned.read(source)) + + @staticmethod + def _convert_egg_info_reqs_to_simple_reqs(sections): + """ + Historically, setuptools would solicit and store 'extra' + requirements, including those with environment markers, + in separate sections. More modern tools expect each + dependency to be defined separately, with any relevant + extras and environment markers attached directly to that + requirement. This method converts the former to the + latter. See _test_deps_from_requires_text for an example. + """ + + def make_condition(name): + return name and f'extra == "{name}"' + + def parse_condition(section): + section = section or '' + extra, sep, markers = section.partition(':') + if extra and markers: + markers = f'({markers})' + conditions = list(filter(None, [markers, make_condition(extra)])) + return '; ' + ' and '.join(conditions) if conditions else '' + + for section in sections: + yield section.value + parse_condition(section.name) + + +class DistributionFinder(MetaPathFinder): + """ + A MetaPathFinder capable of discovering installed distributions. + """ + + class Context: + """ + Keyword arguments presented by the caller to + ``distributions()`` or ``Distribution.discover()`` + to narrow the scope of a search for distributions + in all DistributionFinders. + + Each DistributionFinder may expect any parameters + and should attempt to honor the canonical + parameters defined below when appropriate. + """ + + name = None + """ + Specific name for which a distribution finder should match. + A name of ``None`` matches all distributions. + """ + + def __init__(self, **kwargs): + vars(self).update(kwargs) + + @property + def path(self): + """ + The sequence of directory path that a distribution finder + should search. + + Typically refers to Python installed package paths such as + "site-packages" directories and defaults to ``sys.path``. + """ + return vars(self).get('path', sys.path) + + @abc.abstractmethod + def find_distributions(self, context=Context()): + """ + Find distributions. + + Return an iterable of all Distribution instances capable of + loading the metadata for packages matching the ``context``, + a DistributionFinder.Context instance. + """ + + +class FastPath: + """ + Micro-optimized class for searching a path for + children. + + >>> FastPath('').children() + ['...'] + """ + + @functools.lru_cache() # type: ignore + def __new__(cls, root): + return super().__new__(cls) + + def __init__(self, root): + self.root = str(root) + + def joinpath(self, child): + return pathlib.Path(self.root, child) + + def children(self): + with suppress(Exception): + return os.listdir(self.root or '.') + with suppress(Exception): + return self.zip_children() + return [] + + def zip_children(self): + zip_path = zipp.Path(self.root) + names = zip_path.root.namelist() + self.joinpath = zip_path.joinpath + + return dict.fromkeys(child.split(posixpath.sep, 1)[0] for child in names) + + def search(self, name): + return self.lookup(self.mtime).search(name) + + @property + def mtime(self): + with suppress(OSError): + return os.stat(self.root).st_mtime + self.lookup.cache_clear() + + @method_cache + def lookup(self, mtime): + return Lookup(self) + + +class Lookup: + def __init__(self, path: FastPath): + base = os.path.basename(path.root).lower() + base_is_egg = base.endswith(".egg") + self.infos = FreezableDefaultDict(list) + self.eggs = FreezableDefaultDict(list) + + for child in path.children(): + low = child.lower() + if low.endswith((".dist-info", ".egg-info")): + # rpartition is faster than splitext and suitable for this purpose. + name = low.rpartition(".")[0].partition("-")[0] + normalized = Prepared.normalize(name) + self.infos[normalized].append(path.joinpath(child)) + elif base_is_egg and low == "egg-info": + name = base.rpartition(".")[0].partition("-")[0] + legacy_normalized = Prepared.legacy_normalize(name) + self.eggs[legacy_normalized].append(path.joinpath(child)) + + self.infos.freeze() + self.eggs.freeze() + + def search(self, prepared): + infos = ( + self.infos[prepared.normalized] + if prepared + else itertools.chain.from_iterable(self.infos.values()) + ) + eggs = ( + self.eggs[prepared.legacy_normalized] + if prepared + else itertools.chain.from_iterable(self.eggs.values()) + ) + return itertools.chain(infos, eggs) + + +class Prepared: + """ + A prepared search for metadata on a possibly-named package. + """ + + normalized = None + legacy_normalized = None + + def __init__(self, name): + self.name = name + if name is None: + return + self.normalized = self.normalize(name) + self.legacy_normalized = self.legacy_normalize(name) + + @staticmethod + def normalize(name): + """ + PEP 503 normalization plus dashes as underscores. + """ + return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_') + + @staticmethod + def legacy_normalize(name): + """ + Normalize the package name as found in the convention in + older packaging tools versions and specs. + """ + return name.lower().replace('-', '_') + + def __bool__(self): + return bool(self.name) + + +@install +class MetadataPathFinder(NullFinder, DistributionFinder): + """A degenerate finder for distribution packages on the file system. + + This finder supplies only a find_distributions() method for versions + of Python that do not have a PathFinder find_distributions(). + """ + + def find_distributions(self, context=DistributionFinder.Context()): + """ + Find distributions. + + Return an iterable of all Distribution instances capable of + loading the metadata for packages matching ``context.name`` + (or all names if ``None`` indicated) along the paths in the list + of directories ``context.path``. + """ + found = self._search_paths(context.name, context.path) + return map(PathDistribution, found) + + @classmethod + def _search_paths(cls, name, paths): + """Find metadata directories in paths heuristically.""" + prepared = Prepared(name) + return itertools.chain.from_iterable( + path.search(prepared) for path in map(FastPath, paths) + ) + + def invalidate_caches(cls): + FastPath.__new__.cache_clear() + + +class PathDistribution(Distribution): + def __init__(self, path: SimplePath): + """Construct a distribution. + + :param path: SimplePath indicating the metadata directory. + """ + self._path = path + + def read_text(self, filename): + with suppress( + FileNotFoundError, + IsADirectoryError, + KeyError, + NotADirectoryError, + PermissionError, + ): + return self._path.joinpath(filename).read_text(encoding='utf-8') + + read_text.__doc__ = Distribution.read_text.__doc__ + + def locate_file(self, path): + return self._path.parent / path + + @property + def _normalized_name(self): + """ + Performance optimization: where possible, resolve the + normalized name from the file system path. + """ + stem = os.path.basename(str(self._path)) + return self._name_from_stem(stem) or super()._normalized_name + + def _name_from_stem(self, stem): + name, ext = os.path.splitext(stem) + if ext not in ('.dist-info', '.egg-info'): + return + name, sep, rest = stem.partition('-') + return name + + +def distribution(distribution_name): + """Get the ``Distribution`` instance for the named package. + + :param distribution_name: The name of the distribution package as a string. + :return: A ``Distribution`` instance (or subclass thereof). + """ + return Distribution.from_name(distribution_name) + + +def distributions(**kwargs): + """Get all ``Distribution`` instances in the current environment. + + :return: An iterable of ``Distribution`` instances. + """ + return Distribution.discover(**kwargs) + + +def metadata(distribution_name) -> _meta.PackageMetadata: + """Get the metadata for the named package. + + :param distribution_name: The name of the distribution package to query. + :return: A PackageMetadata containing the parsed metadata. + """ + return Distribution.from_name(distribution_name).metadata + + +def version(distribution_name): + """Get the version string for the named package. + + :param distribution_name: The name of the distribution package to query. + :return: The version string for the package as defined in the package's + "Version" metadata key. + """ + return distribution(distribution_name).version + + +def entry_points(**params) -> Union[EntryPoints, SelectableGroups]: + """Return EntryPoint objects for all installed packages. + + Pass selection parameters (group or name) to filter the + result to entry points matching those properties (see + EntryPoints.select()). + + For compatibility, returns ``SelectableGroups`` object unless + selection parameters are supplied. In the future, this function + will return ``EntryPoints`` instead of ``SelectableGroups`` + even when no selection parameters are supplied. + + For maximum future compatibility, pass selection parameters + or invoke ``.select`` with parameters on the result. + + :return: EntryPoints or SelectableGroups for all installed packages. + """ + norm_name = operator.attrgetter('_normalized_name') + unique = functools.partial(unique_everseen, key=norm_name) + eps = itertools.chain.from_iterable( + dist.entry_points for dist in unique(distributions()) + ) + return SelectableGroups.load(eps).select(**params) + + +def files(distribution_name): + """Return a list of files for the named package. + + :param distribution_name: The name of the distribution package to query. + :return: List of files composing the distribution. + """ + return distribution(distribution_name).files + + +def requires(distribution_name): + """ + Return a list of requirements for the named package. + + :return: An iterator of requirements, suitable for + packaging.requirement.Requirement. + """ + return distribution(distribution_name).requires + + +def packages_distributions() -> Mapping[str, List[str]]: + """ + Return a mapping of top-level packages to their + distributions. + + >>> import collections.abc + >>> pkgs = packages_distributions() + >>> all(isinstance(dist, collections.abc.Sequence) for dist in pkgs.values()) + True + """ + pkg_to_dist = collections.defaultdict(list) + for dist in distributions(): + for pkg in _top_level_declared(dist) or _top_level_inferred(dist): + pkg_to_dist[pkg].append(dist.metadata['Name']) + return dict(pkg_to_dist) + + +def _top_level_declared(dist): + return (dist.read_text('top_level.txt') or '').split() + + +def _top_level_inferred(dist): + return { + f.parts[0] if len(f.parts) > 1 else f.with_suffix('').name + for f in always_iterable(dist.files) + if f.suffix == ".py" + } diff --git a/setuptools/_vendor/importlib_metadata/_adapters.py b/setuptools/_vendor/importlib_metadata/_adapters.py new file mode 100644 index 00000000..aa460d3e --- /dev/null +++ b/setuptools/_vendor/importlib_metadata/_adapters.py @@ -0,0 +1,68 @@ +import re +import textwrap +import email.message + +from ._text import FoldedCase + + +class Message(email.message.Message): + multiple_use_keys = set( + map( + FoldedCase, + [ + 'Classifier', + 'Obsoletes-Dist', + 'Platform', + 'Project-URL', + 'Provides-Dist', + 'Provides-Extra', + 'Requires-Dist', + 'Requires-External', + 'Supported-Platform', + 'Dynamic', + ], + ) + ) + """ + Keys that may be indicated multiple times per PEP 566. + """ + + def __new__(cls, orig: email.message.Message): + res = super().__new__(cls) + vars(res).update(vars(orig)) + return res + + def __init__(self, *args, **kwargs): + self._headers = self._repair_headers() + + # suppress spurious error from mypy + def __iter__(self): + return super().__iter__() + + def _repair_headers(self): + def redent(value): + "Correct for RFC822 indentation" + if not value or '\n' not in value: + return value + return textwrap.dedent(' ' * 8 + value) + + headers = [(key, redent(value)) for key, value in vars(self)['_headers']] + if self._payload: + headers.append(('Description', self.get_payload())) + return headers + + @property + def json(self): + """ + Convert PackageMetadata to a JSON-compatible format + per PEP 0566. + """ + + def transform(key): + value = self.get_all(key) if key in self.multiple_use_keys else self[key] + if key == 'Keywords': + value = re.split(r'\s+', value) + tk = key.lower().replace('-', '_') + return tk, value + + return dict(map(transform, map(FoldedCase, self))) diff --git a/setuptools/_vendor/importlib_metadata/_collections.py b/setuptools/_vendor/importlib_metadata/_collections.py new file mode 100644 index 00000000..cf0954e1 --- /dev/null +++ b/setuptools/_vendor/importlib_metadata/_collections.py @@ -0,0 +1,30 @@ +import collections + + +# from jaraco.collections 3.3 +class FreezableDefaultDict(collections.defaultdict): + """ + Often it is desirable to prevent the mutation of + a default dict after its initial construction, such + as to prevent mutation during iteration. + + >>> dd = FreezableDefaultDict(list) + >>> dd[0].append('1') + >>> dd.freeze() + >>> dd[1] + [] + >>> len(dd) + 1 + """ + + def __missing__(self, key): + return getattr(self, '_frozen', super().__missing__)(key) + + def freeze(self): + self._frozen = lambda key: self.default_factory() + + +class Pair(collections.namedtuple('Pair', 'name value')): + @classmethod + def parse(cls, text): + return cls(*map(str.strip, text.split("=", 1))) diff --git a/setuptools/_vendor/importlib_metadata/_compat.py b/setuptools/_vendor/importlib_metadata/_compat.py new file mode 100644 index 00000000..8fe4e4e3 --- /dev/null +++ b/setuptools/_vendor/importlib_metadata/_compat.py @@ -0,0 +1,71 @@ +import sys +import platform + + +__all__ = ['install', 'NullFinder', 'Protocol'] + + +try: + from typing import Protocol +except ImportError: # pragma: no cover + from typing_extensions import Protocol # type: ignore + + +def install(cls): + """ + Class decorator for installation on sys.meta_path. + + Adds the backport DistributionFinder to sys.meta_path and + attempts to disable the finder functionality of the stdlib + DistributionFinder. + """ + sys.meta_path.append(cls()) + disable_stdlib_finder() + return cls + + +def disable_stdlib_finder(): + """ + Give the backport primacy for discovering path-based distributions + by monkey-patching the stdlib O_O. + + See #91 for more background for rationale on this sketchy + behavior. + """ + + def matches(finder): + return getattr( + finder, '__module__', None + ) == '_frozen_importlib_external' and hasattr(finder, 'find_distributions') + + for finder in filter(matches, sys.meta_path): # pragma: nocover + del finder.find_distributions + + +class NullFinder: + """ + A "Finder" (aka "MetaClassFinder") that never finds any modules, + but may find distributions. + """ + + @staticmethod + def find_spec(*args, **kwargs): + return None + + # In Python 2, the import system requires finders + # to have a find_module() method, but this usage + # is deprecated in Python 3 in favor of find_spec(). + # For the purposes of this finder (i.e. being present + # on sys.meta_path but having no other import + # system functionality), the two methods are identical. + find_module = find_spec + + +def pypy_partial(val): + """ + Adjust for variable stacklevel on partial under PyPy. + + Workaround for #327. + """ + is_pypy = platform.python_implementation() == 'PyPy' + return val + is_pypy diff --git a/setuptools/_vendor/importlib_metadata/_functools.py b/setuptools/_vendor/importlib_metadata/_functools.py new file mode 100644 index 00000000..71f66bd0 --- /dev/null +++ b/setuptools/_vendor/importlib_metadata/_functools.py @@ -0,0 +1,104 @@ +import types +import functools + + +# from jaraco.functools 3.3 +def method_cache(method, cache_wrapper=None): + """ + Wrap lru_cache to support storing the cache data in the object instances. + + Abstracts the common paradigm where the method explicitly saves an + underscore-prefixed protected property on first call and returns that + subsequently. + + >>> class MyClass: + ... calls = 0 + ... + ... @method_cache + ... def method(self, value): + ... self.calls += 1 + ... return value + + >>> a = MyClass() + >>> a.method(3) + 3 + >>> for x in range(75): + ... res = a.method(x) + >>> a.calls + 75 + + Note that the apparent behavior will be exactly like that of lru_cache + except that the cache is stored on each instance, so values in one + instance will not flush values from another, and when an instance is + deleted, so are the cached values for that instance. + + >>> b = MyClass() + >>> for x in range(35): + ... res = b.method(x) + >>> b.calls + 35 + >>> a.method(0) + 0 + >>> a.calls + 75 + + Note that if method had been decorated with ``functools.lru_cache()``, + a.calls would have been 76 (due to the cached value of 0 having been + flushed by the 'b' instance). + + Clear the cache with ``.cache_clear()`` + + >>> a.method.cache_clear() + + Same for a method that hasn't yet been called. + + >>> c = MyClass() + >>> c.method.cache_clear() + + Another cache wrapper may be supplied: + + >>> cache = functools.lru_cache(maxsize=2) + >>> MyClass.method2 = method_cache(lambda self: 3, cache_wrapper=cache) + >>> a = MyClass() + >>> a.method2() + 3 + + Caution - do not subsequently wrap the method with another decorator, such + as ``@property``, which changes the semantics of the function. + + See also + http://code.activestate.com/recipes/577452-a-memoize-decorator-for-instance-methods/ + for another implementation and additional justification. + """ + cache_wrapper = cache_wrapper or functools.lru_cache() + + def wrapper(self, *args, **kwargs): + # it's the first call, replace the method with a cached, bound method + bound_method = types.MethodType(method, self) + cached_method = cache_wrapper(bound_method) + setattr(self, method.__name__, cached_method) + return cached_method(*args, **kwargs) + + # Support cache clear even before cache has been created. + wrapper.cache_clear = lambda: None + + return wrapper + + +# From jaraco.functools 3.3 +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/_vendor/importlib_metadata/_itertools.py b/setuptools/_vendor/importlib_metadata/_itertools.py new file mode 100644 index 00000000..d4ca9b91 --- /dev/null +++ b/setuptools/_vendor/importlib_metadata/_itertools.py @@ -0,0 +1,73 @@ +from itertools import filterfalse + + +def unique_everseen(iterable, key=None): + "List unique elements, preserving order. Remember all elements ever seen." + # unique_everseen('AAAABBBCCDAABBB') --> A B C D + # unique_everseen('ABBCcAD', str.lower) --> A B C D + seen = set() + seen_add = seen.add + if key is None: + for element in filterfalse(seen.__contains__, iterable): + seen_add(element) + yield element + else: + for element in iterable: + k = key(element) + if k not in seen: + seen_add(k) + yield element + + +# copied from more_itertools 8.8 +def always_iterable(obj, base_type=(str, bytes)): + """If *obj* is iterable, return an iterator over its items:: + + >>> obj = (1, 2, 3) + >>> list(always_iterable(obj)) + [1, 2, 3] + + If *obj* is not iterable, return a one-item iterable containing *obj*:: + + >>> obj = 1 + >>> list(always_iterable(obj)) + [1] + + If *obj* is ``None``, return an empty iterable: + + >>> obj = None + >>> list(always_iterable(None)) + [] + + By default, binary and text strings are not considered iterable:: + + >>> obj = 'foo' + >>> list(always_iterable(obj)) + ['foo'] + + If *base_type* is set, objects for which ``isinstance(obj, base_type)`` + returns ``True`` won't be considered iterable. + + >>> obj = {'a': 1} + >>> list(always_iterable(obj)) # Iterate over the dict's keys + ['a'] + >>> list(always_iterable(obj, base_type=dict)) # Treat dicts as a unit + [{'a': 1}] + + Set *base_type* to ``None`` to avoid any special handling and treat objects + Python considers iterable as iterable: + + >>> obj = 'foo' + >>> list(always_iterable(obj, base_type=None)) + ['f', 'o', 'o'] + """ + if obj is None: + return iter(()) + + if (base_type is not None) and isinstance(obj, base_type): + return iter((obj,)) + + try: + return iter(obj) + except TypeError: + return iter((obj,)) diff --git a/setuptools/_vendor/importlib_metadata/_meta.py b/setuptools/_vendor/importlib_metadata/_meta.py new file mode 100644 index 00000000..37ee43e6 --- /dev/null +++ b/setuptools/_vendor/importlib_metadata/_meta.py @@ -0,0 +1,48 @@ +from ._compat import Protocol +from typing import Any, Dict, Iterator, List, TypeVar, Union + + +_T = TypeVar("_T") + + +class PackageMetadata(Protocol): + def __len__(self) -> int: + ... # pragma: no cover + + def __contains__(self, item: str) -> bool: + ... # pragma: no cover + + def __getitem__(self, key: str) -> str: + ... # pragma: no cover + + def __iter__(self) -> Iterator[str]: + ... # pragma: no cover + + def get_all(self, name: str, failobj: _T = ...) -> Union[List[Any], _T]: + """ + Return all values associated with a possibly multi-valued key. + """ + + @property + def json(self) -> Dict[str, Union[str, List[str]]]: + """ + A JSON-compatible form of the metadata. + """ + + +class SimplePath(Protocol): + """ + A minimal subset of pathlib.Path required by PathDistribution. + """ + + def joinpath(self) -> 'SimplePath': + ... # pragma: no cover + + def __truediv__(self) -> 'SimplePath': + ... # pragma: no cover + + def parent(self) -> 'SimplePath': + ... # pragma: no cover + + def read_text(self) -> str: + ... # pragma: no cover diff --git a/setuptools/_vendor/importlib_metadata/_text.py b/setuptools/_vendor/importlib_metadata/_text.py new file mode 100644 index 00000000..c88cfbb2 --- /dev/null +++ b/setuptools/_vendor/importlib_metadata/_text.py @@ -0,0 +1,99 @@ +import re + +from ._functools import method_cache + + +# from jaraco.text 3.5 +class FoldedCase(str): + """ + A case insensitive string class; behaves just like str + except compares equal when the only variation is case. + + >>> s = FoldedCase('hello world') + + >>> s == 'Hello World' + True + + >>> 'Hello World' == s + True + + >>> s != 'Hello World' + False + + >>> s.index('O') + 4 + + >>> s.split('O') + ['hell', ' w', 'rld'] + + >>> sorted(map(FoldedCase, ['GAMMA', 'alpha', 'Beta'])) + ['alpha', 'Beta', 'GAMMA'] + + Sequence membership is straightforward. + + >>> "Hello World" in [s] + True + >>> s in ["Hello World"] + True + + You may test for set inclusion, but candidate and elements + must both be folded. + + >>> FoldedCase("Hello World") in {s} + True + >>> s in {FoldedCase("Hello World")} + True + + String inclusion works as long as the FoldedCase object + is on the right. + + >>> "hello" in FoldedCase("Hello World") + True + + But not if the FoldedCase object is on the left: + + >>> FoldedCase('hello') in 'Hello World' + False + + In that case, use in_: + + >>> FoldedCase('hello').in_('Hello World') + True + + >>> FoldedCase('hello') > FoldedCase('Hello') + False + """ + + def __lt__(self, other): + return self.lower() < other.lower() + + def __gt__(self, other): + return self.lower() > other.lower() + + def __eq__(self, other): + return self.lower() == other.lower() + + def __ne__(self, other): + return self.lower() != other.lower() + + def __hash__(self): + return hash(self.lower()) + + def __contains__(self, other): + return super().lower().__contains__(other.lower()) + + def in_(self, other): + "Does self appear in other?" + return self in FoldedCase(other) + + # cache lower since it's likely to be called frequently. + @method_cache + def lower(self): + return super().lower() + + def index(self, sub): + return self.lower().index(sub.lower()) + + def split(self, splitter=' ', maxsplit=0): + pattern = re.compile(re.escape(splitter), re.I) + return pattern.split(self, maxsplit) diff --git a/setuptools/_vendor/importlib_metadata/py.typed b/setuptools/_vendor/importlib_metadata/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/setuptools/_vendor/importlib_resources/__init__.py b/setuptools/_vendor/importlib_resources/__init__.py new file mode 100644 index 00000000..15f6b26b --- /dev/null +++ b/setuptools/_vendor/importlib_resources/__init__.py @@ -0,0 +1,36 @@ +"""Read resources contained within a package.""" + +from ._common import ( + as_file, + files, + Package, +) + +from ._legacy import ( + contents, + open_binary, + read_binary, + open_text, + read_text, + is_resource, + path, + Resource, +) + +from importlib_resources.abc import ResourceReader + + +__all__ = [ + 'Package', + 'Resource', + 'ResourceReader', + 'as_file', + 'contents', + 'files', + 'is_resource', + 'open_binary', + 'open_text', + 'path', + 'read_binary', + 'read_text', +] diff --git a/setuptools/_vendor/importlib_resources/_adapters.py b/setuptools/_vendor/importlib_resources/_adapters.py new file mode 100644 index 00000000..ea363d86 --- /dev/null +++ b/setuptools/_vendor/importlib_resources/_adapters.py @@ -0,0 +1,170 @@ +from contextlib import suppress +from io import TextIOWrapper + +from . import abc + + +class SpecLoaderAdapter: + """ + Adapt a package spec to adapt the underlying loader. + """ + + def __init__(self, spec, adapter=lambda spec: spec.loader): + self.spec = spec + self.loader = adapter(spec) + + def __getattr__(self, name): + return getattr(self.spec, name) + + +class TraversableResourcesLoader: + """ + Adapt a loader to provide TraversableResources. + """ + + def __init__(self, spec): + self.spec = spec + + def get_resource_reader(self, name): + return CompatibilityFiles(self.spec)._native() + + +def _io_wrapper(file, mode='r', *args, **kwargs): + if mode == 'r': + return TextIOWrapper(file, *args, **kwargs) + elif mode == 'rb': + return file + raise ValueError( + "Invalid mode value '{}', only 'r' and 'rb' are supported".format(mode) + ) + + +class CompatibilityFiles: + """ + Adapter for an existing or non-existent resource reader + to provide a compatibility .files(). + """ + + class SpecPath(abc.Traversable): + """ + Path tied to a module spec. + Can be read and exposes the resource reader children. + """ + + def __init__(self, spec, reader): + self._spec = spec + self._reader = reader + + def iterdir(self): + if not self._reader: + return iter(()) + return iter( + CompatibilityFiles.ChildPath(self._reader, path) + for path in self._reader.contents() + ) + + def is_file(self): + return False + + is_dir = is_file + + def joinpath(self, other): + if not self._reader: + return CompatibilityFiles.OrphanPath(other) + return CompatibilityFiles.ChildPath(self._reader, other) + + @property + def name(self): + return self._spec.name + + def open(self, mode='r', *args, **kwargs): + return _io_wrapper(self._reader.open_resource(None), mode, *args, **kwargs) + + class ChildPath(abc.Traversable): + """ + Path tied to a resource reader child. + Can be read but doesn't expose any meaningful children. + """ + + def __init__(self, reader, name): + self._reader = reader + self._name = name + + def iterdir(self): + return iter(()) + + def is_file(self): + return self._reader.is_resource(self.name) + + def is_dir(self): + return not self.is_file() + + def joinpath(self, other): + return CompatibilityFiles.OrphanPath(self.name, other) + + @property + def name(self): + return self._name + + def open(self, mode='r', *args, **kwargs): + return _io_wrapper( + self._reader.open_resource(self.name), mode, *args, **kwargs + ) + + class OrphanPath(abc.Traversable): + """ + Orphan path, not tied to a module spec or resource reader. + Can't be read and doesn't expose any meaningful children. + """ + + def __init__(self, *path_parts): + if len(path_parts) < 1: + raise ValueError('Need at least one path part to construct a path') + self._path = path_parts + + def iterdir(self): + return iter(()) + + def is_file(self): + return False + + is_dir = is_file + + def joinpath(self, other): + return CompatibilityFiles.OrphanPath(*self._path, other) + + @property + def name(self): + return self._path[-1] + + def open(self, mode='r', *args, **kwargs): + raise FileNotFoundError("Can't open orphan path") + + def __init__(self, spec): + self.spec = spec + + @property + def _reader(self): + with suppress(AttributeError): + return self.spec.loader.get_resource_reader(self.spec.name) + + def _native(self): + """ + Return the native reader if it supports files(). + """ + reader = self._reader + return reader if hasattr(reader, 'files') else self + + def __getattr__(self, attr): + return getattr(self._reader, attr) + + def files(self): + return CompatibilityFiles.SpecPath(self.spec, self._reader) + + +def wrap_spec(package): + """ + Construct a package spec with traversable compatibility + on the spec/loader/reader. + """ + return SpecLoaderAdapter(package.__spec__, TraversableResourcesLoader) diff --git a/setuptools/_vendor/importlib_resources/_common.py b/setuptools/_vendor/importlib_resources/_common.py new file mode 100644 index 00000000..a12e2c75 --- /dev/null +++ b/setuptools/_vendor/importlib_resources/_common.py @@ -0,0 +1,104 @@ +import os +import pathlib +import tempfile +import functools +import contextlib +import types +import importlib + +from typing import Union, Optional +from .abc import ResourceReader, Traversable + +from ._compat import wrap_spec + +Package = Union[types.ModuleType, str] + + +def files(package): + # type: (Package) -> Traversable + """ + Get a Traversable resource from a package + """ + return from_package(get_package(package)) + + +def get_resource_reader(package): + # type: (types.ModuleType) -> Optional[ResourceReader] + """ + Return the package's loader if it's a ResourceReader. + """ + # We can't use + # a issubclass() check here because apparently abc.'s __subclasscheck__() + # hook wants to create a weak reference to the object, but + # zipimport.zipimporter does not support weak references, resulting in a + # TypeError. That seems terrible. + spec = package.__spec__ + reader = getattr(spec.loader, 'get_resource_reader', None) # type: ignore + if reader is None: + return None + return reader(spec.name) # type: ignore + + +def resolve(cand): + # type: (Package) -> types.ModuleType + return cand if isinstance(cand, types.ModuleType) else importlib.import_module(cand) + + +def get_package(package): + # type: (Package) -> types.ModuleType + """Take a package name or module object and return the module. + + Raise an exception if the resolved module is not a package. + """ + resolved = resolve(package) + if wrap_spec(resolved).submodule_search_locations is None: + raise TypeError(f'{package!r} is not a package') + return resolved + + +def from_package(package): + """ + Return a Traversable object for the given package. + + """ + spec = wrap_spec(package) + reader = spec.loader.get_resource_reader(spec.name) + return reader.files() + + +@contextlib.contextmanager +def _tempfile(reader, suffix=''): + # Not using tempfile.NamedTemporaryFile as it leads to deeper 'try' + # blocks due to the need to close the temporary file to work on Windows + # properly. + fd, raw_path = tempfile.mkstemp(suffix=suffix) + try: + try: + os.write(fd, reader()) + finally: + os.close(fd) + del reader + yield pathlib.Path(raw_path) + finally: + try: + os.remove(raw_path) + except FileNotFoundError: + pass + + +@functools.singledispatch +def as_file(path): + """ + Given a Traversable object, return that object as a + path on the local file system in a context manager. + """ + return _tempfile(path.read_bytes, suffix=path.name) + + +@as_file.register(pathlib.Path) +@contextlib.contextmanager +def _(path): + """ + Degenerate behavior for pathlib.Path objects. + """ + yield path diff --git a/setuptools/_vendor/importlib_resources/_compat.py b/setuptools/_vendor/importlib_resources/_compat.py new file mode 100644 index 00000000..61e48d47 --- /dev/null +++ b/setuptools/_vendor/importlib_resources/_compat.py @@ -0,0 +1,98 @@ +# flake8: noqa + +import abc +import sys +import pathlib +from contextlib import suppress + +if sys.version_info >= (3, 10): + from zipfile import Path as ZipPath # type: ignore +else: + from zipp import Path as ZipPath # type: ignore + + +try: + from typing import runtime_checkable # type: ignore +except ImportError: + + def runtime_checkable(cls): # type: ignore + return cls + + +try: + from typing import Protocol # type: ignore +except ImportError: + Protocol = abc.ABC # type: ignore + + +class TraversableResourcesLoader: + """ + Adapt loaders to provide TraversableResources and other + compatibility. + + Used primarily for Python 3.9 and earlier where the native + loaders do not yet implement TraversableResources. + """ + + def __init__(self, spec): + self.spec = spec + + @property + def path(self): + return self.spec.origin + + def get_resource_reader(self, name): + from . import readers, _adapters + + def _zip_reader(spec): + with suppress(AttributeError): + return readers.ZipReader(spec.loader, spec.name) + + def _namespace_reader(spec): + with suppress(AttributeError, ValueError): + return readers.NamespaceReader(spec.submodule_search_locations) + + def _available_reader(spec): + with suppress(AttributeError): + return spec.loader.get_resource_reader(spec.name) + + def _native_reader(spec): + reader = _available_reader(spec) + return reader if hasattr(reader, 'files') else None + + def _file_reader(spec): + try: + path = pathlib.Path(self.path) + except TypeError: + return None + if path.exists(): + return readers.FileReader(self) + + return ( + # native reader if it supplies 'files' + _native_reader(self.spec) + or + # local ZipReader if a zip module + _zip_reader(self.spec) + or + # local NamespaceReader if a namespace module + _namespace_reader(self.spec) + or + # local FileReader + _file_reader(self.spec) + # fallback - adapt the spec ResourceReader to TraversableReader + or _adapters.CompatibilityFiles(self.spec) + ) + + +def wrap_spec(package): + """ + Construct a package spec with traversable compatibility + on the spec/loader/reader. + + Supersedes _adapters.wrap_spec to use TraversableResourcesLoader + from above for older Python compatibility (<3.10). + """ + from . import _adapters + + return _adapters.SpecLoaderAdapter(package.__spec__, TraversableResourcesLoader) diff --git a/setuptools/_vendor/importlib_resources/_itertools.py b/setuptools/_vendor/importlib_resources/_itertools.py new file mode 100644 index 00000000..cce05582 --- /dev/null +++ b/setuptools/_vendor/importlib_resources/_itertools.py @@ -0,0 +1,35 @@ +from itertools import filterfalse + +from typing import ( + Callable, + Iterable, + Iterator, + Optional, + Set, + TypeVar, + Union, +) + +# Type and type variable definitions +_T = TypeVar('_T') +_U = TypeVar('_U') + + +def unique_everseen( + iterable: Iterable[_T], key: Optional[Callable[[_T], _U]] = None +) -> Iterator[_T]: + "List unique elements, preserving order. Remember all elements ever seen." + # unique_everseen('AAAABBBCCDAABBB') --> A B C D + # unique_everseen('ABBCcAD', str.lower) --> A B C D + seen: Set[Union[_T, _U]] = set() + seen_add = seen.add + if key is None: + for element in filterfalse(seen.__contains__, iterable): + seen_add(element) + yield element + else: + for element in iterable: + k = key(element) + if k not in seen: + seen_add(k) + yield element diff --git a/setuptools/_vendor/importlib_resources/_legacy.py b/setuptools/_vendor/importlib_resources/_legacy.py new file mode 100644 index 00000000..1d5d3f1f --- /dev/null +++ b/setuptools/_vendor/importlib_resources/_legacy.py @@ -0,0 +1,121 @@ +import functools +import os +import pathlib +import types +import warnings + +from typing import Union, Iterable, ContextManager, BinaryIO, TextIO, Any + +from . import _common + +Package = Union[types.ModuleType, str] +Resource = str + + +def deprecated(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + warnings.warn( + f"{func.__name__} is deprecated. Use files() instead. " + "Refer to https://importlib-resources.readthedocs.io" + "/en/latest/using.html#migrating-from-legacy for migration advice.", + DeprecationWarning, + stacklevel=2, + ) + return func(*args, **kwargs) + + return wrapper + + +def normalize_path(path): + # type: (Any) -> str + """Normalize a path by ensuring it is a string. + + If the resulting string contains path separators, an exception is raised. + """ + str_path = str(path) + parent, file_name = os.path.split(str_path) + if parent: + raise ValueError(f'{path!r} must be only a file name') + return file_name + + +@deprecated +def open_binary(package: Package, resource: Resource) -> BinaryIO: + """Return a file-like object opened for binary reading of the resource.""" + return (_common.files(package) / normalize_path(resource)).open('rb') + + +@deprecated +def read_binary(package: Package, resource: Resource) -> bytes: + """Return the binary contents of the resource.""" + return (_common.files(package) / normalize_path(resource)).read_bytes() + + +@deprecated +def open_text( + package: Package, + resource: Resource, + encoding: str = 'utf-8', + errors: str = 'strict', +) -> TextIO: + """Return a file-like object opened for text reading of the resource.""" + return (_common.files(package) / normalize_path(resource)).open( + 'r', encoding=encoding, errors=errors + ) + + +@deprecated +def read_text( + package: Package, + resource: Resource, + encoding: str = 'utf-8', + errors: str = 'strict', +) -> str: + """Return the decoded string of the resource. + + The decoding-related arguments have the same semantics as those of + bytes.decode(). + """ + with open_text(package, resource, encoding, errors) as fp: + return fp.read() + + +@deprecated +def contents(package: Package) -> Iterable[str]: + """Return an iterable of entries in `package`. + + Note that not all entries are resources. Specifically, directories are + not considered resources. Use `is_resource()` on each entry returned here + to check if it is a resource or not. + """ + return [path.name for path in _common.files(package).iterdir()] + + +@deprecated +def is_resource(package: Package, name: str) -> bool: + """True if `name` is a resource inside `package`. + + Directories are *not* resources. + """ + resource = normalize_path(name) + return any( + traversable.name == resource and traversable.is_file() + for traversable in _common.files(package).iterdir() + ) + + +@deprecated +def path( + package: Package, + resource: Resource, +) -> ContextManager[pathlib.Path]: + """A context manager providing a file path object to the resource. + + If the resource does not already exist on its own on the file system, + a temporary file will be created. If the file was created, the file + will be deleted upon exiting the context manager (no exception is + raised if the file was deleted prior to the context manager + exiting). + """ + return _common.as_file(_common.files(package) / normalize_path(resource)) diff --git a/setuptools/_vendor/importlib_resources/abc.py b/setuptools/_vendor/importlib_resources/abc.py new file mode 100644 index 00000000..d39dc1ad --- /dev/null +++ b/setuptools/_vendor/importlib_resources/abc.py @@ -0,0 +1,137 @@ +import abc +from typing import BinaryIO, Iterable, Text + +from ._compat import runtime_checkable, Protocol + + +class ResourceReader(metaclass=abc.ABCMeta): + """Abstract base class for loaders to provide resource reading support.""" + + @abc.abstractmethod + def open_resource(self, resource: Text) -> BinaryIO: + """Return an opened, file-like object for binary reading. + + The 'resource' argument is expected to represent only a file name. + If the resource cannot be found, FileNotFoundError is raised. + """ + # This deliberately raises FileNotFoundError instead of + # NotImplementedError so that if this method is accidentally called, + # it'll still do the right thing. + raise FileNotFoundError + + @abc.abstractmethod + def resource_path(self, resource: Text) -> Text: + """Return the file system path to the specified resource. + + The 'resource' argument is expected to represent only a file name. + If the resource does not exist on the file system, raise + FileNotFoundError. + """ + # This deliberately raises FileNotFoundError instead of + # NotImplementedError so that if this method is accidentally called, + # it'll still do the right thing. + raise FileNotFoundError + + @abc.abstractmethod + def is_resource(self, path: Text) -> bool: + """Return True if the named 'path' is a resource. + + Files are resources, directories are not. + """ + raise FileNotFoundError + + @abc.abstractmethod + def contents(self) -> Iterable[str]: + """Return an iterable of entries in `package`.""" + raise FileNotFoundError + + +@runtime_checkable +class Traversable(Protocol): + """ + An object with a subset of pathlib.Path methods suitable for + traversing directories and opening files. + """ + + @abc.abstractmethod + def iterdir(self): + """ + Yield Traversable objects in self + """ + + def read_bytes(self): + """ + Read contents of self as bytes + """ + with self.open('rb') as strm: + return strm.read() + + def read_text(self, encoding=None): + """ + Read contents of self as text + """ + with self.open(encoding=encoding) as strm: + return strm.read() + + @abc.abstractmethod + def is_dir(self) -> bool: + """ + Return True if self is a directory + """ + + @abc.abstractmethod + def is_file(self) -> bool: + """ + Return True if self is a file + """ + + @abc.abstractmethod + def joinpath(self, child): + """ + Return Traversable child in self + """ + + def __truediv__(self, child): + """ + Return Traversable child in self + """ + return self.joinpath(child) + + @abc.abstractmethod + def open(self, mode='r', *args, **kwargs): + """ + mode may be 'r' or 'rb' to open as text or binary. Return a handle + suitable for reading (same as pathlib.Path.open). + + When opening as text, accepts encoding parameters such as those + accepted by io.TextIOWrapper. + """ + + @abc.abstractproperty + def name(self) -> str: + """ + The base name of this object without any parent references. + """ + + +class TraversableResources(ResourceReader): + """ + The required interface for providing traversable + resources. + """ + + @abc.abstractmethod + def files(self): + """Return a Traversable object for the loaded package.""" + + def open_resource(self, resource): + return self.files().joinpath(resource).open('rb') + + def resource_path(self, resource): + raise FileNotFoundError(resource) + + def is_resource(self, path): + return self.files().joinpath(path).is_file() + + def contents(self): + return (item.name for item in self.files().iterdir()) diff --git a/setuptools/_vendor/importlib_resources/py.typed b/setuptools/_vendor/importlib_resources/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/setuptools/_vendor/importlib_resources/readers.py b/setuptools/_vendor/importlib_resources/readers.py new file mode 100644 index 00000000..f1190ca4 --- /dev/null +++ b/setuptools/_vendor/importlib_resources/readers.py @@ -0,0 +1,122 @@ +import collections +import pathlib +import operator + +from . import abc + +from ._itertools import unique_everseen +from ._compat import ZipPath + + +def remove_duplicates(items): + return iter(collections.OrderedDict.fromkeys(items)) + + +class FileReader(abc.TraversableResources): + def __init__(self, loader): + self.path = pathlib.Path(loader.path).parent + + def resource_path(self, resource): + """ + Return the file system path to prevent + `resources.path()` from creating a temporary + copy. + """ + return str(self.path.joinpath(resource)) + + def files(self): + return self.path + + +class ZipReader(abc.TraversableResources): + def __init__(self, loader, module): + _, _, name = module.rpartition('.') + self.prefix = loader.prefix.replace('\\', '/') + name + '/' + self.archive = loader.archive + + def open_resource(self, resource): + try: + return super().open_resource(resource) + except KeyError as exc: + raise FileNotFoundError(exc.args[0]) + + def is_resource(self, path): + # workaround for `zipfile.Path.is_file` returning true + # for non-existent paths. + target = self.files().joinpath(path) + return target.is_file() and target.exists() + + def files(self): + return ZipPath(self.archive, self.prefix) + + +class MultiplexedPath(abc.Traversable): + """ + Given a series of Traversable objects, implement a merged + version of the interface across all objects. Useful for + namespace packages which may be multihomed at a single + name. + """ + + def __init__(self, *paths): + self._paths = list(map(pathlib.Path, remove_duplicates(paths))) + if not self._paths: + message = 'MultiplexedPath must contain at least one path' + raise FileNotFoundError(message) + if not all(path.is_dir() for path in self._paths): + raise NotADirectoryError('MultiplexedPath only supports directories') + + def iterdir(self): + files = (file for path in self._paths for file in path.iterdir()) + return unique_everseen(files, key=operator.attrgetter('name')) + + def read_bytes(self): + raise FileNotFoundError(f'{self} is not a file') + + def read_text(self, *args, **kwargs): + raise FileNotFoundError(f'{self} is not a file') + + def is_dir(self): + return True + + def is_file(self): + return False + + def joinpath(self, child): + # first try to find child in current paths + for file in self.iterdir(): + if file.name == child: + return file + # if it does not exist, construct it with the first path + return self._paths[0] / child + + __truediv__ = joinpath + + def open(self, *args, **kwargs): + raise FileNotFoundError(f'{self} is not a file') + + @property + def name(self): + return self._paths[0].name + + def __repr__(self): + paths = ', '.join(f"'{path}'" for path in self._paths) + return f'MultiplexedPath({paths})' + + +class NamespaceReader(abc.TraversableResources): + def __init__(self, namespace_path): + if 'NamespacePath' not in str(namespace_path): + raise ValueError('Invalid path') + self.path = MultiplexedPath(*list(namespace_path)) + + def resource_path(self, resource): + """ + Return the file system path to prevent + `resources.path()` from creating a temporary + copy. + """ + return str(self.path.joinpath(resource)) + + def files(self): + return self.path diff --git a/setuptools/_vendor/importlib_resources/simple.py b/setuptools/_vendor/importlib_resources/simple.py new file mode 100644 index 00000000..da073cbd --- /dev/null +++ b/setuptools/_vendor/importlib_resources/simple.py @@ -0,0 +1,116 @@ +""" +Interface adapters for low-level readers. +""" + +import abc +import io +import itertools +from typing import BinaryIO, List + +from .abc import Traversable, TraversableResources + + +class SimpleReader(abc.ABC): + """ + The minimum, low-level interface required from a resource + provider. + """ + + @abc.abstractproperty + def package(self): + # type: () -> str + """ + The name of the package for which this reader loads resources. + """ + + @abc.abstractmethod + def children(self): + # type: () -> List['SimpleReader'] + """ + Obtain an iterable of SimpleReader for available + child containers (e.g. directories). + """ + + @abc.abstractmethod + def resources(self): + # type: () -> List[str] + """ + Obtain available named resources for this virtual package. + """ + + @abc.abstractmethod + def open_binary(self, resource): + # type: (str) -> BinaryIO + """ + Obtain a File-like for a named resource. + """ + + @property + def name(self): + return self.package.split('.')[-1] + + +class ResourceHandle(Traversable): + """ + Handle to a named resource in a ResourceReader. + """ + + def __init__(self, parent, name): + # type: (ResourceContainer, str) -> None + self.parent = parent + self.name = name # type: ignore + + def is_file(self): + return True + + def is_dir(self): + return False + + def open(self, mode='r', *args, **kwargs): + stream = self.parent.reader.open_binary(self.name) + if 'b' not in mode: + stream = io.TextIOWrapper(*args, **kwargs) + return stream + + def joinpath(self, name): + raise RuntimeError("Cannot traverse into a resource") + + +class ResourceContainer(Traversable): + """ + Traversable container for a package's resources via its reader. + """ + + def __init__(self, reader): + # type: (SimpleReader) -> None + self.reader = reader + + def is_dir(self): + return True + + def is_file(self): + return False + + def iterdir(self): + files = (ResourceHandle(self, name) for name in self.reader.resources) + dirs = map(ResourceContainer, self.reader.children()) + return itertools.chain(files, dirs) + + def open(self, *args, **kwargs): + raise IsADirectoryError() + + def joinpath(self, name): + return next( + traversable for traversable in self.iterdir() if traversable.name == name + ) + + +class TraversableReader(TraversableResources, SimpleReader): + """ + A TraversableResources based on SimpleReader. Resource providers + may derive from this class to provide the TraversableResources + interface by supplying the SimpleReader interface. + """ + + def files(self): + return ResourceContainer(self) diff --git a/setuptools/_vendor/importlib_resources/tests/__init__.py b/setuptools/_vendor/importlib_resources/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/setuptools/_vendor/importlib_resources/tests/_compat.py b/setuptools/_vendor/importlib_resources/tests/_compat.py new file mode 100644 index 00000000..4c99cffd --- /dev/null +++ b/setuptools/_vendor/importlib_resources/tests/_compat.py @@ -0,0 +1,19 @@ +import os + + +try: + from test.support import import_helper # type: ignore +except ImportError: + # Python 3.9 and earlier + class import_helper: # type: ignore + from test.support import modules_setup, modules_cleanup + + +try: + # Python 3.10 + from test.support.os_helper import unlink +except ImportError: + from test.support import unlink as _unlink + + def unlink(target): + return _unlink(os.fspath(target)) diff --git a/setuptools/_vendor/importlib_resources/tests/data01/__init__.py b/setuptools/_vendor/importlib_resources/tests/data01/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/setuptools/_vendor/importlib_resources/tests/data01/binary.file b/setuptools/_vendor/importlib_resources/tests/data01/binary.file new file mode 100644 index 00000000..eaf36c1d Binary files /dev/null and b/setuptools/_vendor/importlib_resources/tests/data01/binary.file differ diff --git a/setuptools/_vendor/importlib_resources/tests/data01/subdirectory/__init__.py b/setuptools/_vendor/importlib_resources/tests/data01/subdirectory/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/setuptools/_vendor/importlib_resources/tests/data01/subdirectory/binary.file b/setuptools/_vendor/importlib_resources/tests/data01/subdirectory/binary.file new file mode 100644 index 00000000..eaf36c1d Binary files /dev/null and b/setuptools/_vendor/importlib_resources/tests/data01/subdirectory/binary.file differ diff --git a/setuptools/_vendor/importlib_resources/tests/data01/utf-16.file b/setuptools/_vendor/importlib_resources/tests/data01/utf-16.file new file mode 100644 index 00000000..2cb77229 Binary files /dev/null and b/setuptools/_vendor/importlib_resources/tests/data01/utf-16.file differ diff --git a/setuptools/_vendor/importlib_resources/tests/data01/utf-8.file b/setuptools/_vendor/importlib_resources/tests/data01/utf-8.file new file mode 100644 index 00000000..1c0132ad --- /dev/null +++ b/setuptools/_vendor/importlib_resources/tests/data01/utf-8.file @@ -0,0 +1 @@ +Hello, UTF-8 world! diff --git a/setuptools/_vendor/importlib_resources/tests/data02/__init__.py b/setuptools/_vendor/importlib_resources/tests/data02/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/setuptools/_vendor/importlib_resources/tests/data02/one/__init__.py b/setuptools/_vendor/importlib_resources/tests/data02/one/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/setuptools/_vendor/importlib_resources/tests/data02/one/resource1.txt b/setuptools/_vendor/importlib_resources/tests/data02/one/resource1.txt new file mode 100644 index 00000000..61a813e4 --- /dev/null +++ b/setuptools/_vendor/importlib_resources/tests/data02/one/resource1.txt @@ -0,0 +1 @@ +one resource diff --git a/setuptools/_vendor/importlib_resources/tests/data02/two/__init__.py b/setuptools/_vendor/importlib_resources/tests/data02/two/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/setuptools/_vendor/importlib_resources/tests/data02/two/resource2.txt b/setuptools/_vendor/importlib_resources/tests/data02/two/resource2.txt new file mode 100644 index 00000000..a80ce46e --- /dev/null +++ b/setuptools/_vendor/importlib_resources/tests/data02/two/resource2.txt @@ -0,0 +1 @@ +two resource diff --git a/setuptools/_vendor/importlib_resources/tests/namespacedata01/binary.file b/setuptools/_vendor/importlib_resources/tests/namespacedata01/binary.file new file mode 100644 index 00000000..eaf36c1d Binary files /dev/null and b/setuptools/_vendor/importlib_resources/tests/namespacedata01/binary.file differ diff --git a/setuptools/_vendor/importlib_resources/tests/namespacedata01/utf-16.file b/setuptools/_vendor/importlib_resources/tests/namespacedata01/utf-16.file new file mode 100644 index 00000000..2cb77229 Binary files /dev/null and b/setuptools/_vendor/importlib_resources/tests/namespacedata01/utf-16.file differ diff --git a/setuptools/_vendor/importlib_resources/tests/namespacedata01/utf-8.file b/setuptools/_vendor/importlib_resources/tests/namespacedata01/utf-8.file new file mode 100644 index 00000000..1c0132ad --- /dev/null +++ b/setuptools/_vendor/importlib_resources/tests/namespacedata01/utf-8.file @@ -0,0 +1 @@ +Hello, UTF-8 world! diff --git a/setuptools/_vendor/importlib_resources/tests/test_compatibilty_files.py b/setuptools/_vendor/importlib_resources/tests/test_compatibilty_files.py new file mode 100644 index 00000000..d92c7c56 --- /dev/null +++ b/setuptools/_vendor/importlib_resources/tests/test_compatibilty_files.py @@ -0,0 +1,102 @@ +import io +import unittest + +import importlib_resources as resources + +from importlib_resources._adapters import ( + CompatibilityFiles, + wrap_spec, +) + +from . import util + + +class CompatibilityFilesTests(unittest.TestCase): + @property + def package(self): + bytes_data = io.BytesIO(b'Hello, world!') + return util.create_package( + file=bytes_data, + path='some_path', + contents=('a', 'b', 'c'), + ) + + @property + def files(self): + return resources.files(self.package) + + def test_spec_path_iter(self): + self.assertEqual( + sorted(path.name for path in self.files.iterdir()), + ['a', 'b', 'c'], + ) + + def test_child_path_iter(self): + self.assertEqual(list((self.files / 'a').iterdir()), []) + + def test_orphan_path_iter(self): + self.assertEqual(list((self.files / 'a' / 'a').iterdir()), []) + self.assertEqual(list((self.files / 'a' / 'a' / 'a').iterdir()), []) + + def test_spec_path_is(self): + self.assertFalse(self.files.is_file()) + self.assertFalse(self.files.is_dir()) + + def test_child_path_is(self): + self.assertTrue((self.files / 'a').is_file()) + self.assertFalse((self.files / 'a').is_dir()) + + def test_orphan_path_is(self): + self.assertFalse((self.files / 'a' / 'a').is_file()) + self.assertFalse((self.files / 'a' / 'a').is_dir()) + self.assertFalse((self.files / 'a' / 'a' / 'a').is_file()) + self.assertFalse((self.files / 'a' / 'a' / 'a').is_dir()) + + def test_spec_path_name(self): + self.assertEqual(self.files.name, 'testingpackage') + + def test_child_path_name(self): + self.assertEqual((self.files / 'a').name, 'a') + + def test_orphan_path_name(self): + self.assertEqual((self.files / 'a' / 'b').name, 'b') + self.assertEqual((self.files / 'a' / 'b' / 'c').name, 'c') + + def test_spec_path_open(self): + self.assertEqual(self.files.read_bytes(), b'Hello, world!') + self.assertEqual(self.files.read_text(), 'Hello, world!') + + def test_child_path_open(self): + self.assertEqual((self.files / 'a').read_bytes(), b'Hello, world!') + self.assertEqual((self.files / 'a').read_text(), 'Hello, world!') + + def test_orphan_path_open(self): + with self.assertRaises(FileNotFoundError): + (self.files / 'a' / 'b').read_bytes() + with self.assertRaises(FileNotFoundError): + (self.files / 'a' / 'b' / 'c').read_bytes() + + def test_open_invalid_mode(self): + with self.assertRaises(ValueError): + self.files.open('0') + + def test_orphan_path_invalid(self): + with self.assertRaises(ValueError): + CompatibilityFiles.OrphanPath() + + def test_wrap_spec(self): + spec = wrap_spec(self.package) + self.assertIsInstance(spec.loader.get_resource_reader(None), CompatibilityFiles) + + +class CompatibilityFilesNoReaderTests(unittest.TestCase): + @property + def package(self): + return util.create_package_from_loader(None) + + @property + def files(self): + return resources.files(self.package) + + def test_spec_path_joinpath(self): + self.assertIsInstance(self.files / 'a', CompatibilityFiles.OrphanPath) diff --git a/setuptools/_vendor/importlib_resources/tests/test_contents.py b/setuptools/_vendor/importlib_resources/tests/test_contents.py new file mode 100644 index 00000000..525568e8 --- /dev/null +++ b/setuptools/_vendor/importlib_resources/tests/test_contents.py @@ -0,0 +1,43 @@ +import unittest +import importlib_resources as resources + +from . import data01 +from . import util + + +class ContentsTests: + expected = { + '__init__.py', + 'binary.file', + 'subdirectory', + 'utf-16.file', + 'utf-8.file', + } + + def test_contents(self): + contents = {path.name for path in resources.files(self.data).iterdir()} + assert self.expected <= contents + + +class ContentsDiskTests(ContentsTests, unittest.TestCase): + def setUp(self): + self.data = data01 + + +class ContentsZipTests(ContentsTests, util.ZipSetup, unittest.TestCase): + pass + + +class ContentsNamespaceTests(ContentsTests, unittest.TestCase): + expected = { + # no __init__ because of namespace design + # no subdirectory as incidental difference in fixture + 'binary.file', + 'utf-16.file', + 'utf-8.file', + } + + def setUp(self): + from . import namespacedata01 + + self.data = namespacedata01 diff --git a/setuptools/_vendor/importlib_resources/tests/test_files.py b/setuptools/_vendor/importlib_resources/tests/test_files.py new file mode 100644 index 00000000..2676b49e --- /dev/null +++ b/setuptools/_vendor/importlib_resources/tests/test_files.py @@ -0,0 +1,46 @@ +import typing +import unittest + +import importlib_resources as resources +from importlib_resources.abc import Traversable +from . import data01 +from . import util + + +class FilesTests: + def test_read_bytes(self): + files = resources.files(self.data) + actual = files.joinpath('utf-8.file').read_bytes() + assert actual == b'Hello, UTF-8 world!\n' + + def test_read_text(self): + files = resources.files(self.data) + actual = files.joinpath('utf-8.file').read_text(encoding='utf-8') + assert actual == 'Hello, UTF-8 world!\n' + + @unittest.skipUnless( + hasattr(typing, 'runtime_checkable'), + "Only suitable when typing supports runtime_checkable", + ) + def test_traversable(self): + assert isinstance(resources.files(self.data), Traversable) + + +class OpenDiskTests(FilesTests, unittest.TestCase): + def setUp(self): + self.data = data01 + + +class OpenZipTests(FilesTests, util.ZipSetup, unittest.TestCase): + pass + + +class OpenNamespaceTests(FilesTests, unittest.TestCase): + def setUp(self): + from . import namespacedata01 + + self.data = namespacedata01 + + +if __name__ == '__main__': + unittest.main() diff --git a/setuptools/_vendor/importlib_resources/tests/test_open.py b/setuptools/_vendor/importlib_resources/tests/test_open.py new file mode 100644 index 00000000..87b42c3d --- /dev/null +++ b/setuptools/_vendor/importlib_resources/tests/test_open.py @@ -0,0 +1,81 @@ +import unittest + +import importlib_resources as resources +from . import data01 +from . import util + + +class CommonBinaryTests(util.CommonTests, unittest.TestCase): + def execute(self, package, path): + target = resources.files(package).joinpath(path) + with target.open('rb'): + pass + + +class CommonTextTests(util.CommonTests, unittest.TestCase): + def execute(self, package, path): + target = resources.files(package).joinpath(path) + with target.open(): + pass + + +class OpenTests: + def test_open_binary(self): + target = resources.files(self.data) / 'binary.file' + with target.open('rb') as fp: + result = fp.read() + self.assertEqual(result, b'\x00\x01\x02\x03') + + def test_open_text_default_encoding(self): + target = resources.files(self.data) / 'utf-8.file' + with target.open() as fp: + result = fp.read() + self.assertEqual(result, 'Hello, UTF-8 world!\n') + + def test_open_text_given_encoding(self): + target = resources.files(self.data) / 'utf-16.file' + with target.open(encoding='utf-16', errors='strict') as fp: + result = fp.read() + self.assertEqual(result, 'Hello, UTF-16 world!\n') + + def test_open_text_with_errors(self): + # Raises UnicodeError without the 'errors' argument. + target = resources.files(self.data) / 'utf-16.file' + with target.open(encoding='utf-8', errors='strict') as fp: + self.assertRaises(UnicodeError, fp.read) + with target.open(encoding='utf-8', errors='ignore') as fp: + result = fp.read() + self.assertEqual( + result, + 'H\x00e\x00l\x00l\x00o\x00,\x00 ' + '\x00U\x00T\x00F\x00-\x001\x006\x00 ' + '\x00w\x00o\x00r\x00l\x00d\x00!\x00\n\x00', + ) + + def test_open_binary_FileNotFoundError(self): + target = resources.files(self.data) / 'does-not-exist' + self.assertRaises(FileNotFoundError, target.open, 'rb') + + def test_open_text_FileNotFoundError(self): + target = resources.files(self.data) / 'does-not-exist' + self.assertRaises(FileNotFoundError, target.open) + + +class OpenDiskTests(OpenTests, unittest.TestCase): + def setUp(self): + self.data = data01 + + +class OpenDiskNamespaceTests(OpenTests, unittest.TestCase): + def setUp(self): + from . import namespacedata01 + + self.data = namespacedata01 + + +class OpenZipTests(OpenTests, util.ZipSetup, unittest.TestCase): + pass + + +if __name__ == '__main__': + unittest.main() diff --git a/setuptools/_vendor/importlib_resources/tests/test_path.py b/setuptools/_vendor/importlib_resources/tests/test_path.py new file mode 100644 index 00000000..4f4d3943 --- /dev/null +++ b/setuptools/_vendor/importlib_resources/tests/test_path.py @@ -0,0 +1,64 @@ +import io +import unittest + +import importlib_resources as resources +from . import data01 +from . import util + + +class CommonTests(util.CommonTests, unittest.TestCase): + def execute(self, package, path): + with resources.as_file(resources.files(package).joinpath(path)): + pass + + +class PathTests: + def test_reading(self): + # Path should be readable. + # Test also implicitly verifies the returned object is a pathlib.Path + # instance. + target = resources.files(self.data) / 'utf-8.file' + with resources.as_file(target) as path: + self.assertTrue(path.name.endswith("utf-8.file"), repr(path)) + # pathlib.Path.read_text() was introduced in Python 3.5. + with path.open('r', encoding='utf-8') as file: + text = file.read() + self.assertEqual('Hello, UTF-8 world!\n', text) + + +class PathDiskTests(PathTests, unittest.TestCase): + data = data01 + + def test_natural_path(self): + """ + Guarantee the internal implementation detail that + file-system-backed resources do not get the tempdir + treatment. + """ + target = resources.files(self.data) / 'utf-8.file' + with resources.as_file(target) as path: + assert 'data' in str(path) + + +class PathMemoryTests(PathTests, unittest.TestCase): + def setUp(self): + file = io.BytesIO(b'Hello, UTF-8 world!\n') + self.addCleanup(file.close) + self.data = util.create_package( + file=file, path=FileNotFoundError("package exists only in memory") + ) + self.data.__spec__.origin = None + self.data.__spec__.has_location = False + + +class PathZipTests(PathTests, util.ZipSetup, unittest.TestCase): + def test_remove_in_context_manager(self): + # It is not an error if the file that was temporarily stashed on the + # file system is removed inside the `with` stanza. + target = resources.files(self.data) / 'utf-8.file' + with resources.as_file(target) as path: + path.unlink() + + +if __name__ == '__main__': + unittest.main() diff --git a/setuptools/_vendor/importlib_resources/tests/test_read.py b/setuptools/_vendor/importlib_resources/tests/test_read.py new file mode 100644 index 00000000..41dd6db5 --- /dev/null +++ b/setuptools/_vendor/importlib_resources/tests/test_read.py @@ -0,0 +1,76 @@ +import unittest +import importlib_resources as resources + +from . import data01 +from . import util +from importlib import import_module + + +class CommonBinaryTests(util.CommonTests, unittest.TestCase): + def execute(self, package, path): + resources.files(package).joinpath(path).read_bytes() + + +class CommonTextTests(util.CommonTests, unittest.TestCase): + def execute(self, package, path): + resources.files(package).joinpath(path).read_text() + + +class ReadTests: + def test_read_bytes(self): + result = resources.files(self.data).joinpath('binary.file').read_bytes() + self.assertEqual(result, b'\0\1\2\3') + + def test_read_text_default_encoding(self): + result = resources.files(self.data).joinpath('utf-8.file').read_text() + self.assertEqual(result, 'Hello, UTF-8 world!\n') + + def test_read_text_given_encoding(self): + result = ( + resources.files(self.data) + .joinpath('utf-16.file') + .read_text(encoding='utf-16') + ) + self.assertEqual(result, 'Hello, UTF-16 world!\n') + + def test_read_text_with_errors(self): + # Raises UnicodeError without the 'errors' argument. + target = resources.files(self.data) / 'utf-16.file' + self.assertRaises(UnicodeError, target.read_text, encoding='utf-8') + result = target.read_text(encoding='utf-8', errors='ignore') + self.assertEqual( + result, + 'H\x00e\x00l\x00l\x00o\x00,\x00 ' + '\x00U\x00T\x00F\x00-\x001\x006\x00 ' + '\x00w\x00o\x00r\x00l\x00d\x00!\x00\n\x00', + ) + + +class ReadDiskTests(ReadTests, unittest.TestCase): + data = data01 + + +class ReadZipTests(ReadTests, util.ZipSetup, unittest.TestCase): + def test_read_submodule_resource(self): + submodule = import_module('ziptestdata.subdirectory') + result = resources.files(submodule).joinpath('binary.file').read_bytes() + self.assertEqual(result, b'\0\1\2\3') + + def test_read_submodule_resource_by_name(self): + result = ( + resources.files('ziptestdata.subdirectory') + .joinpath('binary.file') + .read_bytes() + ) + self.assertEqual(result, b'\0\1\2\3') + + +class ReadNamespaceTests(ReadTests, unittest.TestCase): + def setUp(self): + from . import namespacedata01 + + self.data = namespacedata01 + + +if __name__ == '__main__': + unittest.main() diff --git a/setuptools/_vendor/importlib_resources/tests/test_reader.py b/setuptools/_vendor/importlib_resources/tests/test_reader.py new file mode 100644 index 00000000..16841a50 --- /dev/null +++ b/setuptools/_vendor/importlib_resources/tests/test_reader.py @@ -0,0 +1,128 @@ +import os.path +import sys +import pathlib +import unittest + +from importlib import import_module +from importlib_resources.readers import MultiplexedPath, NamespaceReader + + +class MultiplexedPathTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + path = pathlib.Path(__file__).parent / 'namespacedata01' + cls.folder = str(path) + + def test_init_no_paths(self): + with self.assertRaises(FileNotFoundError): + MultiplexedPath() + + def test_init_file(self): + with self.assertRaises(NotADirectoryError): + MultiplexedPath(os.path.join(self.folder, 'binary.file')) + + def test_iterdir(self): + contents = {path.name for path in MultiplexedPath(self.folder).iterdir()} + try: + contents.remove('__pycache__') + except (KeyError, ValueError): + pass + self.assertEqual(contents, {'binary.file', 'utf-16.file', 'utf-8.file'}) + + def test_iterdir_duplicate(self): + data01 = os.path.abspath(os.path.join(__file__, '..', 'data01')) + contents = { + path.name for path in MultiplexedPath(self.folder, data01).iterdir() + } + for remove in ('__pycache__', '__init__.pyc'): + try: + contents.remove(remove) + except (KeyError, ValueError): + pass + self.assertEqual( + contents, + {'__init__.py', 'binary.file', 'subdirectory', 'utf-16.file', 'utf-8.file'}, + ) + + def test_is_dir(self): + self.assertEqual(MultiplexedPath(self.folder).is_dir(), True) + + def test_is_file(self): + self.assertEqual(MultiplexedPath(self.folder).is_file(), False) + + def test_open_file(self): + path = MultiplexedPath(self.folder) + with self.assertRaises(FileNotFoundError): + path.read_bytes() + with self.assertRaises(FileNotFoundError): + path.read_text() + with self.assertRaises(FileNotFoundError): + path.open() + + def test_join_path(self): + prefix = os.path.abspath(os.path.join(__file__, '..')) + data01 = os.path.join(prefix, 'data01') + path = MultiplexedPath(self.folder, data01) + self.assertEqual( + str(path.joinpath('binary.file'))[len(prefix) + 1 :], + os.path.join('namespacedata01', 'binary.file'), + ) + self.assertEqual( + str(path.joinpath('subdirectory'))[len(prefix) + 1 :], + os.path.join('data01', 'subdirectory'), + ) + self.assertEqual( + str(path.joinpath('imaginary'))[len(prefix) + 1 :], + os.path.join('namespacedata01', 'imaginary'), + ) + + def test_repr(self): + self.assertEqual( + repr(MultiplexedPath(self.folder)), + f"MultiplexedPath('{self.folder}')", + ) + + def test_name(self): + self.assertEqual( + MultiplexedPath(self.folder).name, + os.path.basename(self.folder), + ) + + +class NamespaceReaderTest(unittest.TestCase): + site_dir = str(pathlib.Path(__file__).parent) + + @classmethod + def setUpClass(cls): + sys.path.append(cls.site_dir) + + @classmethod + def tearDownClass(cls): + sys.path.remove(cls.site_dir) + + def test_init_error(self): + with self.assertRaises(ValueError): + NamespaceReader(['path1', 'path2']) + + def test_resource_path(self): + namespacedata01 = import_module('namespacedata01') + reader = NamespaceReader(namespacedata01.__spec__.submodule_search_locations) + + root = os.path.abspath(os.path.join(__file__, '..', 'namespacedata01')) + self.assertEqual( + reader.resource_path('binary.file'), os.path.join(root, 'binary.file') + ) + self.assertEqual( + reader.resource_path('imaginary'), os.path.join(root, 'imaginary') + ) + + def test_files(self): + namespacedata01 = import_module('namespacedata01') + reader = NamespaceReader(namespacedata01.__spec__.submodule_search_locations) + root = os.path.abspath(os.path.join(__file__, '..', 'namespacedata01')) + self.assertIsInstance(reader.files(), MultiplexedPath) + self.assertEqual(repr(reader.files()), f"MultiplexedPath('{root}')") + + +if __name__ == '__main__': + unittest.main() diff --git a/setuptools/_vendor/importlib_resources/tests/test_resource.py b/setuptools/_vendor/importlib_resources/tests/test_resource.py new file mode 100644 index 00000000..5affd8b0 --- /dev/null +++ b/setuptools/_vendor/importlib_resources/tests/test_resource.py @@ -0,0 +1,252 @@ +import sys +import unittest +import importlib_resources as resources +import uuid +import pathlib + +from . import data01 +from . import zipdata01, zipdata02 +from . import util +from importlib import import_module +from ._compat import import_helper, unlink + + +class ResourceTests: + # Subclasses are expected to set the `data` attribute. + + def test_is_file_exists(self): + target = resources.files(self.data) / 'binary.file' + self.assertTrue(target.is_file()) + + def test_is_file_missing(self): + target = resources.files(self.data) / 'not-a-file' + self.assertFalse(target.is_file()) + + def test_is_dir(self): + target = resources.files(self.data) / 'subdirectory' + self.assertFalse(target.is_file()) + self.assertTrue(target.is_dir()) + + +class ResourceDiskTests(ResourceTests, unittest.TestCase): + def setUp(self): + self.data = data01 + + +class ResourceZipTests(ResourceTests, util.ZipSetup, unittest.TestCase): + pass + + +def names(traversable): + return {item.name for item in traversable.iterdir()} + + +class ResourceLoaderTests(unittest.TestCase): + def test_resource_contents(self): + package = util.create_package( + file=data01, path=data01.__file__, contents=['A', 'B', 'C'] + ) + self.assertEqual(names(resources.files(package)), {'A', 'B', 'C'}) + + def test_is_file(self): + package = util.create_package( + file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F'] + ) + self.assertTrue(resources.files(package).joinpath('B').is_file()) + + def test_is_dir(self): + package = util.create_package( + file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F'] + ) + self.assertTrue(resources.files(package).joinpath('D').is_dir()) + + def test_resource_missing(self): + package = util.create_package( + file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F'] + ) + self.assertFalse(resources.files(package).joinpath('Z').is_file()) + + +class ResourceCornerCaseTests(unittest.TestCase): + def test_package_has_no_reader_fallback(self): + # Test odd ball packages which: + # 1. Do not have a ResourceReader as a loader + # 2. Are not on the file system + # 3. Are not in a zip file + module = util.create_package( + file=data01, path=data01.__file__, contents=['A', 'B', 'C'] + ) + # Give the module a dummy loader. + module.__loader__ = object() + # Give the module a dummy origin. + module.__file__ = '/path/which/shall/not/be/named' + module.__spec__.loader = module.__loader__ + module.__spec__.origin = module.__file__ + self.assertFalse(resources.files(module).joinpath('A').is_file()) + + +class ResourceFromZipsTest01(util.ZipSetupBase, unittest.TestCase): + ZIP_MODULE = zipdata01 # type: ignore + + def test_is_submodule_resource(self): + submodule = import_module('ziptestdata.subdirectory') + self.assertTrue(resources.files(submodule).joinpath('binary.file').is_file()) + + def test_read_submodule_resource_by_name(self): + self.assertTrue( + resources.files('ziptestdata.subdirectory') + .joinpath('binary.file') + .is_file() + ) + + def test_submodule_contents(self): + submodule = import_module('ziptestdata.subdirectory') + self.assertEqual( + names(resources.files(submodule)), {'__init__.py', 'binary.file'} + ) + + def test_submodule_contents_by_name(self): + self.assertEqual( + names(resources.files('ziptestdata.subdirectory')), + {'__init__.py', 'binary.file'}, + ) + + +class ResourceFromZipsTest02(util.ZipSetupBase, unittest.TestCase): + ZIP_MODULE = zipdata02 # type: ignore + + def test_unrelated_contents(self): + """ + Test thata zip with two unrelated subpackages return + distinct resources. Ref python/importlib_resources#44. + """ + self.assertEqual( + names(resources.files('ziptestdata.one')), + {'__init__.py', 'resource1.txt'}, + ) + self.assertEqual( + names(resources.files('ziptestdata.two')), + {'__init__.py', 'resource2.txt'}, + ) + + +class DeletingZipsTest(unittest.TestCase): + """Having accessed resources in a zip file should not keep an open + reference to the zip. + """ + + ZIP_MODULE = zipdata01 + + def setUp(self): + modules = import_helper.modules_setup() + self.addCleanup(import_helper.modules_cleanup, *modules) + + data_path = pathlib.Path(self.ZIP_MODULE.__file__) + data_dir = data_path.parent + self.source_zip_path = data_dir / 'ziptestdata.zip' + self.zip_path = pathlib.Path(f'{uuid.uuid4()}.zip').absolute() + self.zip_path.write_bytes(self.source_zip_path.read_bytes()) + sys.path.append(str(self.zip_path)) + self.data = import_module('ziptestdata') + + def tearDown(self): + try: + sys.path.remove(str(self.zip_path)) + except ValueError: + pass + + try: + del sys.path_importer_cache[str(self.zip_path)] + del sys.modules[self.data.__name__] + except KeyError: + pass + + try: + unlink(self.zip_path) + except OSError: + # If the test fails, this will probably fail too + pass + + def test_iterdir_does_not_keep_open(self): + c = [item.name for item in resources.files('ziptestdata').iterdir()] + self.zip_path.unlink() + del c + + def test_is_file_does_not_keep_open(self): + c = resources.files('ziptestdata').joinpath('binary.file').is_file() + self.zip_path.unlink() + del c + + def test_is_file_failure_does_not_keep_open(self): + c = resources.files('ziptestdata').joinpath('not-present').is_file() + self.zip_path.unlink() + del c + + @unittest.skip("Desired but not supported.") + def test_as_file_does_not_keep_open(self): # pragma: no cover + c = resources.as_file(resources.files('ziptestdata') / 'binary.file') + self.zip_path.unlink() + del c + + def test_entered_path_does_not_keep_open(self): + # This is what certifi does on import to make its bundle + # available for the process duration. + c = resources.as_file( + resources.files('ziptestdata') / 'binary.file' + ).__enter__() + self.zip_path.unlink() + del c + + def test_read_binary_does_not_keep_open(self): + c = resources.files('ziptestdata').joinpath('binary.file').read_bytes() + self.zip_path.unlink() + del c + + def test_read_text_does_not_keep_open(self): + c = resources.files('ziptestdata').joinpath('utf-8.file').read_text() + self.zip_path.unlink() + del c + + +class ResourceFromNamespaceTest01(unittest.TestCase): + site_dir = str(pathlib.Path(__file__).parent) + + @classmethod + def setUpClass(cls): + sys.path.append(cls.site_dir) + + @classmethod + def tearDownClass(cls): + sys.path.remove(cls.site_dir) + + def test_is_submodule_resource(self): + self.assertTrue( + resources.files(import_module('namespacedata01')) + .joinpath('binary.file') + .is_file() + ) + + def test_read_submodule_resource_by_name(self): + self.assertTrue( + resources.files('namespacedata01').joinpath('binary.file').is_file() + ) + + def test_submodule_contents(self): + contents = names(resources.files(import_module('namespacedata01'))) + try: + contents.remove('__pycache__') + except KeyError: + pass + self.assertEqual(contents, {'binary.file', 'utf-8.file', 'utf-16.file'}) + + def test_submodule_contents_by_name(self): + contents = names(resources.files('namespacedata01')) + try: + contents.remove('__pycache__') + except KeyError: + pass + self.assertEqual(contents, {'binary.file', 'utf-8.file', 'utf-16.file'}) + + +if __name__ == '__main__': + unittest.main() diff --git a/setuptools/_vendor/importlib_resources/tests/update-zips.py b/setuptools/_vendor/importlib_resources/tests/update-zips.py new file mode 100644 index 00000000..9ef0224c --- /dev/null +++ b/setuptools/_vendor/importlib_resources/tests/update-zips.py @@ -0,0 +1,53 @@ +""" +Generate the zip test data files. + +Run to build the tests/zipdataNN/ziptestdata.zip files from +files in tests/dataNN. + +Replaces the file with the working copy, but does commit anything +to the source repo. +""" + +import contextlib +import os +import pathlib +import zipfile + + +def main(): + """ + >>> from unittest import mock + >>> monkeypatch = getfixture('monkeypatch') + >>> monkeypatch.setattr(zipfile, 'ZipFile', mock.MagicMock()) + >>> print(); main() # print workaround for bpo-32509 + + ...data01... -> ziptestdata/... + ... + ...data02... -> ziptestdata/... + ... + """ + suffixes = '01', '02' + tuple(map(generate, suffixes)) + + +def generate(suffix): + root = pathlib.Path(__file__).parent.relative_to(os.getcwd()) + zfpath = root / f'zipdata{suffix}/ziptestdata.zip' + with zipfile.ZipFile(zfpath, 'w') as zf: + for src, rel in walk(root / f'data{suffix}'): + dst = 'ziptestdata' / pathlib.PurePosixPath(rel.as_posix()) + print(src, '->', dst) + zf.write(src, dst) + + +def walk(datapath): + for dirpath, dirnames, filenames in os.walk(datapath): + with contextlib.suppress(KeyError): + dirnames.remove('__pycache__') + for filename in filenames: + res = pathlib.Path(dirpath) / filename + rel = res.relative_to(datapath) + yield res, rel + + +__name__ == '__main__' and main() diff --git a/setuptools/_vendor/importlib_resources/tests/util.py b/setuptools/_vendor/importlib_resources/tests/util.py new file mode 100644 index 00000000..c6d83e4b --- /dev/null +++ b/setuptools/_vendor/importlib_resources/tests/util.py @@ -0,0 +1,178 @@ +import abc +import importlib +import io +import sys +import types +from pathlib import Path, PurePath + +from . import data01 +from . import zipdata01 +from ..abc import ResourceReader +from ._compat import import_helper + + +from importlib.machinery import ModuleSpec + + +class Reader(ResourceReader): + def __init__(self, **kwargs): + vars(self).update(kwargs) + + def get_resource_reader(self, package): + return self + + def open_resource(self, path): + self._path = path + if isinstance(self.file, Exception): + raise self.file + return self.file + + def resource_path(self, path_): + self._path = path_ + if isinstance(self.path, Exception): + raise self.path + return self.path + + def is_resource(self, path_): + self._path = path_ + if isinstance(self.path, Exception): + raise self.path + + def part(entry): + return entry.split('/') + + return any( + len(parts) == 1 and parts[0] == path_ for parts in map(part, self._contents) + ) + + def contents(self): + if isinstance(self.path, Exception): + raise self.path + yield from self._contents + + +def create_package_from_loader(loader, is_package=True): + name = 'testingpackage' + module = types.ModuleType(name) + spec = ModuleSpec(name, loader, origin='does-not-exist', is_package=is_package) + module.__spec__ = spec + module.__loader__ = loader + return module + + +def create_package(file=None, path=None, is_package=True, contents=()): + return create_package_from_loader( + Reader(file=file, path=path, _contents=contents), + is_package, + ) + + +class CommonTests(metaclass=abc.ABCMeta): + """ + Tests shared by test_open, test_path, and test_read. + """ + + @abc.abstractmethod + def execute(self, package, path): + """ + Call the pertinent legacy API function (e.g. open_text, path) + on package and path. + """ + + def test_package_name(self): + # Passing in the package name should succeed. + self.execute(data01.__name__, 'utf-8.file') + + def test_package_object(self): + # Passing in the package itself should succeed. + self.execute(data01, 'utf-8.file') + + def test_string_path(self): + # Passing in a string for the path should succeed. + path = 'utf-8.file' + self.execute(data01, path) + + def test_pathlib_path(self): + # Passing in a pathlib.PurePath object for the path should succeed. + path = PurePath('utf-8.file') + self.execute(data01, path) + + def test_importing_module_as_side_effect(self): + # The anchor package can already be imported. + del sys.modules[data01.__name__] + self.execute(data01.__name__, 'utf-8.file') + + def test_non_package_by_name(self): + # The anchor package cannot be a module. + with self.assertRaises(TypeError): + self.execute(__name__, 'utf-8.file') + + def test_non_package_by_package(self): + # The anchor package cannot be a module. + with self.assertRaises(TypeError): + module = sys.modules['importlib_resources.tests.util'] + self.execute(module, 'utf-8.file') + + def test_missing_path(self): + # Attempting to open or read or request the path for a + # non-existent path should succeed if open_resource + # can return a viable data stream. + bytes_data = io.BytesIO(b'Hello, world!') + package = create_package(file=bytes_data, path=FileNotFoundError()) + self.execute(package, 'utf-8.file') + self.assertEqual(package.__loader__._path, 'utf-8.file') + + def test_extant_path(self): + # Attempting to open or read or request the path when the + # path does exist should still succeed. Does not assert + # anything about the result. + bytes_data = io.BytesIO(b'Hello, world!') + # any path that exists + path = __file__ + package = create_package(file=bytes_data, path=path) + self.execute(package, 'utf-8.file') + self.assertEqual(package.__loader__._path, 'utf-8.file') + + def test_useless_loader(self): + package = create_package(file=FileNotFoundError(), path=FileNotFoundError()) + with self.assertRaises(FileNotFoundError): + self.execute(package, 'utf-8.file') + + +class ZipSetupBase: + ZIP_MODULE = None + + @classmethod + def setUpClass(cls): + data_path = Path(cls.ZIP_MODULE.__file__) + data_dir = data_path.parent + cls._zip_path = str(data_dir / 'ziptestdata.zip') + sys.path.append(cls._zip_path) + cls.data = importlib.import_module('ziptestdata') + + @classmethod + def tearDownClass(cls): + try: + sys.path.remove(cls._zip_path) + except ValueError: + pass + + try: + del sys.path_importer_cache[cls._zip_path] + del sys.modules[cls.data.__name__] + except KeyError: + pass + + try: + del cls.data + del cls._zip_path + except AttributeError: + pass + + def setUp(self): + modules = import_helper.modules_setup() + self.addCleanup(import_helper.modules_cleanup, *modules) + + +class ZipSetup(ZipSetupBase): + ZIP_MODULE = zipdata01 # type: ignore diff --git a/setuptools/_vendor/importlib_resources/tests/zipdata01/__init__.py b/setuptools/_vendor/importlib_resources/tests/zipdata01/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/setuptools/_vendor/importlib_resources/tests/zipdata01/ziptestdata.zip b/setuptools/_vendor/importlib_resources/tests/zipdata01/ziptestdata.zip new file mode 100644 index 00000000..9a3bb073 Binary files /dev/null and b/setuptools/_vendor/importlib_resources/tests/zipdata01/ziptestdata.zip differ diff --git a/setuptools/_vendor/importlib_resources/tests/zipdata02/__init__.py b/setuptools/_vendor/importlib_resources/tests/zipdata02/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/setuptools/_vendor/importlib_resources/tests/zipdata02/ziptestdata.zip b/setuptools/_vendor/importlib_resources/tests/zipdata02/ziptestdata.zip new file mode 100644 index 00000000..d63ff512 Binary files /dev/null and b/setuptools/_vendor/importlib_resources/tests/zipdata02/ziptestdata.zip differ diff --git a/setuptools/_vendor/vendored.txt b/setuptools/_vendor/vendored.txt index 8216ec99..580cc7c1 100644 --- a/setuptools/_vendor/vendored.txt +++ b/setuptools/_vendor/vendored.txt @@ -2,3 +2,5 @@ packaging==21.2 pyparsing==2.2.1 ordered-set==3.1.1 more_itertools==8.8.0 +importlib_resources +importlib_metadata diff --git a/setuptools/_vendor/zipp.py b/setuptools/_vendor/zipp.py new file mode 100644 index 00000000..26b723c1 --- /dev/null +++ b/setuptools/_vendor/zipp.py @@ -0,0 +1,329 @@ +import io +import posixpath +import zipfile +import itertools +import contextlib +import sys +import pathlib + +if sys.version_info < (3, 7): + from collections import OrderedDict +else: + OrderedDict = dict + + +__all__ = ['Path'] + + +def _parents(path): + """ + Given a path with elements separated by + posixpath.sep, generate all parents of that path. + + >>> list(_parents('b/d')) + ['b'] + >>> list(_parents('/b/d/')) + ['/b'] + >>> list(_parents('b/d/f/')) + ['b/d', 'b'] + >>> list(_parents('b')) + [] + >>> list(_parents('')) + [] + """ + return itertools.islice(_ancestry(path), 1, None) + + +def _ancestry(path): + """ + Given a path with elements separated by + posixpath.sep, generate all elements of that path + + >>> list(_ancestry('b/d')) + ['b/d', 'b'] + >>> list(_ancestry('/b/d/')) + ['/b/d', '/b'] + >>> list(_ancestry('b/d/f/')) + ['b/d/f', 'b/d', 'b'] + >>> list(_ancestry('b')) + ['b'] + >>> list(_ancestry('')) + [] + """ + path = path.rstrip(posixpath.sep) + while path and path != posixpath.sep: + yield path + path, tail = posixpath.split(path) + + +_dedupe = OrderedDict.fromkeys +"""Deduplicate an iterable in original order""" + + +def _difference(minuend, subtrahend): + """ + Return items in minuend not in subtrahend, retaining order + with O(1) lookup. + """ + return itertools.filterfalse(set(subtrahend).__contains__, minuend) + + +class CompleteDirs(zipfile.ZipFile): + """ + A ZipFile subclass that ensures that implied directories + are always included in the namelist. + """ + + @staticmethod + def _implied_dirs(names): + parents = itertools.chain.from_iterable(map(_parents, names)) + as_dirs = (p + posixpath.sep for p in parents) + return _dedupe(_difference(as_dirs, names)) + + def namelist(self): + names = super(CompleteDirs, self).namelist() + return names + list(self._implied_dirs(names)) + + def _name_set(self): + return set(self.namelist()) + + def resolve_dir(self, name): + """ + If the name represents a directory, return that name + as a directory (with the trailing slash). + """ + names = self._name_set() + dirname = name + '/' + dir_match = name not in names and dirname in names + return dirname if dir_match else name + + @classmethod + def make(cls, source): + """ + Given a source (filename or zipfile), return an + appropriate CompleteDirs subclass. + """ + if isinstance(source, CompleteDirs): + return source + + if not isinstance(source, zipfile.ZipFile): + return cls(_pathlib_compat(source)) + + # Only allow for FastLookup when supplied zipfile is read-only + if 'r' not in source.mode: + cls = CompleteDirs + + source.__class__ = cls + return source + + +class FastLookup(CompleteDirs): + """ + ZipFile subclass to ensure implicit + dirs exist and are resolved rapidly. + """ + + def namelist(self): + with contextlib.suppress(AttributeError): + return self.__names + self.__names = super(FastLookup, self).namelist() + return self.__names + + def _name_set(self): + with contextlib.suppress(AttributeError): + return self.__lookup + self.__lookup = super(FastLookup, self)._name_set() + return self.__lookup + + +def _pathlib_compat(path): + """ + For path-like objects, convert to a filename for compatibility + on Python 3.6.1 and earlier. + """ + try: + return path.__fspath__() + except AttributeError: + return str(path) + + +class Path: + """ + A pathlib-compatible interface for zip files. + + Consider a zip file with this structure:: + + . + ├── a.txt + └── b + ├── c.txt + └── d + └── e.txt + + >>> data = io.BytesIO() + >>> zf = zipfile.ZipFile(data, 'w') + >>> zf.writestr('a.txt', 'content of a') + >>> zf.writestr('b/c.txt', 'content of c') + >>> zf.writestr('b/d/e.txt', 'content of e') + >>> zf.filename = 'mem/abcde.zip' + + Path accepts the zipfile object itself or a filename + + >>> root = Path(zf) + + From there, several path operations are available. + + Directory iteration (including the zip file itself): + + >>> a, b = root.iterdir() + >>> a + Path('mem/abcde.zip', 'a.txt') + >>> b + Path('mem/abcde.zip', 'b/') + + name property: + + >>> b.name + 'b' + + join with divide operator: + + >>> c = b / 'c.txt' + >>> c + Path('mem/abcde.zip', 'b/c.txt') + >>> c.name + 'c.txt' + + Read text: + + >>> c.read_text() + 'content of c' + + existence: + + >>> c.exists() + True + >>> (b / 'missing.txt').exists() + False + + Coercion to string: + + >>> import os + >>> str(c).replace(os.sep, posixpath.sep) + 'mem/abcde.zip/b/c.txt' + + At the root, ``name``, ``filename``, and ``parent`` + resolve to the zipfile. Note these attributes are not + valid and will raise a ``ValueError`` if the zipfile + has no filename. + + >>> root.name + 'abcde.zip' + >>> str(root.filename).replace(os.sep, posixpath.sep) + 'mem/abcde.zip' + >>> str(root.parent) + 'mem' + """ + + __repr = "{self.__class__.__name__}({self.root.filename!r}, {self.at!r})" + + def __init__(self, root, at=""): + """ + Construct a Path from a ZipFile or filename. + + Note: When the source is an existing ZipFile object, + its type (__class__) will be mutated to a + specialized type. If the caller wishes to retain the + original type, the caller should either create a + separate ZipFile object or pass a filename. + """ + self.root = FastLookup.make(root) + self.at = at + + def open(self, mode='r', *args, pwd=None, **kwargs): + """ + Open this entry as text or binary following the semantics + of ``pathlib.Path.open()`` by passing arguments through + to io.TextIOWrapper(). + """ + if self.is_dir(): + raise IsADirectoryError(self) + zip_mode = mode[0] + if not self.exists() and zip_mode == 'r': + raise FileNotFoundError(self) + stream = self.root.open(self.at, zip_mode, pwd=pwd) + if 'b' in mode: + if args or kwargs: + raise ValueError("encoding args invalid for binary operation") + return stream + return io.TextIOWrapper(stream, *args, **kwargs) + + @property + def name(self): + return pathlib.Path(self.at).name or self.filename.name + + @property + def suffix(self): + return pathlib.Path(self.at).suffix or self.filename.suffix + + @property + def suffixes(self): + return pathlib.Path(self.at).suffixes or self.filename.suffixes + + @property + def stem(self): + return pathlib.Path(self.at).stem or self.filename.stem + + @property + def filename(self): + return pathlib.Path(self.root.filename).joinpath(self.at) + + def read_text(self, *args, **kwargs): + with self.open('r', *args, **kwargs) as strm: + return strm.read() + + def read_bytes(self): + with self.open('rb') as strm: + return strm.read() + + def _is_child(self, path): + return posixpath.dirname(path.at.rstrip("/")) == self.at.rstrip("/") + + def _next(self, at): + return self.__class__(self.root, at) + + def is_dir(self): + return not self.at or self.at.endswith("/") + + def is_file(self): + return self.exists() and not self.is_dir() + + def exists(self): + return self.at in self.root._name_set() + + def iterdir(self): + if not self.is_dir(): + raise ValueError("Can't listdir a file") + subs = map(self._next, self.root.namelist()) + return filter(self._is_child, subs) + + def __str__(self): + return posixpath.join(self.root.filename, self.at) + + def __repr__(self): + return self.__repr.format(self=self) + + def joinpath(self, *other): + next = posixpath.join(self.at, *map(_pathlib_compat, other)) + return self._next(self.root.resolve_dir(next)) + + __truediv__ = joinpath + + @property + def parent(self): + if not self.at: + return self.filename.parent + parent_at = posixpath.dirname(self.at.rstrip('/')) + if parent_at: + parent_at += '/' + return self._next(parent_at) diff --git a/setuptools/extern/__init__.py b/setuptools/extern/__init__.py index baca1afa..d2ac8b08 100644 --- a/setuptools/extern/__init__.py +++ b/setuptools/extern/__init__.py @@ -69,5 +69,8 @@ class VendorImporter: sys.meta_path.append(self) -names = 'packaging', 'pyparsing', 'ordered_set', 'more_itertools', +names = ( + 'packaging', 'pyparsing', 'ordered_set', 'more_itertools', 'importlib_metadata', + 'zipp', 'importlib_resources', +) VendorImporter(__name__, names, 'setuptools._vendor').install() -- cgit v1.2.1 From e9cde4e51a38ae232897aa73b8be5af1a18d46fe Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 3 Dec 2021 21:56:52 -0500 Subject: Add module for selectively loading importlib modules. --- setuptools/_importlib.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 setuptools/_importlib.py diff --git a/setuptools/_importlib.py b/setuptools/_importlib.py new file mode 100644 index 00000000..c529ccd3 --- /dev/null +++ b/setuptools/_importlib.py @@ -0,0 +1,13 @@ +import sys + + +if sys.version_info < (3, 10): + from setuptools.extern import importlib_metadata as metadata +else: + import importlib.metadata as metadata # noqa: F401 + + +if sys.version_info < (3, 9): + from setuptools.extern import importlib_resources as resources +else: + import importlib.resources as resources # noqa: F401 -- cgit v1.2.1 From 4cbbb99a953ac5b1fec3b1dfdd106a7781f4293d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 3 Dec 2021 22:17:12 -0500 Subject: Move ensure_directory into setuptools. --- setuptools/_path.py | 7 +++++++ setuptools/archive_util.py | 2 +- setuptools/command/bdist_egg.py | 3 ++- setuptools/command/easy_install.py | 3 ++- setuptools/command/install_egg_info.py | 3 ++- setuptools/command/install_scripts.py | 3 ++- 6 files changed, 16 insertions(+), 5 deletions(-) create mode 100644 setuptools/_path.py diff --git a/setuptools/_path.py b/setuptools/_path.py new file mode 100644 index 00000000..ede9cb00 --- /dev/null +++ b/setuptools/_path.py @@ -0,0 +1,7 @@ +import os + + +def ensure_directory(path): + """Ensure that the parent directory of `path` exists""" + dirname = os.path.dirname(path) + os.makedirs(dirname, exist_ok=True) diff --git a/setuptools/archive_util.py b/setuptools/archive_util.py index 0f702848..73b2db75 100644 --- a/setuptools/archive_util.py +++ b/setuptools/archive_util.py @@ -8,7 +8,7 @@ import posixpath import contextlib from distutils.errors import DistutilsError -from pkg_resources import ensure_directory +from ._path import ensure_directory __all__ = [ "unpack_archive", "unpack_zipfile", "unpack_tarfile", "default_filter", diff --git a/setuptools/command/bdist_egg.py b/setuptools/command/bdist_egg.py index e6b1609f..11a1c6be 100644 --- a/setuptools/command/bdist_egg.py +++ b/setuptools/command/bdist_egg.py @@ -11,9 +11,10 @@ import re import textwrap import marshal -from pkg_resources import get_build_platform, Distribution, ensure_directory +from pkg_resources import get_build_platform, Distribution from setuptools.extension import Library from setuptools import Command +from .._path import ensure_directory from sysconfig import get_path, get_python_version diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index ef1a9b23..b1260dcd 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -56,12 +56,13 @@ from setuptools.package_index import ( from setuptools.command import bdist_egg, egg_info from setuptools.wheel import Wheel from pkg_resources import ( - yield_lines, normalize_path, resource_string, ensure_directory, + yield_lines, normalize_path, resource_string, get_distribution, find_distributions, Environment, Requirement, Distribution, PathMetadata, EggMetadata, WorkingSet, DistributionNotFound, VersionConflict, DEVELOP_DIST, ) import pkg_resources +from .._path import ensure_directory # Turn on PEP440Warnings warnings.filterwarnings("default", category=pkg_resources.PEP440Warning) diff --git a/setuptools/command/install_egg_info.py b/setuptools/command/install_egg_info.py index edc4718b..65ede406 100644 --- a/setuptools/command/install_egg_info.py +++ b/setuptools/command/install_egg_info.py @@ -4,6 +4,7 @@ import os from setuptools import Command from setuptools import namespaces from setuptools.archive_util import unpack_archive +from .._path import ensure_directory import pkg_resources @@ -37,7 +38,7 @@ class install_egg_info(namespaces.Installer, Command): elif os.path.exists(self.target): self.execute(os.unlink, (self.target,), "Removing " + self.target) if not self.dry_run: - pkg_resources.ensure_directory(self.target) + ensure_directory(self.target) self.execute( self.copytree, (), "Copying %s to %s" % (self.source, self.target) ) diff --git a/setuptools/command/install_scripts.py b/setuptools/command/install_scripts.py index 9cd8eb06..aeb0e424 100644 --- a/setuptools/command/install_scripts.py +++ b/setuptools/command/install_scripts.py @@ -4,7 +4,8 @@ from distutils.errors import DistutilsModuleError import os import sys -from pkg_resources import Distribution, PathMetadata, ensure_directory +from pkg_resources import Distribution, PathMetadata +from .._path import ensure_directory class install_scripts(orig.install_scripts): -- cgit v1.2.1 From 4348e77ca8a0da3b804c2b388209284edd2d26dd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 4 Feb 2022 19:30:48 -0500 Subject: Refresh importlib_metadata to 4.10.1. --- setuptools/_vendor/importlib_metadata/__init__.py | 29 ++++++++++------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/setuptools/_vendor/importlib_metadata/__init__.py b/setuptools/_vendor/importlib_metadata/__init__.py index a7379810..7713e1e0 100644 --- a/setuptools/_vendor/importlib_metadata/__init__.py +++ b/setuptools/_vendor/importlib_metadata/__init__.py @@ -161,8 +161,8 @@ class EntryPoint(DeprecatedTuple): pattern = re.compile( r'(?P[\w.]+)\s*' - r'(:\s*(?P[\w.]+))?\s*' - r'(?P\[.*\])?\s*$' + r'(:\s*(?P[\w.]+)\s*)?' + r'((?P\[.*\])\s*)?$' ) """ A regular expression describing the syntax for an entry point, @@ -576,18 +576,6 @@ class Distribution: ) return filter(None, declared) - @classmethod - def _local(cls, root='.'): - from pep517 import build, meta - - system = build.compat_system(root) - builder = functools.partial( - meta.build, - source_dir=root, - system=system, - ) - return PathDistribution(zipp.Path(meta.build_as_zip(builder))) - @property def metadata(self) -> _meta.PackageMetadata: """Return the parsed metadata for this Distribution. @@ -696,7 +684,7 @@ class Distribution: def make_condition(name): return name and f'extra == "{name}"' - def parse_condition(section): + def quoted_marker(section): section = section or '' extra, sep, markers = section.partition(':') if extra and markers: @@ -704,8 +692,17 @@ class Distribution: conditions = list(filter(None, [markers, make_condition(extra)])) return '; ' + ' and '.join(conditions) if conditions else '' + def url_req_space(req): + """ + PEP 508 requires a space between the url_spec and the quoted_marker. + Ref python/importlib_metadata#357. + """ + # '@' is uniquely indicative of a url_req. + return ' ' * ('@' in req) + for section in sections: - yield section.value + parse_condition(section.name) + space = url_req_space(section.value) + yield section.value + space + quoted_marker(section.name) class DistributionFinder(MetaPathFinder): -- cgit v1.2.1 From 430cacd191440bee5140459b4bb1da1d1cba244b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 4 Feb 2022 19:34:50 -0500 Subject: Vendor jaraco.text with setuptools. --- .../importlib_metadata-4.10.1.dist-info/INSTALLER | 1 + .../importlib_metadata-4.10.1.dist-info/LICENSE | 13 + .../importlib_metadata-4.10.1.dist-info/METADATA | 118 ++++ .../importlib_metadata-4.10.1.dist-info/RECORD | 24 + .../importlib_metadata-4.10.1.dist-info/REQUESTED | 0 .../importlib_metadata-4.10.1.dist-info/WHEEL | 5 + .../top_level.txt | 1 + .../importlib_resources-5.4.0.dist-info/INSTALLER | 1 + .../importlib_resources-5.4.0.dist-info/LICENSE | 13 + .../importlib_resources-5.4.0.dist-info/METADATA | 86 +++ .../importlib_resources-5.4.0.dist-info/RECORD | 75 +++ .../importlib_resources-5.4.0.dist-info/REQUESTED | 0 .../importlib_resources-5.4.0.dist-info/WHEEL | 5 + .../top_level.txt | 1 + setuptools/_vendor/importlib_resources/__init__.py | 2 +- setuptools/_vendor/importlib_resources/_compat.py | 2 +- .../jaraco.context-4.1.1.dist-info/INSTALLER | 1 + .../_vendor/jaraco.context-4.1.1.dist-info/LICENSE | 19 + .../jaraco.context-4.1.1.dist-info/METADATA | 52 ++ .../_vendor/jaraco.context-4.1.1.dist-info/RECORD | 8 + .../_vendor/jaraco.context-4.1.1.dist-info/WHEEL | 5 + .../jaraco.context-4.1.1.dist-info/top_level.txt | 1 + .../jaraco.functools-3.5.0.dist-info/INSTALLER | 1 + .../jaraco.functools-3.5.0.dist-info/LICENSE | 19 + .../jaraco.functools-3.5.0.dist-info/METADATA | 58 ++ .../jaraco.functools-3.5.0.dist-info/RECORD | 8 + .../_vendor/jaraco.functools-3.5.0.dist-info/WHEEL | 5 + .../jaraco.functools-3.5.0.dist-info/top_level.txt | 1 + .../_vendor/jaraco.text-3.7.0.dist-info/INSTALLER | 1 + .../_vendor/jaraco.text-3.7.0.dist-info/LICENSE | 19 + .../_vendor/jaraco.text-3.7.0.dist-info/METADATA | 55 ++ .../_vendor/jaraco.text-3.7.0.dist-info/RECORD | 10 + .../_vendor/jaraco.text-3.7.0.dist-info/REQUESTED | 0 .../_vendor/jaraco.text-3.7.0.dist-info/WHEEL | 5 + .../jaraco.text-3.7.0.dist-info/top_level.txt | 1 + setuptools/_vendor/jaraco/context.py | 213 ++++++++ setuptools/_vendor/jaraco/functools.py | 525 ++++++++++++++++++ setuptools/_vendor/jaraco/text/Lorem ipsum.txt | 2 + setuptools/_vendor/jaraco/text/__init__.py | 599 +++++++++++++++++++++ setuptools/_vendor/vendored.txt | 3 + setuptools/_vendor/zipp-3.7.0.dist-info/INSTALLER | 1 + setuptools/_vendor/zipp-3.7.0.dist-info/LICENSE | 19 + setuptools/_vendor/zipp-3.7.0.dist-info/METADATA | 58 ++ setuptools/_vendor/zipp-3.7.0.dist-info/RECORD | 9 + setuptools/_vendor/zipp-3.7.0.dist-info/REQUESTED | 0 setuptools/_vendor/zipp-3.7.0.dist-info/WHEEL | 5 + .../_vendor/zipp-3.7.0.dist-info/top_level.txt | 1 + setuptools/extern/__init__.py | 2 +- tools/vendored.py | 3 + 49 files changed, 2053 insertions(+), 3 deletions(-) create mode 100644 setuptools/_vendor/importlib_metadata-4.10.1.dist-info/INSTALLER create mode 100644 setuptools/_vendor/importlib_metadata-4.10.1.dist-info/LICENSE create mode 100644 setuptools/_vendor/importlib_metadata-4.10.1.dist-info/METADATA create mode 100644 setuptools/_vendor/importlib_metadata-4.10.1.dist-info/RECORD create mode 100644 setuptools/_vendor/importlib_metadata-4.10.1.dist-info/REQUESTED create mode 100644 setuptools/_vendor/importlib_metadata-4.10.1.dist-info/WHEEL create mode 100644 setuptools/_vendor/importlib_metadata-4.10.1.dist-info/top_level.txt create mode 100644 setuptools/_vendor/importlib_resources-5.4.0.dist-info/INSTALLER create mode 100644 setuptools/_vendor/importlib_resources-5.4.0.dist-info/LICENSE create mode 100644 setuptools/_vendor/importlib_resources-5.4.0.dist-info/METADATA create mode 100644 setuptools/_vendor/importlib_resources-5.4.0.dist-info/RECORD create mode 100644 setuptools/_vendor/importlib_resources-5.4.0.dist-info/REQUESTED create mode 100644 setuptools/_vendor/importlib_resources-5.4.0.dist-info/WHEEL create mode 100644 setuptools/_vendor/importlib_resources-5.4.0.dist-info/top_level.txt create mode 100644 setuptools/_vendor/jaraco.context-4.1.1.dist-info/INSTALLER create mode 100644 setuptools/_vendor/jaraco.context-4.1.1.dist-info/LICENSE create mode 100644 setuptools/_vendor/jaraco.context-4.1.1.dist-info/METADATA create mode 100644 setuptools/_vendor/jaraco.context-4.1.1.dist-info/RECORD create mode 100644 setuptools/_vendor/jaraco.context-4.1.1.dist-info/WHEEL create mode 100644 setuptools/_vendor/jaraco.context-4.1.1.dist-info/top_level.txt create mode 100644 setuptools/_vendor/jaraco.functools-3.5.0.dist-info/INSTALLER create mode 100644 setuptools/_vendor/jaraco.functools-3.5.0.dist-info/LICENSE create mode 100644 setuptools/_vendor/jaraco.functools-3.5.0.dist-info/METADATA create mode 100644 setuptools/_vendor/jaraco.functools-3.5.0.dist-info/RECORD create mode 100644 setuptools/_vendor/jaraco.functools-3.5.0.dist-info/WHEEL create mode 100644 setuptools/_vendor/jaraco.functools-3.5.0.dist-info/top_level.txt create mode 100644 setuptools/_vendor/jaraco.text-3.7.0.dist-info/INSTALLER create mode 100644 setuptools/_vendor/jaraco.text-3.7.0.dist-info/LICENSE create mode 100644 setuptools/_vendor/jaraco.text-3.7.0.dist-info/METADATA create mode 100644 setuptools/_vendor/jaraco.text-3.7.0.dist-info/RECORD create mode 100644 setuptools/_vendor/jaraco.text-3.7.0.dist-info/REQUESTED create mode 100644 setuptools/_vendor/jaraco.text-3.7.0.dist-info/WHEEL create mode 100644 setuptools/_vendor/jaraco.text-3.7.0.dist-info/top_level.txt create mode 100644 setuptools/_vendor/jaraco/context.py create mode 100644 setuptools/_vendor/jaraco/functools.py create mode 100644 setuptools/_vendor/jaraco/text/Lorem ipsum.txt create mode 100644 setuptools/_vendor/jaraco/text/__init__.py create mode 100644 setuptools/_vendor/zipp-3.7.0.dist-info/INSTALLER create mode 100644 setuptools/_vendor/zipp-3.7.0.dist-info/LICENSE create mode 100644 setuptools/_vendor/zipp-3.7.0.dist-info/METADATA create mode 100644 setuptools/_vendor/zipp-3.7.0.dist-info/RECORD create mode 100644 setuptools/_vendor/zipp-3.7.0.dist-info/REQUESTED create mode 100644 setuptools/_vendor/zipp-3.7.0.dist-info/WHEEL create mode 100644 setuptools/_vendor/zipp-3.7.0.dist-info/top_level.txt diff --git a/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/INSTALLER b/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/LICENSE b/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/LICENSE new file mode 100644 index 00000000..be7e092b --- /dev/null +++ b/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/LICENSE @@ -0,0 +1,13 @@ +Copyright 2017-2019 Jason R. Coombs, Barry Warsaw + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/METADATA b/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/METADATA new file mode 100644 index 00000000..7327b888 --- /dev/null +++ b/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/METADATA @@ -0,0 +1,118 @@ +Metadata-Version: 2.1 +Name: importlib-metadata +Version: 4.10.1 +Summary: Read metadata from Python packages +Home-page: https://github.com/python/importlib_metadata +Author: Jason R. Coombs +Author-email: jaraco@jaraco.com +License: UNKNOWN +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Requires-Python: >=3.7 +License-File: LICENSE +Requires-Dist: zipp (>=0.5) +Requires-Dist: typing-extensions (>=3.6.4) ; python_version < "3.8" +Provides-Extra: docs +Requires-Dist: sphinx ; extra == 'docs' +Requires-Dist: jaraco.packaging (>=8.2) ; extra == 'docs' +Requires-Dist: rst.linker (>=1.9) ; extra == 'docs' +Provides-Extra: perf +Requires-Dist: ipython ; extra == 'perf' +Provides-Extra: testing +Requires-Dist: pytest (>=6) ; extra == 'testing' +Requires-Dist: pytest-checkdocs (>=2.4) ; extra == 'testing' +Requires-Dist: pytest-flake8 ; extra == 'testing' +Requires-Dist: pytest-cov ; extra == 'testing' +Requires-Dist: pytest-enabler (>=1.0.1) ; extra == 'testing' +Requires-Dist: packaging ; extra == 'testing' +Requires-Dist: pyfakefs ; extra == 'testing' +Requires-Dist: flufl.flake8 ; extra == 'testing' +Requires-Dist: pytest-perf (>=0.9.2) ; extra == 'testing' +Requires-Dist: pytest-black (>=0.3.7) ; (platform_python_implementation != "PyPy") and extra == 'testing' +Requires-Dist: pytest-mypy ; (platform_python_implementation != "PyPy") and extra == 'testing' +Requires-Dist: importlib-resources (>=1.3) ; (python_version < "3.9") and extra == 'testing' + +.. image:: https://img.shields.io/pypi/v/importlib_metadata.svg + :target: `PyPI link`_ + +.. image:: https://img.shields.io/pypi/pyversions/importlib_metadata.svg + :target: `PyPI link`_ + +.. _PyPI link: https://pypi.org/project/importlib_metadata + +.. image:: https://github.com/python/importlib_metadata/workflows/tests/badge.svg + :target: https://github.com/python/importlib_metadata/actions?query=workflow%3A%22tests%22 + :alt: tests + +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + :alt: Code style: Black + +.. image:: https://readthedocs.org/projects/importlib-metadata/badge/?version=latest + :target: https://importlib-metadata.readthedocs.io/en/latest/?badge=latest + +.. image:: https://img.shields.io/badge/skeleton-2021-informational + :target: https://blog.jaraco.com/skeleton + + +Library to access the metadata for a Python package. + +This package supplies third-party access to the functionality of +`importlib.metadata `_ +including improvements added to subsequent Python versions. + + +Compatibility +============= + +New features are introduced in this third-party library and later merged +into CPython. The following table indicates which versions of this library +were contributed to different versions in the standard library: + +.. list-table:: + :header-rows: 1 + + * - importlib_metadata + - stdlib + * - 4.8 + - 3.11 + * - 4.4 + - 3.10 + * - 1.4 + - 3.8 + + +Usage +===== + +See the `online documentation `_ +for usage details. + +`Finder authors +`_ can +also add support for custom package installers. See the above documentation +for details. + + +Caveats +======= + +This project primarily supports third-party packages installed by PyPA +tools (or other conforming packages). It does not support: + +- Packages in the stdlib. +- Packages installed without metadata. + +Project details +=============== + + * Project home: https://github.com/python/importlib_metadata + * Report bugs at: https://github.com/python/importlib_metadata/issues + * Code hosting: https://github.com/python/importlib_metadata + * Documentation: https://importlib_metadata.readthedocs.io/ + + diff --git a/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/RECORD b/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/RECORD new file mode 100644 index 00000000..ebedf904 --- /dev/null +++ b/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/RECORD @@ -0,0 +1,24 @@ +importlib_metadata-4.10.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +importlib_metadata-4.10.1.dist-info/LICENSE,sha256=wNe6dAchmJ1VvVB8D9oTc-gHHadCuaSBAev36sYEM6U,571 +importlib_metadata-4.10.1.dist-info/METADATA,sha256=-HDYj3iK6bcjwN5MAoO58Op6WQIYQfbhl6ZaPqL0IZI,3989 +importlib_metadata-4.10.1.dist-info/RECORD,, +importlib_metadata-4.10.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +importlib_metadata-4.10.1.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92 +importlib_metadata-4.10.1.dist-info/top_level.txt,sha256=CO3fD9yylANiXkrMo4qHLV_mqXL2sC5JFKgt1yWAT-A,19 +importlib_metadata/__init__.py,sha256=7WxDdbPPu4Wy3VeMTApd-JlPQoENgVDyDH6aqyE7acE,30175 +importlib_metadata/__pycache__/__init__.cpython-310.pyc,, +importlib_metadata/__pycache__/_adapters.cpython-310.pyc,, +importlib_metadata/__pycache__/_collections.cpython-310.pyc,, +importlib_metadata/__pycache__/_compat.cpython-310.pyc,, +importlib_metadata/__pycache__/_functools.cpython-310.pyc,, +importlib_metadata/__pycache__/_itertools.cpython-310.pyc,, +importlib_metadata/__pycache__/_meta.cpython-310.pyc,, +importlib_metadata/__pycache__/_text.cpython-310.pyc,, +importlib_metadata/_adapters.py,sha256=B6fCi5-8mLVDFUZj3krI5nAo-mKp1dH_qIavyIyFrJs,1862 +importlib_metadata/_collections.py,sha256=CJ0OTCHIjWA0ZIVS4voORAsn2R4R2cQBEtPsZEJpASY,743 +importlib_metadata/_compat.py,sha256=EU2XCFBPFByuI0Of6XkAuBYbzqSyjwwwwqmsK4ccna0,1826 +importlib_metadata/_functools.py,sha256=PsY2-4rrKX4RVeRC1oGp1lB1pmC9eKN88_f-bD9uOoA,2895 +importlib_metadata/_itertools.py,sha256=cvr_2v8BRbxcIl5x5ldfqdHjhI8Yi8s8yk50G_nm6jQ,2068 +importlib_metadata/_meta.py,sha256=_F48Hu_jFxkfKWz5wcYS8vO23qEygbVdF9r-6qh-hjE,1154 +importlib_metadata/_text.py,sha256=HCsFksZpJLeTP3NEk_ngrAeXVRRtTrtyh9eOABoRP4A,2166 +importlib_metadata/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/REQUESTED b/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/REQUESTED new file mode 100644 index 00000000..e69de29b diff --git a/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/WHEEL b/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/WHEEL new file mode 100644 index 00000000..becc9a66 --- /dev/null +++ b/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.1) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/top_level.txt b/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/top_level.txt new file mode 100644 index 00000000..bbb07547 --- /dev/null +++ b/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/top_level.txt @@ -0,0 +1 @@ +importlib_metadata diff --git a/setuptools/_vendor/importlib_resources-5.4.0.dist-info/INSTALLER b/setuptools/_vendor/importlib_resources-5.4.0.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/setuptools/_vendor/importlib_resources-5.4.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/setuptools/_vendor/importlib_resources-5.4.0.dist-info/LICENSE b/setuptools/_vendor/importlib_resources-5.4.0.dist-info/LICENSE new file mode 100644 index 00000000..378b991a --- /dev/null +++ b/setuptools/_vendor/importlib_resources-5.4.0.dist-info/LICENSE @@ -0,0 +1,13 @@ +Copyright 2017-2019 Brett Cannon, Barry Warsaw + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/setuptools/_vendor/importlib_resources-5.4.0.dist-info/METADATA b/setuptools/_vendor/importlib_resources-5.4.0.dist-info/METADATA new file mode 100644 index 00000000..cdb1e783 --- /dev/null +++ b/setuptools/_vendor/importlib_resources-5.4.0.dist-info/METADATA @@ -0,0 +1,86 @@ +Metadata-Version: 2.1 +Name: importlib-resources +Version: 5.4.0 +Summary: Read resources from Python packages +Home-page: https://github.com/python/importlib_resources +Author: Barry Warsaw +Author-email: barry@python.org +License: UNKNOWN +Project-URL: Documentation, https://importlib-resources.readthedocs.io/ +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Requires-Python: >=3.6 +License-File: LICENSE +Requires-Dist: zipp (>=3.1.0) ; python_version < "3.10" +Provides-Extra: docs +Requires-Dist: sphinx ; extra == 'docs' +Requires-Dist: jaraco.packaging (>=8.2) ; extra == 'docs' +Requires-Dist: rst.linker (>=1.9) ; extra == 'docs' +Provides-Extra: testing +Requires-Dist: pytest (>=6) ; extra == 'testing' +Requires-Dist: pytest-checkdocs (>=2.4) ; extra == 'testing' +Requires-Dist: pytest-flake8 ; extra == 'testing' +Requires-Dist: pytest-cov ; extra == 'testing' +Requires-Dist: pytest-enabler (>=1.0.1) ; extra == 'testing' +Requires-Dist: pytest-black (>=0.3.7) ; (platform_python_implementation != "PyPy") and extra == 'testing' +Requires-Dist: pytest-mypy ; (platform_python_implementation != "PyPy") and extra == 'testing' + +.. image:: https://img.shields.io/pypi/v/importlib_resources.svg + :target: `PyPI link`_ + +.. image:: https://img.shields.io/pypi/pyversions/importlib_resources.svg + :target: `PyPI link`_ + +.. _PyPI link: https://pypi.org/project/importlib_resources + +.. image:: https://github.com/python/importlib_resources/workflows/tests/badge.svg + :target: https://github.com/python/importlib_resources/actions?query=workflow%3A%22tests%22 + :alt: tests + +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + :alt: Code style: Black + +.. image:: https://readthedocs.org/projects/importlib-resources/badge/?version=latest + :target: https://importlib-resources.readthedocs.io/en/latest/?badge=latest + +.. image:: https://img.shields.io/badge/skeleton-2021-informational + :target: https://blog.jaraco.com/skeleton + +``importlib_resources`` is a backport of Python standard library +`importlib.resources +`_ +module for older Pythons. + +The key goal of this module is to replace parts of `pkg_resources +`_ with a +solution in Python's stdlib that relies on well-defined APIs. This makes +reading resources included in packages easier, with more stable and consistent +semantics. + +Compatibility +============= + +New features are introduced in this third-party library and later merged +into CPython. The following table indicates which versions of this library +were contributed to different versions in the standard library: + +.. list-table:: + :header-rows: 1 + + * - importlib_resources + - stdlib + * - 5.2 + - 3.11 + * - 5.0 + - 3.10 + * - 1.3 + - 3.9 + * - 0.5 (?) + - 3.7 + + diff --git a/setuptools/_vendor/importlib_resources-5.4.0.dist-info/RECORD b/setuptools/_vendor/importlib_resources-5.4.0.dist-info/RECORD new file mode 100644 index 00000000..7a68a2f2 --- /dev/null +++ b/setuptools/_vendor/importlib_resources-5.4.0.dist-info/RECORD @@ -0,0 +1,75 @@ +importlib_resources-5.4.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +importlib_resources-5.4.0.dist-info/LICENSE,sha256=uWRjFdYGataJX2ziXk048ItUglQmjng3GWBALaWA36U,568 +importlib_resources-5.4.0.dist-info/METADATA,sha256=i5jH25IbM0Ls6u6UzSSCOa0c8hpDvePxqgnQwh2T5Io,3135 +importlib_resources-5.4.0.dist-info/RECORD,, +importlib_resources-5.4.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +importlib_resources-5.4.0.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92 +importlib_resources-5.4.0.dist-info/top_level.txt,sha256=fHIjHU1GZwAjvcydpmUnUrTnbvdiWjG4OEVZK8by0TQ,20 +importlib_resources/__init__.py,sha256=zuA0lbRgtVVCcAztM0z5LuBiOCV9L_3qtI6mW2p5xAg,525 +importlib_resources/__pycache__/__init__.cpython-310.pyc,, +importlib_resources/__pycache__/_adapters.cpython-310.pyc,, +importlib_resources/__pycache__/_common.cpython-310.pyc,, +importlib_resources/__pycache__/_compat.cpython-310.pyc,, +importlib_resources/__pycache__/_itertools.cpython-310.pyc,, +importlib_resources/__pycache__/_legacy.cpython-310.pyc,, +importlib_resources/__pycache__/abc.cpython-310.pyc,, +importlib_resources/__pycache__/readers.cpython-310.pyc,, +importlib_resources/__pycache__/simple.cpython-310.pyc,, +importlib_resources/_adapters.py,sha256=o51tP2hpVtohP33gSYyAkGNpLfYDBqxxYsadyiRZi1E,4504 +importlib_resources/_common.py,sha256=iIxAaQhotSh6TLLUEfL_ynU2fzEeyHMz9JcL46mUhLg,2741 +importlib_resources/_compat.py,sha256=3LpkIfeN9x4oXjRea5TxZP5VYhPlzuVRhGe-hEv-S0s,2704 +importlib_resources/_itertools.py,sha256=WCdJ1Gs_kNFwKENyIG7TO0Y434IWCu0zjVVSsSbZwU8,884 +importlib_resources/_legacy.py,sha256=TMLkx6aEM6U8xIREPXqGZrMbUhTiPUuPl6ESD7RdYj4,3494 +importlib_resources/abc.py,sha256=MvTJJXajbl74s36Gyeesf76egtbFnh-TMtzQMVhFWXo,3886 +importlib_resources/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +importlib_resources/readers.py,sha256=_9QLGQ5AzrED3PY8S2Zf8V6yLR0-nqqYqtQmgleDJzY,3566 +importlib_resources/simple.py,sha256=xt0qhXbwt3bZ86zuaaKbTiE9A0mDbwu0saRjUq_pcY0,2836 +importlib_resources/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +importlib_resources/tests/__pycache__/__init__.cpython-310.pyc,, +importlib_resources/tests/__pycache__/_compat.cpython-310.pyc,, +importlib_resources/tests/__pycache__/test_compatibilty_files.cpython-310.pyc,, +importlib_resources/tests/__pycache__/test_contents.cpython-310.pyc,, +importlib_resources/tests/__pycache__/test_files.cpython-310.pyc,, +importlib_resources/tests/__pycache__/test_open.cpython-310.pyc,, +importlib_resources/tests/__pycache__/test_path.cpython-310.pyc,, +importlib_resources/tests/__pycache__/test_read.cpython-310.pyc,, +importlib_resources/tests/__pycache__/test_reader.cpython-310.pyc,, +importlib_resources/tests/__pycache__/test_resource.cpython-310.pyc,, +importlib_resources/tests/__pycache__/update-zips.cpython-310.pyc,, +importlib_resources/tests/__pycache__/util.cpython-310.pyc,, +importlib_resources/tests/_compat.py,sha256=QGI_4p0DXybypoYvw0kr3jfQqvls3p8u4wy4Wvf0Z_o,435 +importlib_resources/tests/data01/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +importlib_resources/tests/data01/__pycache__/__init__.cpython-310.pyc,, +importlib_resources/tests/data01/binary.file,sha256=BU7ewdAhH2JP7Qy8qdT5QAsOSRxDdCryxbCr6_DJkNg,4 +importlib_resources/tests/data01/subdirectory/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +importlib_resources/tests/data01/subdirectory/__pycache__/__init__.cpython-310.pyc,, +importlib_resources/tests/data01/subdirectory/binary.file,sha256=BU7ewdAhH2JP7Qy8qdT5QAsOSRxDdCryxbCr6_DJkNg,4 +importlib_resources/tests/data01/utf-16.file,sha256=t5q9qhxX0rYqItBOM8D3ylwG-RHrnOYteTLtQr6sF7g,44 +importlib_resources/tests/data01/utf-8.file,sha256=kwWgYG4yQ-ZF2X_WA66EjYPmxJRn-w8aSOiS9e8tKYY,20 +importlib_resources/tests/data02/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +importlib_resources/tests/data02/__pycache__/__init__.cpython-310.pyc,, +importlib_resources/tests/data02/one/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +importlib_resources/tests/data02/one/__pycache__/__init__.cpython-310.pyc,, +importlib_resources/tests/data02/one/resource1.txt,sha256=10flKac7c-XXFzJ3t-AB5MJjlBy__dSZvPE_dOm2q6U,13 +importlib_resources/tests/data02/two/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +importlib_resources/tests/data02/two/__pycache__/__init__.cpython-310.pyc,, +importlib_resources/tests/data02/two/resource2.txt,sha256=lt2jbN3TMn9QiFKM832X39bU_62UptDdUkoYzkvEbl0,13 +importlib_resources/tests/namespacedata01/binary.file,sha256=BU7ewdAhH2JP7Qy8qdT5QAsOSRxDdCryxbCr6_DJkNg,4 +importlib_resources/tests/namespacedata01/utf-16.file,sha256=t5q9qhxX0rYqItBOM8D3ylwG-RHrnOYteTLtQr6sF7g,44 +importlib_resources/tests/namespacedata01/utf-8.file,sha256=kwWgYG4yQ-ZF2X_WA66EjYPmxJRn-w8aSOiS9e8tKYY,20 +importlib_resources/tests/test_compatibilty_files.py,sha256=NWkbIsylI8Wz3Dwsxo1quT4ZI6ToXFA2mojCG6Dzuxw,3260 +importlib_resources/tests/test_contents.py,sha256=V1Xfk3lqTDdvUsZuV18Kndf0CT_tkM2oEIwk9Vv0rhg,968 +importlib_resources/tests/test_files.py,sha256=1Nqv6VM_MjfwrmtXYL1a1CMT0QhCxi3hNMqwXlfMQTg,1184 +importlib_resources/tests/test_open.py,sha256=pmEgdrSFdM83L6FxtR8U_RT9BfI3JZ4snGmM_ZZIegY,2565 +importlib_resources/tests/test_path.py,sha256=xvPteNA-UKavDhKgLgrQuXSxKWYH7Q4nSNDVfBX95Gs,2103 +importlib_resources/tests/test_read.py,sha256=EyYvpHJ_7F4LuX2EU_c5EerIBQfRhOFmiIR7LOc5Y5E,2408 +importlib_resources/tests/test_reader.py,sha256=hgXHquqAEnioemv20ZZcDlVaiOrcZKADO37_FkiQ00Y,4286 +importlib_resources/tests/test_resource.py,sha256=DqfLNc9kaN5obqxU8kn0sRUWMf9MygagrpfMV5-QfWg,8145 +importlib_resources/tests/update-zips.py,sha256=x3iJVqWnMM5qp4Oob2Pl3o6Yi03sUjEv_5Wf-UCg3ps,1415 +importlib_resources/tests/util.py,sha256=X1j-0C96pu3_tmtJuLhzfBfcfMenOphDLkxtCt5j7t4,5309 +importlib_resources/tests/zipdata01/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +importlib_resources/tests/zipdata01/__pycache__/__init__.cpython-310.pyc,, +importlib_resources/tests/zipdata01/ziptestdata.zip,sha256=z5Of4dsv3T0t-46B0MsVhxlhsPGMz28aUhJDWpj3_oY,876 +importlib_resources/tests/zipdata02/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +importlib_resources/tests/zipdata02/__pycache__/__init__.cpython-310.pyc,, +importlib_resources/tests/zipdata02/ziptestdata.zip,sha256=ydI-_j-xgQ7tDxqBp9cjOqXBGxUp6ZBbwVJu6Xj-nrY,698 diff --git a/setuptools/_vendor/importlib_resources-5.4.0.dist-info/REQUESTED b/setuptools/_vendor/importlib_resources-5.4.0.dist-info/REQUESTED new file mode 100644 index 00000000..e69de29b diff --git a/setuptools/_vendor/importlib_resources-5.4.0.dist-info/WHEEL b/setuptools/_vendor/importlib_resources-5.4.0.dist-info/WHEEL new file mode 100644 index 00000000..5bad85fd --- /dev/null +++ b/setuptools/_vendor/importlib_resources-5.4.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/setuptools/_vendor/importlib_resources-5.4.0.dist-info/top_level.txt b/setuptools/_vendor/importlib_resources-5.4.0.dist-info/top_level.txt new file mode 100644 index 00000000..58ad1bd3 --- /dev/null +++ b/setuptools/_vendor/importlib_resources-5.4.0.dist-info/top_level.txt @@ -0,0 +1 @@ +importlib_resources diff --git a/setuptools/_vendor/importlib_resources/__init__.py b/setuptools/_vendor/importlib_resources/__init__.py index 15f6b26b..34e3a995 100644 --- a/setuptools/_vendor/importlib_resources/__init__.py +++ b/setuptools/_vendor/importlib_resources/__init__.py @@ -17,7 +17,7 @@ from ._legacy import ( Resource, ) -from importlib_resources.abc import ResourceReader +from .abc import ResourceReader __all__ = [ diff --git a/setuptools/_vendor/importlib_resources/_compat.py b/setuptools/_vendor/importlib_resources/_compat.py index 61e48d47..cb9fc820 100644 --- a/setuptools/_vendor/importlib_resources/_compat.py +++ b/setuptools/_vendor/importlib_resources/_compat.py @@ -8,7 +8,7 @@ from contextlib import suppress if sys.version_info >= (3, 10): from zipfile import Path as ZipPath # type: ignore else: - from zipp import Path as ZipPath # type: ignore + from ..zipp import Path as ZipPath # type: ignore try: diff --git a/setuptools/_vendor/jaraco.context-4.1.1.dist-info/INSTALLER b/setuptools/_vendor/jaraco.context-4.1.1.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/setuptools/_vendor/jaraco.context-4.1.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/setuptools/_vendor/jaraco.context-4.1.1.dist-info/LICENSE b/setuptools/_vendor/jaraco.context-4.1.1.dist-info/LICENSE new file mode 100644 index 00000000..353924be --- /dev/null +++ b/setuptools/_vendor/jaraco.context-4.1.1.dist-info/LICENSE @@ -0,0 +1,19 @@ +Copyright Jason R. Coombs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/setuptools/_vendor/jaraco.context-4.1.1.dist-info/METADATA b/setuptools/_vendor/jaraco.context-4.1.1.dist-info/METADATA new file mode 100644 index 00000000..908711b7 --- /dev/null +++ b/setuptools/_vendor/jaraco.context-4.1.1.dist-info/METADATA @@ -0,0 +1,52 @@ +Metadata-Version: 2.1 +Name: jaraco.context +Version: 4.1.1 +Summary: Context managers by jaraco +Home-page: https://github.com/jaraco/jaraco.context +Author: Jason R. Coombs +Author-email: jaraco@jaraco.com +License: UNKNOWN +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Requires-Python: >=3.6 +License-File: LICENSE +Provides-Extra: docs +Requires-Dist: sphinx ; extra == 'docs' +Requires-Dist: jaraco.packaging (>=8.2) ; extra == 'docs' +Requires-Dist: rst.linker (>=1.9) ; extra == 'docs' +Provides-Extra: testing +Requires-Dist: pytest (>=6) ; extra == 'testing' +Requires-Dist: pytest-checkdocs (>=2.4) ; extra == 'testing' +Requires-Dist: pytest-flake8 ; extra == 'testing' +Requires-Dist: pytest-cov ; extra == 'testing' +Requires-Dist: pytest-enabler (>=1.0.1) ; extra == 'testing' +Requires-Dist: pytest-black (>=0.3.7) ; (platform_python_implementation != "PyPy") and extra == 'testing' +Requires-Dist: pytest-mypy ; (platform_python_implementation != "PyPy") and extra == 'testing' + +.. image:: https://img.shields.io/pypi/v/jaraco.context.svg + :target: `PyPI link`_ + +.. image:: https://img.shields.io/pypi/pyversions/jaraco.context.svg + :target: `PyPI link`_ + +.. _PyPI link: https://pypi.org/project/jaraco.context + +.. image:: https://github.com/jaraco/jaraco.context/workflows/tests/badge.svg + :target: https://github.com/jaraco/jaraco.context/actions?query=workflow%3A%22tests%22 + :alt: tests + +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + :alt: Code style: Black + +.. image:: https://readthedocs.org/projects/jaracocontext/badge/?version=latest + :target: https://jaracocontext.readthedocs.io/en/latest/?badge=latest + +.. image:: https://img.shields.io/badge/skeleton-2021-informational + :target: https://blog.jaraco.com/skeleton + + diff --git a/setuptools/_vendor/jaraco.context-4.1.1.dist-info/RECORD b/setuptools/_vendor/jaraco.context-4.1.1.dist-info/RECORD new file mode 100644 index 00000000..f40d48c7 --- /dev/null +++ b/setuptools/_vendor/jaraco.context-4.1.1.dist-info/RECORD @@ -0,0 +1,8 @@ +jaraco.context-4.1.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +jaraco.context-4.1.1.dist-info/LICENSE,sha256=2z8CRrH5J48VhFuZ_sR4uLUG63ZIeZNyL4xuJUKF-vg,1050 +jaraco.context-4.1.1.dist-info/METADATA,sha256=bvqDGCk6Z7TkohUqr5XZm19SbF9mVxrtXjN6uF_BAMQ,2031 +jaraco.context-4.1.1.dist-info/RECORD,, +jaraco.context-4.1.1.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92 +jaraco.context-4.1.1.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7 +jaraco/__pycache__/context.cpython-310.pyc,, +jaraco/context.py,sha256=7X1tpCLc5EN45iWGzGcsH0Unx62REIkvtRvglj0SiUA,5420 diff --git a/setuptools/_vendor/jaraco.context-4.1.1.dist-info/WHEEL b/setuptools/_vendor/jaraco.context-4.1.1.dist-info/WHEEL new file mode 100644 index 00000000..5bad85fd --- /dev/null +++ b/setuptools/_vendor/jaraco.context-4.1.1.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/setuptools/_vendor/jaraco.context-4.1.1.dist-info/top_level.txt b/setuptools/_vendor/jaraco.context-4.1.1.dist-info/top_level.txt new file mode 100644 index 00000000..f6205a5f --- /dev/null +++ b/setuptools/_vendor/jaraco.context-4.1.1.dist-info/top_level.txt @@ -0,0 +1 @@ +jaraco diff --git a/setuptools/_vendor/jaraco.functools-3.5.0.dist-info/INSTALLER b/setuptools/_vendor/jaraco.functools-3.5.0.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/setuptools/_vendor/jaraco.functools-3.5.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/setuptools/_vendor/jaraco.functools-3.5.0.dist-info/LICENSE b/setuptools/_vendor/jaraco.functools-3.5.0.dist-info/LICENSE new file mode 100644 index 00000000..353924be --- /dev/null +++ b/setuptools/_vendor/jaraco.functools-3.5.0.dist-info/LICENSE @@ -0,0 +1,19 @@ +Copyright Jason R. Coombs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/setuptools/_vendor/jaraco.functools-3.5.0.dist-info/METADATA b/setuptools/_vendor/jaraco.functools-3.5.0.dist-info/METADATA new file mode 100644 index 00000000..12dfbdd0 --- /dev/null +++ b/setuptools/_vendor/jaraco.functools-3.5.0.dist-info/METADATA @@ -0,0 +1,58 @@ +Metadata-Version: 2.1 +Name: jaraco.functools +Version: 3.5.0 +Summary: Functools like those found in stdlib +Home-page: https://github.com/jaraco/jaraco.functools +Author: Jason R. Coombs +Author-email: jaraco@jaraco.com +License: UNKNOWN +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Requires-Python: >=3.7 +License-File: LICENSE +Requires-Dist: more-itertools +Provides-Extra: docs +Requires-Dist: sphinx ; extra == 'docs' +Requires-Dist: jaraco.packaging (>=8.2) ; extra == 'docs' +Requires-Dist: rst.linker (>=1.9) ; extra == 'docs' +Provides-Extra: testing +Requires-Dist: pytest (>=6) ; extra == 'testing' +Requires-Dist: pytest-checkdocs (>=2.4) ; extra == 'testing' +Requires-Dist: pytest-flake8 ; extra == 'testing' +Requires-Dist: pytest-cov ; extra == 'testing' +Requires-Dist: pytest-enabler (>=1.0.1) ; extra == 'testing' +Requires-Dist: jaraco.classes ; extra == 'testing' +Requires-Dist: pytest-black (>=0.3.7) ; (platform_python_implementation != "PyPy") and extra == 'testing' +Requires-Dist: pytest-mypy ; (platform_python_implementation != "PyPy") and extra == 'testing' + +.. image:: https://img.shields.io/pypi/v/jaraco.functools.svg + :target: `PyPI link`_ + +.. image:: https://img.shields.io/pypi/pyversions/jaraco.functools.svg + +.. image:: https://img.shields.io/travis/jaraco/jaraco.functools/master.svg + :target: `PyPI link`_ + +.. _PyPI link: https://pypi.org/project/jaraco.functools + +.. image:: https://github.com/jaraco/jaraco.functools/workflows/tests/badge.svg + :target: https://github.com/jaraco/jaraco.functools/actions?query=workflow%3A%22tests%22 + :alt: tests + +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + :alt: Code style: Black + +.. image:: https://readthedocs.org/projects/jaracofunctools/badge/?version=latest + :target: https://jaracofunctools.readthedocs.io/en/latest/?badge=latest + +.. image:: https://img.shields.io/badge/skeleton-2021-informational + :target: https://blog.jaraco.com/skeleton + +Additional functools in the spirit of stdlib's functools. + + diff --git a/setuptools/_vendor/jaraco.functools-3.5.0.dist-info/RECORD b/setuptools/_vendor/jaraco.functools-3.5.0.dist-info/RECORD new file mode 100644 index 00000000..fbda3d1f --- /dev/null +++ b/setuptools/_vendor/jaraco.functools-3.5.0.dist-info/RECORD @@ -0,0 +1,8 @@ +jaraco.functools-3.5.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +jaraco.functools-3.5.0.dist-info/LICENSE,sha256=2z8CRrH5J48VhFuZ_sR4uLUG63ZIeZNyL4xuJUKF-vg,1050 +jaraco.functools-3.5.0.dist-info/METADATA,sha256=cE9C7u9bo_GjLAuw4nML67a25kUaPDiHn4j03lG4jd0,2276 +jaraco.functools-3.5.0.dist-info/RECORD,, +jaraco.functools-3.5.0.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92 +jaraco.functools-3.5.0.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7 +jaraco/__pycache__/functools.cpython-310.pyc,, +jaraco/functools.py,sha256=PtEHbXZstgVJrwje4GvJOsz5pEbgslOcgEn2EJNpr2c,13494 diff --git a/setuptools/_vendor/jaraco.functools-3.5.0.dist-info/WHEEL b/setuptools/_vendor/jaraco.functools-3.5.0.dist-info/WHEEL new file mode 100644 index 00000000..5bad85fd --- /dev/null +++ b/setuptools/_vendor/jaraco.functools-3.5.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/setuptools/_vendor/jaraco.functools-3.5.0.dist-info/top_level.txt b/setuptools/_vendor/jaraco.functools-3.5.0.dist-info/top_level.txt new file mode 100644 index 00000000..f6205a5f --- /dev/null +++ b/setuptools/_vendor/jaraco.functools-3.5.0.dist-info/top_level.txt @@ -0,0 +1 @@ +jaraco diff --git a/setuptools/_vendor/jaraco.text-3.7.0.dist-info/INSTALLER b/setuptools/_vendor/jaraco.text-3.7.0.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/setuptools/_vendor/jaraco.text-3.7.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/setuptools/_vendor/jaraco.text-3.7.0.dist-info/LICENSE b/setuptools/_vendor/jaraco.text-3.7.0.dist-info/LICENSE new file mode 100644 index 00000000..353924be --- /dev/null +++ b/setuptools/_vendor/jaraco.text-3.7.0.dist-info/LICENSE @@ -0,0 +1,19 @@ +Copyright Jason R. Coombs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/setuptools/_vendor/jaraco.text-3.7.0.dist-info/METADATA b/setuptools/_vendor/jaraco.text-3.7.0.dist-info/METADATA new file mode 100644 index 00000000..615a50a4 --- /dev/null +++ b/setuptools/_vendor/jaraco.text-3.7.0.dist-info/METADATA @@ -0,0 +1,55 @@ +Metadata-Version: 2.1 +Name: jaraco.text +Version: 3.7.0 +Summary: Module for text manipulation +Home-page: https://github.com/jaraco/jaraco.text +Author: Jason R. Coombs +Author-email: jaraco@jaraco.com +License: UNKNOWN +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Requires-Python: >=3.6 +License-File: LICENSE +Requires-Dist: jaraco.functools +Requires-Dist: jaraco.context (>=4.1) +Requires-Dist: importlib-resources ; python_version < "3.9" +Provides-Extra: docs +Requires-Dist: sphinx ; extra == 'docs' +Requires-Dist: jaraco.packaging (>=8.2) ; extra == 'docs' +Requires-Dist: rst.linker (>=1.9) ; extra == 'docs' +Provides-Extra: testing +Requires-Dist: pytest (>=6) ; extra == 'testing' +Requires-Dist: pytest-checkdocs (>=2.4) ; extra == 'testing' +Requires-Dist: pytest-flake8 ; extra == 'testing' +Requires-Dist: pytest-cov ; extra == 'testing' +Requires-Dist: pytest-enabler (>=1.0.1) ; extra == 'testing' +Requires-Dist: pytest-black (>=0.3.7) ; (platform_python_implementation != "PyPy") and extra == 'testing' +Requires-Dist: pytest-mypy ; (platform_python_implementation != "PyPy") and extra == 'testing' + +.. image:: https://img.shields.io/pypi/v/jaraco.text.svg + :target: `PyPI link`_ + +.. image:: https://img.shields.io/pypi/pyversions/jaraco.text.svg + :target: `PyPI link`_ + +.. _PyPI link: https://pypi.org/project/jaraco.text + +.. image:: https://github.com/jaraco/jaraco.text/workflows/tests/badge.svg + :target: https://github.com/jaraco/jaraco.text/actions?query=workflow%3A%22tests%22 + :alt: tests + +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + :alt: Code style: Black + +.. image:: https://readthedocs.org/projects/jaracotext/badge/?version=latest + :target: https://jaracotext.readthedocs.io/en/latest/?badge=latest + +.. image:: https://img.shields.io/badge/skeleton-2021-informational + :target: https://blog.jaraco.com/skeleton + + diff --git a/setuptools/_vendor/jaraco.text-3.7.0.dist-info/RECORD b/setuptools/_vendor/jaraco.text-3.7.0.dist-info/RECORD new file mode 100644 index 00000000..916ad7d3 --- /dev/null +++ b/setuptools/_vendor/jaraco.text-3.7.0.dist-info/RECORD @@ -0,0 +1,10 @@ +jaraco.text-3.7.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +jaraco.text-3.7.0.dist-info/LICENSE,sha256=2z8CRrH5J48VhFuZ_sR4uLUG63ZIeZNyL4xuJUKF-vg,1050 +jaraco.text-3.7.0.dist-info/METADATA,sha256=5mcR1dY0cJNrM-VIkAFkpjOgvgzmq6nM1GfD0gwTIhs,2136 +jaraco.text-3.7.0.dist-info/RECORD,, +jaraco.text-3.7.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +jaraco.text-3.7.0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92 +jaraco.text-3.7.0.dist-info/top_level.txt,sha256=0JnN3LfXH4LIRfXL-QFOGCJzQWZO3ELx4R1d_louoQM,7 +jaraco/text/Lorem ipsum.txt,sha256=N_7c_79zxOufBY9HZ3yzMgOkNv-TkOTTio4BydrSjgs,1335 +jaraco/text/__init__.py,sha256=I56MW2ZFwPrYXIxzqxMBe2A1t-T4uZBgEgAKe9-JoqM,15538 +jaraco/text/__pycache__/__init__.cpython-310.pyc,, diff --git a/setuptools/_vendor/jaraco.text-3.7.0.dist-info/REQUESTED b/setuptools/_vendor/jaraco.text-3.7.0.dist-info/REQUESTED new file mode 100644 index 00000000..e69de29b diff --git a/setuptools/_vendor/jaraco.text-3.7.0.dist-info/WHEEL b/setuptools/_vendor/jaraco.text-3.7.0.dist-info/WHEEL new file mode 100644 index 00000000..becc9a66 --- /dev/null +++ b/setuptools/_vendor/jaraco.text-3.7.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.1) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/setuptools/_vendor/jaraco.text-3.7.0.dist-info/top_level.txt b/setuptools/_vendor/jaraco.text-3.7.0.dist-info/top_level.txt new file mode 100644 index 00000000..f6205a5f --- /dev/null +++ b/setuptools/_vendor/jaraco.text-3.7.0.dist-info/top_level.txt @@ -0,0 +1 @@ +jaraco diff --git a/setuptools/_vendor/jaraco/context.py b/setuptools/_vendor/jaraco/context.py new file mode 100644 index 00000000..87a4e3dc --- /dev/null +++ b/setuptools/_vendor/jaraco/context.py @@ -0,0 +1,213 @@ +import os +import subprocess +import contextlib +import functools +import tempfile +import shutil +import operator + + +@contextlib.contextmanager +def pushd(dir): + orig = os.getcwd() + os.chdir(dir) + try: + yield dir + finally: + os.chdir(orig) + + +@contextlib.contextmanager +def tarball_context(url, target_dir=None, runner=None, pushd=pushd): + """ + Get a tarball, extract it, change to that directory, yield, then + clean up. + `runner` is the function to invoke commands. + `pushd` is a context manager for changing the directory. + """ + if target_dir is None: + target_dir = os.path.basename(url).replace('.tar.gz', '').replace('.tgz', '') + if runner is None: + runner = functools.partial(subprocess.check_call, shell=True) + # In the tar command, use --strip-components=1 to strip the first path and + # then + # use -C to cause the files to be extracted to {target_dir}. This ensures + # that we always know where the files were extracted. + runner('mkdir {target_dir}'.format(**vars())) + try: + getter = 'wget {url} -O -' + extract = 'tar x{compression} --strip-components=1 -C {target_dir}' + cmd = ' | '.join((getter, extract)) + runner(cmd.format(compression=infer_compression(url), **vars())) + with pushd(target_dir): + yield target_dir + finally: + runner('rm -Rf {target_dir}'.format(**vars())) + + +def infer_compression(url): + """ + Given a URL or filename, infer the compression code for tar. + """ + # cheat and just assume it's the last two characters + compression_indicator = url[-2:] + mapping = dict(gz='z', bz='j', xz='J') + # Assume 'z' (gzip) if no match + return mapping.get(compression_indicator, 'z') + + +@contextlib.contextmanager +def temp_dir(remover=shutil.rmtree): + """ + Create a temporary directory context. Pass a custom remover + to override the removal behavior. + """ + temp_dir = tempfile.mkdtemp() + try: + yield temp_dir + finally: + remover(temp_dir) + + +@contextlib.contextmanager +def repo_context(url, branch=None, quiet=True, dest_ctx=temp_dir): + """ + Check out the repo indicated by url. + + If dest_ctx is supplied, it should be a context manager + to yield the target directory for the check out. + """ + exe = 'git' if 'git' in url else 'hg' + with dest_ctx() as repo_dir: + cmd = [exe, 'clone', url, repo_dir] + if branch: + cmd.extend(['--branch', branch]) + devnull = open(os.path.devnull, 'w') + stdout = devnull if quiet else None + subprocess.check_call(cmd, stdout=stdout) + yield repo_dir + + +@contextlib.contextmanager +def null(): + yield + + +class ExceptionTrap: + """ + A context manager that will catch certain exceptions and provide an + indication they occurred. + + >>> with ExceptionTrap() as trap: + ... raise Exception() + >>> bool(trap) + True + + >>> with ExceptionTrap() as trap: + ... pass + >>> bool(trap) + False + + >>> with ExceptionTrap(ValueError) as trap: + ... raise ValueError("1 + 1 is not 3") + >>> bool(trap) + True + + >>> with ExceptionTrap(ValueError) as trap: + ... raise Exception() + Traceback (most recent call last): + ... + Exception + + >>> bool(trap) + False + """ + + exc_info = None, None, None + + def __init__(self, exceptions=(Exception,)): + self.exceptions = exceptions + + def __enter__(self): + return self + + @property + def type(self): + return self.exc_info[0] + + @property + def value(self): + return self.exc_info[1] + + @property + def tb(self): + return self.exc_info[2] + + def __exit__(self, *exc_info): + type = exc_info[0] + matches = type and issubclass(type, self.exceptions) + if matches: + self.exc_info = exc_info + return matches + + def __bool__(self): + return bool(self.type) + + def raises(self, func, *, _test=bool): + """ + Wrap func and replace the result with the truth + value of the trap (True if an exception occurred). + + First, give the decorator an alias to support Python 3.8 + Syntax. + + >>> raises = ExceptionTrap(ValueError).raises + + Now decorate a function that always fails. + + >>> @raises + ... def fail(): + ... raise ValueError('failed') + >>> fail() + True + """ + + @functools.wraps(func) + def wrapper(*args, **kwargs): + with ExceptionTrap(self.exceptions) as trap: + func(*args, **kwargs) + return _test(trap) + + return wrapper + + def passes(self, func): + """ + Wrap func and replace the result with the truth + value of the trap (True if no exception). + + First, give the decorator an alias to support Python 3.8 + Syntax. + + >>> passes = ExceptionTrap(ValueError).passes + + Now decorate a function that always fails. + + >>> @passes + ... def fail(): + ... raise ValueError('failed') + + >>> fail() + False + """ + return self.raises(func, _test=operator.not_) + + +class suppress(contextlib.suppress, contextlib.ContextDecorator): + """ + A version of contextlib.suppress with decorator support. + + >>> @suppress(KeyError) + ... def key_error(): + ... {}[''] + >>> key_error() + """ diff --git a/setuptools/_vendor/jaraco/functools.py b/setuptools/_vendor/jaraco/functools.py new file mode 100644 index 00000000..bbd8b29f --- /dev/null +++ b/setuptools/_vendor/jaraco/functools.py @@ -0,0 +1,525 @@ +import functools +import time +import inspect +import collections +import types +import itertools + +import setuptools.extern.more_itertools + +from typing import Callable, TypeVar + + +CallableT = TypeVar("CallableT", bound=Callable[..., object]) + + +def compose(*funcs): + """ + Compose any number of unary functions into a single unary function. + + >>> import textwrap + >>> expected = str.strip(textwrap.dedent(compose.__doc__)) + >>> strip_and_dedent = compose(str.strip, textwrap.dedent) + >>> strip_and_dedent(compose.__doc__) == expected + True + + Compose also allows the innermost function to take arbitrary arguments. + + >>> round_three = lambda x: round(x, ndigits=3) + >>> f = compose(round_three, int.__truediv__) + >>> [f(3*x, x+1) for x in range(1,10)] + [1.5, 2.0, 2.25, 2.4, 2.5, 2.571, 2.625, 2.667, 2.7] + """ + + def compose_two(f1, f2): + return lambda *args, **kwargs: f1(f2(*args, **kwargs)) + + return functools.reduce(compose_two, funcs) + + +def method_caller(method_name, *args, **kwargs): + """ + Return a function that will call a named method on the + target object with optional positional and keyword + arguments. + + >>> lower = method_caller('lower') + >>> lower('MyString') + 'mystring' + """ + + def call_method(target): + func = getattr(target, method_name) + return func(*args, **kwargs) + + return call_method + + +def once(func): + """ + Decorate func so it's only ever called the first time. + + This decorator can ensure that an expensive or non-idempotent function + will not be expensive on subsequent calls and is idempotent. + + >>> add_three = once(lambda a: a+3) + >>> add_three(3) + 6 + >>> add_three(9) + 6 + >>> add_three('12') + 6 + + To reset the stored value, simply clear the property ``saved_result``. + + >>> del add_three.saved_result + >>> add_three(9) + 12 + >>> add_three(8) + 12 + + Or invoke 'reset()' on it. + + >>> add_three.reset() + >>> add_three(-3) + 0 + >>> add_three(0) + 0 + """ + + @functools.wraps(func) + def wrapper(*args, **kwargs): + if not hasattr(wrapper, 'saved_result'): + wrapper.saved_result = func(*args, **kwargs) + return wrapper.saved_result + + wrapper.reset = lambda: vars(wrapper).__delitem__('saved_result') + return wrapper + + +def method_cache( + method: CallableT, + cache_wrapper: Callable[ + [CallableT], CallableT + ] = functools.lru_cache(), # type: ignore[assignment] +) -> CallableT: + """ + Wrap lru_cache to support storing the cache data in the object instances. + + Abstracts the common paradigm where the method explicitly saves an + underscore-prefixed protected property on first call and returns that + subsequently. + + >>> class MyClass: + ... calls = 0 + ... + ... @method_cache + ... def method(self, value): + ... self.calls += 1 + ... return value + + >>> a = MyClass() + >>> a.method(3) + 3 + >>> for x in range(75): + ... res = a.method(x) + >>> a.calls + 75 + + Note that the apparent behavior will be exactly like that of lru_cache + except that the cache is stored on each instance, so values in one + instance will not flush values from another, and when an instance is + deleted, so are the cached values for that instance. + + >>> b = MyClass() + >>> for x in range(35): + ... res = b.method(x) + >>> b.calls + 35 + >>> a.method(0) + 0 + >>> a.calls + 75 + + Note that if method had been decorated with ``functools.lru_cache()``, + a.calls would have been 76 (due to the cached value of 0 having been + flushed by the 'b' instance). + + Clear the cache with ``.cache_clear()`` + + >>> a.method.cache_clear() + + Same for a method that hasn't yet been called. + + >>> c = MyClass() + >>> c.method.cache_clear() + + Another cache wrapper may be supplied: + + >>> cache = functools.lru_cache(maxsize=2) + >>> MyClass.method2 = method_cache(lambda self: 3, cache_wrapper=cache) + >>> a = MyClass() + >>> a.method2() + 3 + + Caution - do not subsequently wrap the method with another decorator, such + as ``@property``, which changes the semantics of the function. + + See also + http://code.activestate.com/recipes/577452-a-memoize-decorator-for-instance-methods/ + for another implementation and additional justification. + """ + + def wrapper(self: object, *args: object, **kwargs: object) -> object: + # it's the first call, replace the method with a cached, bound method + bound_method: CallableT = types.MethodType( # type: ignore[assignment] + method, self + ) + cached_method = cache_wrapper(bound_method) + setattr(self, method.__name__, cached_method) + return cached_method(*args, **kwargs) + + # Support cache clear even before cache has been created. + wrapper.cache_clear = lambda: None # type: ignore[attr-defined] + + return ( # type: ignore[return-value] + _special_method_cache(method, cache_wrapper) or wrapper + ) + + +def _special_method_cache(method, cache_wrapper): + """ + Because Python treats special methods differently, it's not + possible to use instance attributes to implement the cached + methods. + + Instead, install the wrapper method under a different name + and return a simple proxy to that wrapper. + + https://github.com/jaraco/jaraco.functools/issues/5 + """ + name = method.__name__ + special_names = '__getattr__', '__getitem__' + if name not in special_names: + return + + wrapper_name = '__cached' + name + + def proxy(self, *args, **kwargs): + if wrapper_name not in vars(self): + bound = types.MethodType(method, self) + cache = cache_wrapper(bound) + setattr(self, wrapper_name, cache) + else: + cache = getattr(self, wrapper_name) + return cache(*args, **kwargs) + + return proxy + + +def apply(transform): + """ + Decorate a function with a transform function that is + invoked on results returned from the decorated function. + + >>> @apply(reversed) + ... def get_numbers(start): + ... "doc for get_numbers" + ... return range(start, start+3) + >>> list(get_numbers(4)) + [6, 5, 4] + >>> get_numbers.__doc__ + 'doc for get_numbers' + """ + + def wrap(func): + return functools.wraps(func)(compose(transform, func)) + + return wrap + + +def result_invoke(action): + r""" + Decorate a function with an action function that is + invoked on the results returned from the decorated + function (for its side-effect), then return the original + result. + + >>> @result_invoke(print) + ... def add_two(a, b): + ... return a + b + >>> x = add_two(2, 3) + 5 + >>> x + 5 + """ + + def wrap(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + action(result) + return result + + return wrapper + + return wrap + + +def call_aside(f, *args, **kwargs): + """ + Call a function for its side effect after initialization. + + >>> @call_aside + ... def func(): print("called") + called + >>> func() + called + + Use functools.partial to pass parameters to the initial call + + >>> @functools.partial(call_aside, name='bingo') + ... def func(name): print("called with", name) + called with bingo + """ + f(*args, **kwargs) + return f + + +class Throttler: + """ + Rate-limit a function (or other callable) + """ + + def __init__(self, func, max_rate=float('Inf')): + if isinstance(func, Throttler): + func = func.func + self.func = func + self.max_rate = max_rate + self.reset() + + def reset(self): + self.last_called = 0 + + def __call__(self, *args, **kwargs): + self._wait() + return self.func(*args, **kwargs) + + def _wait(self): + "ensure at least 1/max_rate seconds from last call" + elapsed = time.time() - self.last_called + must_wait = 1 / self.max_rate - elapsed + time.sleep(max(0, must_wait)) + self.last_called = time.time() + + def __get__(self, obj, type=None): + return first_invoke(self._wait, functools.partial(self.func, obj)) + + +def first_invoke(func1, func2): + """ + Return a function that when invoked will invoke func1 without + any parameters (for its side-effect) and then invoke func2 + with whatever parameters were passed, returning its result. + """ + + def wrapper(*args, **kwargs): + func1() + return func2(*args, **kwargs) + + return wrapper + + +def retry_call(func, cleanup=lambda: None, retries=0, trap=()): + """ + Given a callable func, trap the indicated exceptions + for up to 'retries' times, invoking cleanup on the + exception. On the final attempt, allow any exceptions + to propagate. + """ + attempts = itertools.count() if retries == float('inf') else range(retries) + for attempt in attempts: + try: + return func() + except trap: + cleanup() + + return func() + + +def retry(*r_args, **r_kwargs): + """ + Decorator wrapper for retry_call. Accepts arguments to retry_call + except func and then returns a decorator for the decorated function. + + Ex: + + >>> @retry(retries=3) + ... def my_func(a, b): + ... "this is my funk" + ... print(a, b) + >>> my_func.__doc__ + 'this is my funk' + """ + + def decorate(func): + @functools.wraps(func) + def wrapper(*f_args, **f_kwargs): + bound = functools.partial(func, *f_args, **f_kwargs) + return retry_call(bound, *r_args, **r_kwargs) + + return wrapper + + return decorate + + +def print_yielded(func): + """ + Convert a generator into a function that prints all yielded elements + + >>> @print_yielded + ... def x(): + ... yield 3; yield None + >>> x() + 3 + None + """ + print_all = functools.partial(map, print) + print_results = compose(more_itertools.consume, print_all, func) + return functools.wraps(func)(print_results) + + +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 + + +def assign_params(func, namespace): + """ + Assign parameters from namespace where func solicits. + + >>> def func(x, y=3): + ... print(x, y) + >>> assigned = assign_params(func, dict(x=2, z=4)) + >>> assigned() + 2 3 + + The usual errors are raised if a function doesn't receive + its required parameters: + + >>> assigned = assign_params(func, dict(y=3, z=4)) + >>> assigned() + Traceback (most recent call last): + TypeError: func() ...argument... + + It even works on methods: + + >>> class Handler: + ... def meth(self, arg): + ... print(arg) + >>> assign_params(Handler().meth, dict(arg='crystal', foo='clear'))() + crystal + """ + sig = inspect.signature(func) + params = sig.parameters.keys() + call_ns = {k: namespace[k] for k in params if k in namespace} + return functools.partial(func, **call_ns) + + +def save_method_args(method): + """ + Wrap a method such that when it is called, the args and kwargs are + saved on the method. + + >>> class MyClass: + ... @save_method_args + ... def method(self, a, b): + ... print(a, b) + >>> my_ob = MyClass() + >>> my_ob.method(1, 2) + 1 2 + >>> my_ob._saved_method.args + (1, 2) + >>> my_ob._saved_method.kwargs + {} + >>> my_ob.method(a=3, b='foo') + 3 foo + >>> my_ob._saved_method.args + () + >>> my_ob._saved_method.kwargs == dict(a=3, b='foo') + True + + The arguments are stored on the instance, allowing for + different instance to save different args. + + >>> your_ob = MyClass() + >>> your_ob.method({str('x'): 3}, b=[4]) + {'x': 3} [4] + >>> your_ob._saved_method.args + ({'x': 3},) + >>> my_ob._saved_method.args + () + """ + args_and_kwargs = collections.namedtuple('args_and_kwargs', 'args kwargs') + + @functools.wraps(method) + def wrapper(self, *args, **kwargs): + attr_name = '_saved_' + method.__name__ + attr = args_and_kwargs(args, kwargs) + setattr(self, attr_name, attr) + return method(self, *args, **kwargs) + + return wrapper + + +def except_(*exceptions, replace=None, use=None): + """ + Replace the indicated exceptions, if raised, with the indicated + literal replacement or evaluated expression (if present). + + >>> safe_int = except_(ValueError)(int) + >>> safe_int('five') + >>> safe_int('5') + 5 + + Specify a literal replacement with ``replace``. + + >>> safe_int_r = except_(ValueError, replace=0)(int) + >>> safe_int_r('five') + 0 + + Provide an expression to ``use`` to pass through particular parameters. + + >>> safe_int_pt = except_(ValueError, use='args[0]')(int) + >>> safe_int_pt('five') + 'five' + + """ + + def decorate(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except exceptions: + try: + return eval(use) + except TypeError: + return replace + + return wrapper + + return decorate diff --git a/setuptools/_vendor/jaraco/text/Lorem ipsum.txt b/setuptools/_vendor/jaraco/text/Lorem ipsum.txt new file mode 100644 index 00000000..986f944b --- /dev/null +++ b/setuptools/_vendor/jaraco/text/Lorem ipsum.txt @@ -0,0 +1,2 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra, est eros bibendum elit, nec luctus magna felis sollicitudin mauris. Integer in mauris eu nibh euismod gravida. Duis ac tellus et risus vulputate vehicula. Donec lobortis risus a elit. Etiam tempor. Ut ullamcorper, ligula eu tempor congue, eros est euismod turpis, id tincidunt sapien risus a quam. Maecenas fermentum consequat mi. Donec fermentum. Pellentesque malesuada nulla a mi. Duis sapien sem, aliquet nec, commodo eget, consequat quis, neque. Aliquam faucibus, elit ut dictum aliquet, felis nisl adipiscing sapien, sed malesuada diam lacus eget erat. Cras mollis scelerisque nunc. Nullam arcu. Aliquam consequat. Curabitur augue lorem, dapibus quis, laoreet et, pretium ac, nisi. Aenean magna nisl, mollis quis, molestie eu, feugiat in, orci. In hac habitasse platea dictumst. diff --git a/setuptools/_vendor/jaraco/text/__init__.py b/setuptools/_vendor/jaraco/text/__init__.py new file mode 100644 index 00000000..a0306d5f --- /dev/null +++ b/setuptools/_vendor/jaraco/text/__init__.py @@ -0,0 +1,599 @@ +import re +import itertools +import textwrap +import functools + +try: + from importlib.resources import files # type: ignore +except ImportError: # pragma: nocover + from setuptools.extern.importlib_resources import files # type: ignore + +from setuptools.extern.jaraco.functools import compose, method_cache +from setuptools.extern.jaraco.context import ExceptionTrap + + +def substitution(old, new): + """ + Return a function that will perform a substitution on a string + """ + return lambda s: s.replace(old, new) + + +def multi_substitution(*substitutions): + """ + Take a sequence of pairs specifying substitutions, and create + a function that performs those substitutions. + + >>> multi_substitution(('foo', 'bar'), ('bar', 'baz'))('foo') + 'baz' + """ + substitutions = itertools.starmap(substitution, substitutions) + # compose function applies last function first, so reverse the + # substitutions to get the expected order. + substitutions = reversed(tuple(substitutions)) + return compose(*substitutions) + + +class FoldedCase(str): + """ + A case insensitive string class; behaves just like str + except compares equal when the only variation is case. + + >>> s = FoldedCase('hello world') + + >>> s == 'Hello World' + True + + >>> 'Hello World' == s + True + + >>> s != 'Hello World' + False + + >>> s.index('O') + 4 + + >>> s.split('O') + ['hell', ' w', 'rld'] + + >>> sorted(map(FoldedCase, ['GAMMA', 'alpha', 'Beta'])) + ['alpha', 'Beta', 'GAMMA'] + + Sequence membership is straightforward. + + >>> "Hello World" in [s] + True + >>> s in ["Hello World"] + True + + You may test for set inclusion, but candidate and elements + must both be folded. + + >>> FoldedCase("Hello World") in {s} + True + >>> s in {FoldedCase("Hello World")} + True + + String inclusion works as long as the FoldedCase object + is on the right. + + >>> "hello" in FoldedCase("Hello World") + True + + But not if the FoldedCase object is on the left: + + >>> FoldedCase('hello') in 'Hello World' + False + + In that case, use ``in_``: + + >>> FoldedCase('hello').in_('Hello World') + True + + >>> FoldedCase('hello') > FoldedCase('Hello') + False + """ + + def __lt__(self, other): + return self.lower() < other.lower() + + def __gt__(self, other): + return self.lower() > other.lower() + + def __eq__(self, other): + return self.lower() == other.lower() + + def __ne__(self, other): + return self.lower() != other.lower() + + def __hash__(self): + return hash(self.lower()) + + def __contains__(self, other): + return super().lower().__contains__(other.lower()) + + def in_(self, other): + "Does self appear in other?" + return self in FoldedCase(other) + + # cache lower since it's likely to be called frequently. + @method_cache + def lower(self): + return super().lower() + + def index(self, sub): + return self.lower().index(sub.lower()) + + def split(self, splitter=' ', maxsplit=0): + pattern = re.compile(re.escape(splitter), re.I) + return pattern.split(self, maxsplit) + + +# Python 3.8 compatibility +_unicode_trap = ExceptionTrap(UnicodeDecodeError) + + +@_unicode_trap.passes +def is_decodable(value): + r""" + Return True if the supplied value is decodable (using the default + encoding). + + >>> is_decodable(b'\xff') + False + >>> is_decodable(b'\x32') + True + """ + value.decode() + + +def is_binary(value): + r""" + Return True if the value appears to be binary (that is, it's a byte + string and isn't decodable). + + >>> is_binary(b'\xff') + True + >>> is_binary('\xff') + False + """ + return isinstance(value, bytes) and not is_decodable(value) + + +def trim(s): + r""" + Trim something like a docstring to remove the whitespace that + is common due to indentation and formatting. + + >>> trim("\n\tfoo = bar\n\t\tbar = baz\n") + 'foo = bar\n\tbar = baz' + """ + return textwrap.dedent(s).strip() + + +def wrap(s): + """ + Wrap lines of text, retaining existing newlines as + paragraph markers. + + >>> print(wrap(lorem_ipsum)) + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad + minim veniam, quis nostrud exercitation ullamco laboris nisi ut + aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in + culpa qui officia deserunt mollit anim id est laborum. + + Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam + varius, turpis et commodo pharetra, est eros bibendum elit, nec luctus + magna felis sollicitudin mauris. Integer in mauris eu nibh euismod + gravida. Duis ac tellus et risus vulputate vehicula. Donec lobortis + risus a elit. Etiam tempor. Ut ullamcorper, ligula eu tempor congue, + eros est euismod turpis, id tincidunt sapien risus a quam. Maecenas + fermentum consequat mi. Donec fermentum. Pellentesque malesuada nulla + a mi. Duis sapien sem, aliquet nec, commodo eget, consequat quis, + neque. Aliquam faucibus, elit ut dictum aliquet, felis nisl adipiscing + sapien, sed malesuada diam lacus eget erat. Cras mollis scelerisque + nunc. Nullam arcu. Aliquam consequat. Curabitur augue lorem, dapibus + quis, laoreet et, pretium ac, nisi. Aenean magna nisl, mollis quis, + molestie eu, feugiat in, orci. In hac habitasse platea dictumst. + """ + paragraphs = s.splitlines() + wrapped = ('\n'.join(textwrap.wrap(para)) for para in paragraphs) + return '\n\n'.join(wrapped) + + +def unwrap(s): + r""" + Given a multi-line string, return an unwrapped version. + + >>> wrapped = wrap(lorem_ipsum) + >>> wrapped.count('\n') + 20 + >>> unwrapped = unwrap(wrapped) + >>> unwrapped.count('\n') + 1 + >>> print(unwrapped) + Lorem ipsum dolor sit amet, consectetur adipiscing ... + Curabitur pretium tincidunt lacus. Nulla gravida orci ... + + """ + paragraphs = re.split(r'\n\n+', s) + cleaned = (para.replace('\n', ' ') for para in paragraphs) + return '\n'.join(cleaned) + + + + +class Splitter(object): + """object that will split a string with the given arguments for each call + + >>> s = Splitter(',') + >>> s('hello, world, this is your, master calling') + ['hello', ' world', ' this is your', ' master calling'] + """ + + def __init__(self, *args): + self.args = args + + def __call__(self, s): + return s.split(*self.args) + + +def indent(string, prefix=' ' * 4): + """ + >>> indent('foo') + ' foo' + """ + return prefix + string + + +class WordSet(tuple): + """ + Given an identifier, return the words that identifier represents, + whether in camel case, underscore-separated, etc. + + >>> WordSet.parse("camelCase") + ('camel', 'Case') + + >>> WordSet.parse("under_sep") + ('under', 'sep') + + Acronyms should be retained + + >>> WordSet.parse("firstSNL") + ('first', 'SNL') + + >>> WordSet.parse("you_and_I") + ('you', 'and', 'I') + + >>> WordSet.parse("A simple test") + ('A', 'simple', 'test') + + Multiple caps should not interfere with the first cap of another word. + + >>> WordSet.parse("myABCClass") + ('my', 'ABC', 'Class') + + The result is a WordSet, so you can get the form you need. + + >>> WordSet.parse("myABCClass").underscore_separated() + 'my_ABC_Class' + + >>> WordSet.parse('a-command').camel_case() + 'ACommand' + + >>> WordSet.parse('someIdentifier').lowered().space_separated() + 'some identifier' + + Slices of the result should return another WordSet. + + >>> WordSet.parse('taken-out-of-context')[1:].underscore_separated() + 'out_of_context' + + >>> WordSet.from_class_name(WordSet()).lowered().space_separated() + 'word set' + + >>> example = WordSet.parse('figured it out') + >>> example.headless_camel_case() + 'figuredItOut' + >>> example.dash_separated() + 'figured-it-out' + + """ + + _pattern = re.compile('([A-Z]?[a-z]+)|([A-Z]+(?![a-z]))') + + def capitalized(self): + return WordSet(word.capitalize() for word in self) + + def lowered(self): + return WordSet(word.lower() for word in self) + + def camel_case(self): + return ''.join(self.capitalized()) + + def headless_camel_case(self): + words = iter(self) + first = next(words).lower() + new_words = itertools.chain((first,), WordSet(words).camel_case()) + return ''.join(new_words) + + def underscore_separated(self): + return '_'.join(self) + + def dash_separated(self): + return '-'.join(self) + + def space_separated(self): + return ' '.join(self) + + def trim_right(self, item): + """ + Remove the item from the end of the set. + + >>> WordSet.parse('foo bar').trim_right('foo') + ('foo', 'bar') + >>> WordSet.parse('foo bar').trim_right('bar') + ('foo',) + >>> WordSet.parse('').trim_right('bar') + () + """ + return self[:-1] if self and self[-1] == item else self + + def trim_left(self, item): + """ + Remove the item from the beginning of the set. + + >>> WordSet.parse('foo bar').trim_left('foo') + ('bar',) + >>> WordSet.parse('foo bar').trim_left('bar') + ('foo', 'bar') + >>> WordSet.parse('').trim_left('bar') + () + """ + return self[1:] if self and self[0] == item else self + + def trim(self, item): + """ + >>> WordSet.parse('foo bar').trim('foo') + ('bar',) + """ + return self.trim_left(item).trim_right(item) + + def __getitem__(self, item): + result = super(WordSet, self).__getitem__(item) + if isinstance(item, slice): + result = WordSet(result) + return result + + @classmethod + def parse(cls, identifier): + matches = cls._pattern.finditer(identifier) + return WordSet(match.group(0) for match in matches) + + @classmethod + def from_class_name(cls, subject): + return cls.parse(subject.__class__.__name__) + + +# for backward compatibility +words = WordSet.parse + + +def simple_html_strip(s): + r""" + Remove HTML from the string `s`. + + >>> str(simple_html_strip('')) + '' + + >>> print(simple_html_strip('A stormy day in paradise')) + A stormy day in paradise + + >>> print(simple_html_strip('Somebody tell the truth.')) + Somebody tell the truth. + + >>> print(simple_html_strip('What about
\nmultiple lines?')) + What about + multiple lines? + """ + html_stripper = re.compile('()|(<[^>]*>)|([^<]+)', re.DOTALL) + texts = (match.group(3) or '' for match in html_stripper.finditer(s)) + return ''.join(texts) + + +class SeparatedValues(str): + """ + A string separated by a separator. Overrides __iter__ for getting + the values. + + >>> list(SeparatedValues('a,b,c')) + ['a', 'b', 'c'] + + Whitespace is stripped and empty values are discarded. + + >>> list(SeparatedValues(' a, b , c, ')) + ['a', 'b', 'c'] + """ + + separator = ',' + + def __iter__(self): + parts = self.split(self.separator) + return filter(None, (part.strip() for part in parts)) + + +class Stripper: + r""" + Given a series of lines, find the common prefix and strip it from them. + + >>> lines = [ + ... 'abcdefg\n', + ... 'abc\n', + ... 'abcde\n', + ... ] + >>> res = Stripper.strip_prefix(lines) + >>> res.prefix + 'abc' + >>> list(res.lines) + ['defg\n', '\n', 'de\n'] + + If no prefix is common, nothing should be stripped. + + >>> lines = [ + ... 'abcd\n', + ... '1234\n', + ... ] + >>> res = Stripper.strip_prefix(lines) + >>> res.prefix = '' + >>> list(res.lines) + ['abcd\n', '1234\n'] + """ + + def __init__(self, prefix, lines): + self.prefix = prefix + self.lines = map(self, lines) + + @classmethod + def strip_prefix(cls, lines): + prefix_lines, lines = itertools.tee(lines) + prefix = functools.reduce(cls.common_prefix, prefix_lines) + return cls(prefix, lines) + + def __call__(self, line): + if not self.prefix: + return line + null, prefix, rest = line.partition(self.prefix) + return rest + + @staticmethod + def common_prefix(s1, s2): + """ + Return the common prefix of two lines. + """ + index = min(len(s1), len(s2)) + while s1[:index] != s2[:index]: + index -= 1 + return s1[:index] + + +def remove_prefix(text, prefix): + """ + Remove the prefix from the text if it exists. + + >>> remove_prefix('underwhelming performance', 'underwhelming ') + 'performance' + + >>> remove_prefix('something special', 'sample') + 'something special' + """ + null, prefix, rest = text.rpartition(prefix) + return rest + + +def remove_suffix(text, suffix): + """ + Remove the suffix from the text if it exists. + + >>> remove_suffix('name.git', '.git') + 'name' + + >>> remove_suffix('something special', 'sample') + 'something special' + """ + rest, suffix, null = text.partition(suffix) + return rest + + +def normalize_newlines(text): + r""" + Replace alternate newlines with the canonical newline. + + >>> normalize_newlines('Lorem Ipsum\u2029') + 'Lorem Ipsum\n' + >>> normalize_newlines('Lorem Ipsum\r\n') + 'Lorem Ipsum\n' + >>> normalize_newlines('Lorem Ipsum\x85') + 'Lorem Ipsum\n' + """ + newlines = ['\r\n', '\r', '\n', '\u0085', '\u2028', '\u2029'] + pattern = '|'.join(newlines) + return re.sub(pattern, '\n', text) + + +def _nonblank(str): + return str and not str.startswith('#') + + +@functools.singledispatch +def yield_lines(iterable): + r""" + Yield valid lines of a string or iterable. + + >>> list(yield_lines('')) + [] + >>> list(yield_lines(['foo', 'bar'])) + ['foo', 'bar'] + >>> list(yield_lines('foo\nbar')) + ['foo', 'bar'] + >>> list(yield_lines('\nfoo\n#bar\nbaz #comment')) + ['foo', 'baz #comment'] + >>> list(yield_lines(['foo\nbar', 'baz', 'bing\n\n\n'])) + ['foo', 'bar', 'baz', 'bing'] + """ + return itertools.chain.from_iterable(map(yield_lines, iterable)) + + +@yield_lines.register(str) +def _(text): + return filter(_nonblank, map(str.strip, text.splitlines())) + + +def drop_comment(line): + """ + Drop comments. + + >>> drop_comment('foo # bar') + 'foo' + + A hash without a space may be in a URL. + + >>> drop_comment('http://example.com/foo#bar') + 'http://example.com/foo#bar' + """ + return line.partition(' #')[0] + + +def join_continuation(lines): + r""" + Join lines continued by a trailing backslash. + + >>> list(join_continuation(['foo \\', 'bar', 'baz'])) + ['foobar', 'baz'] + >>> list(join_continuation(['foo \\', 'bar', 'baz'])) + ['foobar', 'baz'] + >>> list(join_continuation(['foo \\', 'bar \\', 'baz'])) + ['foobarbaz'] + + Not sure why, but... + The character preceeding the backslash is also elided. + + >>> list(join_continuation(['goo\\', 'dly'])) + ['godly'] + + A terrible idea, but... + If no line is available to continue, suppress the lines. + + >>> list(join_continuation(['foo', 'bar\\', 'baz\\'])) + ['foo'] + """ + lines = iter(lines) + for item in lines: + while item.endswith('\\'): + try: + item = item[:-2].strip() + next(lines) + except StopIteration: + return + yield item diff --git a/setuptools/_vendor/vendored.txt b/setuptools/_vendor/vendored.txt index 580cc7c1..283747a9 100644 --- a/setuptools/_vendor/vendored.txt +++ b/setuptools/_vendor/vendored.txt @@ -4,3 +4,6 @@ ordered-set==3.1.1 more_itertools==8.8.0 importlib_resources importlib_metadata +jaraco.text==3.7.0 +# required for importlib_resources on older Pythons +zipp==3.7.0 diff --git a/setuptools/_vendor/zipp-3.7.0.dist-info/INSTALLER b/setuptools/_vendor/zipp-3.7.0.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/setuptools/_vendor/zipp-3.7.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/setuptools/_vendor/zipp-3.7.0.dist-info/LICENSE b/setuptools/_vendor/zipp-3.7.0.dist-info/LICENSE new file mode 100644 index 00000000..353924be --- /dev/null +++ b/setuptools/_vendor/zipp-3.7.0.dist-info/LICENSE @@ -0,0 +1,19 @@ +Copyright Jason R. Coombs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/setuptools/_vendor/zipp-3.7.0.dist-info/METADATA b/setuptools/_vendor/zipp-3.7.0.dist-info/METADATA new file mode 100644 index 00000000..b1308b5f --- /dev/null +++ b/setuptools/_vendor/zipp-3.7.0.dist-info/METADATA @@ -0,0 +1,58 @@ +Metadata-Version: 2.1 +Name: zipp +Version: 3.7.0 +Summary: Backport of pathlib-compatible object wrapper for zip files +Home-page: https://github.com/jaraco/zipp +Author: Jason R. Coombs +Author-email: jaraco@jaraco.com +License: UNKNOWN +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Requires-Python: >=3.7 +License-File: LICENSE +Provides-Extra: docs +Requires-Dist: sphinx ; extra == 'docs' +Requires-Dist: jaraco.packaging (>=8.2) ; extra == 'docs' +Requires-Dist: rst.linker (>=1.9) ; extra == 'docs' +Provides-Extra: testing +Requires-Dist: pytest (>=6) ; extra == 'testing' +Requires-Dist: pytest-checkdocs (>=2.4) ; extra == 'testing' +Requires-Dist: pytest-flake8 ; extra == 'testing' +Requires-Dist: pytest-cov ; extra == 'testing' +Requires-Dist: pytest-enabler (>=1.0.1) ; extra == 'testing' +Requires-Dist: jaraco.itertools ; extra == 'testing' +Requires-Dist: func-timeout ; extra == 'testing' +Requires-Dist: pytest-black (>=0.3.7) ; (platform_python_implementation != "PyPy") and extra == 'testing' +Requires-Dist: pytest-mypy ; (platform_python_implementation != "PyPy") and extra == 'testing' + +.. image:: https://img.shields.io/pypi/v/zipp.svg + :target: `PyPI link`_ + +.. image:: https://img.shields.io/pypi/pyversions/zipp.svg + :target: `PyPI link`_ + +.. _PyPI link: https://pypi.org/project/zipp + +.. image:: https://github.com/jaraco/zipp/workflows/tests/badge.svg + :target: https://github.com/jaraco/zipp/actions?query=workflow%3A%22tests%22 + :alt: tests + +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + :alt: Code style: Black + +.. .. image:: https://readthedocs.org/projects/skeleton/badge/?version=latest +.. :target: https://skeleton.readthedocs.io/en/latest/?badge=latest + +.. image:: https://img.shields.io/badge/skeleton-2021-informational + :target: https://blog.jaraco.com/skeleton + + +A pathlib-compatible Zipfile object wrapper. Official backport of the standard library +`Path object `_. + + diff --git a/setuptools/_vendor/zipp-3.7.0.dist-info/RECORD b/setuptools/_vendor/zipp-3.7.0.dist-info/RECORD new file mode 100644 index 00000000..38d0b21a --- /dev/null +++ b/setuptools/_vendor/zipp-3.7.0.dist-info/RECORD @@ -0,0 +1,9 @@ +__pycache__/zipp.cpython-310.pyc,, +zipp-3.7.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +zipp-3.7.0.dist-info/LICENSE,sha256=2z8CRrH5J48VhFuZ_sR4uLUG63ZIeZNyL4xuJUKF-vg,1050 +zipp-3.7.0.dist-info/METADATA,sha256=ZLzgaXTyZX_MxTU0lcGfhdPY4CjFrT_3vyQ2Fo49pl8,2261 +zipp-3.7.0.dist-info/RECORD,, +zipp-3.7.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +zipp-3.7.0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92 +zipp-3.7.0.dist-info/top_level.txt,sha256=iAbdoSHfaGqBfVb2XuR9JqSQHCoOsOtG6y9C_LSpqFw,5 +zipp.py,sha256=ajztOH-9I7KA_4wqDYygtHa6xUBVZgFpmZ8FE74HHHI,8425 diff --git a/setuptools/_vendor/zipp-3.7.0.dist-info/REQUESTED b/setuptools/_vendor/zipp-3.7.0.dist-info/REQUESTED new file mode 100644 index 00000000..e69de29b diff --git a/setuptools/_vendor/zipp-3.7.0.dist-info/WHEEL b/setuptools/_vendor/zipp-3.7.0.dist-info/WHEEL new file mode 100644 index 00000000..becc9a66 --- /dev/null +++ b/setuptools/_vendor/zipp-3.7.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.1) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/setuptools/_vendor/zipp-3.7.0.dist-info/top_level.txt b/setuptools/_vendor/zipp-3.7.0.dist-info/top_level.txt new file mode 100644 index 00000000..e82f676f --- /dev/null +++ b/setuptools/_vendor/zipp-3.7.0.dist-info/top_level.txt @@ -0,0 +1 @@ +zipp diff --git a/setuptools/extern/__init__.py b/setuptools/extern/__init__.py index d2ac8b08..3570a3b4 100644 --- a/setuptools/extern/__init__.py +++ b/setuptools/extern/__init__.py @@ -71,6 +71,6 @@ class VendorImporter: names = ( 'packaging', 'pyparsing', 'ordered_set', 'more_itertools', 'importlib_metadata', - 'zipp', 'importlib_resources', + 'zipp', 'importlib_resources', 'jaraco', ) VendorImporter(__name__, names, 'setuptools._vendor').install() diff --git a/tools/vendored.py b/tools/vendored.py index 7159928a..9d832a08 100644 --- a/tools/vendored.py +++ b/tools/vendored.py @@ -100,6 +100,9 @@ def update_setuptools(): vendor = Path('setuptools/_vendor') install(vendor) rewrite_packaging(vendor / 'packaging', 'setuptools.extern') + rewrite_jaraco_text(vendor / 'jaraco/text', 'setuptools.extern') + rewrite_jaraco(vendor / 'jaraco', 'setuptools.extern') + rewrite_importlib_resources(vendor / 'importlib_resources', 'setuptools.extern') __name__ == '__main__' and update_vendored() -- cgit v1.2.1 From 275e5e65fbd6abbe857d631fc59796a1de2ed738 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 4 Feb 2022 19:50:56 -0500 Subject: In build_meta, remove dependency on pkg_resources. --- setuptools/build_meta.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index d0ac613b..cdaac360 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -38,7 +38,7 @@ import warnings import setuptools import distutils -from pkg_resources import parse_requirements +import setuptools.extern.jaraco.text as text __all__ = ['get_requires_for_build_sdist', 'get_requires_for_build_wheel', @@ -49,6 +49,15 @@ __all__ = ['get_requires_for_build_sdist', 'SetupRequirementsError'] +def parse_requirements(strs): + """ + Yield requirement strings for each specification in `strs`. + + `strs` must be a string, or a (possibly-nested) iterable thereof. + """ + return text.join_continuation(map(text.drop_comment, text.yield_lines(strs))) + + class SetupRequirementsError(BaseException): def __init__(self, specifiers): self.specifiers = specifiers @@ -56,7 +65,7 @@ class SetupRequirementsError(BaseException): class Distribution(setuptools.dist.Distribution): def fetch_build_eggs(self, specifiers): - specifier_list = list(map(str, parse_requirements(specifiers))) + specifier_list = list(parse_requirements(specifiers)) raise SetupRequirementsError(specifier_list) -- cgit v1.2.1 From f75c3fc7d46c39052f541b0f0c673617a54523dc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 5 Feb 2022 09:24:27 -0500 Subject: Pin vendored importlib dependencies for consistency. --- setuptools/_vendor/vendored.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setuptools/_vendor/vendored.txt b/setuptools/_vendor/vendored.txt index 283747a9..0639990b 100644 --- a/setuptools/_vendor/vendored.txt +++ b/setuptools/_vendor/vendored.txt @@ -2,8 +2,8 @@ packaging==21.2 pyparsing==2.2.1 ordered-set==3.1.1 more_itertools==8.8.0 -importlib_resources -importlib_metadata +importlib_resources==5.4.0 +importlib_metadata==4.10.1 jaraco.text==3.7.0 # required for importlib_resources on older Pythons zipp==3.7.0 -- cgit v1.2.1 From 31c62fe9f5d60d6c913ac68af8469f734f68f363 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 5 Feb 2022 09:34:58 -0500 Subject: Update changelog. --- changelog.d/3085.change.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/3085.change.rst diff --git a/changelog.d/3085.change.rst b/changelog.d/3085.change.rst new file mode 100644 index 00000000..1cadb768 --- /dev/null +++ b/changelog.d/3085.change.rst @@ -0,0 +1 @@ +Setuptools now vendors importlib_resources and importlib_metadata and jaraco.text. build_meta no longer relies on pkg_resources. Setuptools no longer relies on pkg_resources for ensure_directory. -- cgit v1.2.1 From 73e08a8acd3038a79ef37c0e5769d934d609f6c7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 5 Feb 2022 09:48:56 -0500 Subject: Move requirements processing to _reqs module. Add parse function. --- setuptools/_reqs.py | 19 +++++++++++++++++++ setuptools/build_meta.py | 14 ++------------ 2 files changed, 21 insertions(+), 12 deletions(-) create mode 100644 setuptools/_reqs.py diff --git a/setuptools/_reqs.py b/setuptools/_reqs.py new file mode 100644 index 00000000..ca724174 --- /dev/null +++ b/setuptools/_reqs.py @@ -0,0 +1,19 @@ +import setuptools.extern.jaraco.text as text + +from pkg_resources import Requirement + + +def parse_strings(strs): + """ + Yield requirement strings for each specification in `strs`. + + `strs` must be a string, or a (possibly-nested) iterable thereof. + """ + return text.join_continuation(map(text.drop_comment, text.yield_lines(strs))) + + +def parse(strs): + """ + Deprecated drop-in replacement for pkg_resources.parse_requirements. + """ + return map(Requirement, parse_strings(strs)) diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index cdaac360..1daa77c9 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -37,8 +37,7 @@ import warnings import setuptools import distutils - -import setuptools.extern.jaraco.text as text +from ._reqs import parse_strings __all__ = ['get_requires_for_build_sdist', 'get_requires_for_build_wheel', @@ -49,15 +48,6 @@ __all__ = ['get_requires_for_build_sdist', 'SetupRequirementsError'] -def parse_requirements(strs): - """ - Yield requirement strings for each specification in `strs`. - - `strs` must be a string, or a (possibly-nested) iterable thereof. - """ - return text.join_continuation(map(text.drop_comment, text.yield_lines(strs))) - - class SetupRequirementsError(BaseException): def __init__(self, specifiers): self.specifiers = specifiers @@ -65,7 +55,7 @@ class SetupRequirementsError(BaseException): class Distribution(setuptools.dist.Distribution): def fetch_build_eggs(self, specifiers): - specifier_list = list(parse_requirements(specifiers)) + specifier_list = list(parse_strings(specifiers)) raise SetupRequirementsError(specifier_list) -- cgit v1.2.1 From 157e36ed63408713f56e16f25c5f813e82bb7442 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 5 Feb 2022 09:55:05 -0500 Subject: Replace use of parse_requirements with simple constructor. --- setuptools/command/egg_info.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index f2210292..379f9398 100644 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -23,7 +23,7 @@ from setuptools.command.sdist import walk_revctrl from setuptools.command.setopt import edit_config from setuptools.command import bdist_egg from pkg_resources import ( - parse_requirements, safe_name, parse_version, + Requirement, safe_name, parse_version, safe_version, yield_lines, EntryPoint, iter_entry_points, to_filename) import setuptools.unicode_utils as unicode_utils from setuptools.glob import glob @@ -205,12 +205,8 @@ class egg_info(InfoCommon, Command): try: is_version = isinstance(parsed_version, packaging.version.Version) - spec = ( - "%s==%s" if is_version else "%s===%s" - ) - list( - parse_requirements(spec % (self.egg_name, self.egg_version)) - ) + spec = "%s==%s" if is_version else "%s===%s" + Requirement(spec % (self.egg_name, self.egg_version)) except ValueError as e: raise distutils.errors.DistutilsOptionError( "Invalid distribution name or version syntax: %s-%s" % -- cgit v1.2.1 From a43f99fde07a2860729dca16e2761e442cd1e165 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 5 Feb 2022 09:59:09 -0500 Subject: Replace use of parse_requirements with _reqs.parse. --- setuptools/dist.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index f4a56b0e..733ae14f 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -39,6 +39,7 @@ from setuptools.monkey import get_unpatched from setuptools.config import parse_configuration import pkg_resources from setuptools.extern.packaging import version +from . import _reqs if TYPE_CHECKING: from email.message import Message @@ -280,7 +281,7 @@ def _check_extra(extra, reqs): name, sep, marker = extra.partition(':') if marker and pkg_resources.invalid_marker(marker): raise DistutilsSetupError("Invalid environment marker: " + marker) - list(pkg_resources.parse_requirements(reqs)) + list(_reqs.parse(reqs)) def assert_bool(dist, attr, value): @@ -300,7 +301,7 @@ def invalid_unless_false(dist, attr, value): def check_requirements(dist, attr, value): """Verify that install_requires is a valid requirements list""" try: - list(pkg_resources.parse_requirements(value)) + list(_reqs.parse(value)) if isinstance(value, (dict, set)): raise TypeError("Unordered types are not allowed") except (TypeError, ValueError) as error: @@ -552,7 +553,7 @@ class Distribution(_Distribution): for section, v in spec_ext_reqs.items(): # Do not strip empty sections. self._tmp_extras_require[section] - for r in pkg_resources.parse_requirements(v): + for r in _reqs.parse(v): suffix = self._suffix_for(r) self._tmp_extras_require[section + suffix].append(r) @@ -578,7 +579,7 @@ class Distribution(_Distribution): return not req.marker spec_inst_reqs = getattr(self, 'install_requires', None) or () - inst_reqs = list(pkg_resources.parse_requirements(spec_inst_reqs)) + inst_reqs = list(_reqs.parse(spec_inst_reqs)) simple_reqs = filter(is_simple_req, inst_reqs) complex_reqs = itertools.filterfalse(is_simple_req, inst_reqs) self.install_requires = list(map(str, simple_reqs)) @@ -818,7 +819,7 @@ class Distribution(_Distribution): def fetch_build_eggs(self, requires): """Resolve pre-setup requirements""" resolved_dists = pkg_resources.working_set.resolve( - pkg_resources.parse_requirements(requires), + _reqs.parse(requires), installer=self.fetch_build_egg, replace_conflicting=True, ) -- cgit v1.2.1 From 0e682326743103aae12d98fc231a22902e43f316 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 5 Feb 2022 09:59:38 -0500 Subject: Update changelog. --- changelog.d/3085.change.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/3085.change.rst b/changelog.d/3085.change.rst index 1cadb768..6c0a5bea 100644 --- a/changelog.d/3085.change.rst +++ b/changelog.d/3085.change.rst @@ -1 +1 @@ -Setuptools now vendors importlib_resources and importlib_metadata and jaraco.text. build_meta no longer relies on pkg_resources. Setuptools no longer relies on pkg_resources for ensure_directory. +Setuptools now vendors importlib_resources and importlib_metadata and jaraco.text. Setuptools no longer relies on pkg_resources for ensure_directory nor parse_requirements. -- cgit v1.2.1 From badffe9af9b79dff781f6768bcf48fbd8abd0945 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 5 Feb 2022 14:29:09 -0500 Subject: Use the parent category PytestDeprecationWarning, which is available on older pytest versions. Fixes jaraco/skeleton#57. --- pytest.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pytest.ini b/pytest.ini index 52f19bea..cbbe3b15 100644 --- a/pytest.ini +++ b/pytest.ini @@ -8,11 +8,11 @@ filterwarnings= # 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.PytestRemovedIn8Warning + ignore:The \(fspath. py.path.local\) argument to BlackItem is deprecated.:pytest.PytestDeprecationWarning # 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.PytestRemovedIn8Warning + ignore:The \(fspath. py.path.local\) argument to Flake8Item is deprecated.:pytest.PytestDeprecationWarning # dbader/pytest-mypy#131 - ignore:The \(fspath. py.path.local\) argument to MypyFile is deprecated.:pytest.PytestRemovedIn8Warning + ignore:The \(fspath. py.path.local\) argument to MypyFile is deprecated.:pytest.PytestDeprecationWarning -- cgit v1.2.1 From e5530cbbec3197cdd17b904563862a233c286c71 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 5 Feb 2022 21:42:17 -0500 Subject: =?UTF-8?q?Bump=20version:=2060.7.1=20=E2=86=92=2060.8.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 9 +++++++++ changelog.d/3085.change.rst | 1 - setup.cfg | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/3085.change.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 93a72b39..91356119 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 60.7.1 +current_version = 60.8.0 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index 3f8b182a..e6d359a8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,12 @@ +v60.8.0 +------- + + +Changes +^^^^^^^ +* #3085: Setuptools now vendors importlib_resources and importlib_metadata and jaraco.text. Setuptools no longer relies on pkg_resources for ensure_directory nor parse_requirements. + + v60.7.1 ------- diff --git a/changelog.d/3085.change.rst b/changelog.d/3085.change.rst deleted file mode 100644 index 6c0a5bea..00000000 --- a/changelog.d/3085.change.rst +++ /dev/null @@ -1 +0,0 @@ -Setuptools now vendors importlib_resources and importlib_metadata and jaraco.text. Setuptools no longer relies on pkg_resources for ensure_directory nor parse_requirements. diff --git a/setup.cfg b/setup.cfg index 7fee29b2..753687f3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 60.7.1 +version = 60.8.0 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From 8afd3a3a61949aba151c9dc0c9d7520d73ee8b9e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 6 Feb 2022 10:22:27 -0500 Subject: Ensure that _vendor/jaraco is available as a module. Fixes #3084. --- changelog.d/3084.misc.rst | 1 + pkg_resources/_vendor/jaraco/__init__.py | 0 setuptools/_vendor/jaraco/__init__.py | 0 tools/vendored.py | 2 ++ 4 files changed, 3 insertions(+) create mode 100644 changelog.d/3084.misc.rst create mode 100644 pkg_resources/_vendor/jaraco/__init__.py create mode 100644 setuptools/_vendor/jaraco/__init__.py diff --git a/changelog.d/3084.misc.rst b/changelog.d/3084.misc.rst new file mode 100644 index 00000000..4e81fcaf --- /dev/null +++ b/changelog.d/3084.misc.rst @@ -0,0 +1 @@ +When vendoring jaraco packages, ensure the namespace package is converted to a simple package to support zip importer. diff --git a/pkg_resources/_vendor/jaraco/__init__.py b/pkg_resources/_vendor/jaraco/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/setuptools/_vendor/jaraco/__init__.py b/setuptools/_vendor/jaraco/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/vendored.py b/tools/vendored.py index 9d832a08..57e28d53 100644 --- a/tools/vendored.py +++ b/tools/vendored.py @@ -50,6 +50,8 @@ def rewrite_jaraco(pkg_files, new_root): text = file.read_text() text = re.sub(r' (more_itertools)', rf' {new_root}.\1', text) file.write_text(text) + # required for zip-packaged setuptools #3084 + pkg_files.joinpath('__init__.py').write_text('') def rewrite_importlib_resources(pkg_files, new_root): -- cgit v1.2.1 From da71fab6210e14e81047909c1b66444062842cfe Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 6 Feb 2022 10:31:58 -0500 Subject: =?UTF-8?q?Bump=20version:=2060.8.0=20=E2=86=92=2060.8.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 9 +++++++++ changelog.d/3084.misc.rst | 1 - setup.cfg | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/3084.misc.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 91356119..77754fc0 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 60.8.0 +current_version = 60.8.1 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index e6d359a8..0183241b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,12 @@ +v60.8.1 +------- + + +Misc +^^^^ +* #3084: When vendoring jaraco packages, ensure the namespace package is converted to a simple package to support zip importer. + + v60.8.0 ------- diff --git a/changelog.d/3084.misc.rst b/changelog.d/3084.misc.rst deleted file mode 100644 index 4e81fcaf..00000000 --- a/changelog.d/3084.misc.rst +++ /dev/null @@ -1 +0,0 @@ -When vendoring jaraco packages, ensure the namespace package is converted to a simple package to support zip importer. diff --git a/setup.cfg b/setup.cfg index 753687f3..d6f08caa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 60.8.0 +version = 60.8.1 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From 2d6cc80fae6d17455a124391eab39eb4eddfee2f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 5 Feb 2022 23:02:50 -0500 Subject: Replaced use of iter_entry_points in setuptools.dist --- setuptools/dist.py | 62 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index 733ae14f..b0aea37b 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -28,7 +28,9 @@ from distutils.util import rfc822_escape from setuptools.extern import packaging from setuptools.extern import ordered_set -from setuptools.extern.more_itertools import unique_everseen +from setuptools.extern.more_itertools import unique_everseen, always_iterable + +from ._importlib import metadata from . import SetuptoolsDeprecationWarning @@ -38,7 +40,7 @@ from setuptools import windows_support from setuptools.monkey import get_unpatched from setuptools.config import parse_configuration import pkg_resources -from setuptools.extern.packaging import version +from setuptools.extern.packaging import version, requirements from . import _reqs if TYPE_CHECKING: @@ -450,7 +452,7 @@ class Distribution(_Distribution): self.patch_missing_pkg_info(attrs) self.dependency_links = attrs.pop('dependency_links', []) self.setup_requires = attrs.pop('setup_requires', []) - for ep in pkg_resources.iter_entry_points('distutils.setup_keywords'): + for ep in metadata.entry_points(group='distutils.setup_keywords'): vars(self).setdefault(ep.name, None) _Distribution.__init__( self, @@ -720,7 +722,10 @@ class Distribution(_Distribution): return opt underscore_opt = opt.replace('-', '_') - commands = distutils.command.__all__ + self._setuptools_commands() + commands = list(itertools.chain( + distutils.command.__all__, + self._setuptools_commands(), + )) if ( not section.startswith('options') and section != 'metadata' @@ -738,9 +743,8 @@ class Distribution(_Distribution): def _setuptools_commands(self): try: - dist = pkg_resources.get_distribution('setuptools') - return list(dist.get_entry_map('distutils.commands')) - except pkg_resources.DistributionNotFound: + return metadata.distribution('setuptools').entry_points.names + except metadata.PackageNotFoundError: # during bootstrapping, distribution doesn't exist return [] @@ -839,7 +843,7 @@ class Distribution(_Distribution): def by_order(hook): return getattr(hook, 'order', 0) - defined = pkg_resources.iter_entry_points(group) + defined = metadata.entry_points(group=group) filtered = itertools.filterfalse(self._removed, defined) loaded = map(lambda e: e.load(), filtered) for ep in sorted(loaded, key=by_order): @@ -860,12 +864,36 @@ class Distribution(_Distribution): return ep.name in removed def _finalize_setup_keywords(self): - for ep in pkg_resources.iter_entry_points('distutils.setup_keywords'): + for ep in metadata.entry_points(group='distutils.setup_keywords'): value = getattr(self, ep.name, None) if value is not None: - ep.require(installer=self.fetch_build_egg) + self._install_dependencies(ep) ep.load()(self, ep.name, value) + def _install_dependencies(self, ep): + """ + Given an entry point, ensure that any declared extras for + its distribution are installed. + """ + reqs = { + req + for req in map(requirements.Requirement, always_iterable(ep.dist.requires)) + for extra in ep.extras + if extra in req.extras + } + missing = itertools.filterfalse(self._is_installed, reqs) + for req in missing: + # fetch_build_egg expects pkg_resources.Requirement + self.fetch_build_egg(pkg_resources.Requirement(str(req))) + + def _is_installed(self, req): + try: + dist = metadata.distribution(req.name) + except metadata.PackageNotFoundError: + return False + found_ver = packaging.version.Version(dist.version()) + return found_ver in req.specifier + def get_egg_cache_dir(self): egg_cache_dir = os.path.join(os.curdir, '.eggs') if not os.path.exists(egg_cache_dir): @@ -896,27 +924,25 @@ class Distribution(_Distribution): if command in self.cmdclass: return self.cmdclass[command] - eps = pkg_resources.iter_entry_points('distutils.commands', command) + eps = metadata.entry_points(group='distutils.commands', name=command) for ep in eps: - ep.require(installer=self.fetch_build_egg) + self._install_dependencies(ep) self.cmdclass[command] = cmdclass = ep.load() return cmdclass else: return _Distribution.get_command_class(self, command) def print_commands(self): - for ep in pkg_resources.iter_entry_points('distutils.commands'): + for ep in metadata.entry_points(group='distutils.commands'): if ep.name not in self.cmdclass: - # don't require extras as the commands won't be invoked - cmdclass = ep.resolve() + cmdclass = ep.load() self.cmdclass[ep.name] = cmdclass return _Distribution.print_commands(self) def get_command_list(self): - for ep in pkg_resources.iter_entry_points('distutils.commands'): + for ep in metadata.entry_points(group='distutils.commands'): if ep.name not in self.cmdclass: - # don't require extras as the commands won't be invoked - cmdclass = ep.resolve() + cmdclass = ep.load() self.cmdclass[ep.name] = cmdclass return _Distribution.get_command_list(self) -- cgit v1.2.1 From 988bf129164b8d46985c4d8b150086eedd95e37d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 6 Feb 2022 10:08:21 -0500 Subject: Update vendoring for importlib_metadata to support vendored dependencies typing_extensions and zipp. --- setuptools/_vendor/importlib_metadata/__init__.py | 2 +- setuptools/_vendor/importlib_metadata/_compat.py | 2 +- .../typing_extensions-4.0.1.dist-info/INSTALLER | 1 + .../typing_extensions-4.0.1.dist-info/LICENSE | 254 +++ .../typing_extensions-4.0.1.dist-info/METADATA | 35 + .../typing_extensions-4.0.1.dist-info/RECORD | 8 + .../typing_extensions-4.0.1.dist-info/REQUESTED | 0 .../typing_extensions-4.0.1.dist-info/WHEEL | 4 + setuptools/_vendor/typing_extensions.py | 2296 ++++++++++++++++++++ setuptools/_vendor/vendored.txt | 6 +- setuptools/extern/__init__.py | 2 +- tools/vendored.py | 11 + 12 files changed, 2616 insertions(+), 5 deletions(-) create mode 100644 setuptools/_vendor/typing_extensions-4.0.1.dist-info/INSTALLER create mode 100644 setuptools/_vendor/typing_extensions-4.0.1.dist-info/LICENSE create mode 100644 setuptools/_vendor/typing_extensions-4.0.1.dist-info/METADATA create mode 100644 setuptools/_vendor/typing_extensions-4.0.1.dist-info/RECORD create mode 100644 setuptools/_vendor/typing_extensions-4.0.1.dist-info/REQUESTED create mode 100644 setuptools/_vendor/typing_extensions-4.0.1.dist-info/WHEEL create mode 100644 setuptools/_vendor/typing_extensions.py diff --git a/setuptools/_vendor/importlib_metadata/__init__.py b/setuptools/_vendor/importlib_metadata/__init__.py index 7713e1e0..45541179 100644 --- a/setuptools/_vendor/importlib_metadata/__init__.py +++ b/setuptools/_vendor/importlib_metadata/__init__.py @@ -3,7 +3,7 @@ import re import abc import csv import sys -import zipp +from .. import zipp import email import pathlib import operator diff --git a/setuptools/_vendor/importlib_metadata/_compat.py b/setuptools/_vendor/importlib_metadata/_compat.py index 8fe4e4e3..ef3136f8 100644 --- a/setuptools/_vendor/importlib_metadata/_compat.py +++ b/setuptools/_vendor/importlib_metadata/_compat.py @@ -8,7 +8,7 @@ __all__ = ['install', 'NullFinder', 'Protocol'] try: from typing import Protocol except ImportError: # pragma: no cover - from typing_extensions import Protocol # type: ignore + from ..typing_extensions import Protocol # type: ignore def install(cls): diff --git a/setuptools/_vendor/typing_extensions-4.0.1.dist-info/INSTALLER b/setuptools/_vendor/typing_extensions-4.0.1.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/setuptools/_vendor/typing_extensions-4.0.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/setuptools/_vendor/typing_extensions-4.0.1.dist-info/LICENSE b/setuptools/_vendor/typing_extensions-4.0.1.dist-info/LICENSE new file mode 100644 index 00000000..583f9f6e --- /dev/null +++ b/setuptools/_vendor/typing_extensions-4.0.1.dist-info/LICENSE @@ -0,0 +1,254 @@ +A. HISTORY OF THE SOFTWARE +========================== + +Python was created in the early 1990s by Guido van Rossum at Stichting +Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands +as a successor of a language called ABC. Guido remains Python's +principal author, although it includes many contributions from others. + +In 1995, Guido continued his work on Python at the Corporation for +National Research Initiatives (CNRI, see http://www.cnri.reston.va.us) +in Reston, Virginia where he released several versions of the +software. + +In May 2000, Guido and the Python core development team moved to +BeOpen.com to form the BeOpen PythonLabs team. In October of the same +year, the PythonLabs team moved to Digital Creations (now Zope +Corporation, see http://www.zope.com). In 2001, the Python Software +Foundation (PSF, see http://www.python.org/psf/) was formed, a +non-profit organization created specifically to own Python-related +Intellectual Property. Zope Corporation is a sponsoring member of +the PSF. + +All Python releases are Open Source (see http://www.opensource.org for +the Open Source Definition). Historically, most, but not all, Python +releases have also been GPL-compatible; the table below summarizes +the various releases. + + Release Derived Year Owner GPL- + from compatible? (1) + + 0.9.0 thru 1.2 1991-1995 CWI yes + 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes + 1.6 1.5.2 2000 CNRI no + 2.0 1.6 2000 BeOpen.com no + 1.6.1 1.6 2001 CNRI yes (2) + 2.1 2.0+1.6.1 2001 PSF no + 2.0.1 2.0+1.6.1 2001 PSF yes + 2.1.1 2.1+2.0.1 2001 PSF yes + 2.1.2 2.1.1 2002 PSF yes + 2.1.3 2.1.2 2002 PSF yes + 2.2 and above 2.1.1 2001-now PSF yes + +Footnotes: + +(1) GPL-compatible doesn't mean that we're distributing Python under + the GPL. All Python licenses, unlike the GPL, let you distribute + a modified version without making your changes open source. The + GPL-compatible licenses make it possible to combine Python with + other software that is released under the GPL; the others don't. + +(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, + because its license has a choice of law clause. According to + CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 + is "not incompatible" with the GPL. + +Thanks to the many outside volunteers who have worked under Guido's +direction to make these releases possible. + + +B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON +=============================================================== + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014 Python Software Foundation; All Rights Reserved" are +retained in Python alone or in any derivative version prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the Internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the Internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/setuptools/_vendor/typing_extensions-4.0.1.dist-info/METADATA b/setuptools/_vendor/typing_extensions-4.0.1.dist-info/METADATA new file mode 100644 index 00000000..fe10dfd0 --- /dev/null +++ b/setuptools/_vendor/typing_extensions-4.0.1.dist-info/METADATA @@ -0,0 +1,35 @@ +Metadata-Version: 2.1 +Name: typing_extensions +Version: 4.0.1 +Summary: Backported and Experimental Type Hints for Python 3.6+ +Keywords: annotations,backport,checker,checking,function,hinting,hints,type,typechecking,typehinting,typehints,typing +Author-email: "Guido van Rossum, Jukka Lehtosalo, Łukasz Langa, Michael Lee" +Requires-Python: >=3.6 +Description-Content-Type: text/x-rst +Classifier: Development Status :: 3 - Alpha +Classifier: Environment :: Console +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Python Software Foundation License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Topic :: Software Development +Project-URL: Home, https://github.com/python/typing/blob/master/typing_extensions/README.rst + +Typing Extensions -- Backported and Experimental Type Hints for Python + +The ``typing`` module was added to the standard library in Python 3.5, but +many new features have been added to the module since then. +This means users of older Python versions who are unable to upgrade will not be +able to take advantage of new types added to the ``typing`` module, such as +``typing.Protocol`` or ``typing.TypedDict``. + +The ``typing_extensions`` module contains backports of these changes. +Experimental types that may eventually be added to the ``typing`` +module are also included in ``typing_extensions``. + diff --git a/setuptools/_vendor/typing_extensions-4.0.1.dist-info/RECORD b/setuptools/_vendor/typing_extensions-4.0.1.dist-info/RECORD new file mode 100644 index 00000000..9a7f6007 --- /dev/null +++ b/setuptools/_vendor/typing_extensions-4.0.1.dist-info/RECORD @@ -0,0 +1,8 @@ +__pycache__/typing_extensions.cpython-310.pyc,, +typing_extensions-4.0.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +typing_extensions-4.0.1.dist-info/LICENSE,sha256=_xfOlOECAk3raHc-scx0ynbaTmWPNzUx8Kwi1oprsa0,12755 +typing_extensions-4.0.1.dist-info/METADATA,sha256=iZ_5HONZZBXtF4kroz-IPZYIl9M8IE1B00R82dWcBqE,1736 +typing_extensions-4.0.1.dist-info/RECORD,, +typing_extensions-4.0.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +typing_extensions-4.0.1.dist-info/WHEEL,sha256=LVOPL_YDMEiGvRLgDK1hLkfhFCnTcxcAYZJtpNFses0,81 +typing_extensions.py,sha256=1uqi_RSlI7gos4eJB_NEV3d5wQwzTUQHd3_jrkbTo8Q,87149 diff --git a/setuptools/_vendor/typing_extensions-4.0.1.dist-info/REQUESTED b/setuptools/_vendor/typing_extensions-4.0.1.dist-info/REQUESTED new file mode 100644 index 00000000..e69de29b diff --git a/setuptools/_vendor/typing_extensions-4.0.1.dist-info/WHEEL b/setuptools/_vendor/typing_extensions-4.0.1.dist-info/WHEEL new file mode 100644 index 00000000..884ceb56 --- /dev/null +++ b/setuptools/_vendor/typing_extensions-4.0.1.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 3.5.1 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/setuptools/_vendor/typing_extensions.py b/setuptools/_vendor/typing_extensions.py new file mode 100644 index 00000000..9f1c7aa3 --- /dev/null +++ b/setuptools/_vendor/typing_extensions.py @@ -0,0 +1,2296 @@ +import abc +import collections +import collections.abc +import operator +import sys +import typing + +# After PEP 560, internal typing API was substantially reworked. +# This is especially important for Protocol class which uses internal APIs +# quite extensively. +PEP_560 = sys.version_info[:3] >= (3, 7, 0) + +if PEP_560: + GenericMeta = type +else: + # 3.6 + from typing import GenericMeta, _type_vars # noqa + +# The two functions below are copies of typing internal helpers. +# They are needed by _ProtocolMeta + + +def _no_slots_copy(dct): + dict_copy = dict(dct) + if '__slots__' in dict_copy: + for slot in dict_copy['__slots__']: + dict_copy.pop(slot, None) + return dict_copy + + +def _check_generic(cls, parameters): + if not cls.__parameters__: + raise TypeError(f"{cls} is not a generic class") + alen = len(parameters) + elen = len(cls.__parameters__) + if alen != elen: + raise TypeError(f"Too {'many' if alen > elen else 'few'} arguments for {cls};" + f" actual {alen}, expected {elen}") + + +# Please keep __all__ alphabetized within each category. +__all__ = [ + # Super-special typing primitives. + 'ClassVar', + 'Concatenate', + 'Final', + 'ParamSpec', + 'Self', + 'Type', + + # ABCs (from collections.abc). + 'Awaitable', + 'AsyncIterator', + 'AsyncIterable', + 'Coroutine', + 'AsyncGenerator', + 'AsyncContextManager', + 'ChainMap', + + # Concrete collection types. + 'ContextManager', + 'Counter', + 'Deque', + 'DefaultDict', + 'OrderedDict', + 'TypedDict', + + # Structural checks, a.k.a. protocols. + 'SupportsIndex', + + # One-off things. + 'Annotated', + 'final', + 'IntVar', + 'Literal', + 'NewType', + 'overload', + 'Protocol', + 'runtime', + 'runtime_checkable', + 'Text', + 'TypeAlias', + 'TypeGuard', + 'TYPE_CHECKING', +] + +if PEP_560: + __all__.extend(["get_args", "get_origin", "get_type_hints"]) + +# 3.6.2+ +if hasattr(typing, 'NoReturn'): + NoReturn = typing.NoReturn +# 3.6.0-3.6.1 +else: + class _NoReturn(typing._FinalTypingBase, _root=True): + """Special type indicating functions that never return. + Example:: + + from typing import NoReturn + + def stop() -> NoReturn: + raise Exception('no way') + + This type is invalid in other positions, e.g., ``List[NoReturn]`` + will fail in static type checkers. + """ + __slots__ = () + + def __instancecheck__(self, obj): + raise TypeError("NoReturn cannot be used with isinstance().") + + def __subclasscheck__(self, cls): + raise TypeError("NoReturn cannot be used with issubclass().") + + NoReturn = _NoReturn(_root=True) + +# Some unconstrained type variables. These are used by the container types. +# (These are not for export.) +T = typing.TypeVar('T') # Any type. +KT = typing.TypeVar('KT') # Key type. +VT = typing.TypeVar('VT') # Value type. +T_co = typing.TypeVar('T_co', covariant=True) # Any type covariant containers. +T_contra = typing.TypeVar('T_contra', contravariant=True) # Ditto contravariant. + +ClassVar = typing.ClassVar + +# On older versions of typing there is an internal class named "Final". +# 3.8+ +if hasattr(typing, 'Final') and sys.version_info[:2] >= (3, 7): + Final = typing.Final +# 3.7 +elif sys.version_info[:2] >= (3, 7): + class _FinalForm(typing._SpecialForm, _root=True): + + def __repr__(self): + return 'typing_extensions.' + self._name + + def __getitem__(self, parameters): + item = typing._type_check(parameters, + f'{self._name} accepts only single type') + return typing._GenericAlias(self, (item,)) + + Final = _FinalForm('Final', + doc="""A special typing construct to indicate that a name + cannot be re-assigned or overridden in a subclass. + For example: + + MAX_SIZE: Final = 9000 + MAX_SIZE += 1 # Error reported by type checker + + class Connection: + TIMEOUT: Final[int] = 10 + class FastConnector(Connection): + TIMEOUT = 1 # Error reported by type checker + + There is no runtime checking of these properties.""") +# 3.6 +else: + class _Final(typing._FinalTypingBase, _root=True): + """A special typing construct to indicate that a name + cannot be re-assigned or overridden in a subclass. + For example: + + MAX_SIZE: Final = 9000 + MAX_SIZE += 1 # Error reported by type checker + + class Connection: + TIMEOUT: Final[int] = 10 + class FastConnector(Connection): + TIMEOUT = 1 # Error reported by type checker + + There is no runtime checking of these properties. + """ + + __slots__ = ('__type__',) + + def __init__(self, tp=None, **kwds): + self.__type__ = tp + + def __getitem__(self, item): + cls = type(self) + if self.__type__ is None: + return cls(typing._type_check(item, + f'{cls.__name__[1:]} accepts only single type.'), + _root=True) + raise TypeError(f'{cls.__name__[1:]} cannot be further subscripted') + + def _eval_type(self, globalns, localns): + new_tp = typing._eval_type(self.__type__, globalns, localns) + if new_tp == self.__type__: + return self + return type(self)(new_tp, _root=True) + + def __repr__(self): + r = super().__repr__() + if self.__type__ is not None: + r += f'[{typing._type_repr(self.__type__)}]' + return r + + def __hash__(self): + return hash((type(self).__name__, self.__type__)) + + def __eq__(self, other): + if not isinstance(other, _Final): + return NotImplemented + if self.__type__ is not None: + return self.__type__ == other.__type__ + return self is other + + Final = _Final(_root=True) + + +# 3.8+ +if hasattr(typing, 'final'): + final = typing.final +# 3.6-3.7 +else: + def final(f): + """This decorator can be used to indicate to type checkers that + the decorated method cannot be overridden, and decorated class + cannot be subclassed. For example: + + class Base: + @final + def done(self) -> None: + ... + class Sub(Base): + def done(self) -> None: # Error reported by type checker + ... + @final + class Leaf: + ... + class Other(Leaf): # Error reported by type checker + ... + + There is no runtime checking of these properties. + """ + return f + + +def IntVar(name): + return typing.TypeVar(name) + + +# 3.8+: +if hasattr(typing, 'Literal'): + Literal = typing.Literal +# 3.7: +elif sys.version_info[:2] >= (3, 7): + class _LiteralForm(typing._SpecialForm, _root=True): + + def __repr__(self): + return 'typing_extensions.' + self._name + + def __getitem__(self, parameters): + return typing._GenericAlias(self, parameters) + + Literal = _LiteralForm('Literal', + doc="""A type that can be used to indicate to type checkers + that the corresponding value has a value literally equivalent + to the provided parameter. For example: + + var: Literal[4] = 4 + + The type checker understands that 'var' is literally equal to + the value 4 and no other value. + + Literal[...] cannot be subclassed. There is no runtime + checking verifying that the parameter is actually a value + instead of a type.""") +# 3.6: +else: + class _Literal(typing._FinalTypingBase, _root=True): + """A type that can be used to indicate to type checkers that the + corresponding value has a value literally equivalent to the + provided parameter. For example: + + var: Literal[4] = 4 + + The type checker understands that 'var' is literally equal to the + value 4 and no other value. + + Literal[...] cannot be subclassed. There is no runtime checking + verifying that the parameter is actually a value instead of a type. + """ + + __slots__ = ('__values__',) + + def __init__(self, values=None, **kwds): + self.__values__ = values + + def __getitem__(self, values): + cls = type(self) + if self.__values__ is None: + if not isinstance(values, tuple): + values = (values,) + return cls(values, _root=True) + raise TypeError(f'{cls.__name__[1:]} cannot be further subscripted') + + def _eval_type(self, globalns, localns): + return self + + def __repr__(self): + r = super().__repr__() + if self.__values__ is not None: + r += f'[{", ".join(map(typing._type_repr, self.__values__))}]' + return r + + def __hash__(self): + return hash((type(self).__name__, self.__values__)) + + def __eq__(self, other): + if not isinstance(other, _Literal): + return NotImplemented + if self.__values__ is not None: + return self.__values__ == other.__values__ + return self is other + + Literal = _Literal(_root=True) + + +_overload_dummy = typing._overload_dummy # noqa +overload = typing.overload + + +# This is not a real generic class. Don't use outside annotations. +Type = typing.Type + +# Various ABCs mimicking those in collections.abc. +# A few are simply re-exported for completeness. + + +class _ExtensionsGenericMeta(GenericMeta): + def __subclasscheck__(self, subclass): + """This mimics a more modern GenericMeta.__subclasscheck__() logic + (that does not have problems with recursion) to work around interactions + between collections, typing, and typing_extensions on older + versions of Python, see https://github.com/python/typing/issues/501. + """ + if self.__origin__ is not None: + if sys._getframe(1).f_globals['__name__'] not in ['abc', 'functools']: + raise TypeError("Parameterized generics cannot be used with class " + "or instance checks") + return False + if not self.__extra__: + return super().__subclasscheck__(subclass) + res = self.__extra__.__subclasshook__(subclass) + if res is not NotImplemented: + return res + if self.__extra__ in subclass.__mro__: + return True + for scls in self.__extra__.__subclasses__(): + if isinstance(scls, GenericMeta): + continue + if issubclass(subclass, scls): + return True + return False + + +Awaitable = typing.Awaitable +Coroutine = typing.Coroutine +AsyncIterable = typing.AsyncIterable +AsyncIterator = typing.AsyncIterator + +# 3.6.1+ +if hasattr(typing, 'Deque'): + Deque = typing.Deque +# 3.6.0 +else: + class Deque(collections.deque, typing.MutableSequence[T], + metaclass=_ExtensionsGenericMeta, + extra=collections.deque): + __slots__ = () + + def __new__(cls, *args, **kwds): + if cls._gorg is Deque: + return collections.deque(*args, **kwds) + return typing._generic_new(collections.deque, cls, *args, **kwds) + +ContextManager = typing.ContextManager +# 3.6.2+ +if hasattr(typing, 'AsyncContextManager'): + AsyncContextManager = typing.AsyncContextManager +# 3.6.0-3.6.1 +else: + from _collections_abc import _check_methods as _check_methods_in_mro # noqa + + class AsyncContextManager(typing.Generic[T_co]): + __slots__ = () + + async def __aenter__(self): + return self + + @abc.abstractmethod + async def __aexit__(self, exc_type, exc_value, traceback): + return None + + @classmethod + def __subclasshook__(cls, C): + if cls is AsyncContextManager: + return _check_methods_in_mro(C, "__aenter__", "__aexit__") + return NotImplemented + +DefaultDict = typing.DefaultDict + +# 3.7.2+ +if hasattr(typing, 'OrderedDict'): + OrderedDict = typing.OrderedDict +# 3.7.0-3.7.2 +elif (3, 7, 0) <= sys.version_info[:3] < (3, 7, 2): + OrderedDict = typing._alias(collections.OrderedDict, (KT, VT)) +# 3.6 +else: + class OrderedDict(collections.OrderedDict, typing.MutableMapping[KT, VT], + metaclass=_ExtensionsGenericMeta, + extra=collections.OrderedDict): + + __slots__ = () + + def __new__(cls, *args, **kwds): + if cls._gorg is OrderedDict: + return collections.OrderedDict(*args, **kwds) + return typing._generic_new(collections.OrderedDict, cls, *args, **kwds) + +# 3.6.2+ +if hasattr(typing, 'Counter'): + Counter = typing.Counter +# 3.6.0-3.6.1 +else: + class Counter(collections.Counter, + typing.Dict[T, int], + metaclass=_ExtensionsGenericMeta, extra=collections.Counter): + + __slots__ = () + + def __new__(cls, *args, **kwds): + if cls._gorg is Counter: + return collections.Counter(*args, **kwds) + return typing._generic_new(collections.Counter, cls, *args, **kwds) + +# 3.6.1+ +if hasattr(typing, 'ChainMap'): + ChainMap = typing.ChainMap +elif hasattr(collections, 'ChainMap'): + class ChainMap(collections.ChainMap, typing.MutableMapping[KT, VT], + metaclass=_ExtensionsGenericMeta, + extra=collections.ChainMap): + + __slots__ = () + + def __new__(cls, *args, **kwds): + if cls._gorg is ChainMap: + return collections.ChainMap(*args, **kwds) + return typing._generic_new(collections.ChainMap, cls, *args, **kwds) + +# 3.6.1+ +if hasattr(typing, 'AsyncGenerator'): + AsyncGenerator = typing.AsyncGenerator +# 3.6.0 +else: + class AsyncGenerator(AsyncIterator[T_co], typing.Generic[T_co, T_contra], + metaclass=_ExtensionsGenericMeta, + extra=collections.abc.AsyncGenerator): + __slots__ = () + +NewType = typing.NewType +Text = typing.Text +TYPE_CHECKING = typing.TYPE_CHECKING + + +def _gorg(cls): + """This function exists for compatibility with old typing versions.""" + assert isinstance(cls, GenericMeta) + if hasattr(cls, '_gorg'): + return cls._gorg + while cls.__origin__ is not None: + cls = cls.__origin__ + return cls + + +_PROTO_WHITELIST = ['Callable', 'Awaitable', + 'Iterable', 'Iterator', 'AsyncIterable', 'AsyncIterator', + 'Hashable', 'Sized', 'Container', 'Collection', 'Reversible', + 'ContextManager', 'AsyncContextManager'] + + +def _get_protocol_attrs(cls): + attrs = set() + for base in cls.__mro__[:-1]: # without object + if base.__name__ in ('Protocol', 'Generic'): + continue + annotations = getattr(base, '__annotations__', {}) + for attr in list(base.__dict__.keys()) + list(annotations.keys()): + if (not attr.startswith('_abc_') and attr not in ( + '__abstractmethods__', '__annotations__', '__weakref__', + '_is_protocol', '_is_runtime_protocol', '__dict__', + '__args__', '__slots__', + '__next_in_mro__', '__parameters__', '__origin__', + '__orig_bases__', '__extra__', '__tree_hash__', + '__doc__', '__subclasshook__', '__init__', '__new__', + '__module__', '_MutableMapping__marker', '_gorg')): + attrs.add(attr) + return attrs + + +def _is_callable_members_only(cls): + return all(callable(getattr(cls, attr, None)) for attr in _get_protocol_attrs(cls)) + + +# 3.8+ +if hasattr(typing, 'Protocol'): + Protocol = typing.Protocol +# 3.7 +elif PEP_560: + from typing import _collect_type_vars # noqa + + def _no_init(self, *args, **kwargs): + if type(self)._is_protocol: + raise TypeError('Protocols cannot be instantiated') + + class _ProtocolMeta(abc.ABCMeta): + # This metaclass is a bit unfortunate and exists only because of the lack + # of __instancehook__. + def __instancecheck__(cls, instance): + # We need this method for situations where attributes are + # assigned in __init__. + if ((not getattr(cls, '_is_protocol', False) or + _is_callable_members_only(cls)) and + issubclass(instance.__class__, cls)): + return True + if cls._is_protocol: + if all(hasattr(instance, attr) and + (not callable(getattr(cls, attr, None)) or + getattr(instance, attr) is not None) + for attr in _get_protocol_attrs(cls)): + return True + return super().__instancecheck__(instance) + + class Protocol(metaclass=_ProtocolMeta): + # There is quite a lot of overlapping code with typing.Generic. + # Unfortunately it is hard to avoid this while these live in two different + # modules. The duplicated code will be removed when Protocol is moved to typing. + """Base class for protocol classes. Protocol classes are defined as:: + + class Proto(Protocol): + def meth(self) -> int: + ... + + Such classes are primarily used with static type checkers that recognize + structural subtyping (static duck-typing), for example:: + + class C: + def meth(self) -> int: + return 0 + + def func(x: Proto) -> int: + return x.meth() + + func(C()) # Passes static type check + + See PEP 544 for details. Protocol classes decorated with + @typing_extensions.runtime act as simple-minded runtime protocol that checks + only the presence of given attributes, ignoring their type signatures. + + Protocol classes can be generic, they are defined as:: + + class GenProto(Protocol[T]): + def meth(self) -> T: + ... + """ + __slots__ = () + _is_protocol = True + + def __new__(cls, *args, **kwds): + if cls is Protocol: + raise TypeError("Type Protocol cannot be instantiated; " + "it can only be used as a base class") + return super().__new__(cls) + + @typing._tp_cache + def __class_getitem__(cls, params): + if not isinstance(params, tuple): + params = (params,) + if not params and cls is not typing.Tuple: + raise TypeError( + f"Parameter list to {cls.__qualname__}[...] cannot be empty") + msg = "Parameters to generic types must be types." + params = tuple(typing._type_check(p, msg) for p in params) # noqa + if cls is Protocol: + # Generic can only be subscripted with unique type variables. + if not all(isinstance(p, typing.TypeVar) for p in params): + i = 0 + while isinstance(params[i], typing.TypeVar): + i += 1 + raise TypeError( + "Parameters to Protocol[...] must all be type variables." + f" Parameter {i + 1} is {params[i]}") + if len(set(params)) != len(params): + raise TypeError( + "Parameters to Protocol[...] must all be unique") + else: + # Subscripting a regular Generic subclass. + _check_generic(cls, params) + return typing._GenericAlias(cls, params) + + def __init_subclass__(cls, *args, **kwargs): + tvars = [] + if '__orig_bases__' in cls.__dict__: + error = typing.Generic in cls.__orig_bases__ + else: + error = typing.Generic in cls.__bases__ + if error: + raise TypeError("Cannot inherit from plain Generic") + if '__orig_bases__' in cls.__dict__: + tvars = _collect_type_vars(cls.__orig_bases__) + # Look for Generic[T1, ..., Tn] or Protocol[T1, ..., Tn]. + # If found, tvars must be a subset of it. + # If not found, tvars is it. + # Also check for and reject plain Generic, + # and reject multiple Generic[...] and/or Protocol[...]. + gvars = None + for base in cls.__orig_bases__: + if (isinstance(base, typing._GenericAlias) and + base.__origin__ in (typing.Generic, Protocol)): + # for error messages + the_base = base.__origin__.__name__ + if gvars is not None: + raise TypeError( + "Cannot inherit from Generic[...]" + " and/or Protocol[...] multiple types.") + gvars = base.__parameters__ + if gvars is None: + gvars = tvars + else: + tvarset = set(tvars) + gvarset = set(gvars) + if not tvarset <= gvarset: + s_vars = ', '.join(str(t) for t in tvars if t not in gvarset) + s_args = ', '.join(str(g) for g in gvars) + raise TypeError(f"Some type variables ({s_vars}) are" + f" not listed in {the_base}[{s_args}]") + tvars = gvars + cls.__parameters__ = tuple(tvars) + + # Determine if this is a protocol or a concrete subclass. + if not cls.__dict__.get('_is_protocol', None): + cls._is_protocol = any(b is Protocol for b in cls.__bases__) + + # Set (or override) the protocol subclass hook. + def _proto_hook(other): + if not cls.__dict__.get('_is_protocol', None): + return NotImplemented + if not getattr(cls, '_is_runtime_protocol', False): + if sys._getframe(2).f_globals['__name__'] in ['abc', 'functools']: + return NotImplemented + raise TypeError("Instance and class checks can only be used with" + " @runtime protocols") + if not _is_callable_members_only(cls): + if sys._getframe(2).f_globals['__name__'] in ['abc', 'functools']: + return NotImplemented + raise TypeError("Protocols with non-method members" + " don't support issubclass()") + if not isinstance(other, type): + # Same error as for issubclass(1, int) + raise TypeError('issubclass() arg 1 must be a class') + for attr in _get_protocol_attrs(cls): + for base in other.__mro__: + if attr in base.__dict__: + if base.__dict__[attr] is None: + return NotImplemented + break + annotations = getattr(base, '__annotations__', {}) + if (isinstance(annotations, typing.Mapping) and + attr in annotations and + isinstance(other, _ProtocolMeta) and + other._is_protocol): + break + else: + return NotImplemented + return True + if '__subclasshook__' not in cls.__dict__: + cls.__subclasshook__ = _proto_hook + + # We have nothing more to do for non-protocols. + if not cls._is_protocol: + return + + # Check consistency of bases. + for base in cls.__bases__: + if not (base in (object, typing.Generic) or + base.__module__ == 'collections.abc' and + base.__name__ in _PROTO_WHITELIST or + isinstance(base, _ProtocolMeta) and base._is_protocol): + raise TypeError('Protocols can only inherit from other' + f' protocols, got {repr(base)}') + cls.__init__ = _no_init +# 3.6 +else: + from typing import _next_in_mro, _type_check # noqa + + def _no_init(self, *args, **kwargs): + if type(self)._is_protocol: + raise TypeError('Protocols cannot be instantiated') + + class _ProtocolMeta(GenericMeta): + """Internal metaclass for Protocol. + + This exists so Protocol classes can be generic without deriving + from Generic. + """ + def __new__(cls, name, bases, namespace, + tvars=None, args=None, origin=None, extra=None, orig_bases=None): + # This is just a version copied from GenericMeta.__new__ that + # includes "Protocol" special treatment. (Comments removed for brevity.) + assert extra is None # Protocols should not have extra + if tvars is not None: + assert origin is not None + assert all(isinstance(t, typing.TypeVar) for t in tvars), tvars + else: + tvars = _type_vars(bases) + gvars = None + for base in bases: + if base is typing.Generic: + raise TypeError("Cannot inherit from plain Generic") + if (isinstance(base, GenericMeta) and + base.__origin__ in (typing.Generic, Protocol)): + if gvars is not None: + raise TypeError( + "Cannot inherit from Generic[...] or" + " Protocol[...] multiple times.") + gvars = base.__parameters__ + if gvars is None: + gvars = tvars + else: + tvarset = set(tvars) + gvarset = set(gvars) + if not tvarset <= gvarset: + s_vars = ", ".join(str(t) for t in tvars if t not in gvarset) + s_args = ", ".join(str(g) for g in gvars) + cls_name = "Generic" if any(b.__origin__ is typing.Generic + for b in bases) else "Protocol" + raise TypeError(f"Some type variables ({s_vars}) are" + f" not listed in {cls_name}[{s_args}]") + tvars = gvars + + initial_bases = bases + if (extra is not None and type(extra) is abc.ABCMeta and + extra not in bases): + bases = (extra,) + bases + bases = tuple(_gorg(b) if isinstance(b, GenericMeta) else b + for b in bases) + if any(isinstance(b, GenericMeta) and b is not typing.Generic for b in bases): + bases = tuple(b for b in bases if b is not typing.Generic) + namespace.update({'__origin__': origin, '__extra__': extra}) + self = super(GenericMeta, cls).__new__(cls, name, bases, namespace, + _root=True) + super(GenericMeta, self).__setattr__('_gorg', + self if not origin else + _gorg(origin)) + self.__parameters__ = tvars + self.__args__ = tuple(... if a is typing._TypingEllipsis else + () if a is typing._TypingEmpty else + a for a in args) if args else None + self.__next_in_mro__ = _next_in_mro(self) + if orig_bases is None: + self.__orig_bases__ = initial_bases + elif origin is not None: + self._abc_registry = origin._abc_registry + self._abc_cache = origin._abc_cache + if hasattr(self, '_subs_tree'): + self.__tree_hash__ = (hash(self._subs_tree()) if origin else + super(GenericMeta, self).__hash__()) + return self + + def __init__(cls, *args, **kwargs): + super().__init__(*args, **kwargs) + if not cls.__dict__.get('_is_protocol', None): + cls._is_protocol = any(b is Protocol or + isinstance(b, _ProtocolMeta) and + b.__origin__ is Protocol + for b in cls.__bases__) + if cls._is_protocol: + for base in cls.__mro__[1:]: + if not (base in (object, typing.Generic) or + base.__module__ == 'collections.abc' and + base.__name__ in _PROTO_WHITELIST or + isinstance(base, typing.TypingMeta) and base._is_protocol or + isinstance(base, GenericMeta) and + base.__origin__ is typing.Generic): + raise TypeError(f'Protocols can only inherit from other' + f' protocols, got {repr(base)}') + + cls.__init__ = _no_init + + def _proto_hook(other): + if not cls.__dict__.get('_is_protocol', None): + return NotImplemented + if not isinstance(other, type): + # Same error as for issubclass(1, int) + raise TypeError('issubclass() arg 1 must be a class') + for attr in _get_protocol_attrs(cls): + for base in other.__mro__: + if attr in base.__dict__: + if base.__dict__[attr] is None: + return NotImplemented + break + annotations = getattr(base, '__annotations__', {}) + if (isinstance(annotations, typing.Mapping) and + attr in annotations and + isinstance(other, _ProtocolMeta) and + other._is_protocol): + break + else: + return NotImplemented + return True + if '__subclasshook__' not in cls.__dict__: + cls.__subclasshook__ = _proto_hook + + def __instancecheck__(self, instance): + # We need this method for situations where attributes are + # assigned in __init__. + if ((not getattr(self, '_is_protocol', False) or + _is_callable_members_only(self)) and + issubclass(instance.__class__, self)): + return True + if self._is_protocol: + if all(hasattr(instance, attr) and + (not callable(getattr(self, attr, None)) or + getattr(instance, attr) is not None) + for attr in _get_protocol_attrs(self)): + return True + return super(GenericMeta, self).__instancecheck__(instance) + + def __subclasscheck__(self, cls): + if self.__origin__ is not None: + if sys._getframe(1).f_globals['__name__'] not in ['abc', 'functools']: + raise TypeError("Parameterized generics cannot be used with class " + "or instance checks") + return False + if (self.__dict__.get('_is_protocol', None) and + not self.__dict__.get('_is_runtime_protocol', None)): + if sys._getframe(1).f_globals['__name__'] in ['abc', + 'functools', + 'typing']: + return False + raise TypeError("Instance and class checks can only be used with" + " @runtime protocols") + if (self.__dict__.get('_is_runtime_protocol', None) and + not _is_callable_members_only(self)): + if sys._getframe(1).f_globals['__name__'] in ['abc', + 'functools', + 'typing']: + return super(GenericMeta, self).__subclasscheck__(cls) + raise TypeError("Protocols with non-method members" + " don't support issubclass()") + return super(GenericMeta, self).__subclasscheck__(cls) + + @typing._tp_cache + def __getitem__(self, params): + # We also need to copy this from GenericMeta.__getitem__ to get + # special treatment of "Protocol". (Comments removed for brevity.) + if not isinstance(params, tuple): + params = (params,) + if not params and _gorg(self) is not typing.Tuple: + raise TypeError( + f"Parameter list to {self.__qualname__}[...] cannot be empty") + msg = "Parameters to generic types must be types." + params = tuple(_type_check(p, msg) for p in params) + if self in (typing.Generic, Protocol): + if not all(isinstance(p, typing.TypeVar) for p in params): + raise TypeError( + f"Parameters to {repr(self)}[...] must all be type variables") + if len(set(params)) != len(params): + raise TypeError( + f"Parameters to {repr(self)}[...] must all be unique") + tvars = params + args = params + elif self in (typing.Tuple, typing.Callable): + tvars = _type_vars(params) + args = params + elif self.__origin__ in (typing.Generic, Protocol): + raise TypeError(f"Cannot subscript already-subscripted {repr(self)}") + else: + _check_generic(self, params) + tvars = _type_vars(params) + args = params + + prepend = (self,) if self.__origin__ is None else () + return self.__class__(self.__name__, + prepend + self.__bases__, + _no_slots_copy(self.__dict__), + tvars=tvars, + args=args, + origin=self, + extra=self.__extra__, + orig_bases=self.__orig_bases__) + + class Protocol(metaclass=_ProtocolMeta): + """Base class for protocol classes. Protocol classes are defined as:: + + class Proto(Protocol): + def meth(self) -> int: + ... + + Such classes are primarily used with static type checkers that recognize + structural subtyping (static duck-typing), for example:: + + class C: + def meth(self) -> int: + return 0 + + def func(x: Proto) -> int: + return x.meth() + + func(C()) # Passes static type check + + See PEP 544 for details. Protocol classes decorated with + @typing_extensions.runtime act as simple-minded runtime protocol that checks + only the presence of given attributes, ignoring their type signatures. + + Protocol classes can be generic, they are defined as:: + + class GenProto(Protocol[T]): + def meth(self) -> T: + ... + """ + __slots__ = () + _is_protocol = True + + def __new__(cls, *args, **kwds): + if _gorg(cls) is Protocol: + raise TypeError("Type Protocol cannot be instantiated; " + "it can be used only as a base class") + return typing._generic_new(cls.__next_in_mro__, cls, *args, **kwds) + + +# 3.8+ +if hasattr(typing, 'runtime_checkable'): + runtime_checkable = typing.runtime_checkable +# 3.6-3.7 +else: + def runtime_checkable(cls): + """Mark a protocol class as a runtime protocol, so that it + can be used with isinstance() and issubclass(). Raise TypeError + if applied to a non-protocol class. + + This allows a simple-minded structural check very similar to the + one-offs in collections.abc such as Hashable. + """ + if not isinstance(cls, _ProtocolMeta) or not cls._is_protocol: + raise TypeError('@runtime_checkable can be only applied to protocol classes,' + f' got {cls!r}') + cls._is_runtime_protocol = True + return cls + + +# Exists for backwards compatibility. +runtime = runtime_checkable + + +# 3.8+ +if hasattr(typing, 'SupportsIndex'): + SupportsIndex = typing.SupportsIndex +# 3.6-3.7 +else: + @runtime_checkable + class SupportsIndex(Protocol): + __slots__ = () + + @abc.abstractmethod + def __index__(self) -> int: + pass + + +if sys.version_info >= (3, 9, 2): + # The standard library TypedDict in Python 3.8 does not store runtime information + # about which (if any) keys are optional. See https://bugs.python.org/issue38834 + # The standard library TypedDict in Python 3.9.0/1 does not honour the "total" + # keyword with old-style TypedDict(). See https://bugs.python.org/issue42059 + TypedDict = typing.TypedDict +else: + def _check_fails(cls, other): + try: + if sys._getframe(1).f_globals['__name__'] not in ['abc', + 'functools', + 'typing']: + # Typed dicts are only for static structural subtyping. + raise TypeError('TypedDict does not support instance and class checks') + except (AttributeError, ValueError): + pass + return False + + def _dict_new(*args, **kwargs): + if not args: + raise TypeError('TypedDict.__new__(): not enough arguments') + _, args = args[0], args[1:] # allow the "cls" keyword be passed + return dict(*args, **kwargs) + + _dict_new.__text_signature__ = '($cls, _typename, _fields=None, /, **kwargs)' + + def _typeddict_new(*args, total=True, **kwargs): + if not args: + raise TypeError('TypedDict.__new__(): not enough arguments') + _, args = args[0], args[1:] # allow the "cls" keyword be passed + if args: + typename, args = args[0], args[1:] # allow the "_typename" keyword be passed + elif '_typename' in kwargs: + typename = kwargs.pop('_typename') + import warnings + warnings.warn("Passing '_typename' as keyword argument is deprecated", + DeprecationWarning, stacklevel=2) + else: + raise TypeError("TypedDict.__new__() missing 1 required positional " + "argument: '_typename'") + if args: + try: + fields, = args # allow the "_fields" keyword be passed + except ValueError: + raise TypeError('TypedDict.__new__() takes from 2 to 3 ' + f'positional arguments but {len(args) + 2} ' + 'were given') + elif '_fields' in kwargs and len(kwargs) == 1: + fields = kwargs.pop('_fields') + import warnings + warnings.warn("Passing '_fields' as keyword argument is deprecated", + DeprecationWarning, stacklevel=2) + else: + fields = None + + if fields is None: + fields = kwargs + elif kwargs: + raise TypeError("TypedDict takes either a dict or keyword arguments," + " but not both") + + ns = {'__annotations__': dict(fields)} + try: + # Setting correct module is necessary to make typed dict classes pickleable. + ns['__module__'] = sys._getframe(1).f_globals.get('__name__', '__main__') + except (AttributeError, ValueError): + pass + + return _TypedDictMeta(typename, (), ns, total=total) + + _typeddict_new.__text_signature__ = ('($cls, _typename, _fields=None,' + ' /, *, total=True, **kwargs)') + + class _TypedDictMeta(type): + def __init__(cls, name, bases, ns, total=True): + super().__init__(name, bases, ns) + + def __new__(cls, name, bases, ns, total=True): + # Create new typed dict class object. + # This method is called directly when TypedDict is subclassed, + # or via _typeddict_new when TypedDict is instantiated. This way + # TypedDict supports all three syntaxes described in its docstring. + # Subclasses and instances of TypedDict return actual dictionaries + # via _dict_new. + ns['__new__'] = _typeddict_new if name == 'TypedDict' else _dict_new + tp_dict = super().__new__(cls, name, (dict,), ns) + + annotations = {} + own_annotations = ns.get('__annotations__', {}) + own_annotation_keys = set(own_annotations.keys()) + msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type" + own_annotations = { + n: typing._type_check(tp, msg) for n, tp in own_annotations.items() + } + required_keys = set() + optional_keys = set() + + for base in bases: + annotations.update(base.__dict__.get('__annotations__', {})) + required_keys.update(base.__dict__.get('__required_keys__', ())) + optional_keys.update(base.__dict__.get('__optional_keys__', ())) + + annotations.update(own_annotations) + if total: + required_keys.update(own_annotation_keys) + else: + optional_keys.update(own_annotation_keys) + + tp_dict.__annotations__ = annotations + tp_dict.__required_keys__ = frozenset(required_keys) + tp_dict.__optional_keys__ = frozenset(optional_keys) + if not hasattr(tp_dict, '__total__'): + tp_dict.__total__ = total + return tp_dict + + __instancecheck__ = __subclasscheck__ = _check_fails + + TypedDict = _TypedDictMeta('TypedDict', (dict,), {}) + TypedDict.__module__ = __name__ + TypedDict.__doc__ = \ + """A simple typed name space. At runtime it is equivalent to a plain dict. + + TypedDict creates a dictionary type that expects all of its + instances to have a certain set of keys, with each key + associated with a value of a consistent type. This expectation + is not checked at runtime but is only enforced by type checkers. + Usage:: + + class Point2D(TypedDict): + x: int + y: int + label: str + + a: Point2D = {'x': 1, 'y': 2, 'label': 'good'} # OK + b: Point2D = {'z': 3, 'label': 'bad'} # Fails type check + + assert Point2D(x=1, y=2, label='first') == dict(x=1, y=2, label='first') + + The type info can be accessed via the Point2D.__annotations__ dict, and + the Point2D.__required_keys__ and Point2D.__optional_keys__ frozensets. + TypedDict supports two additional equivalent forms:: + + Point2D = TypedDict('Point2D', x=int, y=int, label=str) + Point2D = TypedDict('Point2D', {'x': int, 'y': int, 'label': str}) + + The class syntax is only supported in Python 3.6+, while two other + syntax forms work for Python 2.7 and 3.2+ + """ + + +# Python 3.9+ has PEP 593 (Annotated and modified get_type_hints) +if hasattr(typing, 'Annotated'): + Annotated = typing.Annotated + get_type_hints = typing.get_type_hints + # Not exported and not a public API, but needed for get_origin() and get_args() + # to work. + _AnnotatedAlias = typing._AnnotatedAlias +# 3.7-3.8 +elif PEP_560: + class _AnnotatedAlias(typing._GenericAlias, _root=True): + """Runtime representation of an annotated type. + + At its core 'Annotated[t, dec1, dec2, ...]' is an alias for the type 't' + with extra annotations. The alias behaves like a normal typing alias, + instantiating is the same as instantiating the underlying type, binding + it to types is also the same. + """ + def __init__(self, origin, metadata): + if isinstance(origin, _AnnotatedAlias): + metadata = origin.__metadata__ + metadata + origin = origin.__origin__ + super().__init__(origin, origin) + self.__metadata__ = metadata + + def copy_with(self, params): + assert len(params) == 1 + new_type = params[0] + return _AnnotatedAlias(new_type, self.__metadata__) + + def __repr__(self): + return (f"typing_extensions.Annotated[{typing._type_repr(self.__origin__)}, " + f"{', '.join(repr(a) for a in self.__metadata__)}]") + + def __reduce__(self): + return operator.getitem, ( + Annotated, (self.__origin__,) + self.__metadata__ + ) + + def __eq__(self, other): + if not isinstance(other, _AnnotatedAlias): + return NotImplemented + if self.__origin__ != other.__origin__: + return False + return self.__metadata__ == other.__metadata__ + + def __hash__(self): + return hash((self.__origin__, self.__metadata__)) + + class Annotated: + """Add context specific metadata to a type. + + Example: Annotated[int, runtime_check.Unsigned] indicates to the + hypothetical runtime_check module that this type is an unsigned int. + Every other consumer of this type can ignore this metadata and treat + this type as int. + + The first argument to Annotated must be a valid type (and will be in + the __origin__ field), the remaining arguments are kept as a tuple in + the __extra__ field. + + Details: + + - It's an error to call `Annotated` with less than two arguments. + - Nested Annotated are flattened:: + + Annotated[Annotated[T, Ann1, Ann2], Ann3] == Annotated[T, Ann1, Ann2, Ann3] + + - Instantiating an annotated type is equivalent to instantiating the + underlying type:: + + Annotated[C, Ann1](5) == C(5) + + - Annotated can be used as a generic type alias:: + + Optimized = Annotated[T, runtime.Optimize()] + Optimized[int] == Annotated[int, runtime.Optimize()] + + OptimizedList = Annotated[List[T], runtime.Optimize()] + OptimizedList[int] == Annotated[List[int], runtime.Optimize()] + """ + + __slots__ = () + + def __new__(cls, *args, **kwargs): + raise TypeError("Type Annotated cannot be instantiated.") + + @typing._tp_cache + def __class_getitem__(cls, params): + if not isinstance(params, tuple) or len(params) < 2: + raise TypeError("Annotated[...] should be used " + "with at least two arguments (a type and an " + "annotation).") + msg = "Annotated[t, ...]: t must be a type." + origin = typing._type_check(params[0], msg) + metadata = tuple(params[1:]) + return _AnnotatedAlias(origin, metadata) + + def __init_subclass__(cls, *args, **kwargs): + raise TypeError( + f"Cannot subclass {cls.__module__}.Annotated" + ) + + def _strip_annotations(t): + """Strips the annotations from a given type. + """ + if isinstance(t, _AnnotatedAlias): + return _strip_annotations(t.__origin__) + if isinstance(t, typing._GenericAlias): + stripped_args = tuple(_strip_annotations(a) for a in t.__args__) + if stripped_args == t.__args__: + return t + res = t.copy_with(stripped_args) + res._special = t._special + return res + return t + + def get_type_hints(obj, globalns=None, localns=None, include_extras=False): + """Return type hints for an object. + + This is often the same as obj.__annotations__, but it handles + forward references encoded as string literals, adds Optional[t] if a + default value equal to None is set and recursively replaces all + 'Annotated[T, ...]' with 'T' (unless 'include_extras=True'). + + The argument may be a module, class, method, or function. The annotations + are returned as a dictionary. For classes, annotations include also + inherited members. + + TypeError is raised if the argument is not of a type that can contain + annotations, and an empty dictionary is returned if no annotations are + present. + + BEWARE -- the behavior of globalns and localns is counterintuitive + (unless you are familiar with how eval() and exec() work). The + search order is locals first, then globals. + + - If no dict arguments are passed, an attempt is made to use the + globals from obj (or the respective module's globals for classes), + and these are also used as the locals. If the object does not appear + to have globals, an empty dictionary is used. + + - If one dict argument is passed, it is used for both globals and + locals. + + - If two dict arguments are passed, they specify globals and + locals, respectively. + """ + hint = typing.get_type_hints(obj, globalns=globalns, localns=localns) + if include_extras: + return hint + return {k: _strip_annotations(t) for k, t in hint.items()} +# 3.6 +else: + + def _is_dunder(name): + """Returns True if name is a __dunder_variable_name__.""" + return len(name) > 4 and name.startswith('__') and name.endswith('__') + + # Prior to Python 3.7 types did not have `copy_with`. A lot of the equality + # checks, argument expansion etc. are done on the _subs_tre. As a result we + # can't provide a get_type_hints function that strips out annotations. + + class AnnotatedMeta(typing.GenericMeta): + """Metaclass for Annotated""" + + def __new__(cls, name, bases, namespace, **kwargs): + if any(b is not object for b in bases): + raise TypeError("Cannot subclass " + str(Annotated)) + return super().__new__(cls, name, bases, namespace, **kwargs) + + @property + def __metadata__(self): + return self._subs_tree()[2] + + def _tree_repr(self, tree): + cls, origin, metadata = tree + if not isinstance(origin, tuple): + tp_repr = typing._type_repr(origin) + else: + tp_repr = origin[0]._tree_repr(origin) + metadata_reprs = ", ".join(repr(arg) for arg in metadata) + return f'{cls}[{tp_repr}, {metadata_reprs}]' + + def _subs_tree(self, tvars=None, args=None): # noqa + if self is Annotated: + return Annotated + res = super()._subs_tree(tvars=tvars, args=args) + # Flatten nested Annotated + if isinstance(res[1], tuple) and res[1][0] is Annotated: + sub_tp = res[1][1] + sub_annot = res[1][2] + return (Annotated, sub_tp, sub_annot + res[2]) + return res + + def _get_cons(self): + """Return the class used to create instance of this type.""" + if self.__origin__ is None: + raise TypeError("Cannot get the underlying type of a " + "non-specialized Annotated type.") + tree = self._subs_tree() + while isinstance(tree, tuple) and tree[0] is Annotated: + tree = tree[1] + if isinstance(tree, tuple): + return tree[0] + else: + return tree + + @typing._tp_cache + def __getitem__(self, params): + if not isinstance(params, tuple): + params = (params,) + if self.__origin__ is not None: # specializing an instantiated type + return super().__getitem__(params) + elif not isinstance(params, tuple) or len(params) < 2: + raise TypeError("Annotated[...] should be instantiated " + "with at least two arguments (a type and an " + "annotation).") + else: + msg = "Annotated[t, ...]: t must be a type." + tp = typing._type_check(params[0], msg) + metadata = tuple(params[1:]) + return self.__class__( + self.__name__, + self.__bases__, + _no_slots_copy(self.__dict__), + tvars=_type_vars((tp,)), + # Metadata is a tuple so it won't be touched by _replace_args et al. + args=(tp, metadata), + origin=self, + ) + + def __call__(self, *args, **kwargs): + cons = self._get_cons() + result = cons(*args, **kwargs) + try: + result.__orig_class__ = self + except AttributeError: + pass + return result + + def __getattr__(self, attr): + # For simplicity we just don't relay all dunder names + if self.__origin__ is not None and not _is_dunder(attr): + return getattr(self._get_cons(), attr) + raise AttributeError(attr) + + def __setattr__(self, attr, value): + if _is_dunder(attr) or attr.startswith('_abc_'): + super().__setattr__(attr, value) + elif self.__origin__ is None: + raise AttributeError(attr) + else: + setattr(self._get_cons(), attr, value) + + def __instancecheck__(self, obj): + raise TypeError("Annotated cannot be used with isinstance().") + + def __subclasscheck__(self, cls): + raise TypeError("Annotated cannot be used with issubclass().") + + class Annotated(metaclass=AnnotatedMeta): + """Add context specific metadata to a type. + + Example: Annotated[int, runtime_check.Unsigned] indicates to the + hypothetical runtime_check module that this type is an unsigned int. + Every other consumer of this type can ignore this metadata and treat + this type as int. + + The first argument to Annotated must be a valid type, the remaining + arguments are kept as a tuple in the __metadata__ field. + + Details: + + - It's an error to call `Annotated` with less than two arguments. + - Nested Annotated are flattened:: + + Annotated[Annotated[T, Ann1, Ann2], Ann3] == Annotated[T, Ann1, Ann2, Ann3] + + - Instantiating an annotated type is equivalent to instantiating the + underlying type:: + + Annotated[C, Ann1](5) == C(5) + + - Annotated can be used as a generic type alias:: + + Optimized = Annotated[T, runtime.Optimize()] + Optimized[int] == Annotated[int, runtime.Optimize()] + + OptimizedList = Annotated[List[T], runtime.Optimize()] + OptimizedList[int] == Annotated[List[int], runtime.Optimize()] + """ + +# Python 3.8 has get_origin() and get_args() but those implementations aren't +# Annotated-aware, so we can't use those. Python 3.9's versions don't support +# ParamSpecArgs and ParamSpecKwargs, so only Python 3.10's versions will do. +if sys.version_info[:2] >= (3, 10): + get_origin = typing.get_origin + get_args = typing.get_args +# 3.7-3.9 +elif PEP_560: + try: + # 3.9+ + from typing import _BaseGenericAlias + except ImportError: + _BaseGenericAlias = typing._GenericAlias + try: + # 3.9+ + from typing import GenericAlias + except ImportError: + GenericAlias = typing._GenericAlias + + def get_origin(tp): + """Get the unsubscripted version of a type. + + This supports generic types, Callable, Tuple, Union, Literal, Final, ClassVar + and Annotated. Return None for unsupported types. Examples:: + + get_origin(Literal[42]) is Literal + get_origin(int) is None + get_origin(ClassVar[int]) is ClassVar + get_origin(Generic) is Generic + get_origin(Generic[T]) is Generic + get_origin(Union[T, int]) is Union + get_origin(List[Tuple[T, T]][int]) == list + get_origin(P.args) is P + """ + if isinstance(tp, _AnnotatedAlias): + return Annotated + if isinstance(tp, (typing._GenericAlias, GenericAlias, _BaseGenericAlias, + ParamSpecArgs, ParamSpecKwargs)): + return tp.__origin__ + if tp is typing.Generic: + return typing.Generic + return None + + def get_args(tp): + """Get type arguments with all substitutions performed. + + For unions, basic simplifications used by Union constructor are performed. + Examples:: + get_args(Dict[str, int]) == (str, int) + get_args(int) == () + get_args(Union[int, Union[T, int], str][int]) == (int, str) + get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int]) + get_args(Callable[[], T][int]) == ([], int) + """ + if isinstance(tp, _AnnotatedAlias): + return (tp.__origin__,) + tp.__metadata__ + if isinstance(tp, (typing._GenericAlias, GenericAlias)): + if getattr(tp, "_special", False): + return () + res = tp.__args__ + if get_origin(tp) is collections.abc.Callable and res[0] is not Ellipsis: + res = (list(res[:-1]), res[-1]) + return res + return () + + +# 3.10+ +if hasattr(typing, 'TypeAlias'): + TypeAlias = typing.TypeAlias +# 3.9 +elif sys.version_info[:2] >= (3, 9): + class _TypeAliasForm(typing._SpecialForm, _root=True): + def __repr__(self): + return 'typing_extensions.' + self._name + + @_TypeAliasForm + def TypeAlias(self, parameters): + """Special marker indicating that an assignment should + be recognized as a proper type alias definition by type + checkers. + + For example:: + + Predicate: TypeAlias = Callable[..., bool] + + It's invalid when used anywhere except as in the example above. + """ + raise TypeError(f"{self} is not subscriptable") +# 3.7-3.8 +elif sys.version_info[:2] >= (3, 7): + class _TypeAliasForm(typing._SpecialForm, _root=True): + def __repr__(self): + return 'typing_extensions.' + self._name + + TypeAlias = _TypeAliasForm('TypeAlias', + doc="""Special marker indicating that an assignment should + be recognized as a proper type alias definition by type + checkers. + + For example:: + + Predicate: TypeAlias = Callable[..., bool] + + It's invalid when used anywhere except as in the example + above.""") +# 3.6 +else: + class _TypeAliasMeta(typing.TypingMeta): + """Metaclass for TypeAlias""" + + def __repr__(self): + return 'typing_extensions.TypeAlias' + + class _TypeAliasBase(typing._FinalTypingBase, metaclass=_TypeAliasMeta, _root=True): + """Special marker indicating that an assignment should + be recognized as a proper type alias definition by type + checkers. + + For example:: + + Predicate: TypeAlias = Callable[..., bool] + + It's invalid when used anywhere except as in the example above. + """ + __slots__ = () + + def __instancecheck__(self, obj): + raise TypeError("TypeAlias cannot be used with isinstance().") + + def __subclasscheck__(self, cls): + raise TypeError("TypeAlias cannot be used with issubclass().") + + def __repr__(self): + return 'typing_extensions.TypeAlias' + + TypeAlias = _TypeAliasBase(_root=True) + + +# Python 3.10+ has PEP 612 +if hasattr(typing, 'ParamSpecArgs'): + ParamSpecArgs = typing.ParamSpecArgs + ParamSpecKwargs = typing.ParamSpecKwargs +# 3.6-3.9 +else: + class _Immutable: + """Mixin to indicate that object should not be copied.""" + __slots__ = () + + def __copy__(self): + return self + + def __deepcopy__(self, memo): + return self + + class ParamSpecArgs(_Immutable): + """The args for a ParamSpec object. + + Given a ParamSpec object P, P.args is an instance of ParamSpecArgs. + + ParamSpecArgs objects have a reference back to their ParamSpec: + + P.args.__origin__ is P + + This type is meant for runtime introspection and has no special meaning to + static type checkers. + """ + def __init__(self, origin): + self.__origin__ = origin + + def __repr__(self): + return f"{self.__origin__.__name__}.args" + + class ParamSpecKwargs(_Immutable): + """The kwargs for a ParamSpec object. + + Given a ParamSpec object P, P.kwargs is an instance of ParamSpecKwargs. + + ParamSpecKwargs objects have a reference back to their ParamSpec: + + P.kwargs.__origin__ is P + + This type is meant for runtime introspection and has no special meaning to + static type checkers. + """ + def __init__(self, origin): + self.__origin__ = origin + + def __repr__(self): + return f"{self.__origin__.__name__}.kwargs" + +# 3.10+ +if hasattr(typing, 'ParamSpec'): + ParamSpec = typing.ParamSpec +# 3.6-3.9 +else: + + # Inherits from list as a workaround for Callable checks in Python < 3.9.2. + class ParamSpec(list): + """Parameter specification variable. + + Usage:: + + P = ParamSpec('P') + + Parameter specification variables exist primarily for the benefit of static + type checkers. They are used to forward the parameter types of one + callable to another callable, a pattern commonly found in higher order + functions and decorators. They are only valid when used in ``Concatenate``, + or s the first argument to ``Callable``. In Python 3.10 and higher, + they are also supported in user-defined Generics at runtime. + See class Generic for more information on generic types. An + example for annotating a decorator:: + + T = TypeVar('T') + P = ParamSpec('P') + + def add_logging(f: Callable[P, T]) -> Callable[P, T]: + '''A type-safe decorator to add logging to a function.''' + def inner(*args: P.args, **kwargs: P.kwargs) -> T: + logging.info(f'{f.__name__} was called') + return f(*args, **kwargs) + return inner + + @add_logging + def add_two(x: float, y: float) -> float: + '''Add two numbers together.''' + return x + y + + Parameter specification variables defined with covariant=True or + contravariant=True can be used to declare covariant or contravariant + generic types. These keyword arguments are valid, but their actual semantics + are yet to be decided. See PEP 612 for details. + + Parameter specification variables can be introspected. e.g.: + + P.__name__ == 'T' + P.__bound__ == None + P.__covariant__ == False + P.__contravariant__ == False + + Note that only parameter specification variables defined in global scope can + be pickled. + """ + + # Trick Generic __parameters__. + __class__ = typing.TypeVar + + @property + def args(self): + return ParamSpecArgs(self) + + @property + def kwargs(self): + return ParamSpecKwargs(self) + + def __init__(self, name, *, bound=None, covariant=False, contravariant=False): + super().__init__([self]) + self.__name__ = name + self.__covariant__ = bool(covariant) + self.__contravariant__ = bool(contravariant) + if bound: + self.__bound__ = typing._type_check(bound, 'Bound must be a type.') + else: + self.__bound__ = None + + # for pickling: + try: + def_mod = sys._getframe(1).f_globals.get('__name__', '__main__') + except (AttributeError, ValueError): + def_mod = None + if def_mod != 'typing_extensions': + self.__module__ = def_mod + + def __repr__(self): + if self.__covariant__: + prefix = '+' + elif self.__contravariant__: + prefix = '-' + else: + prefix = '~' + return prefix + self.__name__ + + def __hash__(self): + return object.__hash__(self) + + def __eq__(self, other): + return self is other + + def __reduce__(self): + return self.__name__ + + # Hack to get typing._type_check to pass. + def __call__(self, *args, **kwargs): + pass + + if not PEP_560: + # Only needed in 3.6. + def _get_type_vars(self, tvars): + if self not in tvars: + tvars.append(self) + + +# 3.6-3.9 +if not hasattr(typing, 'Concatenate'): + # Inherits from list as a workaround for Callable checks in Python < 3.9.2. + class _ConcatenateGenericAlias(list): + + # Trick Generic into looking into this for __parameters__. + if PEP_560: + __class__ = typing._GenericAlias + else: + __class__ = typing._TypingBase + + # Flag in 3.8. + _special = False + # Attribute in 3.6 and earlier. + _gorg = typing.Generic + + def __init__(self, origin, args): + super().__init__(args) + self.__origin__ = origin + self.__args__ = args + + def __repr__(self): + _type_repr = typing._type_repr + return (f'{_type_repr(self.__origin__)}' + f'[{", ".join(_type_repr(arg) for arg in self.__args__)}]') + + def __hash__(self): + return hash((self.__origin__, self.__args__)) + + # Hack to get typing._type_check to pass in Generic. + def __call__(self, *args, **kwargs): + pass + + @property + def __parameters__(self): + return tuple( + tp for tp in self.__args__ if isinstance(tp, (typing.TypeVar, ParamSpec)) + ) + + if not PEP_560: + # Only required in 3.6. + def _get_type_vars(self, tvars): + if self.__origin__ and self.__parameters__: + typing._get_type_vars(self.__parameters__, tvars) + + +# 3.6-3.9 +@typing._tp_cache +def _concatenate_getitem(self, parameters): + if parameters == (): + raise TypeError("Cannot take a Concatenate of no types.") + if not isinstance(parameters, tuple): + parameters = (parameters,) + if not isinstance(parameters[-1], ParamSpec): + raise TypeError("The last parameter to Concatenate should be a " + "ParamSpec variable.") + msg = "Concatenate[arg, ...]: each arg must be a type." + parameters = tuple(typing._type_check(p, msg) for p in parameters) + return _ConcatenateGenericAlias(self, parameters) + + +# 3.10+ +if hasattr(typing, 'Concatenate'): + Concatenate = typing.Concatenate + _ConcatenateGenericAlias = typing._ConcatenateGenericAlias # noqa +# 3.9 +elif sys.version_info[:2] >= (3, 9): + @_TypeAliasForm + def Concatenate(self, parameters): + """Used in conjunction with ``ParamSpec`` and ``Callable`` to represent a + higher order function which adds, removes or transforms parameters of a + callable. + + For example:: + + Callable[Concatenate[int, P], int] + + See PEP 612 for detailed information. + """ + return _concatenate_getitem(self, parameters) +# 3.7-8 +elif sys.version_info[:2] >= (3, 7): + class _ConcatenateForm(typing._SpecialForm, _root=True): + def __repr__(self): + return 'typing_extensions.' + self._name + + def __getitem__(self, parameters): + return _concatenate_getitem(self, parameters) + + Concatenate = _ConcatenateForm( + 'Concatenate', + doc="""Used in conjunction with ``ParamSpec`` and ``Callable`` to represent a + higher order function which adds, removes or transforms parameters of a + callable. + + For example:: + + Callable[Concatenate[int, P], int] + + See PEP 612 for detailed information. + """) +# 3.6 +else: + class _ConcatenateAliasMeta(typing.TypingMeta): + """Metaclass for Concatenate.""" + + def __repr__(self): + return 'typing_extensions.Concatenate' + + class _ConcatenateAliasBase(typing._FinalTypingBase, + metaclass=_ConcatenateAliasMeta, + _root=True): + """Used in conjunction with ``ParamSpec`` and ``Callable`` to represent a + higher order function which adds, removes or transforms parameters of a + callable. + + For example:: + + Callable[Concatenate[int, P], int] + + See PEP 612 for detailed information. + """ + __slots__ = () + + def __instancecheck__(self, obj): + raise TypeError("Concatenate cannot be used with isinstance().") + + def __subclasscheck__(self, cls): + raise TypeError("Concatenate cannot be used with issubclass().") + + def __repr__(self): + return 'typing_extensions.Concatenate' + + def __getitem__(self, parameters): + return _concatenate_getitem(self, parameters) + + Concatenate = _ConcatenateAliasBase(_root=True) + +# 3.10+ +if hasattr(typing, 'TypeGuard'): + TypeGuard = typing.TypeGuard +# 3.9 +elif sys.version_info[:2] >= (3, 9): + class _TypeGuardForm(typing._SpecialForm, _root=True): + def __repr__(self): + return 'typing_extensions.' + self._name + + @_TypeGuardForm + def TypeGuard(self, parameters): + """Special typing form used to annotate the return type of a user-defined + type guard function. ``TypeGuard`` only accepts a single type argument. + At runtime, functions marked this way should return a boolean. + + ``TypeGuard`` aims to benefit *type narrowing* -- a technique used by static + type checkers to determine a more precise type of an expression within a + program's code flow. Usually type narrowing is done by analyzing + conditional code flow and applying the narrowing to a block of code. The + conditional expression here is sometimes referred to as a "type guard". + + Sometimes it would be convenient to use a user-defined boolean function + as a type guard. Such a function should use ``TypeGuard[...]`` as its + return type to alert static type checkers to this intention. + + Using ``-> TypeGuard`` tells the static type checker that for a given + function: + + 1. The return value is a boolean. + 2. If the return value is ``True``, the type of its argument + is the type inside ``TypeGuard``. + + For example:: + + def is_str(val: Union[str, float]): + # "isinstance" type guard + if isinstance(val, str): + # Type of ``val`` is narrowed to ``str`` + ... + else: + # Else, type of ``val`` is narrowed to ``float``. + ... + + Strict type narrowing is not enforced -- ``TypeB`` need not be a narrower + form of ``TypeA`` (it can even be a wider form) and this may lead to + type-unsafe results. The main reason is to allow for things like + narrowing ``List[object]`` to ``List[str]`` even though the latter is not + a subtype of the former, since ``List`` is invariant. The responsibility of + writing type-safe type guards is left to the user. + + ``TypeGuard`` also works with type variables. For more information, see + PEP 647 (User-Defined Type Guards). + """ + item = typing._type_check(parameters, f'{self} accepts only single type.') + return typing._GenericAlias(self, (item,)) +# 3.7-3.8 +elif sys.version_info[:2] >= (3, 7): + class _TypeGuardForm(typing._SpecialForm, _root=True): + + def __repr__(self): + return 'typing_extensions.' + self._name + + def __getitem__(self, parameters): + item = typing._type_check(parameters, + f'{self._name} accepts only a single type') + return typing._GenericAlias(self, (item,)) + + TypeGuard = _TypeGuardForm( + 'TypeGuard', + doc="""Special typing form used to annotate the return type of a user-defined + type guard function. ``TypeGuard`` only accepts a single type argument. + At runtime, functions marked this way should return a boolean. + + ``TypeGuard`` aims to benefit *type narrowing* -- a technique used by static + type checkers to determine a more precise type of an expression within a + program's code flow. Usually type narrowing is done by analyzing + conditional code flow and applying the narrowing to a block of code. The + conditional expression here is sometimes referred to as a "type guard". + + Sometimes it would be convenient to use a user-defined boolean function + as a type guard. Such a function should use ``TypeGuard[...]`` as its + return type to alert static type checkers to this intention. + + Using ``-> TypeGuard`` tells the static type checker that for a given + function: + + 1. The return value is a boolean. + 2. If the return value is ``True``, the type of its argument + is the type inside ``TypeGuard``. + + For example:: + + def is_str(val: Union[str, float]): + # "isinstance" type guard + if isinstance(val, str): + # Type of ``val`` is narrowed to ``str`` + ... + else: + # Else, type of ``val`` is narrowed to ``float``. + ... + + Strict type narrowing is not enforced -- ``TypeB`` need not be a narrower + form of ``TypeA`` (it can even be a wider form) and this may lead to + type-unsafe results. The main reason is to allow for things like + narrowing ``List[object]`` to ``List[str]`` even though the latter is not + a subtype of the former, since ``List`` is invariant. The responsibility of + writing type-safe type guards is left to the user. + + ``TypeGuard`` also works with type variables. For more information, see + PEP 647 (User-Defined Type Guards). + """) +# 3.6 +else: + class _TypeGuard(typing._FinalTypingBase, _root=True): + """Special typing form used to annotate the return type of a user-defined + type guard function. ``TypeGuard`` only accepts a single type argument. + At runtime, functions marked this way should return a boolean. + + ``TypeGuard`` aims to benefit *type narrowing* -- a technique used by static + type checkers to determine a more precise type of an expression within a + program's code flow. Usually type narrowing is done by analyzing + conditional code flow and applying the narrowing to a block of code. The + conditional expression here is sometimes referred to as a "type guard". + + Sometimes it would be convenient to use a user-defined boolean function + as a type guard. Such a function should use ``TypeGuard[...]`` as its + return type to alert static type checkers to this intention. + + Using ``-> TypeGuard`` tells the static type checker that for a given + function: + + 1. The return value is a boolean. + 2. If the return value is ``True``, the type of its argument + is the type inside ``TypeGuard``. + + For example:: + + def is_str(val: Union[str, float]): + # "isinstance" type guard + if isinstance(val, str): + # Type of ``val`` is narrowed to ``str`` + ... + else: + # Else, type of ``val`` is narrowed to ``float``. + ... + + Strict type narrowing is not enforced -- ``TypeB`` need not be a narrower + form of ``TypeA`` (it can even be a wider form) and this may lead to + type-unsafe results. The main reason is to allow for things like + narrowing ``List[object]`` to ``List[str]`` even though the latter is not + a subtype of the former, since ``List`` is invariant. The responsibility of + writing type-safe type guards is left to the user. + + ``TypeGuard`` also works with type variables. For more information, see + PEP 647 (User-Defined Type Guards). + """ + + __slots__ = ('__type__',) + + def __init__(self, tp=None, **kwds): + self.__type__ = tp + + def __getitem__(self, item): + cls = type(self) + if self.__type__ is None: + return cls(typing._type_check(item, + f'{cls.__name__[1:]} accepts only a single type.'), + _root=True) + raise TypeError(f'{cls.__name__[1:]} cannot be further subscripted') + + def _eval_type(self, globalns, localns): + new_tp = typing._eval_type(self.__type__, globalns, localns) + if new_tp == self.__type__: + return self + return type(self)(new_tp, _root=True) + + def __repr__(self): + r = super().__repr__() + if self.__type__ is not None: + r += f'[{typing._type_repr(self.__type__)}]' + return r + + def __hash__(self): + return hash((type(self).__name__, self.__type__)) + + def __eq__(self, other): + if not isinstance(other, _TypeGuard): + return NotImplemented + if self.__type__ is not None: + return self.__type__ == other.__type__ + return self is other + + TypeGuard = _TypeGuard(_root=True) + +if hasattr(typing, "Self"): + Self = typing.Self +elif sys.version_info[:2] >= (3, 7): + # Vendored from cpython typing._SpecialFrom + class _SpecialForm(typing._Final, _root=True): + __slots__ = ('_name', '__doc__', '_getitem') + + def __init__(self, getitem): + self._getitem = getitem + self._name = getitem.__name__ + self.__doc__ = getitem.__doc__ + + def __getattr__(self, item): + if item in {'__name__', '__qualname__'}: + return self._name + + raise AttributeError(item) + + def __mro_entries__(self, bases): + raise TypeError(f"Cannot subclass {self!r}") + + def __repr__(self): + return f'typing_extensions.{self._name}' + + def __reduce__(self): + return self._name + + def __call__(self, *args, **kwds): + raise TypeError(f"Cannot instantiate {self!r}") + + def __or__(self, other): + return typing.Union[self, other] + + def __ror__(self, other): + return typing.Union[other, self] + + def __instancecheck__(self, obj): + raise TypeError(f"{self} cannot be used with isinstance()") + + def __subclasscheck__(self, cls): + raise TypeError(f"{self} cannot be used with issubclass()") + + @typing._tp_cache + def __getitem__(self, parameters): + return self._getitem(self, parameters) + + @_SpecialForm + def Self(self, params): + """Used to spell the type of "self" in classes. + + Example:: + + from typing import Self + + class ReturnsSelf: + def parse(self, data: bytes) -> Self: + ... + return self + + """ + + raise TypeError(f"{self} is not subscriptable") +else: + class _Self(typing._FinalTypingBase, _root=True): + """Used to spell the type of "self" in classes. + + Example:: + + from typing import Self + + class ReturnsSelf: + def parse(self, data: bytes) -> Self: + ... + return self + + """ + + __slots__ = () + + def __instancecheck__(self, obj): + raise TypeError(f"{self} cannot be used with isinstance().") + + def __subclasscheck__(self, cls): + raise TypeError(f"{self} cannot be used with issubclass().") + + Self = _Self(_root=True) + + +if hasattr(typing, 'Required'): + Required = typing.Required + NotRequired = typing.NotRequired +elif sys.version_info[:2] >= (3, 9): + class _ExtensionsSpecialForm(typing._SpecialForm, _root=True): + def __repr__(self): + return 'typing_extensions.' + self._name + + @_ExtensionsSpecialForm + def Required(self, parameters): + """A special typing construct to mark a key of a total=False TypedDict + as required. For example: + + class Movie(TypedDict, total=False): + title: Required[str] + year: int + + m = Movie( + title='The Matrix', # typechecker error if key is omitted + year=1999, + ) + + There is no runtime checking that a required key is actually provided + when instantiating a related TypedDict. + """ + item = typing._type_check(parameters, f'{self._name} accepts only single type') + return typing._GenericAlias(self, (item,)) + + @_ExtensionsSpecialForm + def NotRequired(self, parameters): + """A special typing construct to mark a key of a TypedDict as + potentially missing. For example: + + class Movie(TypedDict): + title: str + year: NotRequired[int] + + m = Movie( + title='The Matrix', # typechecker error if key is omitted + year=1999, + ) + """ + item = typing._type_check(parameters, f'{self._name} accepts only single type') + return typing._GenericAlias(self, (item,)) + +elif sys.version_info[:2] >= (3, 7): + class _RequiredForm(typing._SpecialForm, _root=True): + def __repr__(self): + return 'typing_extensions.' + self._name + + def __getitem__(self, parameters): + item = typing._type_check(parameters, + '{} accepts only single type'.format(self._name)) + return typing._GenericAlias(self, (item,)) + + Required = _RequiredForm( + 'Required', + doc="""A special typing construct to mark a key of a total=False TypedDict + as required. For example: + + class Movie(TypedDict, total=False): + title: Required[str] + year: int + + m = Movie( + title='The Matrix', # typechecker error if key is omitted + year=1999, + ) + + There is no runtime checking that a required key is actually provided + when instantiating a related TypedDict. + """) + NotRequired = _RequiredForm( + 'NotRequired', + doc="""A special typing construct to mark a key of a TypedDict as + potentially missing. For example: + + class Movie(TypedDict): + title: str + year: NotRequired[int] + + m = Movie( + title='The Matrix', # typechecker error if key is omitted + year=1999, + ) + """) +else: + # NOTE: Modeled after _Final's implementation when _FinalTypingBase available + class _MaybeRequired(typing._FinalTypingBase, _root=True): + __slots__ = ('__type__',) + + def __init__(self, tp=None, **kwds): + self.__type__ = tp + + def __getitem__(self, item): + cls = type(self) + if self.__type__ is None: + return cls(typing._type_check(item, + '{} accepts only single type.'.format(cls.__name__[1:])), + _root=True) + raise TypeError('{} cannot be further subscripted' + .format(cls.__name__[1:])) + + def _eval_type(self, globalns, localns): + new_tp = typing._eval_type(self.__type__, globalns, localns) + if new_tp == self.__type__: + return self + return type(self)(new_tp, _root=True) + + def __repr__(self): + r = super().__repr__() + if self.__type__ is not None: + r += '[{}]'.format(typing._type_repr(self.__type__)) + return r + + def __hash__(self): + return hash((type(self).__name__, self.__type__)) + + def __eq__(self, other): + if not isinstance(other, type(self)): + return NotImplemented + if self.__type__ is not None: + return self.__type__ == other.__type__ + return self is other + + class _Required(_MaybeRequired, _root=True): + """A special typing construct to mark a key of a total=False TypedDict + as required. For example: + + class Movie(TypedDict, total=False): + title: Required[str] + year: int + + m = Movie( + title='The Matrix', # typechecker error if key is omitted + year=1999, + ) + + There is no runtime checking that a required key is actually provided + when instantiating a related TypedDict. + """ + + class _NotRequired(_MaybeRequired, _root=True): + """A special typing construct to mark a key of a TypedDict as + potentially missing. For example: + + class Movie(TypedDict): + title: str + year: NotRequired[int] + + m = Movie( + title='The Matrix', # typechecker error if key is omitted + year=1999, + ) + """ + + Required = _Required(_root=True) + NotRequired = _NotRequired(_root=True) diff --git a/setuptools/_vendor/vendored.txt b/setuptools/_vendor/vendored.txt index 0639990b..251742ed 100644 --- a/setuptools/_vendor/vendored.txt +++ b/setuptools/_vendor/vendored.txt @@ -2,8 +2,10 @@ packaging==21.2 pyparsing==2.2.1 ordered-set==3.1.1 more_itertools==8.8.0 +jaraco.text==3.7.0 importlib_resources==5.4.0 importlib_metadata==4.10.1 -jaraco.text==3.7.0 -# required for importlib_resources on older Pythons +# required for importlib_metadata on older Pythons +typing_extensions==4.0.1 +# required for importlib_resources and _metadata on older Pythons zipp==3.7.0 diff --git a/setuptools/extern/__init__.py b/setuptools/extern/__init__.py index 3570a3b4..98235a4b 100644 --- a/setuptools/extern/__init__.py +++ b/setuptools/extern/__init__.py @@ -71,6 +71,6 @@ class VendorImporter: names = ( 'packaging', 'pyparsing', 'ordered_set', 'more_itertools', 'importlib_metadata', - 'zipp', 'importlib_resources', 'jaraco', + 'zipp', 'importlib_resources', 'jaraco', 'typing_extensions', ) VendorImporter(__name__, names, 'setuptools._vendor').install() diff --git a/tools/vendored.py b/tools/vendored.py index 57e28d53..74478902 100644 --- a/tools/vendored.py +++ b/tools/vendored.py @@ -64,6 +64,16 @@ def rewrite_importlib_resources(pkg_files, new_root): file.write_text(text) +def rewrite_importlib_metadata(pkg_files, new_root): + """ + Rewrite imports in importlib_metadata to redirect to vendored copies. + """ + for file in pkg_files.glob('*.py'): + text = file.read_text().replace('typing_extensions', '..typing_extensions') + text = text.replace('import zipp', 'from .. import zipp') + file.write_text(text) + + def clean(vendor): """ Remove all files out of the vendor directory except the meta @@ -105,6 +115,7 @@ def update_setuptools(): rewrite_jaraco_text(vendor / 'jaraco/text', 'setuptools.extern') rewrite_jaraco(vendor / 'jaraco', 'setuptools.extern') rewrite_importlib_resources(vendor / 'importlib_resources', 'setuptools.extern') + rewrite_importlib_metadata(vendor / 'importlib_metadata', 'setuptools.extern') __name__ == '__main__' and update_vendored() -- cgit v1.2.1 From ff3447a694f3b08dae8bd5268e64aa43f05a47a9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 6 Feb 2022 11:20:38 -0500 Subject: Migrate remainder of 'iter_entry_points' to importlib_metadata. --- setuptools/command/egg_info.py | 11 ++++++----- setuptools/command/sdist.py | 4 ++-- setuptools/command/upload_docs.py | 10 ++++++---- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index 379f9398..d0e73002 100644 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -17,6 +17,8 @@ import warnings import time import collections +from .._importlib import metadata + from setuptools import Command from setuptools.command.sdist import sdist from setuptools.command.sdist import walk_revctrl @@ -24,7 +26,7 @@ from setuptools.command.setopt import edit_config from setuptools.command import bdist_egg from pkg_resources import ( Requirement, safe_name, parse_version, - safe_version, yield_lines, EntryPoint, iter_entry_points, to_filename) + safe_version, yield_lines, EntryPoint, to_filename) import setuptools.unicode_utils as unicode_utils from setuptools.glob import glob @@ -281,10 +283,9 @@ class egg_info(InfoCommon, Command): def run(self): self.mkpath(self.egg_info) os.utime(self.egg_info, None) - installer = self.distribution.fetch_build_egg - for ep in iter_entry_points('egg_info.writers'): - ep.require(installer=installer) - writer = ep.resolve() + for ep in metadata.entry_points(group='egg_info.writers'): + self.distribution._install_dependencies(ep) + writer = ep.load() writer(self, ep.name, os.path.join(self.egg_info, ep.name)) # Get rid of native_libs.txt if it was put there by older bdist_egg diff --git a/setuptools/command/sdist.py b/setuptools/command/sdist.py index 0285b690..0ffeacf3 100644 --- a/setuptools/command/sdist.py +++ b/setuptools/command/sdist.py @@ -7,14 +7,14 @@ import contextlib from .py36compat import sdist_add_defaults -import pkg_resources +from .._importlib import metadata _default_revctrl = list def walk_revctrl(dirname=''): """Find all files under revision control""" - for ep in pkg_resources.iter_entry_points('setuptools.file_finders'): + for ep in metadata.entry_points(group='setuptools.file_finders'): for item in ep.load()(dirname): yield item diff --git a/setuptools/command/upload_docs.py b/setuptools/command/upload_docs.py index 845bff44..f429f568 100644 --- a/setuptools/command/upload_docs.py +++ b/setuptools/command/upload_docs.py @@ -18,7 +18,8 @@ import functools import http.client import urllib.parse -from pkg_resources import iter_entry_points +from .._importlib import metadata + from .upload import upload @@ -43,9 +44,10 @@ class upload_docs(upload): boolean_options = upload.boolean_options def has_sphinx(self): - if self.upload_dir is None: - for ep in iter_entry_points('distutils.commands', 'build_sphinx'): - return True + return bool( + self.upload_dir is None + and metadata.entry_points(group='distutils.commands', name='build_sphinx') + ) sub_commands = [('build_sphinx', has_sphinx)] -- cgit v1.2.1 From 867147f45c2b929f32b364284448b9d08c397dcb Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 6 Feb 2022 11:28:02 -0500 Subject: Avoid dual-use variable. --- setuptools/command/egg_info.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index d0e73002..17955207 100644 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -721,13 +721,13 @@ def write_entries(cmd, basename, filename): if isinstance(ep, str) or ep is None: data = ep elif ep is not None: - data = [] + lines = [] for section, contents in sorted(ep.items()): if not isinstance(contents, str): contents = EntryPoint.parse_group(section, contents) contents = '\n'.join(sorted(map(str, contents.values()))) - data.append('[%s]\n%s\n\n' % (section, contents)) - data = ''.join(data) + lines.append('[%s]\n%s\n\n' % (section, contents)) + data = ''.join(lines) cmd.write_or_delete_file('entry points', filename, data, True) -- cgit v1.2.1 From 282f2120979d0d97ae52feb557a19c094e548c87 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 6 Feb 2022 11:29:10 -0500 Subject: Remove duplicate check on ep is None. --- setuptools/command/egg_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index 17955207..439fe213 100644 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -720,7 +720,7 @@ def write_entries(cmd, basename, filename): if isinstance(ep, str) or ep is None: data = ep - elif ep is not None: + else: lines = [] for section, contents in sorted(ep.items()): if not isinstance(contents, str): -- cgit v1.2.1 From c5f7e3b19c153712f4e77e3c71ce5a7ba9668bc6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 6 Feb 2022 11:41:00 -0500 Subject: Refactor to construct data in a single expression and extract 'to_str'. --- setuptools/command/egg_info.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index 439fe213..473b7aa4 100644 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -721,13 +721,15 @@ def write_entries(cmd, basename, filename): if isinstance(ep, str) or ep is None: data = ep else: - lines = [] - for section, contents in sorted(ep.items()): - if not isinstance(contents, str): - contents = EntryPoint.parse_group(section, contents) - contents = '\n'.join(sorted(map(str, contents.values()))) - lines.append('[%s]\n%s\n\n' % (section, contents)) - data = ''.join(lines) + def to_str(contents): + if isinstance(contents, str): + return contents + eps = EntryPoint.parse_group('anything', contents) + return '\n'.join(sorted(map(str, eps.values()))) + data = ''.join( + f'[{section}]\n{to_str(contents)}\n\n' + for section, contents in sorted(ep.items()) + ) cmd.write_or_delete_file('entry points', filename, data, True) -- cgit v1.2.1 From d47d35616920f2f373cc6afbdaf4f30f3faca90f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 6 Feb 2022 11:52:27 -0500 Subject: Refactor to extract entry_points_definition generation. --- setuptools/command/egg_info.py | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index 473b7aa4..b98b84d4 100644 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -715,23 +715,30 @@ def write_arg(cmd, basename, filename, force=False): cmd.write_or_delete_file(argname, filename, value, force) -def write_entries(cmd, basename, filename): - ep = cmd.distribution.entry_points - - if isinstance(ep, str) or ep is None: - data = ep - else: - def to_str(contents): - if isinstance(contents, str): - return contents - eps = EntryPoint.parse_group('anything', contents) - return '\n'.join(sorted(map(str, eps.values()))) - data = ''.join( - f'[{section}]\n{to_str(contents)}\n\n' - for section, contents in sorted(ep.items()) - ) +@functools.singledispatch +def entry_points_definition(eps): + """ + Given a Distribution.entry_points, produce a multiline + string definition of those entry points. + """ + def to_str(contents): + if isinstance(contents, str): + return contents + parsed = EntryPoint.parse_group('anything', contents) + return '\n'.join(sorted(map(str, parsed.values()))) + return ''.join( + f'[{section}]\n{to_str(contents)}\n\n' + for section, contents in sorted(eps.items()) + ) - cmd.write_or_delete_file('entry points', filename, data, True) + +entry_points_definition.register(type(None), lambda x: x) +entry_points_definition.register(str, lambda x: x) + + +def write_entries(cmd, basename, filename): + defn = entry_points_definition(cmd.distribution.entry_points) + cmd.write_or_delete_file('entry points', filename, defn, True) def get_pkg_info_revision(): -- cgit v1.2.1 From 161ff0ff6f679967d323e9fd461eff312d0f12e6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 6 Feb 2022 12:00:14 -0500 Subject: Extract function for converting entry points to a string. --- setuptools/command/egg_info.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index b98b84d4..afab5cd6 100644 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -715,19 +715,27 @@ def write_arg(cmd, basename, filename, force=False): cmd.write_or_delete_file(argname, filename, value, force) +@functools.singledispatch +def entry_point_definition_to_str(value): + """ + Given a value of an entry point or series of entry points, + return each entry point on a single line. + """ + parsed = EntryPoint.parse_group('anything', value) + return '\n'.join(sorted(map(str, parsed.values()))) + + +entry_point_definition_to_str.register(str, lambda x: x) + + @functools.singledispatch def entry_points_definition(eps): """ Given a Distribution.entry_points, produce a multiline string definition of those entry points. """ - def to_str(contents): - if isinstance(contents, str): - return contents - parsed = EntryPoint.parse_group('anything', contents) - return '\n'.join(sorted(map(str, parsed.values()))) return ''.join( - f'[{section}]\n{to_str(contents)}\n\n' + f'[{section}]\n{entry_point_definition_to_str(contents)}\n\n' for section, contents in sorted(eps.items()) ) -- cgit v1.2.1 From b257d137ae2bcf1ef2e188f20e60f3ca5770e090 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 6 Feb 2022 12:43:29 -0500 Subject: In egg_info, port use of pkg_resources.EntryPoint to importlib.metadata --- setuptools/_itertools.py | 23 +++++++++++++++++++++++ setuptools/command/egg_info.py | 14 +++++++++++--- 2 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 setuptools/_itertools.py diff --git a/setuptools/_itertools.py b/setuptools/_itertools.py new file mode 100644 index 00000000..b8bf6d21 --- /dev/null +++ b/setuptools/_itertools.py @@ -0,0 +1,23 @@ +from setuptools.extern.more_itertools import consume # noqa: F401 + + +# copied from jaraco.itertools 6.1 +def ensure_unique(iterable, key=lambda x: x): + """ + Wrap an iterable to raise a ValueError if non-unique values are encountered. + + >>> list(ensure_unique('abc')) + ['a', 'b', 'c'] + >>> consume(ensure_unique('abca')) + Traceback (most recent call last): + ... + ValueError: Duplicate element 'a' encountered. + """ + seen = set() + seen_add = seen.add + for element in iterable: + k = key(element) + if k in seen: + raise ValueError(f"Duplicate element {element!r} encountered.") + seen_add(k) + yield element diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index afab5cd6..2ed58eef 100644 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -16,8 +16,10 @@ import io import warnings import time import collections +import operator from .._importlib import metadata +from .._itertools import ensure_unique from setuptools import Command from setuptools.command.sdist import sdist @@ -26,7 +28,7 @@ from setuptools.command.setopt import edit_config from setuptools.command import bdist_egg from pkg_resources import ( Requirement, safe_name, parse_version, - safe_version, yield_lines, EntryPoint, to_filename) + safe_version, yield_lines, to_filename) import setuptools.unicode_utils as unicode_utils from setuptools.glob import glob @@ -721,8 +723,14 @@ def entry_point_definition_to_str(value): Given a value of an entry point or series of entry points, return each entry point on a single line. """ - parsed = EntryPoint.parse_group('anything', value) - return '\n'.join(sorted(map(str, parsed.values()))) + # normalize to a single sequence of lines + lines = yield_lines(value) + parsed = metadata.EntryPoints._from_text('[x]\n' + '\n'.join(lines)) + valid = ensure_unique(parsed, key=operator.attrgetter('name')) + + def ep_to_str(ep): + return f'{ep.name} = {ep.value}' + return '\n'.join(sorted(map(ep_to_str, valid))) entry_point_definition_to_str.register(str, lambda x: x) -- cgit v1.2.1 From abf002112b77c26102a60116a0336ad2e4f56611 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 6 Feb 2022 13:31:44 -0500 Subject: In test command, rely on metadata.EntryPoint for loading the value. --- setuptools/command/test.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/setuptools/command/test.py b/setuptools/command/test.py index 4a389e4d..652f3e4a 100644 --- a/setuptools/command/test.py +++ b/setuptools/command/test.py @@ -16,10 +16,11 @@ from pkg_resources import ( evaluate_marker, add_activation_listener, require, - EntryPoint, ) +from .._importlib import metadata from setuptools import Command from setuptools.extern.more_itertools import unique_everseen +from setuptools.extern.jaraco.functools import pass_none class ScanningLoader(TestLoader): @@ -241,12 +242,10 @@ class test(Command): return ['unittest'] + self.test_args @staticmethod + @pass_none def _resolve_as_ep(val): """ Load the indicated attribute value, called, as a as if it were specified as an entry point. """ - if val is None: - return - parsed = EntryPoint.parse("x=" + val) - return parsed.resolve()() + return metadata.EntryPoint(value=val, name=None, group=None).load()() -- cgit v1.2.1 From f507f76273467a737f340ecd6a2926f48fa44386 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 6 Feb 2022 14:25:00 -0500 Subject: Port check_importable to metadata.EntryPoint --- setuptools/dist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index b0aea37b..9aaaf31c 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -225,7 +225,7 @@ sequence = tuple, list def check_importable(dist, attr, value): try: - ep = pkg_resources.EntryPoint.parse('x=' + value) + ep = metadata.EntryPoint(value=value, name=None, group=None) assert not ep.extras except (TypeError, ValueError, AttributeError, AssertionError) as e: raise DistutilsSetupError( -- cgit v1.2.1 From 67b25e3986aef5ac04b57be1a5c569e18f95a3d1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 6 Feb 2022 14:30:08 -0500 Subject: Extract module for entry point management. --- setuptools/_entry_points.py | 41 +++++++++++++++++++++++++++++++++++++++++ setuptools/command/egg_info.py | 40 ++-------------------------------------- 2 files changed, 43 insertions(+), 38 deletions(-) create mode 100644 setuptools/_entry_points.py diff --git a/setuptools/_entry_points.py b/setuptools/_entry_points.py new file mode 100644 index 00000000..a3c909d5 --- /dev/null +++ b/setuptools/_entry_points.py @@ -0,0 +1,41 @@ +import functools +import operator + +from pkg_resources import yield_lines +from ._importlib import metadata +from ._itertools import ensure_unique + + +@functools.singledispatch +def render_items(value): + """ + Given a value of an entry point or series of entry points, + return each entry point on a single line. + """ + # normalize to a single sequence of lines + lines = yield_lines(value) + parsed = metadata.EntryPoints._from_text('[x]\n' + '\n'.join(lines)) + valid = ensure_unique(parsed, key=operator.attrgetter('name')) + + def ep_to_str(ep): + return f'{ep.name} = {ep.value}' + return '\n'.join(sorted(map(ep_to_str, valid))) + + +render_items.register(str, lambda x: x) + + +@functools.singledispatch +def render(eps): + """ + Given a Distribution.entry_points, produce a multiline + string definition of those entry points. + """ + return ''.join( + f'[{section}]\n{render_items(contents)}\n\n' + for section, contents in sorted(eps.items()) + ) + + +render.register(type(None), lambda x: x) +render.register(str, lambda x: x) diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index 2ed58eef..2e8ca4b7 100644 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -16,10 +16,9 @@ import io import warnings import time import collections -import operator from .._importlib import metadata -from .._itertools import ensure_unique +from .. import _entry_points from setuptools import Command from setuptools.command.sdist import sdist @@ -717,43 +716,8 @@ def write_arg(cmd, basename, filename, force=False): cmd.write_or_delete_file(argname, filename, value, force) -@functools.singledispatch -def entry_point_definition_to_str(value): - """ - Given a value of an entry point or series of entry points, - return each entry point on a single line. - """ - # normalize to a single sequence of lines - lines = yield_lines(value) - parsed = metadata.EntryPoints._from_text('[x]\n' + '\n'.join(lines)) - valid = ensure_unique(parsed, key=operator.attrgetter('name')) - - def ep_to_str(ep): - return f'{ep.name} = {ep.value}' - return '\n'.join(sorted(map(ep_to_str, valid))) - - -entry_point_definition_to_str.register(str, lambda x: x) - - -@functools.singledispatch -def entry_points_definition(eps): - """ - Given a Distribution.entry_points, produce a multiline - string definition of those entry points. - """ - return ''.join( - f'[{section}]\n{entry_point_definition_to_str(contents)}\n\n' - for section, contents in sorted(eps.items()) - ) - - -entry_points_definition.register(type(None), lambda x: x) -entry_points_definition.register(str, lambda x: x) - - def write_entries(cmd, basename, filename): - defn = entry_points_definition(cmd.distribution.entry_points) + defn = _entry_points.render(cmd.distribution.entry_points) cmd.write_or_delete_file('entry points', filename, defn, True) -- cgit v1.2.1 From c49b7d36f872438020b3ed3b2c1ec0a4a5978f92 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 6 Feb 2022 14:31:58 -0500 Subject: Prefer jaraco.text for yield_lines. --- setuptools/_entry_points.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setuptools/_entry_points.py b/setuptools/_entry_points.py index a3c909d5..35109e11 100644 --- a/setuptools/_entry_points.py +++ b/setuptools/_entry_points.py @@ -1,7 +1,7 @@ import functools import operator -from pkg_resources import yield_lines +from .extern.jaraco.text import yield_lines from ._importlib import metadata from ._itertools import ensure_unique -- cgit v1.2.1 From ebdaa76c3c6c55d5cffd1a80903484d80cf146c6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 6 Feb 2022 15:22:38 -0500 Subject: Refactor _entry_points to separate loading from rendering. Explicitly validate and restore validation of entry points that don't match the pattern. --- setuptools/_entry_points.py | 72 ++++++++++++++++++++++++++++++++---------- setuptools/command/egg_info.py | 3 +- 2 files changed, 57 insertions(+), 18 deletions(-) diff --git a/setuptools/_entry_points.py b/setuptools/_entry_points.py index 35109e11..816e61b6 100644 --- a/setuptools/_entry_points.py +++ b/setuptools/_entry_points.py @@ -1,41 +1,79 @@ import functools import operator +import itertools from .extern.jaraco.text import yield_lines +from .extern.jaraco.functools import pass_none from ._importlib import metadata from ._itertools import ensure_unique -@functools.singledispatch -def render_items(value): +def ensure_valid(ep): + """ + Exercise one of the dynamic properties to trigger + the pattern match. + """ + ep.extras + return ep + + +def load_group(value, group): """ Given a value of an entry point or series of entry points, - return each entry point on a single line. + return each as an EntryPoint. """ # normalize to a single sequence of lines lines = yield_lines(value) - parsed = metadata.EntryPoints._from_text('[x]\n' + '\n'.join(lines)) - valid = ensure_unique(parsed, key=operator.attrgetter('name')) + text = f'[{group}]\n' + '\n'.join(lines) + return metadata.EntryPoints._from_text(text) + - def ep_to_str(ep): - return f'{ep.name} = {ep.value}' - return '\n'.join(sorted(map(ep_to_str, valid))) +def by_group_and_name(ep): + return ep.group, ep.name -render_items.register(str, lambda x: x) +def validate(eps: metadata.EntryPoints): + """ + Ensure entry points are unique by group and name and validate the pattern. + """ + for ep in ensure_unique(eps, key=by_group_and_name): + # exercise one of the dynamic properties to trigger validation + ep.extras + return eps @functools.singledispatch -def render(eps): +def load(eps): """ - Given a Distribution.entry_points, produce a multiline - string definition of those entry points. + Given a Distribution.entry_points, produce EntryPoints. """ - return ''.join( - f'[{section}]\n{render_items(contents)}\n\n' - for section, contents in sorted(eps.items()) + groups = itertools.chain.from_iterable( + load_group(value, group) + for group, value in eps.items()) + return validate(metadata.EntryPoints(groups)) + + +@load.register(str) +def _(eps): + return validate(metadata.EntryPoints._from_text(eps)) + + +load.register(type(None), lambda x: x) + + +@pass_none +def render(eps: metadata.EntryPoints): + by_group = operator.attrgetter('group') + groups = itertools.groupby(sorted(eps, key=by_group), by_group) + + return '\n'.join( + f'[{group}]\n{render_items(items)}\n' + for group, items in groups ) -render.register(type(None), lambda x: x) -render.register(str, lambda x: x) +def render_items(eps): + return '\n'.join( + f'{ep.name} = {ep.value}' + for ep in sorted(eps) + ) diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index 2e8ca4b7..8af018f4 100644 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -717,7 +717,8 @@ def write_arg(cmd, basename, filename, force=False): def write_entries(cmd, basename, filename): - defn = _entry_points.render(cmd.distribution.entry_points) + eps = _entry_points.load(cmd.distribution.entry_points) + defn = _entry_points.render(eps) cmd.write_or_delete_file('entry points', filename, defn, True) -- cgit v1.2.1 From 4e18004a36ea2350079dd9cf246b7c4ac9b676f5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 6 Feb 2022 15:30:44 -0500 Subject: Use new _entry_points.load to validate entry points. --- setuptools/dist.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index 9aaaf31c..e825785e 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -42,6 +42,7 @@ from setuptools.config import parse_configuration import pkg_resources from setuptools.extern.packaging import version, requirements from . import _reqs +from . import _entry_points if TYPE_CHECKING: from email.message import Message @@ -328,8 +329,8 @@ def check_specifier(dist, attr, value): def check_entry_points(dist, attr, value): """Verify that entry_points map is parseable""" try: - pkg_resources.EntryPoint.parse_map(value) - except ValueError as e: + _entry_points.load(value) + except Exception as e: raise DistutilsSetupError(e) from e -- cgit v1.2.1 From 17702477180f2f3807c284d1aa1ef8f5a7a22fe7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 6 Feb 2022 15:39:46 -0500 Subject: Update test_sdist not to rely on pkg_resources. --- setuptools/tests/test_sdist.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/setuptools/tests/test_sdist.py b/setuptools/tests/test_sdist.py index 66f46ad0..302cff73 100644 --- a/setuptools/tests/test_sdist.py +++ b/setuptools/tests/test_sdist.py @@ -10,7 +10,7 @@ from unittest import mock import pytest -import pkg_resources +from setuptools._importlib import metadata from setuptools import SetuptoolsDeprecationWarning from setuptools.command.sdist import sdist from setuptools.command.egg_info import manifest_maker @@ -529,7 +529,9 @@ def test_default_revctrl(): This interface must be maintained until Ubuntu 12.04 is no longer supported (by Setuptools). """ - ep_def = 'svn_cvs = setuptools.command.sdist:_default_revctrl' - ep = pkg_resources.EntryPoint.parse(ep_def) - res = ep.resolve() + ep, = metadata.EntryPoints._from_text(""" + [setuptools.file_finders] + svn_cvs = setuptools.command.sdist:_default_revctrl + """) + res = ep.load() assert hasattr(res, '__iter__') -- cgit v1.2.1 From 412d35454a0fd934bc9663130a2108ab6779bb4d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 6 Feb 2022 15:41:35 -0500 Subject: Update changelog. --- changelog.d/3085.change.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/3085.change.rst diff --git a/changelog.d/3085.change.rst b/changelog.d/3085.change.rst new file mode 100644 index 00000000..3dd3045c --- /dev/null +++ b/changelog.d/3085.change.rst @@ -0,0 +1 @@ +Setuptools no longer relies on pkg_resources for entry point handling. -- cgit v1.2.1 From 740c3b13427aac1b353c0ad6f776d4c6f2655957 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 6 Feb 2022 15:43:03 -0500 Subject: Prefer jaraco.text for yield_lines. --- setuptools/command/easy_install.py | 4 +++- setuptools/command/egg_info.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index b1260dcd..5b73e6e9 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -56,13 +56,15 @@ from setuptools.package_index import ( from setuptools.command import bdist_egg, egg_info from setuptools.wheel import Wheel from pkg_resources import ( - yield_lines, normalize_path, resource_string, + normalize_path, resource_string, get_distribution, find_distributions, Environment, Requirement, Distribution, PathMetadata, EggMetadata, WorkingSet, DistributionNotFound, VersionConflict, DEVELOP_DIST, ) import pkg_resources from .._path import ensure_directory +from ..extern.jaraco.text import yield_lines + # Turn on PEP440Warnings warnings.filterwarnings("default", category=pkg_resources.PEP440Warning) diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index 8af018f4..63389654 100644 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -27,11 +27,12 @@ from setuptools.command.setopt import edit_config from setuptools.command import bdist_egg from pkg_resources import ( Requirement, safe_name, parse_version, - safe_version, yield_lines, to_filename) + safe_version, to_filename) import setuptools.unicode_utils as unicode_utils from setuptools.glob import glob from setuptools.extern import packaging +from setuptools.extern.jaraco.text import yield_lines from setuptools import SetuptoolsDeprecationWarning -- cgit v1.2.1 From 557787c8b3957f7a4f04094118b64afa3e175a78 Mon Sep 17 00:00:00 2001 From: Maciej Pasternacki Date: Tue, 8 Feb 2022 20:07:31 +0100 Subject: Remove more_itertools.more from vendored libs (fixes pypa/setuptools#3090) --- pkg_resources/_vendor/more_itertools/__init__.py | 1 - pkg_resources/_vendor/more_itertools/more.py | 4317 ---------------------- setuptools/_vendor/more_itertools/__init__.py | 1 - setuptools/_vendor/more_itertools/more.py | 3825 ------------------- tools/vendored.py | 12 + 5 files changed, 12 insertions(+), 8144 deletions(-) delete mode 100644 pkg_resources/_vendor/more_itertools/more.py delete mode 100644 setuptools/_vendor/more_itertools/more.py diff --git a/pkg_resources/_vendor/more_itertools/__init__.py b/pkg_resources/_vendor/more_itertools/__init__.py index ea38bef1..64bd1018 100644 --- a/pkg_resources/_vendor/more_itertools/__init__.py +++ b/pkg_resources/_vendor/more_itertools/__init__.py @@ -1,4 +1,3 @@ -from .more import * # noqa from .recipes import * # noqa __version__ = '8.12.0' diff --git a/pkg_resources/_vendor/more_itertools/more.py b/pkg_resources/_vendor/more_itertools/more.py deleted file mode 100644 index 630af973..00000000 --- a/pkg_resources/_vendor/more_itertools/more.py +++ /dev/null @@ -1,4317 +0,0 @@ -import warnings - -from collections import Counter, defaultdict, deque, abc -from collections.abc import Sequence -from concurrent.futures import ThreadPoolExecutor -from functools import partial, reduce, wraps -from heapq import merge, heapify, heapreplace, heappop -from itertools import ( - chain, - compress, - count, - cycle, - dropwhile, - groupby, - islice, - repeat, - starmap, - takewhile, - tee, - zip_longest, -) -from math import exp, factorial, floor, log -from queue import Empty, Queue -from random import random, randrange, uniform -from operator import itemgetter, mul, sub, gt, lt, ge, le -from sys import hexversion, maxsize -from time import monotonic - -from .recipes import ( - consume, - flatten, - pairwise, - powerset, - take, - unique_everseen, -) - -__all__ = [ - 'AbortThread', - 'SequenceView', - 'UnequalIterablesError', - 'adjacent', - 'all_unique', - 'always_iterable', - 'always_reversible', - 'bucket', - 'callback_iter', - 'chunked', - 'chunked_even', - 'circular_shifts', - 'collapse', - 'collate', - 'combination_index', - 'consecutive_groups', - 'consumer', - 'count_cycle', - 'countable', - 'difference', - 'distinct_combinations', - 'distinct_permutations', - 'distribute', - 'divide', - 'duplicates_everseen', - 'duplicates_justseen', - 'exactly_n', - 'filter_except', - 'first', - 'groupby_transform', - 'ichunked', - 'ilen', - 'interleave', - 'interleave_evenly', - 'interleave_longest', - 'intersperse', - 'is_sorted', - 'islice_extended', - 'iterate', - 'last', - 'locate', - 'lstrip', - 'make_decorator', - 'map_except', - 'map_if', - 'map_reduce', - 'mark_ends', - 'minmax', - 'nth_or_last', - 'nth_permutation', - 'nth_product', - 'numeric_range', - 'one', - 'only', - 'padded', - 'partitions', - 'peekable', - 'permutation_index', - 'product_index', - 'raise_', - 'repeat_each', - 'repeat_last', - 'replace', - 'rlocate', - 'rstrip', - 'run_length', - 'sample', - 'seekable', - 'set_partitions', - 'side_effect', - 'sliced', - 'sort_together', - 'split_after', - 'split_at', - 'split_before', - 'split_into', - 'split_when', - 'spy', - 'stagger', - 'strip', - 'strictly_n', - 'substrings', - 'substrings_indexes', - 'time_limited', - 'unique_in_window', - 'unique_to_each', - 'unzip', - 'value_chain', - 'windowed', - 'windowed_complete', - 'with_iter', - 'zip_broadcast', - 'zip_equal', - 'zip_offset', -] - - -_marker = object() - - -def chunked(iterable, n, strict=False): - """Break *iterable* into lists of length *n*: - - >>> list(chunked([1, 2, 3, 4, 5, 6], 3)) - [[1, 2, 3], [4, 5, 6]] - - By the default, the last yielded list will have fewer than *n* elements - if the length of *iterable* is not divisible by *n*: - - >>> list(chunked([1, 2, 3, 4, 5, 6, 7, 8], 3)) - [[1, 2, 3], [4, 5, 6], [7, 8]] - - To use a fill-in value instead, see the :func:`grouper` recipe. - - If the length of *iterable* is not divisible by *n* and *strict* is - ``True``, then ``ValueError`` will be raised before the last - list is yielded. - - """ - iterator = iter(partial(take, n, iter(iterable)), []) - if strict: - if n is None: - raise ValueError('n must not be None when using strict mode.') - - def ret(): - for chunk in iterator: - if len(chunk) != n: - raise ValueError('iterable is not divisible by n.') - yield chunk - - return iter(ret()) - else: - return iterator - - -def first(iterable, default=_marker): - """Return the first item of *iterable*, or *default* if *iterable* is - empty. - - >>> first([0, 1, 2, 3]) - 0 - >>> first([], 'some default') - 'some default' - - If *default* is not provided and there are no items in the iterable, - raise ``ValueError``. - - :func:`first` is useful when you have a generator of expensive-to-retrieve - values and want any arbitrary one. It is marginally shorter than - ``next(iter(iterable), default)``. - - """ - try: - return next(iter(iterable)) - except StopIteration as e: - if default is _marker: - raise ValueError( - 'first() was called on an empty iterable, and no ' - 'default value was provided.' - ) from e - return default - - -def last(iterable, default=_marker): - """Return the last item of *iterable*, or *default* if *iterable* is - empty. - - >>> last([0, 1, 2, 3]) - 3 - >>> last([], 'some default') - 'some default' - - If *default* is not provided and there are no items in the iterable, - raise ``ValueError``. - """ - try: - if isinstance(iterable, Sequence): - return iterable[-1] - # Work around https://bugs.python.org/issue38525 - elif hasattr(iterable, '__reversed__') and (hexversion != 0x030800F0): - return next(reversed(iterable)) - else: - return deque(iterable, maxlen=1)[-1] - except (IndexError, TypeError, StopIteration): - if default is _marker: - raise ValueError( - 'last() was called on an empty iterable, and no default was ' - 'provided.' - ) - return default - - -def nth_or_last(iterable, n, default=_marker): - """Return the nth or the last item of *iterable*, - or *default* if *iterable* is empty. - - >>> nth_or_last([0, 1, 2, 3], 2) - 2 - >>> nth_or_last([0, 1], 2) - 1 - >>> nth_or_last([], 0, 'some default') - 'some default' - - If *default* is not provided and there are no items in the iterable, - raise ``ValueError``. - """ - return last(islice(iterable, n + 1), default=default) - - -class peekable: - """Wrap an iterator to allow lookahead and prepending elements. - - Call :meth:`peek` on the result to get the value that will be returned - by :func:`next`. This won't advance the iterator: - - >>> p = peekable(['a', 'b']) - >>> p.peek() - 'a' - >>> next(p) - 'a' - - Pass :meth:`peek` a default value to return that instead of raising - ``StopIteration`` when the iterator is exhausted. - - >>> p = peekable([]) - >>> p.peek('hi') - 'hi' - - peekables also offer a :meth:`prepend` method, which "inserts" items - at the head of the iterable: - - >>> p = peekable([1, 2, 3]) - >>> p.prepend(10, 11, 12) - >>> next(p) - 10 - >>> p.peek() - 11 - >>> list(p) - [11, 12, 1, 2, 3] - - peekables can be indexed. Index 0 is the item that will be returned by - :func:`next`, index 1 is the item after that, and so on: - The values up to the given index will be cached. - - >>> p = peekable(['a', 'b', 'c', 'd']) - >>> p[0] - 'a' - >>> p[1] - 'b' - >>> next(p) - 'a' - - Negative indexes are supported, but be aware that they will cache the - remaining items in the source iterator, which may require significant - storage. - - To check whether a peekable is exhausted, check its truth value: - - >>> p = peekable(['a', 'b']) - >>> if p: # peekable has items - ... list(p) - ['a', 'b'] - >>> if not p: # peekable is exhausted - ... list(p) - [] - - """ - - def __init__(self, iterable): - self._it = iter(iterable) - self._cache = deque() - - def __iter__(self): - return self - - def __bool__(self): - try: - self.peek() - except StopIteration: - return False - return True - - def peek(self, default=_marker): - """Return the item that will be next returned from ``next()``. - - Return ``default`` if there are no items left. If ``default`` is not - provided, raise ``StopIteration``. - - """ - if not self._cache: - try: - self._cache.append(next(self._it)) - except StopIteration: - if default is _marker: - raise - return default - return self._cache[0] - - def prepend(self, *items): - """Stack up items to be the next ones returned from ``next()`` or - ``self.peek()``. The items will be returned in - first in, first out order:: - - >>> p = peekable([1, 2, 3]) - >>> p.prepend(10, 11, 12) - >>> next(p) - 10 - >>> list(p) - [11, 12, 1, 2, 3] - - It is possible, by prepending items, to "resurrect" a peekable that - previously raised ``StopIteration``. - - >>> p = peekable([]) - >>> next(p) - Traceback (most recent call last): - ... - StopIteration - >>> p.prepend(1) - >>> next(p) - 1 - >>> next(p) - Traceback (most recent call last): - ... - StopIteration - - """ - self._cache.extendleft(reversed(items)) - - def __next__(self): - if self._cache: - return self._cache.popleft() - - return next(self._it) - - def _get_slice(self, index): - # Normalize the slice's arguments - step = 1 if (index.step is None) else index.step - if step > 0: - start = 0 if (index.start is None) else index.start - stop = maxsize if (index.stop is None) else index.stop - elif step < 0: - start = -1 if (index.start is None) else index.start - stop = (-maxsize - 1) if (index.stop is None) else index.stop - else: - raise ValueError('slice step cannot be zero') - - # If either the start or stop index is negative, we'll need to cache - # the rest of the iterable in order to slice from the right side. - if (start < 0) or (stop < 0): - self._cache.extend(self._it) - # Otherwise we'll need to find the rightmost index and cache to that - # point. - else: - n = min(max(start, stop) + 1, maxsize) - cache_len = len(self._cache) - if n >= cache_len: - self._cache.extend(islice(self._it, n - cache_len)) - - return list(self._cache)[index] - - def __getitem__(self, index): - if isinstance(index, slice): - return self._get_slice(index) - - cache_len = len(self._cache) - if index < 0: - self._cache.extend(self._it) - elif index >= cache_len: - self._cache.extend(islice(self._it, index + 1 - cache_len)) - - return self._cache[index] - - -def collate(*iterables, **kwargs): - """Return a sorted merge of the items from each of several already-sorted - *iterables*. - - >>> list(collate('ACDZ', 'AZ', 'JKL')) - ['A', 'A', 'C', 'D', 'J', 'K', 'L', 'Z', 'Z'] - - Works lazily, keeping only the next value from each iterable in memory. Use - :func:`collate` to, for example, perform a n-way mergesort of items that - don't fit in memory. - - If a *key* function is specified, the iterables will be sorted according - to its result: - - >>> key = lambda s: int(s) # Sort by numeric value, not by string - >>> list(collate(['1', '10'], ['2', '11'], key=key)) - ['1', '2', '10', '11'] - - - If the *iterables* are sorted in descending order, set *reverse* to - ``True``: - - >>> list(collate([5, 3, 1], [4, 2, 0], reverse=True)) - [5, 4, 3, 2, 1, 0] - - If the elements of the passed-in iterables are out of order, you might get - unexpected results. - - On Python 3.5+, this function is an alias for :func:`heapq.merge`. - - """ - warnings.warn( - "collate is no longer part of more_itertools, use heapq.merge", - DeprecationWarning, - ) - return merge(*iterables, **kwargs) - - -def consumer(func): - """Decorator that automatically advances a PEP-342-style "reverse iterator" - to its first yield point so you don't have to call ``next()`` on it - manually. - - >>> @consumer - ... def tally(): - ... i = 0 - ... while True: - ... print('Thing number %s is %s.' % (i, (yield))) - ... i += 1 - ... - >>> t = tally() - >>> t.send('red') - Thing number 0 is red. - >>> t.send('fish') - Thing number 1 is fish. - - Without the decorator, you would have to call ``next(t)`` before - ``t.send()`` could be used. - - """ - - @wraps(func) - def wrapper(*args, **kwargs): - gen = func(*args, **kwargs) - next(gen) - return gen - - return wrapper - - -def ilen(iterable): - """Return the number of items in *iterable*. - - >>> ilen(x for x in range(1000000) if x % 3 == 0) - 333334 - - This consumes the iterable, so handle with care. - - """ - # This approach was selected because benchmarks showed it's likely the - # fastest of the known implementations at the time of writing. - # See GitHub tracker: #236, #230. - counter = count() - deque(zip(iterable, counter), maxlen=0) - return next(counter) - - -def iterate(func, start): - """Return ``start``, ``func(start)``, ``func(func(start))``, ... - - >>> from itertools import islice - >>> list(islice(iterate(lambda x: 2*x, 1), 10)) - [1, 2, 4, 8, 16, 32, 64, 128, 256, 512] - - """ - while True: - yield start - start = func(start) - - -def with_iter(context_manager): - """Wrap an iterable in a ``with`` statement, so it closes once exhausted. - - For example, this will close the file when the iterator is exhausted:: - - upper_lines = (line.upper() for line in with_iter(open('foo'))) - - Any context manager which returns an iterable is a candidate for - ``with_iter``. - - """ - with context_manager as iterable: - yield from iterable - - -def one(iterable, too_short=None, too_long=None): - """Return the first item from *iterable*, which is expected to contain only - that item. Raise an exception if *iterable* is empty or has more than one - item. - - :func:`one` is useful for ensuring that an iterable contains only one item. - For example, it can be used to retrieve the result of a database query - that is expected to return a single row. - - If *iterable* is empty, ``ValueError`` will be raised. You may specify a - different exception with the *too_short* keyword: - - >>> it = [] - >>> one(it) # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ... - ValueError: too many items in iterable (expected 1)' - >>> too_short = IndexError('too few items') - >>> one(it, too_short=too_short) # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ... - IndexError: too few items - - Similarly, if *iterable* contains more than one item, ``ValueError`` will - be raised. You may specify a different exception with the *too_long* - keyword: - - >>> it = ['too', 'many'] - >>> one(it) # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ... - ValueError: Expected exactly one item in iterable, but got 'too', - 'many', and perhaps more. - >>> too_long = RuntimeError - >>> one(it, too_long=too_long) # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ... - RuntimeError - - Note that :func:`one` attempts to advance *iterable* twice to ensure there - is only one item. See :func:`spy` or :func:`peekable` to check iterable - contents less destructively. - - """ - it = iter(iterable) - - try: - first_value = next(it) - except StopIteration as e: - raise ( - too_short or ValueError('too few items in iterable (expected 1)') - ) from e - - try: - second_value = next(it) - except StopIteration: - pass - else: - msg = ( - 'Expected exactly one item in iterable, but got {!r}, {!r}, ' - 'and perhaps more.'.format(first_value, second_value) - ) - raise too_long or ValueError(msg) - - return first_value - - -def raise_(exception, *args): - raise exception(*args) - - -def strictly_n(iterable, n, too_short=None, too_long=None): - """Validate that *iterable* has exactly *n* items and return them if - it does. If it has fewer than *n* items, call function *too_short* - with those items. If it has more than *n* items, call function - *too_long* with the first ``n + 1`` items. - - >>> iterable = ['a', 'b', 'c', 'd'] - >>> n = 4 - >>> list(strictly_n(iterable, n)) - ['a', 'b', 'c', 'd'] - - By default, *too_short* and *too_long* are functions that raise - ``ValueError``. - - >>> list(strictly_n('ab', 3)) # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ... - ValueError: too few items in iterable (got 2) - - >>> list(strictly_n('abc', 2)) # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ... - ValueError: too many items in iterable (got at least 3) - - You can instead supply functions that do something else. - *too_short* will be called with the number of items in *iterable*. - *too_long* will be called with `n + 1`. - - >>> def too_short(item_count): - ... raise RuntimeError - >>> it = strictly_n('abcd', 6, too_short=too_short) - >>> list(it) # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ... - RuntimeError - - >>> def too_long(item_count): - ... print('The boss is going to hear about this') - >>> it = strictly_n('abcdef', 4, too_long=too_long) - >>> list(it) - The boss is going to hear about this - ['a', 'b', 'c', 'd'] - - """ - if too_short is None: - too_short = lambda item_count: raise_( - ValueError, - 'Too few items in iterable (got {})'.format(item_count), - ) - - if too_long is None: - too_long = lambda item_count: raise_( - ValueError, - 'Too many items in iterable (got at least {})'.format(item_count), - ) - - it = iter(iterable) - for i in range(n): - try: - item = next(it) - except StopIteration: - too_short(i) - return - else: - yield item - - try: - next(it) - except StopIteration: - pass - else: - too_long(n + 1) - - -def distinct_permutations(iterable, r=None): - """Yield successive distinct permutations of the elements in *iterable*. - - >>> sorted(distinct_permutations([1, 0, 1])) - [(0, 1, 1), (1, 0, 1), (1, 1, 0)] - - Equivalent to ``set(permutations(iterable))``, except duplicates are not - generated and thrown away. For larger input sequences this is much more - efficient. - - Duplicate permutations arise when there are duplicated elements in the - input iterable. The number of items returned is - `n! / (x_1! * x_2! * ... * x_n!)`, where `n` is the total number of - items input, and each `x_i` is the count of a distinct item in the input - sequence. - - If *r* is given, only the *r*-length permutations are yielded. - - >>> sorted(distinct_permutations([1, 0, 1], r=2)) - [(0, 1), (1, 0), (1, 1)] - >>> sorted(distinct_permutations(range(3), r=2)) - [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)] - - """ - # Algorithm: https://w.wiki/Qai - def _full(A): - while True: - # Yield the permutation we have - yield tuple(A) - - # Find the largest index i such that A[i] < A[i + 1] - for i in range(size - 2, -1, -1): - if A[i] < A[i + 1]: - break - # If no such index exists, this permutation is the last one - else: - return - - # Find the largest index j greater than j such that A[i] < A[j] - for j in range(size - 1, i, -1): - if A[i] < A[j]: - break - - # Swap the value of A[i] with that of A[j], then reverse the - # sequence from A[i + 1] to form the new permutation - A[i], A[j] = A[j], A[i] - A[i + 1 :] = A[: i - size : -1] # A[i + 1:][::-1] - - # Algorithm: modified from the above - def _partial(A, r): - # Split A into the first r items and the last r items - head, tail = A[:r], A[r:] - right_head_indexes = range(r - 1, -1, -1) - left_tail_indexes = range(len(tail)) - - while True: - # Yield the permutation we have - yield tuple(head) - - # Starting from the right, find the first index of the head with - # value smaller than the maximum value of the tail - call it i. - pivot = tail[-1] - for i in right_head_indexes: - if head[i] < pivot: - break - pivot = head[i] - else: - return - - # Starting from the left, find the first value of the tail - # with a value greater than head[i] and swap. - for j in left_tail_indexes: - if tail[j] > head[i]: - head[i], tail[j] = tail[j], head[i] - break - # If we didn't find one, start from the right and find the first - # index of the head with a value greater than head[i] and swap. - else: - for j in right_head_indexes: - if head[j] > head[i]: - head[i], head[j] = head[j], head[i] - break - - # Reverse head[i + 1:] and swap it with tail[:r - (i + 1)] - tail += head[: i - r : -1] # head[i + 1:][::-1] - i += 1 - head[i:], tail[:] = tail[: r - i], tail[r - i :] - - items = sorted(iterable) - - size = len(items) - if r is None: - r = size - - if 0 < r <= size: - return _full(items) if (r == size) else _partial(items, r) - - return iter(() if r else ((),)) - - -def intersperse(e, iterable, n=1): - """Intersperse filler element *e* among the items in *iterable*, leaving - *n* items between each filler element. - - >>> list(intersperse('!', [1, 2, 3, 4, 5])) - [1, '!', 2, '!', 3, '!', 4, '!', 5] - - >>> list(intersperse(None, [1, 2, 3, 4, 5], n=2)) - [1, 2, None, 3, 4, None, 5] - - """ - if n == 0: - raise ValueError('n must be > 0') - elif n == 1: - # interleave(repeat(e), iterable) -> e, x_0, e, x_1, e, x_2... - # islice(..., 1, None) -> x_0, e, x_1, e, x_2... - return islice(interleave(repeat(e), iterable), 1, None) - else: - # interleave(filler, chunks) -> [e], [x_0, x_1], [e], [x_2, x_3]... - # islice(..., 1, None) -> [x_0, x_1], [e], [x_2, x_3]... - # flatten(...) -> x_0, x_1, e, x_2, x_3... - filler = repeat([e]) - chunks = chunked(iterable, n) - return flatten(islice(interleave(filler, chunks), 1, None)) - - -def unique_to_each(*iterables): - """Return the elements from each of the input iterables that aren't in the - other input iterables. - - For example, suppose you have a set of packages, each with a set of - dependencies:: - - {'pkg_1': {'A', 'B'}, 'pkg_2': {'B', 'C'}, 'pkg_3': {'B', 'D'}} - - If you remove one package, which dependencies can also be removed? - - If ``pkg_1`` is removed, then ``A`` is no longer necessary - it is not - associated with ``pkg_2`` or ``pkg_3``. Similarly, ``C`` is only needed for - ``pkg_2``, and ``D`` is only needed for ``pkg_3``:: - - >>> unique_to_each({'A', 'B'}, {'B', 'C'}, {'B', 'D'}) - [['A'], ['C'], ['D']] - - If there are duplicates in one input iterable that aren't in the others - they will be duplicated in the output. Input order is preserved:: - - >>> unique_to_each("mississippi", "missouri") - [['p', 'p'], ['o', 'u', 'r']] - - It is assumed that the elements of each iterable are hashable. - - """ - pool = [list(it) for it in iterables] - counts = Counter(chain.from_iterable(map(set, pool))) - uniques = {element for element in counts if counts[element] == 1} - return [list(filter(uniques.__contains__, it)) for it in pool] - - -def windowed(seq, n, fillvalue=None, step=1): - """Return a sliding window of width *n* over the given iterable. - - >>> all_windows = windowed([1, 2, 3, 4, 5], 3) - >>> list(all_windows) - [(1, 2, 3), (2, 3, 4), (3, 4, 5)] - - When the window is larger than the iterable, *fillvalue* is used in place - of missing values: - - >>> list(windowed([1, 2, 3], 4)) - [(1, 2, 3, None)] - - Each window will advance in increments of *step*: - - >>> list(windowed([1, 2, 3, 4, 5, 6], 3, fillvalue='!', step=2)) - [(1, 2, 3), (3, 4, 5), (5, 6, '!')] - - To slide into the iterable's items, use :func:`chain` to add filler items - to the left: - - >>> iterable = [1, 2, 3, 4] - >>> n = 3 - >>> padding = [None] * (n - 1) - >>> list(windowed(chain(padding, iterable), 3)) - [(None, None, 1), (None, 1, 2), (1, 2, 3), (2, 3, 4)] - """ - if n < 0: - raise ValueError('n must be >= 0') - if n == 0: - yield tuple() - return - if step < 1: - raise ValueError('step must be >= 1') - - window = deque(maxlen=n) - i = n - for _ in map(window.append, seq): - i -= 1 - if not i: - i = step - yield tuple(window) - - size = len(window) - if size < n: - yield tuple(chain(window, repeat(fillvalue, n - size))) - elif 0 < i < min(step, n): - window += (fillvalue,) * i - yield tuple(window) - - -def substrings(iterable): - """Yield all of the substrings of *iterable*. - - >>> [''.join(s) for s in substrings('more')] - ['m', 'o', 'r', 'e', 'mo', 'or', 're', 'mor', 'ore', 'more'] - - Note that non-string iterables can also be subdivided. - - >>> list(substrings([0, 1, 2])) - [(0,), (1,), (2,), (0, 1), (1, 2), (0, 1, 2)] - - """ - # The length-1 substrings - seq = [] - for item in iter(iterable): - seq.append(item) - yield (item,) - seq = tuple(seq) - item_count = len(seq) - - # And the rest - for n in range(2, item_count + 1): - for i in range(item_count - n + 1): - yield seq[i : i + n] - - -def substrings_indexes(seq, reverse=False): - """Yield all substrings and their positions in *seq* - - The items yielded will be a tuple of the form ``(substr, i, j)``, where - ``substr == seq[i:j]``. - - This function only works for iterables that support slicing, such as - ``str`` objects. - - >>> for item in substrings_indexes('more'): - ... print(item) - ('m', 0, 1) - ('o', 1, 2) - ('r', 2, 3) - ('e', 3, 4) - ('mo', 0, 2) - ('or', 1, 3) - ('re', 2, 4) - ('mor', 0, 3) - ('ore', 1, 4) - ('more', 0, 4) - - Set *reverse* to ``True`` to yield the same items in the opposite order. - - - """ - r = range(1, len(seq) + 1) - if reverse: - r = reversed(r) - return ( - (seq[i : i + L], i, i + L) for L in r for i in range(len(seq) - L + 1) - ) - - -class bucket: - """Wrap *iterable* and return an object that buckets it iterable into - child iterables based on a *key* function. - - >>> iterable = ['a1', 'b1', 'c1', 'a2', 'b2', 'c2', 'b3'] - >>> s = bucket(iterable, key=lambda x: x[0]) # Bucket by 1st character - >>> sorted(list(s)) # Get the keys - ['a', 'b', 'c'] - >>> a_iterable = s['a'] - >>> next(a_iterable) - 'a1' - >>> next(a_iterable) - 'a2' - >>> list(s['b']) - ['b1', 'b2', 'b3'] - - The original iterable will be advanced and its items will be cached until - they are used by the child iterables. This may require significant storage. - - By default, attempting to select a bucket to which no items belong will - exhaust the iterable and cache all values. - If you specify a *validator* function, selected buckets will instead be - checked against it. - - >>> from itertools import count - >>> it = count(1, 2) # Infinite sequence of odd numbers - >>> key = lambda x: x % 10 # Bucket by last digit - >>> validator = lambda x: x in {1, 3, 5, 7, 9} # Odd digits only - >>> s = bucket(it, key=key, validator=validator) - >>> 2 in s - False - >>> list(s[2]) - [] - - """ - - def __init__(self, iterable, key, validator=None): - self._it = iter(iterable) - self._key = key - self._cache = defaultdict(deque) - self._validator = validator or (lambda x: True) - - def __contains__(self, value): - if not self._validator(value): - return False - - try: - item = next(self[value]) - except StopIteration: - return False - else: - self._cache[value].appendleft(item) - - return True - - def _get_values(self, value): - """ - Helper to yield items from the parent iterator that match *value*. - Items that don't match are stored in the local cache as they - are encountered. - """ - while True: - # If we've cached some items that match the target value, emit - # the first one and evict it from the cache. - if self._cache[value]: - yield self._cache[value].popleft() - # Otherwise we need to advance the parent iterator to search for - # a matching item, caching the rest. - else: - while True: - try: - item = next(self._it) - except StopIteration: - return - item_value = self._key(item) - if item_value == value: - yield item - break - elif self._validator(item_value): - self._cache[item_value].append(item) - - def __iter__(self): - for item in self._it: - item_value = self._key(item) - if self._validator(item_value): - self._cache[item_value].append(item) - - yield from self._cache.keys() - - def __getitem__(self, value): - if not self._validator(value): - return iter(()) - - return self._get_values(value) - - -def spy(iterable, n=1): - """Return a 2-tuple with a list containing the first *n* elements of - *iterable*, and an iterator with the same items as *iterable*. - This allows you to "look ahead" at the items in the iterable without - advancing it. - - There is one item in the list by default: - - >>> iterable = 'abcdefg' - >>> head, iterable = spy(iterable) - >>> head - ['a'] - >>> list(iterable) - ['a', 'b', 'c', 'd', 'e', 'f', 'g'] - - You may use unpacking to retrieve items instead of lists: - - >>> (head,), iterable = spy('abcdefg') - >>> head - 'a' - >>> (first, second), iterable = spy('abcdefg', 2) - >>> first - 'a' - >>> second - 'b' - - The number of items requested can be larger than the number of items in - the iterable: - - >>> iterable = [1, 2, 3, 4, 5] - >>> head, iterable = spy(iterable, 10) - >>> head - [1, 2, 3, 4, 5] - >>> list(iterable) - [1, 2, 3, 4, 5] - - """ - it = iter(iterable) - head = take(n, it) - - return head.copy(), chain(head, it) - - -def interleave(*iterables): - """Return a new iterable yielding from each iterable in turn, - until the shortest is exhausted. - - >>> list(interleave([1, 2, 3], [4, 5], [6, 7, 8])) - [1, 4, 6, 2, 5, 7] - - For a version that doesn't terminate after the shortest iterable is - exhausted, see :func:`interleave_longest`. - - """ - return chain.from_iterable(zip(*iterables)) - - -def interleave_longest(*iterables): - """Return a new iterable yielding from each iterable in turn, - skipping any that are exhausted. - - >>> list(interleave_longest([1, 2, 3], [4, 5], [6, 7, 8])) - [1, 4, 6, 2, 5, 7, 3, 8] - - This function produces the same output as :func:`roundrobin`, but may - perform better for some inputs (in particular when the number of iterables - is large). - - """ - i = chain.from_iterable(zip_longest(*iterables, fillvalue=_marker)) - return (x for x in i if x is not _marker) - - -def interleave_evenly(iterables, lengths=None): - """ - Interleave multiple iterables so that their elements are evenly distributed - throughout the output sequence. - - >>> iterables = [1, 2, 3, 4, 5], ['a', 'b'] - >>> list(interleave_evenly(iterables)) - [1, 2, 'a', 3, 4, 'b', 5] - - >>> iterables = [[1, 2, 3], [4, 5], [6, 7, 8]] - >>> list(interleave_evenly(iterables)) - [1, 6, 4, 2, 7, 3, 8, 5] - - This function requires iterables of known length. Iterables without - ``__len__()`` can be used by manually specifying lengths with *lengths*: - - >>> from itertools import combinations, repeat - >>> iterables = [combinations(range(4), 2), ['a', 'b', 'c']] - >>> lengths = [4 * (4 - 1) // 2, 3] - >>> list(interleave_evenly(iterables, lengths=lengths)) - [(0, 1), (0, 2), 'a', (0, 3), (1, 2), 'b', (1, 3), (2, 3), 'c'] - - Based on Bresenham's algorithm. - """ - if lengths is None: - try: - lengths = [len(it) for it in iterables] - except TypeError: - raise ValueError( - 'Iterable lengths could not be determined automatically. ' - 'Specify them with the lengths keyword.' - ) - elif len(iterables) != len(lengths): - raise ValueError('Mismatching number of iterables and lengths.') - - dims = len(lengths) - - # sort iterables by length, descending - lengths_permute = sorted( - range(dims), key=lambda i: lengths[i], reverse=True - ) - lengths_desc = [lengths[i] for i in lengths_permute] - iters_desc = [iter(iterables[i]) for i in lengths_permute] - - # the longest iterable is the primary one (Bresenham: the longest - # distance along an axis) - delta_primary, deltas_secondary = lengths_desc[0], lengths_desc[1:] - iter_primary, iters_secondary = iters_desc[0], iters_desc[1:] - errors = [delta_primary // dims] * len(deltas_secondary) - - to_yield = sum(lengths) - while to_yield: - yield next(iter_primary) - to_yield -= 1 - # update errors for each secondary iterable - errors = [e - delta for e, delta in zip(errors, deltas_secondary)] - - # those iterables for which the error is negative are yielded - # ("diagonal step" in Bresenham) - for i, e in enumerate(errors): - if e < 0: - yield next(iters_secondary[i]) - to_yield -= 1 - errors[i] += delta_primary - - -def collapse(iterable, base_type=None, levels=None): - """Flatten an iterable with multiple levels of nesting (e.g., a list of - lists of tuples) into non-iterable types. - - >>> iterable = [(1, 2), ([3, 4], [[5], [6]])] - >>> list(collapse(iterable)) - [1, 2, 3, 4, 5, 6] - - Binary and text strings are not considered iterable and - will not be collapsed. - - To avoid collapsing other types, specify *base_type*: - - >>> iterable = ['ab', ('cd', 'ef'), ['gh', 'ij']] - >>> list(collapse(iterable, base_type=tuple)) - ['ab', ('cd', 'ef'), 'gh', 'ij'] - - Specify *levels* to stop flattening after a certain level: - - >>> iterable = [('a', ['b']), ('c', ['d'])] - >>> list(collapse(iterable)) # Fully flattened - ['a', 'b', 'c', 'd'] - >>> list(collapse(iterable, levels=1)) # Only one level flattened - ['a', ['b'], 'c', ['d']] - - """ - - def walk(node, level): - if ( - ((levels is not None) and (level > levels)) - or isinstance(node, (str, bytes)) - or ((base_type is not None) and isinstance(node, base_type)) - ): - yield node - return - - try: - tree = iter(node) - except TypeError: - yield node - return - else: - for child in tree: - yield from walk(child, level + 1) - - yield from walk(iterable, 0) - - -def side_effect(func, iterable, chunk_size=None, before=None, after=None): - """Invoke *func* on each item in *iterable* (or on each *chunk_size* group - of items) before yielding the item. - - `func` must be a function that takes a single argument. Its return value - will be discarded. - - *before* and *after* are optional functions that take no arguments. They - will be executed before iteration starts and after it ends, respectively. - - `side_effect` can be used for logging, updating progress bars, or anything - that is not functionally "pure." - - Emitting a status message: - - >>> from more_itertools import consume - >>> func = lambda item: print('Received {}'.format(item)) - >>> consume(side_effect(func, range(2))) - Received 0 - Received 1 - - Operating on chunks of items: - - >>> pair_sums = [] - >>> func = lambda chunk: pair_sums.append(sum(chunk)) - >>> list(side_effect(func, [0, 1, 2, 3, 4, 5], 2)) - [0, 1, 2, 3, 4, 5] - >>> list(pair_sums) - [1, 5, 9] - - Writing to a file-like object: - - >>> from io import StringIO - >>> from more_itertools import consume - >>> f = StringIO() - >>> func = lambda x: print(x, file=f) - >>> before = lambda: print(u'HEADER', file=f) - >>> after = f.close - >>> it = [u'a', u'b', u'c'] - >>> consume(side_effect(func, it, before=before, after=after)) - >>> f.closed - True - - """ - try: - if before is not None: - before() - - if chunk_size is None: - for item in iterable: - func(item) - yield item - else: - for chunk in chunked(iterable, chunk_size): - func(chunk) - yield from chunk - finally: - if after is not None: - after() - - -def sliced(seq, n, strict=False): - """Yield slices of length *n* from the sequence *seq*. - - >>> list(sliced((1, 2, 3, 4, 5, 6), 3)) - [(1, 2, 3), (4, 5, 6)] - - By the default, the last yielded slice will have fewer than *n* elements - if the length of *seq* is not divisible by *n*: - - >>> list(sliced((1, 2, 3, 4, 5, 6, 7, 8), 3)) - [(1, 2, 3), (4, 5, 6), (7, 8)] - - If the length of *seq* is not divisible by *n* and *strict* is - ``True``, then ``ValueError`` will be raised before the last - slice is yielded. - - This function will only work for iterables that support slicing. - For non-sliceable iterables, see :func:`chunked`. - - """ - iterator = takewhile(len, (seq[i : i + n] for i in count(0, n))) - if strict: - - def ret(): - for _slice in iterator: - if len(_slice) != n: - raise ValueError("seq is not divisible by n.") - yield _slice - - return iter(ret()) - else: - return iterator - - -def split_at(iterable, pred, maxsplit=-1, keep_separator=False): - """Yield lists of items from *iterable*, where each list is delimited by - an item where callable *pred* returns ``True``. - - >>> list(split_at('abcdcba', lambda x: x == 'b')) - [['a'], ['c', 'd', 'c'], ['a']] - - >>> list(split_at(range(10), lambda n: n % 2 == 1)) - [[0], [2], [4], [6], [8], []] - - At most *maxsplit* splits are done. If *maxsplit* is not specified or -1, - then there is no limit on the number of splits: - - >>> list(split_at(range(10), lambda n: n % 2 == 1, maxsplit=2)) - [[0], [2], [4, 5, 6, 7, 8, 9]] - - By default, the delimiting items are not included in the output. - The include them, set *keep_separator* to ``True``. - - >>> list(split_at('abcdcba', lambda x: x == 'b', keep_separator=True)) - [['a'], ['b'], ['c', 'd', 'c'], ['b'], ['a']] - - """ - if maxsplit == 0: - yield list(iterable) - return - - buf = [] - it = iter(iterable) - for item in it: - if pred(item): - yield buf - if keep_separator: - yield [item] - if maxsplit == 1: - yield list(it) - return - buf = [] - maxsplit -= 1 - else: - buf.append(item) - yield buf - - -def split_before(iterable, pred, maxsplit=-1): - """Yield lists of items from *iterable*, where each list ends just before - an item for which callable *pred* returns ``True``: - - >>> list(split_before('OneTwo', lambda s: s.isupper())) - [['O', 'n', 'e'], ['T', 'w', 'o']] - - >>> list(split_before(range(10), lambda n: n % 3 == 0)) - [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]] - - At most *maxsplit* splits are done. If *maxsplit* is not specified or -1, - then there is no limit on the number of splits: - - >>> list(split_before(range(10), lambda n: n % 3 == 0, maxsplit=2)) - [[0, 1, 2], [3, 4, 5], [6, 7, 8, 9]] - """ - if maxsplit == 0: - yield list(iterable) - return - - buf = [] - it = iter(iterable) - for item in it: - if pred(item) and buf: - yield buf - if maxsplit == 1: - yield [item] + list(it) - return - buf = [] - maxsplit -= 1 - buf.append(item) - if buf: - yield buf - - -def split_after(iterable, pred, maxsplit=-1): - """Yield lists of items from *iterable*, where each list ends with an - item where callable *pred* returns ``True``: - - >>> list(split_after('one1two2', lambda s: s.isdigit())) - [['o', 'n', 'e', '1'], ['t', 'w', 'o', '2']] - - >>> list(split_after(range(10), lambda n: n % 3 == 0)) - [[0], [1, 2, 3], [4, 5, 6], [7, 8, 9]] - - At most *maxsplit* splits are done. If *maxsplit* is not specified or -1, - then there is no limit on the number of splits: - - >>> list(split_after(range(10), lambda n: n % 3 == 0, maxsplit=2)) - [[0], [1, 2, 3], [4, 5, 6, 7, 8, 9]] - - """ - if maxsplit == 0: - yield list(iterable) - return - - buf = [] - it = iter(iterable) - for item in it: - buf.append(item) - if pred(item) and buf: - yield buf - if maxsplit == 1: - yield list(it) - return - buf = [] - maxsplit -= 1 - if buf: - yield buf - - -def split_when(iterable, pred, maxsplit=-1): - """Split *iterable* into pieces based on the output of *pred*. - *pred* should be a function that takes successive pairs of items and - returns ``True`` if the iterable should be split in between them. - - For example, to find runs of increasing numbers, split the iterable when - element ``i`` is larger than element ``i + 1``: - - >>> list(split_when([1, 2, 3, 3, 2, 5, 2, 4, 2], lambda x, y: x > y)) - [[1, 2, 3, 3], [2, 5], [2, 4], [2]] - - At most *maxsplit* splits are done. If *maxsplit* is not specified or -1, - then there is no limit on the number of splits: - - >>> list(split_when([1, 2, 3, 3, 2, 5, 2, 4, 2], - ... lambda x, y: x > y, maxsplit=2)) - [[1, 2, 3, 3], [2, 5], [2, 4, 2]] - - """ - if maxsplit == 0: - yield list(iterable) - return - - it = iter(iterable) - try: - cur_item = next(it) - except StopIteration: - return - - buf = [cur_item] - for next_item in it: - if pred(cur_item, next_item): - yield buf - if maxsplit == 1: - yield [next_item] + list(it) - return - buf = [] - maxsplit -= 1 - - buf.append(next_item) - cur_item = next_item - - yield buf - - -def split_into(iterable, sizes): - """Yield a list of sequential items from *iterable* of length 'n' for each - integer 'n' in *sizes*. - - >>> list(split_into([1,2,3,4,5,6], [1,2,3])) - [[1], [2, 3], [4, 5, 6]] - - If the sum of *sizes* is smaller than the length of *iterable*, then the - remaining items of *iterable* will not be returned. - - >>> list(split_into([1,2,3,4,5,6], [2,3])) - [[1, 2], [3, 4, 5]] - - If the sum of *sizes* is larger than the length of *iterable*, fewer items - will be returned in the iteration that overruns *iterable* and further - lists will be empty: - - >>> list(split_into([1,2,3,4], [1,2,3,4])) - [[1], [2, 3], [4], []] - - When a ``None`` object is encountered in *sizes*, the returned list will - contain items up to the end of *iterable* the same way that itertools.slice - does: - - >>> list(split_into([1,2,3,4,5,6,7,8,9,0], [2,3,None])) - [[1, 2], [3, 4, 5], [6, 7, 8, 9, 0]] - - :func:`split_into` can be useful for grouping a series of items where the - sizes of the groups are not uniform. An example would be where in a row - from a table, multiple columns represent elements of the same feature - (e.g. a point represented by x,y,z) but, the format is not the same for - all columns. - """ - # convert the iterable argument into an iterator so its contents can - # be consumed by islice in case it is a generator - it = iter(iterable) - - for size in sizes: - if size is None: - yield list(it) - return - else: - yield list(islice(it, size)) - - -def padded(iterable, fillvalue=None, n=None, next_multiple=False): - """Yield the elements from *iterable*, followed by *fillvalue*, such that - at least *n* items are emitted. - - >>> list(padded([1, 2, 3], '?', 5)) - [1, 2, 3, '?', '?'] - - If *next_multiple* is ``True``, *fillvalue* will be emitted until the - number of items emitted is a multiple of *n*:: - - >>> list(padded([1, 2, 3, 4], n=3, next_multiple=True)) - [1, 2, 3, 4, None, None] - - If *n* is ``None``, *fillvalue* will be emitted indefinitely. - - """ - it = iter(iterable) - if n is None: - yield from chain(it, repeat(fillvalue)) - elif n < 1: - raise ValueError('n must be at least 1') - else: - item_count = 0 - for item in it: - yield item - item_count += 1 - - remaining = (n - item_count) % n if next_multiple else n - item_count - for _ in range(remaining): - yield fillvalue - - -def repeat_each(iterable, n=2): - """Repeat each element in *iterable* *n* times. - - >>> list(repeat_each('ABC', 3)) - ['A', 'A', 'A', 'B', 'B', 'B', 'C', 'C', 'C'] - """ - return chain.from_iterable(map(repeat, iterable, repeat(n))) - - -def repeat_last(iterable, default=None): - """After the *iterable* is exhausted, keep yielding its last element. - - >>> list(islice(repeat_last(range(3)), 5)) - [0, 1, 2, 2, 2] - - If the iterable is empty, yield *default* forever:: - - >>> list(islice(repeat_last(range(0), 42), 5)) - [42, 42, 42, 42, 42] - - """ - item = _marker - for item in iterable: - yield item - final = default if item is _marker else item - yield from repeat(final) - - -def distribute(n, iterable): - """Distribute the items from *iterable* among *n* smaller iterables. - - >>> group_1, group_2 = distribute(2, [1, 2, 3, 4, 5, 6]) - >>> list(group_1) - [1, 3, 5] - >>> list(group_2) - [2, 4, 6] - - If the length of *iterable* is not evenly divisible by *n*, then the - length of the returned iterables will not be identical: - - >>> children = distribute(3, [1, 2, 3, 4, 5, 6, 7]) - >>> [list(c) for c in children] - [[1, 4, 7], [2, 5], [3, 6]] - - If the length of *iterable* is smaller than *n*, then the last returned - iterables will be empty: - - >>> children = distribute(5, [1, 2, 3]) - >>> [list(c) for c in children] - [[1], [2], [3], [], []] - - This function uses :func:`itertools.tee` and may require significant - storage. If you need the order items in the smaller iterables to match the - original iterable, see :func:`divide`. - - """ - if n < 1: - raise ValueError('n must be at least 1') - - children = tee(iterable, n) - return [islice(it, index, None, n) for index, it in enumerate(children)] - - -def stagger(iterable, offsets=(-1, 0, 1), longest=False, fillvalue=None): - """Yield tuples whose elements are offset from *iterable*. - The amount by which the `i`-th item in each tuple is offset is given by - the `i`-th item in *offsets*. - - >>> list(stagger([0, 1, 2, 3])) - [(None, 0, 1), (0, 1, 2), (1, 2, 3)] - >>> list(stagger(range(8), offsets=(0, 2, 4))) - [(0, 2, 4), (1, 3, 5), (2, 4, 6), (3, 5, 7)] - - By default, the sequence will end when the final element of a tuple is the - last item in the iterable. To continue until the first element of a tuple - is the last item in the iterable, set *longest* to ``True``:: - - >>> list(stagger([0, 1, 2, 3], longest=True)) - [(None, 0, 1), (0, 1, 2), (1, 2, 3), (2, 3, None), (3, None, None)] - - By default, ``None`` will be used to replace offsets beyond the end of the - sequence. Specify *fillvalue* to use some other value. - - """ - children = tee(iterable, len(offsets)) - - return zip_offset( - *children, offsets=offsets, longest=longest, fillvalue=fillvalue - ) - - -class UnequalIterablesError(ValueError): - def __init__(self, details=None): - msg = 'Iterables have different lengths' - if details is not None: - msg += (': index 0 has length {}; index {} has length {}').format( - *details - ) - - super().__init__(msg) - - -def _zip_equal_generator(iterables): - for combo in zip_longest(*iterables, fillvalue=_marker): - for val in combo: - if val is _marker: - raise UnequalIterablesError() - yield combo - - -def _zip_equal(*iterables): - # Check whether the iterables are all the same size. - try: - first_size = len(iterables[0]) - for i, it in enumerate(iterables[1:], 1): - size = len(it) - if size != first_size: - break - else: - # If we didn't break out, we can use the built-in zip. - return zip(*iterables) - - # If we did break out, there was a mismatch. - raise UnequalIterablesError(details=(first_size, i, size)) - # If any one of the iterables didn't have a length, start reading - # them until one runs out. - except TypeError: - return _zip_equal_generator(iterables) - - -def zip_equal(*iterables): - """``zip`` the input *iterables* together, but raise - ``UnequalIterablesError`` if they aren't all the same length. - - >>> it_1 = range(3) - >>> it_2 = iter('abc') - >>> list(zip_equal(it_1, it_2)) - [(0, 'a'), (1, 'b'), (2, 'c')] - - >>> it_1 = range(3) - >>> it_2 = iter('abcd') - >>> list(zip_equal(it_1, it_2)) # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ... - more_itertools.more.UnequalIterablesError: Iterables have different - lengths - - """ - if hexversion >= 0x30A00A6: - warnings.warn( - ( - 'zip_equal will be removed in a future version of ' - 'more-itertools. Use the builtin zip function with ' - 'strict=True instead.' - ), - DeprecationWarning, - ) - - return _zip_equal(*iterables) - - -def zip_offset(*iterables, offsets, longest=False, fillvalue=None): - """``zip`` the input *iterables* together, but offset the `i`-th iterable - by the `i`-th item in *offsets*. - - >>> list(zip_offset('0123', 'abcdef', offsets=(0, 1))) - [('0', 'b'), ('1', 'c'), ('2', 'd'), ('3', 'e')] - - This can be used as a lightweight alternative to SciPy or pandas to analyze - data sets in which some series have a lead or lag relationship. - - By default, the sequence will end when the shortest iterable is exhausted. - To continue until the longest iterable is exhausted, set *longest* to - ``True``. - - >>> list(zip_offset('0123', 'abcdef', offsets=(0, 1), longest=True)) - [('0', 'b'), ('1', 'c'), ('2', 'd'), ('3', 'e'), (None, 'f')] - - By default, ``None`` will be used to replace offsets beyond the end of the - sequence. Specify *fillvalue* to use some other value. - - """ - if len(iterables) != len(offsets): - raise ValueError("Number of iterables and offsets didn't match") - - staggered = [] - for it, n in zip(iterables, offsets): - if n < 0: - staggered.append(chain(repeat(fillvalue, -n), it)) - elif n > 0: - staggered.append(islice(it, n, None)) - else: - staggered.append(it) - - if longest: - return zip_longest(*staggered, fillvalue=fillvalue) - - return zip(*staggered) - - -def sort_together(iterables, key_list=(0,), key=None, reverse=False): - """Return the input iterables sorted together, with *key_list* as the - priority for sorting. All iterables are trimmed to the length of the - shortest one. - - This can be used like the sorting function in a spreadsheet. If each - iterable represents a column of data, the key list determines which - columns are used for sorting. - - By default, all iterables are sorted using the ``0``-th iterable:: - - >>> iterables = [(4, 3, 2, 1), ('a', 'b', 'c', 'd')] - >>> sort_together(iterables) - [(1, 2, 3, 4), ('d', 'c', 'b', 'a')] - - Set a different key list to sort according to another iterable. - Specifying multiple keys dictates how ties are broken:: - - >>> iterables = [(3, 1, 2), (0, 1, 0), ('c', 'b', 'a')] - >>> sort_together(iterables, key_list=(1, 2)) - [(2, 3, 1), (0, 0, 1), ('a', 'c', 'b')] - - To sort by a function of the elements of the iterable, pass a *key* - function. Its arguments are the elements of the iterables corresponding to - the key list:: - - >>> names = ('a', 'b', 'c') - >>> lengths = (1, 2, 3) - >>> widths = (5, 2, 1) - >>> def area(length, width): - ... return length * width - >>> sort_together([names, lengths, widths], key_list=(1, 2), key=area) - [('c', 'b', 'a'), (3, 2, 1), (1, 2, 5)] - - Set *reverse* to ``True`` to sort in descending order. - - >>> sort_together([(1, 2, 3), ('c', 'b', 'a')], reverse=True) - [(3, 2, 1), ('a', 'b', 'c')] - - """ - if key is None: - # if there is no key function, the key argument to sorted is an - # itemgetter - key_argument = itemgetter(*key_list) - else: - # if there is a key function, call it with the items at the offsets - # specified by the key function as arguments - key_list = list(key_list) - if len(key_list) == 1: - # if key_list contains a single item, pass the item at that offset - # as the only argument to the key function - key_offset = key_list[0] - key_argument = lambda zipped_items: key(zipped_items[key_offset]) - else: - # if key_list contains multiple items, use itemgetter to return a - # tuple of items, which we pass as *args to the key function - get_key_items = itemgetter(*key_list) - key_argument = lambda zipped_items: key( - *get_key_items(zipped_items) - ) - - return list( - zip(*sorted(zip(*iterables), key=key_argument, reverse=reverse)) - ) - - -def unzip(iterable): - """The inverse of :func:`zip`, this function disaggregates the elements - of the zipped *iterable*. - - The ``i``-th iterable contains the ``i``-th element from each element - of the zipped iterable. The first element is used to to determine the - length of the remaining elements. - - >>> iterable = [('a', 1), ('b', 2), ('c', 3), ('d', 4)] - >>> letters, numbers = unzip(iterable) - >>> list(letters) - ['a', 'b', 'c', 'd'] - >>> list(numbers) - [1, 2, 3, 4] - - This is similar to using ``zip(*iterable)``, but it avoids reading - *iterable* into memory. Note, however, that this function uses - :func:`itertools.tee` and thus may require significant storage. - - """ - head, iterable = spy(iter(iterable)) - if not head: - # empty iterable, e.g. zip([], [], []) - return () - # spy returns a one-length iterable as head - head = head[0] - iterables = tee(iterable, len(head)) - - def itemgetter(i): - def getter(obj): - try: - return obj[i] - except IndexError: - # basically if we have an iterable like - # iter([(1, 2, 3), (4, 5), (6,)]) - # the second unzipped iterable would fail at the third tuple - # since it would try to access tup[1] - # same with the third unzipped iterable and the second tuple - # to support these "improperly zipped" iterables, - # we create a custom itemgetter - # which just stops the unzipped iterables - # at first length mismatch - raise StopIteration - - return getter - - return tuple(map(itemgetter(i), it) for i, it in enumerate(iterables)) - - -def divide(n, iterable): - """Divide the elements from *iterable* into *n* parts, maintaining - order. - - >>> group_1, group_2 = divide(2, [1, 2, 3, 4, 5, 6]) - >>> list(group_1) - [1, 2, 3] - >>> list(group_2) - [4, 5, 6] - - If the length of *iterable* is not evenly divisible by *n*, then the - length of the returned iterables will not be identical: - - >>> children = divide(3, [1, 2, 3, 4, 5, 6, 7]) - >>> [list(c) for c in children] - [[1, 2, 3], [4, 5], [6, 7]] - - If the length of the iterable is smaller than n, then the last returned - iterables will be empty: - - >>> children = divide(5, [1, 2, 3]) - >>> [list(c) for c in children] - [[1], [2], [3], [], []] - - This function will exhaust the iterable before returning and may require - significant storage. If order is not important, see :func:`distribute`, - which does not first pull the iterable into memory. - - """ - if n < 1: - raise ValueError('n must be at least 1') - - try: - iterable[:0] - except TypeError: - seq = tuple(iterable) - else: - seq = iterable - - q, r = divmod(len(seq), n) - - ret = [] - stop = 0 - for i in range(1, n + 1): - start = stop - stop += q + 1 if i <= r else q - ret.append(iter(seq[start:stop])) - - return ret - - -def always_iterable(obj, base_type=(str, bytes)): - """If *obj* is iterable, return an iterator over its items:: - - >>> obj = (1, 2, 3) - >>> list(always_iterable(obj)) - [1, 2, 3] - - If *obj* is not iterable, return a one-item iterable containing *obj*:: - - >>> obj = 1 - >>> list(always_iterable(obj)) - [1] - - If *obj* is ``None``, return an empty iterable: - - >>> obj = None - >>> list(always_iterable(None)) - [] - - By default, binary and text strings are not considered iterable:: - - >>> obj = 'foo' - >>> list(always_iterable(obj)) - ['foo'] - - If *base_type* is set, objects for which ``isinstance(obj, base_type)`` - returns ``True`` won't be considered iterable. - - >>> obj = {'a': 1} - >>> list(always_iterable(obj)) # Iterate over the dict's keys - ['a'] - >>> list(always_iterable(obj, base_type=dict)) # Treat dicts as a unit - [{'a': 1}] - - Set *base_type* to ``None`` to avoid any special handling and treat objects - Python considers iterable as iterable: - - >>> obj = 'foo' - >>> list(always_iterable(obj, base_type=None)) - ['f', 'o', 'o'] - """ - if obj is None: - return iter(()) - - if (base_type is not None) and isinstance(obj, base_type): - return iter((obj,)) - - try: - return iter(obj) - except TypeError: - return iter((obj,)) - - -def adjacent(predicate, iterable, distance=1): - """Return an iterable over `(bool, item)` tuples where the `item` is - drawn from *iterable* and the `bool` indicates whether - that item satisfies the *predicate* or is adjacent to an item that does. - - For example, to find whether items are adjacent to a ``3``:: - - >>> list(adjacent(lambda x: x == 3, range(6))) - [(False, 0), (False, 1), (True, 2), (True, 3), (True, 4), (False, 5)] - - Set *distance* to change what counts as adjacent. For example, to find - whether items are two places away from a ``3``: - - >>> list(adjacent(lambda x: x == 3, range(6), distance=2)) - [(False, 0), (True, 1), (True, 2), (True, 3), (True, 4), (True, 5)] - - This is useful for contextualizing the results of a search function. - For example, a code comparison tool might want to identify lines that - have changed, but also surrounding lines to give the viewer of the diff - context. - - The predicate function will only be called once for each item in the - iterable. - - See also :func:`groupby_transform`, which can be used with this function - to group ranges of items with the same `bool` value. - - """ - # Allow distance=0 mainly for testing that it reproduces results with map() - if distance < 0: - raise ValueError('distance must be at least 0') - - i1, i2 = tee(iterable) - padding = [False] * distance - selected = chain(padding, map(predicate, i1), padding) - adjacent_to_selected = map(any, windowed(selected, 2 * distance + 1)) - return zip(adjacent_to_selected, i2) - - -def groupby_transform(iterable, keyfunc=None, valuefunc=None, reducefunc=None): - """An extension of :func:`itertools.groupby` that can apply transformations - to the grouped data. - - * *keyfunc* is a function computing a key value for each item in *iterable* - * *valuefunc* is a function that transforms the individual items from - *iterable* after grouping - * *reducefunc* is a function that transforms each group of items - - >>> iterable = 'aAAbBBcCC' - >>> keyfunc = lambda k: k.upper() - >>> valuefunc = lambda v: v.lower() - >>> reducefunc = lambda g: ''.join(g) - >>> list(groupby_transform(iterable, keyfunc, valuefunc, reducefunc)) - [('A', 'aaa'), ('B', 'bbb'), ('C', 'ccc')] - - Each optional argument defaults to an identity function if not specified. - - :func:`groupby_transform` is useful when grouping elements of an iterable - using a separate iterable as the key. To do this, :func:`zip` the iterables - and pass a *keyfunc* that extracts the first element and a *valuefunc* - that extracts the second element:: - - >>> from operator import itemgetter - >>> keys = [0, 0, 1, 1, 1, 2, 2, 2, 3] - >>> values = 'abcdefghi' - >>> iterable = zip(keys, values) - >>> grouper = groupby_transform(iterable, itemgetter(0), itemgetter(1)) - >>> [(k, ''.join(g)) for k, g in grouper] - [(0, 'ab'), (1, 'cde'), (2, 'fgh'), (3, 'i')] - - Note that the order of items in the iterable is significant. - Only adjacent items are grouped together, so if you don't want any - duplicate groups, you should sort the iterable by the key function. - - """ - ret = groupby(iterable, keyfunc) - if valuefunc: - ret = ((k, map(valuefunc, g)) for k, g in ret) - if reducefunc: - ret = ((k, reducefunc(g)) for k, g in ret) - - return ret - - -class numeric_range(abc.Sequence, abc.Hashable): - """An extension of the built-in ``range()`` function whose arguments can - be any orderable numeric type. - - With only *stop* specified, *start* defaults to ``0`` and *step* - defaults to ``1``. The output items will match the type of *stop*: - - >>> list(numeric_range(3.5)) - [0.0, 1.0, 2.0, 3.0] - - With only *start* and *stop* specified, *step* defaults to ``1``. The - output items will match the type of *start*: - - >>> from decimal import Decimal - >>> start = Decimal('2.1') - >>> stop = Decimal('5.1') - >>> list(numeric_range(start, stop)) - [Decimal('2.1'), Decimal('3.1'), Decimal('4.1')] - - With *start*, *stop*, and *step* specified the output items will match - the type of ``start + step``: - - >>> from fractions import Fraction - >>> start = Fraction(1, 2) # Start at 1/2 - >>> stop = Fraction(5, 2) # End at 5/2 - >>> step = Fraction(1, 2) # Count by 1/2 - >>> list(numeric_range(start, stop, step)) - [Fraction(1, 2), Fraction(1, 1), Fraction(3, 2), Fraction(2, 1)] - - If *step* is zero, ``ValueError`` is raised. Negative steps are supported: - - >>> list(numeric_range(3, -1, -1.0)) - [3.0, 2.0, 1.0, 0.0] - - Be aware of the limitations of floating point numbers; the representation - of the yielded numbers may be surprising. - - ``datetime.datetime`` objects can be used for *start* and *stop*, if *step* - is a ``datetime.timedelta`` object: - - >>> import datetime - >>> start = datetime.datetime(2019, 1, 1) - >>> stop = datetime.datetime(2019, 1, 3) - >>> step = datetime.timedelta(days=1) - >>> items = iter(numeric_range(start, stop, step)) - >>> next(items) - datetime.datetime(2019, 1, 1, 0, 0) - >>> next(items) - datetime.datetime(2019, 1, 2, 0, 0) - - """ - - _EMPTY_HASH = hash(range(0, 0)) - - def __init__(self, *args): - argc = len(args) - if argc == 1: - (self._stop,) = args - self._start = type(self._stop)(0) - self._step = type(self._stop - self._start)(1) - elif argc == 2: - self._start, self._stop = args - self._step = type(self._stop - self._start)(1) - elif argc == 3: - self._start, self._stop, self._step = args - elif argc == 0: - raise TypeError( - 'numeric_range expected at least ' - '1 argument, got {}'.format(argc) - ) - else: - raise TypeError( - 'numeric_range expected at most ' - '3 arguments, got {}'.format(argc) - ) - - self._zero = type(self._step)(0) - if self._step == self._zero: - raise ValueError('numeric_range() arg 3 must not be zero') - self._growing = self._step > self._zero - self._init_len() - - def __bool__(self): - if self._growing: - return self._start < self._stop - else: - return self._start > self._stop - - def __contains__(self, elem): - if self._growing: - if self._start <= elem < self._stop: - return (elem - self._start) % self._step == self._zero - else: - if self._start >= elem > self._stop: - return (self._start - elem) % (-self._step) == self._zero - - return False - - def __eq__(self, other): - if isinstance(other, numeric_range): - empty_self = not bool(self) - empty_other = not bool(other) - if empty_self or empty_other: - return empty_self and empty_other # True if both empty - else: - return ( - self._start == other._start - and self._step == other._step - and self._get_by_index(-1) == other._get_by_index(-1) - ) - else: - return False - - def __getitem__(self, key): - if isinstance(key, int): - return self._get_by_index(key) - elif isinstance(key, slice): - step = self._step if key.step is None else key.step * self._step - - if key.start is None or key.start <= -self._len: - start = self._start - elif key.start >= self._len: - start = self._stop - else: # -self._len < key.start < self._len - start = self._get_by_index(key.start) - - if key.stop is None or key.stop >= self._len: - stop = self._stop - elif key.stop <= -self._len: - stop = self._start - else: # -self._len < key.stop < self._len - stop = self._get_by_index(key.stop) - - return numeric_range(start, stop, step) - else: - raise TypeError( - 'numeric range indices must be ' - 'integers or slices, not {}'.format(type(key).__name__) - ) - - def __hash__(self): - if self: - return hash((self._start, self._get_by_index(-1), self._step)) - else: - return self._EMPTY_HASH - - def __iter__(self): - values = (self._start + (n * self._step) for n in count()) - if self._growing: - return takewhile(partial(gt, self._stop), values) - else: - return takewhile(partial(lt, self._stop), values) - - def __len__(self): - return self._len - - def _init_len(self): - if self._growing: - start = self._start - stop = self._stop - step = self._step - else: - start = self._stop - stop = self._start - step = -self._step - distance = stop - start - if distance <= self._zero: - self._len = 0 - else: # distance > 0 and step > 0: regular euclidean division - q, r = divmod(distance, step) - self._len = int(q) + int(r != self._zero) - - def __reduce__(self): - return numeric_range, (self._start, self._stop, self._step) - - def __repr__(self): - if self._step == 1: - return "numeric_range({}, {})".format( - repr(self._start), repr(self._stop) - ) - else: - return "numeric_range({}, {}, {})".format( - repr(self._start), repr(self._stop), repr(self._step) - ) - - def __reversed__(self): - return iter( - numeric_range( - self._get_by_index(-1), self._start - self._step, -self._step - ) - ) - - def count(self, value): - return int(value in self) - - def index(self, value): - if self._growing: - if self._start <= value < self._stop: - q, r = divmod(value - self._start, self._step) - if r == self._zero: - return int(q) - else: - if self._start >= value > self._stop: - q, r = divmod(self._start - value, -self._step) - if r == self._zero: - return int(q) - - raise ValueError("{} is not in numeric range".format(value)) - - def _get_by_index(self, i): - if i < 0: - i += self._len - if i < 0 or i >= self._len: - raise IndexError("numeric range object index out of range") - return self._start + i * self._step - - -def count_cycle(iterable, n=None): - """Cycle through the items from *iterable* up to *n* times, yielding - the number of completed cycles along with each item. If *n* is omitted the - process repeats indefinitely. - - >>> list(count_cycle('AB', 3)) - [(0, 'A'), (0, 'B'), (1, 'A'), (1, 'B'), (2, 'A'), (2, 'B')] - - """ - iterable = tuple(iterable) - if not iterable: - return iter(()) - counter = count() if n is None else range(n) - return ((i, item) for i in counter for item in iterable) - - -def mark_ends(iterable): - """Yield 3-tuples of the form ``(is_first, is_last, item)``. - - >>> list(mark_ends('ABC')) - [(True, False, 'A'), (False, False, 'B'), (False, True, 'C')] - - Use this when looping over an iterable to take special action on its first - and/or last items: - - >>> iterable = ['Header', 100, 200, 'Footer'] - >>> total = 0 - >>> for is_first, is_last, item in mark_ends(iterable): - ... if is_first: - ... continue # Skip the header - ... if is_last: - ... continue # Skip the footer - ... total += item - >>> print(total) - 300 - """ - it = iter(iterable) - - try: - b = next(it) - except StopIteration: - return - - try: - for i in count(): - a = b - b = next(it) - yield i == 0, False, a - - except StopIteration: - yield i == 0, True, a - - -def locate(iterable, pred=bool, window_size=None): - """Yield the index of each item in *iterable* for which *pred* returns - ``True``. - - *pred* defaults to :func:`bool`, which will select truthy items: - - >>> list(locate([0, 1, 1, 0, 1, 0, 0])) - [1, 2, 4] - - Set *pred* to a custom function to, e.g., find the indexes for a particular - item. - - >>> list(locate(['a', 'b', 'c', 'b'], lambda x: x == 'b')) - [1, 3] - - If *window_size* is given, then the *pred* function will be called with - that many items. This enables searching for sub-sequences: - - >>> iterable = [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3] - >>> pred = lambda *args: args == (1, 2, 3) - >>> list(locate(iterable, pred=pred, window_size=3)) - [1, 5, 9] - - Use with :func:`seekable` to find indexes and then retrieve the associated - items: - - >>> from itertools import count - >>> from more_itertools import seekable - >>> source = (3 * n + 1 if (n % 2) else n // 2 for n in count()) - >>> it = seekable(source) - >>> pred = lambda x: x > 100 - >>> indexes = locate(it, pred=pred) - >>> i = next(indexes) - >>> it.seek(i) - >>> next(it) - 106 - - """ - if window_size is None: - return compress(count(), map(pred, iterable)) - - if window_size < 1: - raise ValueError('window size must be at least 1') - - it = windowed(iterable, window_size, fillvalue=_marker) - return compress(count(), starmap(pred, it)) - - -def lstrip(iterable, pred): - """Yield the items from *iterable*, but strip any from the beginning - for which *pred* returns ``True``. - - For example, to remove a set of items from the start of an iterable: - - >>> iterable = (None, False, None, 1, 2, None, 3, False, None) - >>> pred = lambda x: x in {None, False, ''} - >>> list(lstrip(iterable, pred)) - [1, 2, None, 3, False, None] - - This function is analogous to to :func:`str.lstrip`, and is essentially - an wrapper for :func:`itertools.dropwhile`. - - """ - return dropwhile(pred, iterable) - - -def rstrip(iterable, pred): - """Yield the items from *iterable*, but strip any from the end - for which *pred* returns ``True``. - - For example, to remove a set of items from the end of an iterable: - - >>> iterable = (None, False, None, 1, 2, None, 3, False, None) - >>> pred = lambda x: x in {None, False, ''} - >>> list(rstrip(iterable, pred)) - [None, False, None, 1, 2, None, 3] - - This function is analogous to :func:`str.rstrip`. - - """ - cache = [] - cache_append = cache.append - cache_clear = cache.clear - for x in iterable: - if pred(x): - cache_append(x) - else: - yield from cache - cache_clear() - yield x - - -def strip(iterable, pred): - """Yield the items from *iterable*, but strip any from the - beginning and end for which *pred* returns ``True``. - - For example, to remove a set of items from both ends of an iterable: - - >>> iterable = (None, False, None, 1, 2, None, 3, False, None) - >>> pred = lambda x: x in {None, False, ''} - >>> list(strip(iterable, pred)) - [1, 2, None, 3] - - This function is analogous to :func:`str.strip`. - - """ - return rstrip(lstrip(iterable, pred), pred) - - -class islice_extended: - """An extension of :func:`itertools.islice` that supports negative values - for *stop*, *start*, and *step*. - - >>> iterable = iter('abcdefgh') - >>> list(islice_extended(iterable, -4, -1)) - ['e', 'f', 'g'] - - Slices with negative values require some caching of *iterable*, but this - function takes care to minimize the amount of memory required. - - For example, you can use a negative step with an infinite iterator: - - >>> from itertools import count - >>> list(islice_extended(count(), 110, 99, -2)) - [110, 108, 106, 104, 102, 100] - - You can also use slice notation directly: - - >>> iterable = map(str, count()) - >>> it = islice_extended(iterable)[10:20:2] - >>> list(it) - ['10', '12', '14', '16', '18'] - - """ - - def __init__(self, iterable, *args): - it = iter(iterable) - if args: - self._iterable = _islice_helper(it, slice(*args)) - else: - self._iterable = it - - def __iter__(self): - return self - - def __next__(self): - return next(self._iterable) - - def __getitem__(self, key): - if isinstance(key, slice): - return islice_extended(_islice_helper(self._iterable, key)) - - raise TypeError('islice_extended.__getitem__ argument must be a slice') - - -def _islice_helper(it, s): - start = s.start - stop = s.stop - if s.step == 0: - raise ValueError('step argument must be a non-zero integer or None.') - step = s.step or 1 - - if step > 0: - start = 0 if (start is None) else start - - if start < 0: - # Consume all but the last -start items - cache = deque(enumerate(it, 1), maxlen=-start) - len_iter = cache[-1][0] if cache else 0 - - # Adjust start to be positive - i = max(len_iter + start, 0) - - # Adjust stop to be positive - if stop is None: - j = len_iter - elif stop >= 0: - j = min(stop, len_iter) - else: - j = max(len_iter + stop, 0) - - # Slice the cache - n = j - i - if n <= 0: - return - - for index, item in islice(cache, 0, n, step): - yield item - elif (stop is not None) and (stop < 0): - # Advance to the start position - next(islice(it, start, start), None) - - # When stop is negative, we have to carry -stop items while - # iterating - cache = deque(islice(it, -stop), maxlen=-stop) - - for index, item in enumerate(it): - cached_item = cache.popleft() - if index % step == 0: - yield cached_item - cache.append(item) - else: - # When both start and stop are positive we have the normal case - yield from islice(it, start, stop, step) - else: - start = -1 if (start is None) else start - - if (stop is not None) and (stop < 0): - # Consume all but the last items - n = -stop - 1 - cache = deque(enumerate(it, 1), maxlen=n) - len_iter = cache[-1][0] if cache else 0 - - # If start and stop are both negative they are comparable and - # we can just slice. Otherwise we can adjust start to be negative - # and then slice. - if start < 0: - i, j = start, stop - else: - i, j = min(start - len_iter, -1), None - - for index, item in list(cache)[i:j:step]: - yield item - else: - # Advance to the stop position - if stop is not None: - m = stop + 1 - next(islice(it, m, m), None) - - # stop is positive, so if start is negative they are not comparable - # and we need the rest of the items. - if start < 0: - i = start - n = None - # stop is None and start is positive, so we just need items up to - # the start index. - elif stop is None: - i = None - n = start + 1 - # Both stop and start are positive, so they are comparable. - else: - i = None - n = start - stop - if n <= 0: - return - - cache = list(islice(it, n)) - - yield from cache[i::step] - - -def always_reversible(iterable): - """An extension of :func:`reversed` that supports all iterables, not - just those which implement the ``Reversible`` or ``Sequence`` protocols. - - >>> print(*always_reversible(x for x in range(3))) - 2 1 0 - - If the iterable is already reversible, this function returns the - result of :func:`reversed()`. If the iterable is not reversible, - this function will cache the remaining items in the iterable and - yield them in reverse order, which may require significant storage. - """ - try: - return reversed(iterable) - except TypeError: - return reversed(list(iterable)) - - -def consecutive_groups(iterable, ordering=lambda x: x): - """Yield groups of consecutive items using :func:`itertools.groupby`. - The *ordering* function determines whether two items are adjacent by - returning their position. - - By default, the ordering function is the identity function. This is - suitable for finding runs of numbers: - - >>> iterable = [1, 10, 11, 12, 20, 30, 31, 32, 33, 40] - >>> for group in consecutive_groups(iterable): - ... print(list(group)) - [1] - [10, 11, 12] - [20] - [30, 31, 32, 33] - [40] - - For finding runs of adjacent letters, try using the :meth:`index` method - of a string of letters: - - >>> from string import ascii_lowercase - >>> iterable = 'abcdfgilmnop' - >>> ordering = ascii_lowercase.index - >>> for group in consecutive_groups(iterable, ordering): - ... print(list(group)) - ['a', 'b', 'c', 'd'] - ['f', 'g'] - ['i'] - ['l', 'm', 'n', 'o', 'p'] - - Each group of consecutive items is an iterator that shares it source with - *iterable*. When an an output group is advanced, the previous group is - no longer available unless its elements are copied (e.g., into a ``list``). - - >>> iterable = [1, 2, 11, 12, 21, 22] - >>> saved_groups = [] - >>> for group in consecutive_groups(iterable): - ... saved_groups.append(list(group)) # Copy group elements - >>> saved_groups - [[1, 2], [11, 12], [21, 22]] - - """ - for k, g in groupby( - enumerate(iterable), key=lambda x: x[0] - ordering(x[1]) - ): - yield map(itemgetter(1), g) - - -def difference(iterable, func=sub, *, initial=None): - """This function is the inverse of :func:`itertools.accumulate`. By default - it will compute the first difference of *iterable* using - :func:`operator.sub`: - - >>> from itertools import accumulate - >>> iterable = accumulate([0, 1, 2, 3, 4]) # produces 0, 1, 3, 6, 10 - >>> list(difference(iterable)) - [0, 1, 2, 3, 4] - - *func* defaults to :func:`operator.sub`, but other functions can be - specified. They will be applied as follows:: - - A, B, C, D, ... --> A, func(B, A), func(C, B), func(D, C), ... - - For example, to do progressive division: - - >>> iterable = [1, 2, 6, 24, 120] - >>> func = lambda x, y: x // y - >>> list(difference(iterable, func)) - [1, 2, 3, 4, 5] - - If the *initial* keyword is set, the first element will be skipped when - computing successive differences. - - >>> it = [10, 11, 13, 16] # from accumulate([1, 2, 3], initial=10) - >>> list(difference(it, initial=10)) - [1, 2, 3] - - """ - a, b = tee(iterable) - try: - first = [next(b)] - except StopIteration: - return iter([]) - - if initial is not None: - first = [] - - return chain(first, starmap(func, zip(b, a))) - - -class SequenceView(Sequence): - """Return a read-only view of the sequence object *target*. - - :class:`SequenceView` objects are analogous to Python's built-in - "dictionary view" types. They provide a dynamic view of a sequence's items, - meaning that when the sequence updates, so does the view. - - >>> seq = ['0', '1', '2'] - >>> view = SequenceView(seq) - >>> view - SequenceView(['0', '1', '2']) - >>> seq.append('3') - >>> view - SequenceView(['0', '1', '2', '3']) - - Sequence views support indexing, slicing, and length queries. They act - like the underlying sequence, except they don't allow assignment: - - >>> view[1] - '1' - >>> view[1:-1] - ['1', '2'] - >>> len(view) - 4 - - Sequence views are useful as an alternative to copying, as they don't - require (much) extra storage. - - """ - - def __init__(self, target): - if not isinstance(target, Sequence): - raise TypeError - self._target = target - - def __getitem__(self, index): - return self._target[index] - - def __len__(self): - return len(self._target) - - def __repr__(self): - return '{}({})'.format(self.__class__.__name__, repr(self._target)) - - -class seekable: - """Wrap an iterator to allow for seeking backward and forward. This - progressively caches the items in the source iterable so they can be - re-visited. - - Call :meth:`seek` with an index to seek to that position in the source - iterable. - - To "reset" an iterator, seek to ``0``: - - >>> from itertools import count - >>> it = seekable((str(n) for n in count())) - >>> next(it), next(it), next(it) - ('0', '1', '2') - >>> it.seek(0) - >>> next(it), next(it), next(it) - ('0', '1', '2') - >>> next(it) - '3' - - You can also seek forward: - - >>> it = seekable((str(n) for n in range(20))) - >>> it.seek(10) - >>> next(it) - '10' - >>> it.seek(20) # Seeking past the end of the source isn't a problem - >>> list(it) - [] - >>> it.seek(0) # Resetting works even after hitting the end - >>> next(it), next(it), next(it) - ('0', '1', '2') - - Call :meth:`peek` to look ahead one item without advancing the iterator: - - >>> it = seekable('1234') - >>> it.peek() - '1' - >>> list(it) - ['1', '2', '3', '4'] - >>> it.peek(default='empty') - 'empty' - - Before the iterator is at its end, calling :func:`bool` on it will return - ``True``. After it will return ``False``: - - >>> it = seekable('5678') - >>> bool(it) - True - >>> list(it) - ['5', '6', '7', '8'] - >>> bool(it) - False - - You may view the contents of the cache with the :meth:`elements` method. - That returns a :class:`SequenceView`, a view that updates automatically: - - >>> it = seekable((str(n) for n in range(10))) - >>> next(it), next(it), next(it) - ('0', '1', '2') - >>> elements = it.elements() - >>> elements - SequenceView(['0', '1', '2']) - >>> next(it) - '3' - >>> elements - SequenceView(['0', '1', '2', '3']) - - By default, the cache grows as the source iterable progresses, so beware of - wrapping very large or infinite iterables. Supply *maxlen* to limit the - size of the cache (this of course limits how far back you can seek). - - >>> from itertools import count - >>> it = seekable((str(n) for n in count()), maxlen=2) - >>> next(it), next(it), next(it), next(it) - ('0', '1', '2', '3') - >>> list(it.elements()) - ['2', '3'] - >>> it.seek(0) - >>> next(it), next(it), next(it), next(it) - ('2', '3', '4', '5') - >>> next(it) - '6' - - """ - - def __init__(self, iterable, maxlen=None): - self._source = iter(iterable) - if maxlen is None: - self._cache = [] - else: - self._cache = deque([], maxlen) - self._index = None - - def __iter__(self): - return self - - def __next__(self): - if self._index is not None: - try: - item = self._cache[self._index] - except IndexError: - self._index = None - else: - self._index += 1 - return item - - item = next(self._source) - self._cache.append(item) - return item - - def __bool__(self): - try: - self.peek() - except StopIteration: - return False - return True - - def peek(self, default=_marker): - try: - peeked = next(self) - except StopIteration: - if default is _marker: - raise - return default - if self._index is None: - self._index = len(self._cache) - self._index -= 1 - return peeked - - def elements(self): - return SequenceView(self._cache) - - def seek(self, index): - self._index = index - remainder = index - len(self._cache) - if remainder > 0: - consume(self, remainder) - - -class run_length: - """ - :func:`run_length.encode` compresses an iterable with run-length encoding. - It yields groups of repeated items with the count of how many times they - were repeated: - - >>> uncompressed = 'abbcccdddd' - >>> list(run_length.encode(uncompressed)) - [('a', 1), ('b', 2), ('c', 3), ('d', 4)] - - :func:`run_length.decode` decompresses an iterable that was previously - compressed with run-length encoding. It yields the items of the - decompressed iterable: - - >>> compressed = [('a', 1), ('b', 2), ('c', 3), ('d', 4)] - >>> list(run_length.decode(compressed)) - ['a', 'b', 'b', 'c', 'c', 'c', 'd', 'd', 'd', 'd'] - - """ - - @staticmethod - def encode(iterable): - return ((k, ilen(g)) for k, g in groupby(iterable)) - - @staticmethod - def decode(iterable): - return chain.from_iterable(repeat(k, n) for k, n in iterable) - - -def exactly_n(iterable, n, predicate=bool): - """Return ``True`` if exactly ``n`` items in the iterable are ``True`` - according to the *predicate* function. - - >>> exactly_n([True, True, False], 2) - True - >>> exactly_n([True, True, False], 1) - False - >>> exactly_n([0, 1, 2, 3, 4, 5], 3, lambda x: x < 3) - True - - The iterable will be advanced until ``n + 1`` truthy items are encountered, - so avoid calling it on infinite iterables. - - """ - return len(take(n + 1, filter(predicate, iterable))) == n - - -def circular_shifts(iterable): - """Return a list of circular shifts of *iterable*. - - >>> circular_shifts(range(4)) - [(0, 1, 2, 3), (1, 2, 3, 0), (2, 3, 0, 1), (3, 0, 1, 2)] - """ - lst = list(iterable) - return take(len(lst), windowed(cycle(lst), len(lst))) - - -def make_decorator(wrapping_func, result_index=0): - """Return a decorator version of *wrapping_func*, which is a function that - modifies an iterable. *result_index* is the position in that function's - signature where the iterable goes. - - This lets you use itertools on the "production end," i.e. at function - definition. This can augment what the function returns without changing the - function's code. - - For example, to produce a decorator version of :func:`chunked`: - - >>> from more_itertools import chunked - >>> chunker = make_decorator(chunked, result_index=0) - >>> @chunker(3) - ... def iter_range(n): - ... return iter(range(n)) - ... - >>> list(iter_range(9)) - [[0, 1, 2], [3, 4, 5], [6, 7, 8]] - - To only allow truthy items to be returned: - - >>> truth_serum = make_decorator(filter, result_index=1) - >>> @truth_serum(bool) - ... def boolean_test(): - ... return [0, 1, '', ' ', False, True] - ... - >>> list(boolean_test()) - [1, ' ', True] - - The :func:`peekable` and :func:`seekable` wrappers make for practical - decorators: - - >>> from more_itertools import peekable - >>> peekable_function = make_decorator(peekable) - >>> @peekable_function() - ... def str_range(*args): - ... return (str(x) for x in range(*args)) - ... - >>> it = str_range(1, 20, 2) - >>> next(it), next(it), next(it) - ('1', '3', '5') - >>> it.peek() - '7' - >>> next(it) - '7' - - """ - # See https://sites.google.com/site/bbayles/index/decorator_factory for - # notes on how this works. - def decorator(*wrapping_args, **wrapping_kwargs): - def outer_wrapper(f): - def inner_wrapper(*args, **kwargs): - result = f(*args, **kwargs) - wrapping_args_ = list(wrapping_args) - wrapping_args_.insert(result_index, result) - return wrapping_func(*wrapping_args_, **wrapping_kwargs) - - return inner_wrapper - - return outer_wrapper - - return decorator - - -def map_reduce(iterable, keyfunc, valuefunc=None, reducefunc=None): - """Return a dictionary that maps the items in *iterable* to categories - defined by *keyfunc*, transforms them with *valuefunc*, and - then summarizes them by category with *reducefunc*. - - *valuefunc* defaults to the identity function if it is unspecified. - If *reducefunc* is unspecified, no summarization takes place: - - >>> keyfunc = lambda x: x.upper() - >>> result = map_reduce('abbccc', keyfunc) - >>> sorted(result.items()) - [('A', ['a']), ('B', ['b', 'b']), ('C', ['c', 'c', 'c'])] - - Specifying *valuefunc* transforms the categorized items: - - >>> keyfunc = lambda x: x.upper() - >>> valuefunc = lambda x: 1 - >>> result = map_reduce('abbccc', keyfunc, valuefunc) - >>> sorted(result.items()) - [('A', [1]), ('B', [1, 1]), ('C', [1, 1, 1])] - - Specifying *reducefunc* summarizes the categorized items: - - >>> keyfunc = lambda x: x.upper() - >>> valuefunc = lambda x: 1 - >>> reducefunc = sum - >>> result = map_reduce('abbccc', keyfunc, valuefunc, reducefunc) - >>> sorted(result.items()) - [('A', 1), ('B', 2), ('C', 3)] - - You may want to filter the input iterable before applying the map/reduce - procedure: - - >>> all_items = range(30) - >>> items = [x for x in all_items if 10 <= x <= 20] # Filter - >>> keyfunc = lambda x: x % 2 # Evens map to 0; odds to 1 - >>> categories = map_reduce(items, keyfunc=keyfunc) - >>> sorted(categories.items()) - [(0, [10, 12, 14, 16, 18, 20]), (1, [11, 13, 15, 17, 19])] - >>> summaries = map_reduce(items, keyfunc=keyfunc, reducefunc=sum) - >>> sorted(summaries.items()) - [(0, 90), (1, 75)] - - Note that all items in the iterable are gathered into a list before the - summarization step, which may require significant storage. - - The returned object is a :obj:`collections.defaultdict` with the - ``default_factory`` set to ``None``, such that it behaves like a normal - dictionary. - - """ - valuefunc = (lambda x: x) if (valuefunc is None) else valuefunc - - ret = defaultdict(list) - for item in iterable: - key = keyfunc(item) - value = valuefunc(item) - ret[key].append(value) - - if reducefunc is not None: - for key, value_list in ret.items(): - ret[key] = reducefunc(value_list) - - ret.default_factory = None - return ret - - -def rlocate(iterable, pred=bool, window_size=None): - """Yield the index of each item in *iterable* for which *pred* returns - ``True``, starting from the right and moving left. - - *pred* defaults to :func:`bool`, which will select truthy items: - - >>> list(rlocate([0, 1, 1, 0, 1, 0, 0])) # Truthy at 1, 2, and 4 - [4, 2, 1] - - Set *pred* to a custom function to, e.g., find the indexes for a particular - item: - - >>> iterable = iter('abcb') - >>> pred = lambda x: x == 'b' - >>> list(rlocate(iterable, pred)) - [3, 1] - - If *window_size* is given, then the *pred* function will be called with - that many items. This enables searching for sub-sequences: - - >>> iterable = [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3] - >>> pred = lambda *args: args == (1, 2, 3) - >>> list(rlocate(iterable, pred=pred, window_size=3)) - [9, 5, 1] - - Beware, this function won't return anything for infinite iterables. - If *iterable* is reversible, ``rlocate`` will reverse it and search from - the right. Otherwise, it will search from the left and return the results - in reverse order. - - See :func:`locate` to for other example applications. - - """ - if window_size is None: - try: - len_iter = len(iterable) - return (len_iter - i - 1 for i in locate(reversed(iterable), pred)) - except TypeError: - pass - - return reversed(list(locate(iterable, pred, window_size))) - - -def replace(iterable, pred, substitutes, count=None, window_size=1): - """Yield the items from *iterable*, replacing the items for which *pred* - returns ``True`` with the items from the iterable *substitutes*. - - >>> iterable = [1, 1, 0, 1, 1, 0, 1, 1] - >>> pred = lambda x: x == 0 - >>> substitutes = (2, 3) - >>> list(replace(iterable, pred, substitutes)) - [1, 1, 2, 3, 1, 1, 2, 3, 1, 1] - - If *count* is given, the number of replacements will be limited: - - >>> iterable = [1, 1, 0, 1, 1, 0, 1, 1, 0] - >>> pred = lambda x: x == 0 - >>> substitutes = [None] - >>> list(replace(iterable, pred, substitutes, count=2)) - [1, 1, None, 1, 1, None, 1, 1, 0] - - Use *window_size* to control the number of items passed as arguments to - *pred*. This allows for locating and replacing subsequences. - - >>> iterable = [0, 1, 2, 5, 0, 1, 2, 5] - >>> window_size = 3 - >>> pred = lambda *args: args == (0, 1, 2) # 3 items passed to pred - >>> substitutes = [3, 4] # Splice in these items - >>> list(replace(iterable, pred, substitutes, window_size=window_size)) - [3, 4, 5, 3, 4, 5] - - """ - if window_size < 1: - raise ValueError('window_size must be at least 1') - - # Save the substitutes iterable, since it's used more than once - substitutes = tuple(substitutes) - - # Add padding such that the number of windows matches the length of the - # iterable - it = chain(iterable, [_marker] * (window_size - 1)) - windows = windowed(it, window_size) - - n = 0 - for w in windows: - # If the current window matches our predicate (and we haven't hit - # our maximum number of replacements), splice in the substitutes - # and then consume the following windows that overlap with this one. - # For example, if the iterable is (0, 1, 2, 3, 4...) - # and the window size is 2, we have (0, 1), (1, 2), (2, 3)... - # If the predicate matches on (0, 1), we need to zap (0, 1) and (1, 2) - if pred(*w): - if (count is None) or (n < count): - n += 1 - yield from substitutes - consume(windows, window_size - 1) - continue - - # If there was no match (or we've reached the replacement limit), - # yield the first item from the window. - if w and (w[0] is not _marker): - yield w[0] - - -def partitions(iterable): - """Yield all possible order-preserving partitions of *iterable*. - - >>> iterable = 'abc' - >>> for part in partitions(iterable): - ... print([''.join(p) for p in part]) - ['abc'] - ['a', 'bc'] - ['ab', 'c'] - ['a', 'b', 'c'] - - This is unrelated to :func:`partition`. - - """ - sequence = list(iterable) - n = len(sequence) - for i in powerset(range(1, n)): - yield [sequence[i:j] for i, j in zip((0,) + i, i + (n,))] - - -def set_partitions(iterable, k=None): - """ - Yield the set partitions of *iterable* into *k* parts. Set partitions are - not order-preserving. - - >>> iterable = 'abc' - >>> for part in set_partitions(iterable, 2): - ... print([''.join(p) for p in part]) - ['a', 'bc'] - ['ab', 'c'] - ['b', 'ac'] - - - If *k* is not given, every set partition is generated. - - >>> iterable = 'abc' - >>> for part in set_partitions(iterable): - ... print([''.join(p) for p in part]) - ['abc'] - ['a', 'bc'] - ['ab', 'c'] - ['b', 'ac'] - ['a', 'b', 'c'] - - """ - L = list(iterable) - n = len(L) - if k is not None: - if k < 1: - raise ValueError( - "Can't partition in a negative or zero number of groups" - ) - elif k > n: - return - - def set_partitions_helper(L, k): - n = len(L) - if k == 1: - yield [L] - elif n == k: - yield [[s] for s in L] - else: - e, *M = L - for p in set_partitions_helper(M, k - 1): - yield [[e], *p] - for p in set_partitions_helper(M, k): - for i in range(len(p)): - yield p[:i] + [[e] + p[i]] + p[i + 1 :] - - if k is None: - for k in range(1, n + 1): - yield from set_partitions_helper(L, k) - else: - yield from set_partitions_helper(L, k) - - -class time_limited: - """ - Yield items from *iterable* until *limit_seconds* have passed. - If the time limit expires before all items have been yielded, the - ``timed_out`` parameter will be set to ``True``. - - >>> from time import sleep - >>> def generator(): - ... yield 1 - ... yield 2 - ... sleep(0.2) - ... yield 3 - >>> iterable = time_limited(0.1, generator()) - >>> list(iterable) - [1, 2] - >>> iterable.timed_out - True - - Note that the time is checked before each item is yielded, and iteration - stops if the time elapsed is greater than *limit_seconds*. If your time - limit is 1 second, but it takes 2 seconds to generate the first item from - the iterable, the function will run for 2 seconds and not yield anything. - - """ - - def __init__(self, limit_seconds, iterable): - if limit_seconds < 0: - raise ValueError('limit_seconds must be positive') - self.limit_seconds = limit_seconds - self._iterable = iter(iterable) - self._start_time = monotonic() - self.timed_out = False - - def __iter__(self): - return self - - def __next__(self): - item = next(self._iterable) - if monotonic() - self._start_time > self.limit_seconds: - self.timed_out = True - raise StopIteration - - return item - - -def only(iterable, default=None, too_long=None): - """If *iterable* has only one item, return it. - If it has zero items, return *default*. - If it has more than one item, raise the exception given by *too_long*, - which is ``ValueError`` by default. - - >>> only([], default='missing') - 'missing' - >>> only([1]) - 1 - >>> only([1, 2]) # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ... - ValueError: Expected exactly one item in iterable, but got 1, 2, - and perhaps more.' - >>> only([1, 2], too_long=TypeError) # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ... - TypeError - - Note that :func:`only` attempts to advance *iterable* twice to ensure there - is only one item. See :func:`spy` or :func:`peekable` to check - iterable contents less destructively. - """ - it = iter(iterable) - first_value = next(it, default) - - try: - second_value = next(it) - except StopIteration: - pass - else: - msg = ( - 'Expected exactly one item in iterable, but got {!r}, {!r}, ' - 'and perhaps more.'.format(first_value, second_value) - ) - raise too_long or ValueError(msg) - - return first_value - - -def ichunked(iterable, n): - """Break *iterable* into sub-iterables with *n* elements each. - :func:`ichunked` is like :func:`chunked`, but it yields iterables - instead of lists. - - If the sub-iterables are read in order, the elements of *iterable* - won't be stored in memory. - If they are read out of order, :func:`itertools.tee` is used to cache - elements as necessary. - - >>> from itertools import count - >>> all_chunks = ichunked(count(), 4) - >>> c_1, c_2, c_3 = next(all_chunks), next(all_chunks), next(all_chunks) - >>> list(c_2) # c_1's elements have been cached; c_3's haven't been - [4, 5, 6, 7] - >>> list(c_1) - [0, 1, 2, 3] - >>> list(c_3) - [8, 9, 10, 11] - - """ - source = iter(iterable) - - while True: - # Check to see whether we're at the end of the source iterable - item = next(source, _marker) - if item is _marker: - return - - # Clone the source and yield an n-length slice - source, it = tee(chain([item], source)) - yield islice(it, n) - - # Advance the source iterable - consume(source, n) - - -def distinct_combinations(iterable, r): - """Yield the distinct combinations of *r* items taken from *iterable*. - - >>> list(distinct_combinations([0, 0, 1], 2)) - [(0, 0), (0, 1)] - - Equivalent to ``set(combinations(iterable))``, except duplicates are not - generated and thrown away. For larger input sequences this is much more - efficient. - - """ - if r < 0: - raise ValueError('r must be non-negative') - elif r == 0: - yield () - return - pool = tuple(iterable) - generators = [unique_everseen(enumerate(pool), key=itemgetter(1))] - current_combo = [None] * r - level = 0 - while generators: - try: - cur_idx, p = next(generators[-1]) - except StopIteration: - generators.pop() - level -= 1 - continue - current_combo[level] = p - if level + 1 == r: - yield tuple(current_combo) - else: - generators.append( - unique_everseen( - enumerate(pool[cur_idx + 1 :], cur_idx + 1), - key=itemgetter(1), - ) - ) - level += 1 - - -def filter_except(validator, iterable, *exceptions): - """Yield the items from *iterable* for which the *validator* function does - not raise one of the specified *exceptions*. - - *validator* is called for each item in *iterable*. - It should be a function that accepts one argument and raises an exception - if that item is not valid. - - >>> iterable = ['1', '2', 'three', '4', None] - >>> list(filter_except(int, iterable, ValueError, TypeError)) - ['1', '2', '4'] - - If an exception other than one given by *exceptions* is raised by - *validator*, it is raised like normal. - """ - for item in iterable: - try: - validator(item) - except exceptions: - pass - else: - yield item - - -def map_except(function, iterable, *exceptions): - """Transform each item from *iterable* with *function* and yield the - result, unless *function* raises one of the specified *exceptions*. - - *function* is called to transform each item in *iterable*. - It should accept one argument. - - >>> iterable = ['1', '2', 'three', '4', None] - >>> list(map_except(int, iterable, ValueError, TypeError)) - [1, 2, 4] - - If an exception other than one given by *exceptions* is raised by - *function*, it is raised like normal. - """ - for item in iterable: - try: - yield function(item) - except exceptions: - pass - - -def map_if(iterable, pred, func, func_else=lambda x: x): - """Evaluate each item from *iterable* using *pred*. If the result is - equivalent to ``True``, transform the item with *func* and yield it. - Otherwise, transform the item with *func_else* and yield it. - - *pred*, *func*, and *func_else* should each be functions that accept - one argument. By default, *func_else* is the identity function. - - >>> from math import sqrt - >>> iterable = list(range(-5, 5)) - >>> iterable - [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4] - >>> list(map_if(iterable, lambda x: x > 3, lambda x: 'toobig')) - [-5, -4, -3, -2, -1, 0, 1, 2, 3, 'toobig'] - >>> list(map_if(iterable, lambda x: x >= 0, - ... lambda x: f'{sqrt(x):.2f}', lambda x: None)) - [None, None, None, None, None, '0.00', '1.00', '1.41', '1.73', '2.00'] - """ - for item in iterable: - yield func(item) if pred(item) else func_else(item) - - -def _sample_unweighted(iterable, k): - # Implementation of "Algorithm L" from the 1994 paper by Kim-Hung Li: - # "Reservoir-Sampling Algorithms of Time Complexity O(n(1+log(N/n)))". - - # Fill up the reservoir (collection of samples) with the first `k` samples - reservoir = take(k, iterable) - - # Generate random number that's the largest in a sample of k U(0,1) numbers - # Largest order statistic: https://en.wikipedia.org/wiki/Order_statistic - W = exp(log(random()) / k) - - # The number of elements to skip before changing the reservoir is a random - # number with a geometric distribution. Sample it using random() and logs. - next_index = k + floor(log(random()) / log(1 - W)) - - for index, element in enumerate(iterable, k): - - if index == next_index: - reservoir[randrange(k)] = element - # The new W is the largest in a sample of k U(0, `old_W`) numbers - W *= exp(log(random()) / k) - next_index += floor(log(random()) / log(1 - W)) + 1 - - return reservoir - - -def _sample_weighted(iterable, k, weights): - # Implementation of "A-ExpJ" from the 2006 paper by Efraimidis et al. : - # "Weighted random sampling with a reservoir". - - # Log-transform for numerical stability for weights that are small/large - weight_keys = (log(random()) / weight for weight in weights) - - # Fill up the reservoir (collection of samples) with the first `k` - # weight-keys and elements, then heapify the list. - reservoir = take(k, zip(weight_keys, iterable)) - heapify(reservoir) - - # The number of jumps before changing the reservoir is a random variable - # with an exponential distribution. Sample it using random() and logs. - smallest_weight_key, _ = reservoir[0] - weights_to_skip = log(random()) / smallest_weight_key - - for weight, element in zip(weights, iterable): - if weight >= weights_to_skip: - # The notation here is consistent with the paper, but we store - # the weight-keys in log-space for better numerical stability. - smallest_weight_key, _ = reservoir[0] - t_w = exp(weight * smallest_weight_key) - r_2 = uniform(t_w, 1) # generate U(t_w, 1) - weight_key = log(r_2) / weight - heapreplace(reservoir, (weight_key, element)) - smallest_weight_key, _ = reservoir[0] - weights_to_skip = log(random()) / smallest_weight_key - else: - weights_to_skip -= weight - - # Equivalent to [element for weight_key, element in sorted(reservoir)] - return [heappop(reservoir)[1] for _ in range(k)] - - -def sample(iterable, k, weights=None): - """Return a *k*-length list of elements chosen (without replacement) - from the *iterable*. Like :func:`random.sample`, but works on iterables - of unknown length. - - >>> iterable = range(100) - >>> sample(iterable, 5) # doctest: +SKIP - [81, 60, 96, 16, 4] - - An iterable with *weights* may also be given: - - >>> iterable = range(100) - >>> weights = (i * i + 1 for i in range(100)) - >>> sampled = sample(iterable, 5, weights=weights) # doctest: +SKIP - [79, 67, 74, 66, 78] - - The algorithm can also be used to generate weighted random permutations. - The relative weight of each item determines the probability that it - appears late in the permutation. - - >>> data = "abcdefgh" - >>> weights = range(1, len(data) + 1) - >>> sample(data, k=len(data), weights=weights) # doctest: +SKIP - ['c', 'a', 'b', 'e', 'g', 'd', 'h', 'f'] - """ - if k == 0: - return [] - - iterable = iter(iterable) - if weights is None: - return _sample_unweighted(iterable, k) - else: - weights = iter(weights) - return _sample_weighted(iterable, k, weights) - - -def is_sorted(iterable, key=None, reverse=False, strict=False): - """Returns ``True`` if the items of iterable are in sorted order, and - ``False`` otherwise. *key* and *reverse* have the same meaning that they do - in the built-in :func:`sorted` function. - - >>> is_sorted(['1', '2', '3', '4', '5'], key=int) - True - >>> is_sorted([5, 4, 3, 1, 2], reverse=True) - False - - If *strict*, tests for strict sorting, that is, returns ``False`` if equal - elements are found: - - >>> is_sorted([1, 2, 2]) - True - >>> is_sorted([1, 2, 2], strict=True) - False - - The function returns ``False`` after encountering the first out-of-order - item. If there are no out-of-order items, the iterable is exhausted. - """ - - compare = (le if reverse else ge) if strict else (lt if reverse else gt) - it = iterable if key is None else map(key, iterable) - return not any(starmap(compare, pairwise(it))) - - -class AbortThread(BaseException): - pass - - -class callback_iter: - """Convert a function that uses callbacks to an iterator. - - Let *func* be a function that takes a `callback` keyword argument. - For example: - - >>> def func(callback=None): - ... for i, c in [(1, 'a'), (2, 'b'), (3, 'c')]: - ... if callback: - ... callback(i, c) - ... return 4 - - - Use ``with callback_iter(func)`` to get an iterator over the parameters - that are delivered to the callback. - - >>> with callback_iter(func) as it: - ... for args, kwargs in it: - ... print(args) - (1, 'a') - (2, 'b') - (3, 'c') - - The function will be called in a background thread. The ``done`` property - indicates whether it has completed execution. - - >>> it.done - True - - If it completes successfully, its return value will be available - in the ``result`` property. - - >>> it.result - 4 - - Notes: - - * If the function uses some keyword argument besides ``callback``, supply - *callback_kwd*. - * If it finished executing, but raised an exception, accessing the - ``result`` property will raise the same exception. - * If it hasn't finished executing, accessing the ``result`` - property from within the ``with`` block will raise ``RuntimeError``. - * If it hasn't finished executing, accessing the ``result`` property from - outside the ``with`` block will raise a - ``more_itertools.AbortThread`` exception. - * Provide *wait_seconds* to adjust how frequently the it is polled for - output. - - """ - - def __init__(self, func, callback_kwd='callback', wait_seconds=0.1): - self._func = func - self._callback_kwd = callback_kwd - self._aborted = False - self._future = None - self._wait_seconds = wait_seconds - self._executor = ThreadPoolExecutor(max_workers=1) - self._iterator = self._reader() - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - self._aborted = True - self._executor.shutdown() - - def __iter__(self): - return self - - def __next__(self): - return next(self._iterator) - - @property - def done(self): - if self._future is None: - return False - return self._future.done() - - @property - def result(self): - if not self.done: - raise RuntimeError('Function has not yet completed') - - return self._future.result() - - def _reader(self): - q = Queue() - - def callback(*args, **kwargs): - if self._aborted: - raise AbortThread('canceled by user') - - q.put((args, kwargs)) - - self._future = self._executor.submit( - self._func, **{self._callback_kwd: callback} - ) - - while True: - try: - item = q.get(timeout=self._wait_seconds) - except Empty: - pass - else: - q.task_done() - yield item - - if self._future.done(): - break - - remaining = [] - while True: - try: - item = q.get_nowait() - except Empty: - break - else: - q.task_done() - remaining.append(item) - q.join() - yield from remaining - - -def windowed_complete(iterable, n): - """ - Yield ``(beginning, middle, end)`` tuples, where: - - * Each ``middle`` has *n* items from *iterable* - * Each ``beginning`` has the items before the ones in ``middle`` - * Each ``end`` has the items after the ones in ``middle`` - - >>> iterable = range(7) - >>> n = 3 - >>> for beginning, middle, end in windowed_complete(iterable, n): - ... print(beginning, middle, end) - () (0, 1, 2) (3, 4, 5, 6) - (0,) (1, 2, 3) (4, 5, 6) - (0, 1) (2, 3, 4) (5, 6) - (0, 1, 2) (3, 4, 5) (6,) - (0, 1, 2, 3) (4, 5, 6) () - - Note that *n* must be at least 0 and most equal to the length of - *iterable*. - - This function will exhaust the iterable and may require significant - storage. - """ - if n < 0: - raise ValueError('n must be >= 0') - - seq = tuple(iterable) - size = len(seq) - - if n > size: - raise ValueError('n must be <= len(seq)') - - for i in range(size - n + 1): - beginning = seq[:i] - middle = seq[i : i + n] - end = seq[i + n :] - yield beginning, middle, end - - -def all_unique(iterable, key=None): - """ - Returns ``True`` if all the elements of *iterable* are unique (no two - elements are equal). - - >>> all_unique('ABCB') - False - - If a *key* function is specified, it will be used to make comparisons. - - >>> all_unique('ABCb') - True - >>> all_unique('ABCb', str.lower) - False - - The function returns as soon as the first non-unique element is - encountered. Iterables with a mix of hashable and unhashable items can - be used, but the function will be slower for unhashable items. - """ - seenset = set() - seenset_add = seenset.add - seenlist = [] - seenlist_add = seenlist.append - for element in map(key, iterable) if key else iterable: - try: - if element in seenset: - return False - seenset_add(element) - except TypeError: - if element in seenlist: - return False - seenlist_add(element) - return True - - -def nth_product(index, *args): - """Equivalent to ``list(product(*args))[index]``. - - The products of *args* can be ordered lexicographically. - :func:`nth_product` computes the product at sort position *index* without - computing the previous products. - - >>> nth_product(8, range(2), range(2), range(2), range(2)) - (1, 0, 0, 0) - - ``IndexError`` will be raised if the given *index* is invalid. - """ - pools = list(map(tuple, reversed(args))) - ns = list(map(len, pools)) - - c = reduce(mul, ns) - - if index < 0: - index += c - - if not 0 <= index < c: - raise IndexError - - result = [] - for pool, n in zip(pools, ns): - result.append(pool[index % n]) - index //= n - - return tuple(reversed(result)) - - -def nth_permutation(iterable, r, index): - """Equivalent to ``list(permutations(iterable, r))[index]``` - - The subsequences of *iterable* that are of length *r* where order is - important can be ordered lexicographically. :func:`nth_permutation` - computes the subsequence at sort position *index* directly, without - computing the previous subsequences. - - >>> nth_permutation('ghijk', 2, 5) - ('h', 'i') - - ``ValueError`` will be raised If *r* is negative or greater than the length - of *iterable*. - ``IndexError`` will be raised if the given *index* is invalid. - """ - pool = list(iterable) - n = len(pool) - - if r is None or r == n: - r, c = n, factorial(n) - elif not 0 <= r < n: - raise ValueError - else: - c = factorial(n) // factorial(n - r) - - if index < 0: - index += c - - if not 0 <= index < c: - raise IndexError - - if c == 0: - return tuple() - - result = [0] * r - q = index * factorial(n) // c if r < n else index - for d in range(1, n + 1): - q, i = divmod(q, d) - if 0 <= n - d < r: - result[n - d] = i - if q == 0: - break - - return tuple(map(pool.pop, result)) - - -def value_chain(*args): - """Yield all arguments passed to the function in the same order in which - they were passed. If an argument itself is iterable then iterate over its - values. - - >>> list(value_chain(1, 2, 3, [4, 5, 6])) - [1, 2, 3, 4, 5, 6] - - Binary and text strings are not considered iterable and are emitted - as-is: - - >>> list(value_chain('12', '34', ['56', '78'])) - ['12', '34', '56', '78'] - - - Multiple levels of nesting are not flattened. - - """ - for value in args: - if isinstance(value, (str, bytes)): - yield value - continue - try: - yield from value - except TypeError: - yield value - - -def product_index(element, *args): - """Equivalent to ``list(product(*args)).index(element)`` - - The products of *args* can be ordered lexicographically. - :func:`product_index` computes the first index of *element* without - computing the previous products. - - >>> product_index([8, 2], range(10), range(5)) - 42 - - ``ValueError`` will be raised if the given *element* isn't in the product - of *args*. - """ - index = 0 - - for x, pool in zip_longest(element, args, fillvalue=_marker): - if x is _marker or pool is _marker: - raise ValueError('element is not a product of args') - - pool = tuple(pool) - index = index * len(pool) + pool.index(x) - - return index - - -def combination_index(element, iterable): - """Equivalent to ``list(combinations(iterable, r)).index(element)`` - - The subsequences of *iterable* that are of length *r* can be ordered - lexicographically. :func:`combination_index` computes the index of the - first *element*, without computing the previous combinations. - - >>> combination_index('adf', 'abcdefg') - 10 - - ``ValueError`` will be raised if the given *element* isn't one of the - combinations of *iterable*. - """ - element = enumerate(element) - k, y = next(element, (None, None)) - if k is None: - return 0 - - indexes = [] - pool = enumerate(iterable) - for n, x in pool: - if x == y: - indexes.append(n) - tmp, y = next(element, (None, None)) - if tmp is None: - break - else: - k = tmp - else: - raise ValueError('element is not a combination of iterable') - - n, _ = last(pool, default=(n, None)) - - # Python versiosn below 3.8 don't have math.comb - index = 1 - for i, j in enumerate(reversed(indexes), start=1): - j = n - j - if i <= j: - index += factorial(j) // (factorial(i) * factorial(j - i)) - - return factorial(n + 1) // (factorial(k + 1) * factorial(n - k)) - index - - -def permutation_index(element, iterable): - """Equivalent to ``list(permutations(iterable, r)).index(element)``` - - The subsequences of *iterable* that are of length *r* where order is - important can be ordered lexicographically. :func:`permutation_index` - computes the index of the first *element* directly, without computing - the previous permutations. - - >>> permutation_index([1, 3, 2], range(5)) - 19 - - ``ValueError`` will be raised if the given *element* isn't one of the - permutations of *iterable*. - """ - index = 0 - pool = list(iterable) - for i, x in zip(range(len(pool), -1, -1), element): - r = pool.index(x) - index = index * i + r - del pool[r] - - return index - - -class countable: - """Wrap *iterable* and keep a count of how many items have been consumed. - - The ``items_seen`` attribute starts at ``0`` and increments as the iterable - is consumed: - - >>> iterable = map(str, range(10)) - >>> it = countable(iterable) - >>> it.items_seen - 0 - >>> next(it), next(it) - ('0', '1') - >>> list(it) - ['2', '3', '4', '5', '6', '7', '8', '9'] - >>> it.items_seen - 10 - """ - - def __init__(self, iterable): - self._it = iter(iterable) - self.items_seen = 0 - - def __iter__(self): - return self - - def __next__(self): - item = next(self._it) - self.items_seen += 1 - - return item - - -def chunked_even(iterable, n): - """Break *iterable* into lists of approximately length *n*. - Items are distributed such the lengths of the lists differ by at most - 1 item. - - >>> iterable = [1, 2, 3, 4, 5, 6, 7] - >>> n = 3 - >>> list(chunked_even(iterable, n)) # List lengths: 3, 2, 2 - [[1, 2, 3], [4, 5], [6, 7]] - >>> list(chunked(iterable, n)) # List lengths: 3, 3, 1 - [[1, 2, 3], [4, 5, 6], [7]] - - """ - - len_method = getattr(iterable, '__len__', None) - - if len_method is None: - return _chunked_even_online(iterable, n) - else: - return _chunked_even_finite(iterable, len_method(), n) - - -def _chunked_even_online(iterable, n): - buffer = [] - maxbuf = n + (n - 2) * (n - 1) - for x in iterable: - buffer.append(x) - if len(buffer) == maxbuf: - yield buffer[:n] - buffer = buffer[n:] - yield from _chunked_even_finite(buffer, len(buffer), n) - - -def _chunked_even_finite(iterable, N, n): - if N < 1: - return - - # Lists are either size `full_size <= n` or `partial_size = full_size - 1` - q, r = divmod(N, n) - num_lists = q + (1 if r > 0 else 0) - q, r = divmod(N, num_lists) - full_size = q + (1 if r > 0 else 0) - partial_size = full_size - 1 - num_full = N - partial_size * num_lists - num_partial = num_lists - num_full - - buffer = [] - iterator = iter(iterable) - - # Yield num_full lists of full_size - for x in iterator: - buffer.append(x) - if len(buffer) == full_size: - yield buffer - buffer = [] - num_full -= 1 - if num_full <= 0: - break - - # Yield num_partial lists of partial_size - for x in iterator: - buffer.append(x) - if len(buffer) == partial_size: - yield buffer - buffer = [] - num_partial -= 1 - - -def zip_broadcast(*objects, scalar_types=(str, bytes), strict=False): - """A version of :func:`zip` that "broadcasts" any scalar - (i.e., non-iterable) items into output tuples. - - >>> iterable_1 = [1, 2, 3] - >>> iterable_2 = ['a', 'b', 'c'] - >>> scalar = '_' - >>> list(zip_broadcast(iterable_1, iterable_2, scalar)) - [(1, 'a', '_'), (2, 'b', '_'), (3, 'c', '_')] - - The *scalar_types* keyword argument determines what types are considered - scalar. It is set to ``(str, bytes)`` by default. Set it to ``None`` to - treat strings and byte strings as iterable: - - >>> list(zip_broadcast('abc', 0, 'xyz', scalar_types=None)) - [('a', 0, 'x'), ('b', 0, 'y'), ('c', 0, 'z')] - - If the *strict* keyword argument is ``True``, then - ``UnequalIterablesError`` will be raised if any of the iterables have - different lengthss. - """ - - def is_scalar(obj): - if scalar_types and isinstance(obj, scalar_types): - return True - try: - iter(obj) - except TypeError: - return True - else: - return False - - size = len(objects) - if not size: - return - - iterables, iterable_positions = [], [] - scalars, scalar_positions = [], [] - for i, obj in enumerate(objects): - if is_scalar(obj): - scalars.append(obj) - scalar_positions.append(i) - else: - iterables.append(iter(obj)) - iterable_positions.append(i) - - if len(scalars) == size: - yield tuple(objects) - return - - zipper = _zip_equal if strict else zip - for item in zipper(*iterables): - new_item = [None] * size - - for i, elem in zip(iterable_positions, item): - new_item[i] = elem - - for i, elem in zip(scalar_positions, scalars): - new_item[i] = elem - - yield tuple(new_item) - - -def unique_in_window(iterable, n, key=None): - """Yield the items from *iterable* that haven't been seen recently. - *n* is the size of the lookback window. - - >>> iterable = [0, 1, 0, 2, 3, 0] - >>> n = 3 - >>> list(unique_in_window(iterable, n)) - [0, 1, 2, 3, 0] - - The *key* function, if provided, will be used to determine uniqueness: - - >>> list(unique_in_window('abAcda', 3, key=lambda x: x.lower())) - ['a', 'b', 'c', 'd', 'a'] - - The items in *iterable* must be hashable. - - """ - if n <= 0: - raise ValueError('n must be greater than 0') - - window = deque(maxlen=n) - uniques = set() - use_key = key is not None - - for item in iterable: - k = key(item) if use_key else item - if k in uniques: - continue - - if len(uniques) == n: - uniques.discard(window[0]) - - uniques.add(k) - window.append(k) - - yield item - - -def duplicates_everseen(iterable, key=None): - """Yield duplicate elements after their first appearance. - - >>> list(duplicates_everseen('mississippi')) - ['s', 'i', 's', 's', 'i', 'p', 'i'] - >>> list(duplicates_everseen('AaaBbbCccAaa', str.lower)) - ['a', 'a', 'b', 'b', 'c', 'c', 'A', 'a', 'a'] - - This function is analagous to :func:`unique_everseen` and is subject to - the same performance considerations. - - """ - seen_set = set() - seen_list = [] - use_key = key is not None - - for element in iterable: - k = key(element) if use_key else element - try: - if k not in seen_set: - seen_set.add(k) - else: - yield element - except TypeError: - if k not in seen_list: - seen_list.append(k) - else: - yield element - - -def duplicates_justseen(iterable, key=None): - """Yields serially-duplicate elements after their first appearance. - - >>> list(duplicates_justseen('mississippi')) - ['s', 's', 'p'] - >>> list(duplicates_justseen('AaaBbbCccAaa', str.lower)) - ['a', 'a', 'b', 'b', 'c', 'c', 'a', 'a'] - - This function is analagous to :func:`unique_justseen`. - - """ - return flatten( - map( - lambda group_tuple: islice_extended(group_tuple[1])[1:], - groupby(iterable, key), - ) - ) - - -def minmax(iterable_or_value, *others, key=None, default=_marker): - """Returns both the smallest and largest items in an iterable - or the largest of two or more arguments. - - >>> minmax([3, 1, 5]) - (1, 5) - - >>> minmax(4, 2, 6) - (2, 6) - - If a *key* function is provided, it will be used to transform the input - items for comparison. - - >>> minmax([5, 30], key=str) # '30' sorts before '5' - (30, 5) - - If a *default* value is provided, it will be returned if there are no - input items. - - >>> minmax([], default=(0, 0)) - (0, 0) - - Otherwise ``ValueError`` is raised. - - This function is based on the - `recipe `__ by - Raymond Hettinger and takes care to minimize the number of comparisons - performed. - """ - iterable = (iterable_or_value, *others) if others else iterable_or_value - - it = iter(iterable) - - try: - lo = hi = next(it) - except StopIteration as e: - if default is _marker: - raise ValueError( - '`minmax()` argument is an empty iterable. ' - 'Provide a `default` value to suppress this error.' - ) from e - return default - - # Different branches depending on the presence of key. This saves a lot - # of unimportant copies which would slow the "key=None" branch - # significantly down. - if key is None: - for x, y in zip_longest(it, it, fillvalue=lo): - if y < x: - x, y = y, x - if x < lo: - lo = x - if hi < y: - hi = y - - else: - lo_key = hi_key = key(lo) - - for x, y in zip_longest(it, it, fillvalue=lo): - - x_key, y_key = key(x), key(y) - - if y_key < x_key: - x, y, x_key, y_key = y, x, y_key, x_key - if x_key < lo_key: - lo, lo_key = x, x_key - if hi_key < y_key: - hi, hi_key = y, y_key - - return lo, hi diff --git a/setuptools/_vendor/more_itertools/__init__.py b/setuptools/_vendor/more_itertools/__init__.py index 19a169fc..53cf238c 100644 --- a/setuptools/_vendor/more_itertools/__init__.py +++ b/setuptools/_vendor/more_itertools/__init__.py @@ -1,4 +1,3 @@ -from .more import * # noqa from .recipes import * # noqa __version__ = '8.8.0' diff --git a/setuptools/_vendor/more_itertools/more.py b/setuptools/_vendor/more_itertools/more.py deleted file mode 100644 index 0f7d282a..00000000 --- a/setuptools/_vendor/more_itertools/more.py +++ /dev/null @@ -1,3825 +0,0 @@ -import warnings - -from collections import Counter, defaultdict, deque, abc -from collections.abc import Sequence -from concurrent.futures import ThreadPoolExecutor -from functools import partial, reduce, wraps -from heapq import merge, heapify, heapreplace, heappop -from itertools import ( - chain, - compress, - count, - cycle, - dropwhile, - groupby, - islice, - repeat, - starmap, - takewhile, - tee, - zip_longest, -) -from math import exp, factorial, floor, log -from queue import Empty, Queue -from random import random, randrange, uniform -from operator import itemgetter, mul, sub, gt, lt -from sys import hexversion, maxsize -from time import monotonic - -from .recipes import ( - consume, - flatten, - pairwise, - powerset, - take, - unique_everseen, -) - -__all__ = [ - 'AbortThread', - 'adjacent', - 'always_iterable', - 'always_reversible', - 'bucket', - 'callback_iter', - 'chunked', - 'circular_shifts', - 'collapse', - 'collate', - 'consecutive_groups', - 'consumer', - 'countable', - 'count_cycle', - 'mark_ends', - 'difference', - 'distinct_combinations', - 'distinct_permutations', - 'distribute', - 'divide', - 'exactly_n', - 'filter_except', - 'first', - 'groupby_transform', - 'ilen', - 'interleave_longest', - 'interleave', - 'intersperse', - 'islice_extended', - 'iterate', - 'ichunked', - 'is_sorted', - 'last', - 'locate', - 'lstrip', - 'make_decorator', - 'map_except', - 'map_reduce', - 'nth_or_last', - 'nth_permutation', - 'nth_product', - 'numeric_range', - 'one', - 'only', - 'padded', - 'partitions', - 'set_partitions', - 'peekable', - 'repeat_last', - 'replace', - 'rlocate', - 'rstrip', - 'run_length', - 'sample', - 'seekable', - 'SequenceView', - 'side_effect', - 'sliced', - 'sort_together', - 'split_at', - 'split_after', - 'split_before', - 'split_when', - 'split_into', - 'spy', - 'stagger', - 'strip', - 'substrings', - 'substrings_indexes', - 'time_limited', - 'unique_to_each', - 'unzip', - 'windowed', - 'with_iter', - 'UnequalIterablesError', - 'zip_equal', - 'zip_offset', - 'windowed_complete', - 'all_unique', - 'value_chain', - 'product_index', - 'combination_index', - 'permutation_index', -] - -_marker = object() - - -def chunked(iterable, n, strict=False): - """Break *iterable* into lists of length *n*: - - >>> list(chunked([1, 2, 3, 4, 5, 6], 3)) - [[1, 2, 3], [4, 5, 6]] - - By the default, the last yielded list will have fewer than *n* elements - if the length of *iterable* is not divisible by *n*: - - >>> list(chunked([1, 2, 3, 4, 5, 6, 7, 8], 3)) - [[1, 2, 3], [4, 5, 6], [7, 8]] - - To use a fill-in value instead, see the :func:`grouper` recipe. - - If the length of *iterable* is not divisible by *n* and *strict* is - ``True``, then ``ValueError`` will be raised before the last - list is yielded. - - """ - iterator = iter(partial(take, n, iter(iterable)), []) - if strict: - - def ret(): - for chunk in iterator: - if len(chunk) != n: - raise ValueError('iterable is not divisible by n.') - yield chunk - - return iter(ret()) - else: - return iterator - - -def first(iterable, default=_marker): - """Return the first item of *iterable*, or *default* if *iterable* is - empty. - - >>> first([0, 1, 2, 3]) - 0 - >>> first([], 'some default') - 'some default' - - If *default* is not provided and there are no items in the iterable, - raise ``ValueError``. - - :func:`first` is useful when you have a generator of expensive-to-retrieve - values and want any arbitrary one. It is marginally shorter than - ``next(iter(iterable), default)``. - - """ - try: - return next(iter(iterable)) - except StopIteration as e: - if default is _marker: - raise ValueError( - 'first() was called on an empty iterable, and no ' - 'default value was provided.' - ) from e - return default - - -def last(iterable, default=_marker): - """Return the last item of *iterable*, or *default* if *iterable* is - empty. - - >>> last([0, 1, 2, 3]) - 3 - >>> last([], 'some default') - 'some default' - - If *default* is not provided and there are no items in the iterable, - raise ``ValueError``. - """ - try: - if isinstance(iterable, Sequence): - return iterable[-1] - # Work around https://bugs.python.org/issue38525 - elif hasattr(iterable, '__reversed__') and (hexversion != 0x030800F0): - return next(reversed(iterable)) - else: - return deque(iterable, maxlen=1)[-1] - except (IndexError, TypeError, StopIteration): - if default is _marker: - raise ValueError( - 'last() was called on an empty iterable, and no default was ' - 'provided.' - ) - return default - - -def nth_or_last(iterable, n, default=_marker): - """Return the nth or the last item of *iterable*, - or *default* if *iterable* is empty. - - >>> nth_or_last([0, 1, 2, 3], 2) - 2 - >>> nth_or_last([0, 1], 2) - 1 - >>> nth_or_last([], 0, 'some default') - 'some default' - - If *default* is not provided and there are no items in the iterable, - raise ``ValueError``. - """ - return last(islice(iterable, n + 1), default=default) - - -class peekable: - """Wrap an iterator to allow lookahead and prepending elements. - - Call :meth:`peek` on the result to get the value that will be returned - by :func:`next`. This won't advance the iterator: - - >>> p = peekable(['a', 'b']) - >>> p.peek() - 'a' - >>> next(p) - 'a' - - Pass :meth:`peek` a default value to return that instead of raising - ``StopIteration`` when the iterator is exhausted. - - >>> p = peekable([]) - >>> p.peek('hi') - 'hi' - - peekables also offer a :meth:`prepend` method, which "inserts" items - at the head of the iterable: - - >>> p = peekable([1, 2, 3]) - >>> p.prepend(10, 11, 12) - >>> next(p) - 10 - >>> p.peek() - 11 - >>> list(p) - [11, 12, 1, 2, 3] - - peekables can be indexed. Index 0 is the item that will be returned by - :func:`next`, index 1 is the item after that, and so on: - The values up to the given index will be cached. - - >>> p = peekable(['a', 'b', 'c', 'd']) - >>> p[0] - 'a' - >>> p[1] - 'b' - >>> next(p) - 'a' - - Negative indexes are supported, but be aware that they will cache the - remaining items in the source iterator, which may require significant - storage. - - To check whether a peekable is exhausted, check its truth value: - - >>> p = peekable(['a', 'b']) - >>> if p: # peekable has items - ... list(p) - ['a', 'b'] - >>> if not p: # peekable is exhausted - ... list(p) - [] - - """ - - def __init__(self, iterable): - self._it = iter(iterable) - self._cache = deque() - - def __iter__(self): - return self - - def __bool__(self): - try: - self.peek() - except StopIteration: - return False - return True - - def peek(self, default=_marker): - """Return the item that will be next returned from ``next()``. - - Return ``default`` if there are no items left. If ``default`` is not - provided, raise ``StopIteration``. - - """ - if not self._cache: - try: - self._cache.append(next(self._it)) - except StopIteration: - if default is _marker: - raise - return default - return self._cache[0] - - def prepend(self, *items): - """Stack up items to be the next ones returned from ``next()`` or - ``self.peek()``. The items will be returned in - first in, first out order:: - - >>> p = peekable([1, 2, 3]) - >>> p.prepend(10, 11, 12) - >>> next(p) - 10 - >>> list(p) - [11, 12, 1, 2, 3] - - It is possible, by prepending items, to "resurrect" a peekable that - previously raised ``StopIteration``. - - >>> p = peekable([]) - >>> next(p) - Traceback (most recent call last): - ... - StopIteration - >>> p.prepend(1) - >>> next(p) - 1 - >>> next(p) - Traceback (most recent call last): - ... - StopIteration - - """ - self._cache.extendleft(reversed(items)) - - def __next__(self): - if self._cache: - return self._cache.popleft() - - return next(self._it) - - def _get_slice(self, index): - # Normalize the slice's arguments - step = 1 if (index.step is None) else index.step - if step > 0: - start = 0 if (index.start is None) else index.start - stop = maxsize if (index.stop is None) else index.stop - elif step < 0: - start = -1 if (index.start is None) else index.start - stop = (-maxsize - 1) if (index.stop is None) else index.stop - else: - raise ValueError('slice step cannot be zero') - - # If either the start or stop index is negative, we'll need to cache - # the rest of the iterable in order to slice from the right side. - if (start < 0) or (stop < 0): - self._cache.extend(self._it) - # Otherwise we'll need to find the rightmost index and cache to that - # point. - else: - n = min(max(start, stop) + 1, maxsize) - cache_len = len(self._cache) - if n >= cache_len: - self._cache.extend(islice(self._it, n - cache_len)) - - return list(self._cache)[index] - - def __getitem__(self, index): - if isinstance(index, slice): - return self._get_slice(index) - - cache_len = len(self._cache) - if index < 0: - self._cache.extend(self._it) - elif index >= cache_len: - self._cache.extend(islice(self._it, index + 1 - cache_len)) - - return self._cache[index] - - -def collate(*iterables, **kwargs): - """Return a sorted merge of the items from each of several already-sorted - *iterables*. - - >>> list(collate('ACDZ', 'AZ', 'JKL')) - ['A', 'A', 'C', 'D', 'J', 'K', 'L', 'Z', 'Z'] - - Works lazily, keeping only the next value from each iterable in memory. Use - :func:`collate` to, for example, perform a n-way mergesort of items that - don't fit in memory. - - If a *key* function is specified, the iterables will be sorted according - to its result: - - >>> key = lambda s: int(s) # Sort by numeric value, not by string - >>> list(collate(['1', '10'], ['2', '11'], key=key)) - ['1', '2', '10', '11'] - - - If the *iterables* are sorted in descending order, set *reverse* to - ``True``: - - >>> list(collate([5, 3, 1], [4, 2, 0], reverse=True)) - [5, 4, 3, 2, 1, 0] - - If the elements of the passed-in iterables are out of order, you might get - unexpected results. - - On Python 3.5+, this function is an alias for :func:`heapq.merge`. - - """ - warnings.warn( - "collate is no longer part of more_itertools, use heapq.merge", - DeprecationWarning, - ) - return merge(*iterables, **kwargs) - - -def consumer(func): - """Decorator that automatically advances a PEP-342-style "reverse iterator" - to its first yield point so you don't have to call ``next()`` on it - manually. - - >>> @consumer - ... def tally(): - ... i = 0 - ... while True: - ... print('Thing number %s is %s.' % (i, (yield))) - ... i += 1 - ... - >>> t = tally() - >>> t.send('red') - Thing number 0 is red. - >>> t.send('fish') - Thing number 1 is fish. - - Without the decorator, you would have to call ``next(t)`` before - ``t.send()`` could be used. - - """ - - @wraps(func) - def wrapper(*args, **kwargs): - gen = func(*args, **kwargs) - next(gen) - return gen - - return wrapper - - -def ilen(iterable): - """Return the number of items in *iterable*. - - >>> ilen(x for x in range(1000000) if x % 3 == 0) - 333334 - - This consumes the iterable, so handle with care. - - """ - # This approach was selected because benchmarks showed it's likely the - # fastest of the known implementations at the time of writing. - # See GitHub tracker: #236, #230. - counter = count() - deque(zip(iterable, counter), maxlen=0) - return next(counter) - - -def iterate(func, start): - """Return ``start``, ``func(start)``, ``func(func(start))``, ... - - >>> from itertools import islice - >>> list(islice(iterate(lambda x: 2*x, 1), 10)) - [1, 2, 4, 8, 16, 32, 64, 128, 256, 512] - - """ - while True: - yield start - start = func(start) - - -def with_iter(context_manager): - """Wrap an iterable in a ``with`` statement, so it closes once exhausted. - - For example, this will close the file when the iterator is exhausted:: - - upper_lines = (line.upper() for line in with_iter(open('foo'))) - - Any context manager which returns an iterable is a candidate for - ``with_iter``. - - """ - with context_manager as iterable: - yield from iterable - - -def one(iterable, too_short=None, too_long=None): - """Return the first item from *iterable*, which is expected to contain only - that item. Raise an exception if *iterable* is empty or has more than one - item. - - :func:`one` is useful for ensuring that an iterable contains only one item. - For example, it can be used to retrieve the result of a database query - that is expected to return a single row. - - If *iterable* is empty, ``ValueError`` will be raised. You may specify a - different exception with the *too_short* keyword: - - >>> it = [] - >>> one(it) # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ... - ValueError: too many items in iterable (expected 1)' - >>> too_short = IndexError('too few items') - >>> one(it, too_short=too_short) # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ... - IndexError: too few items - - Similarly, if *iterable* contains more than one item, ``ValueError`` will - be raised. You may specify a different exception with the *too_long* - keyword: - - >>> it = ['too', 'many'] - >>> one(it) # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ... - ValueError: Expected exactly one item in iterable, but got 'too', - 'many', and perhaps more. - >>> too_long = RuntimeError - >>> one(it, too_long=too_long) # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ... - RuntimeError - - Note that :func:`one` attempts to advance *iterable* twice to ensure there - is only one item. See :func:`spy` or :func:`peekable` to check iterable - contents less destructively. - - """ - it = iter(iterable) - - try: - first_value = next(it) - except StopIteration as e: - raise ( - too_short or ValueError('too few items in iterable (expected 1)') - ) from e - - try: - second_value = next(it) - except StopIteration: - pass - else: - msg = ( - 'Expected exactly one item in iterable, but got {!r}, {!r}, ' - 'and perhaps more.'.format(first_value, second_value) - ) - raise too_long or ValueError(msg) - - return first_value - - -def distinct_permutations(iterable, r=None): - """Yield successive distinct permutations of the elements in *iterable*. - - >>> sorted(distinct_permutations([1, 0, 1])) - [(0, 1, 1), (1, 0, 1), (1, 1, 0)] - - Equivalent to ``set(permutations(iterable))``, except duplicates are not - generated and thrown away. For larger input sequences this is much more - efficient. - - Duplicate permutations arise when there are duplicated elements in the - input iterable. The number of items returned is - `n! / (x_1! * x_2! * ... * x_n!)`, where `n` is the total number of - items input, and each `x_i` is the count of a distinct item in the input - sequence. - - If *r* is given, only the *r*-length permutations are yielded. - - >>> sorted(distinct_permutations([1, 0, 1], r=2)) - [(0, 1), (1, 0), (1, 1)] - >>> sorted(distinct_permutations(range(3), r=2)) - [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)] - - """ - # Algorithm: https://w.wiki/Qai - def _full(A): - while True: - # Yield the permutation we have - yield tuple(A) - - # Find the largest index i such that A[i] < A[i + 1] - for i in range(size - 2, -1, -1): - if A[i] < A[i + 1]: - break - # If no such index exists, this permutation is the last one - else: - return - - # Find the largest index j greater than j such that A[i] < A[j] - for j in range(size - 1, i, -1): - if A[i] < A[j]: - break - - # Swap the value of A[i] with that of A[j], then reverse the - # sequence from A[i + 1] to form the new permutation - A[i], A[j] = A[j], A[i] - A[i + 1 :] = A[: i - size : -1] # A[i + 1:][::-1] - - # Algorithm: modified from the above - def _partial(A, r): - # Split A into the first r items and the last r items - head, tail = A[:r], A[r:] - right_head_indexes = range(r - 1, -1, -1) - left_tail_indexes = range(len(tail)) - - while True: - # Yield the permutation we have - yield tuple(head) - - # Starting from the right, find the first index of the head with - # value smaller than the maximum value of the tail - call it i. - pivot = tail[-1] - for i in right_head_indexes: - if head[i] < pivot: - break - pivot = head[i] - else: - return - - # Starting from the left, find the first value of the tail - # with a value greater than head[i] and swap. - for j in left_tail_indexes: - if tail[j] > head[i]: - head[i], tail[j] = tail[j], head[i] - break - # If we didn't find one, start from the right and find the first - # index of the head with a value greater than head[i] and swap. - else: - for j in right_head_indexes: - if head[j] > head[i]: - head[i], head[j] = head[j], head[i] - break - - # Reverse head[i + 1:] and swap it with tail[:r - (i + 1)] - tail += head[: i - r : -1] # head[i + 1:][::-1] - i += 1 - head[i:], tail[:] = tail[: r - i], tail[r - i :] - - items = sorted(iterable) - - size = len(items) - if r is None: - r = size - - if 0 < r <= size: - return _full(items) if (r == size) else _partial(items, r) - - return iter(() if r else ((),)) - - -def intersperse(e, iterable, n=1): - """Intersperse filler element *e* among the items in *iterable*, leaving - *n* items between each filler element. - - >>> list(intersperse('!', [1, 2, 3, 4, 5])) - [1, '!', 2, '!', 3, '!', 4, '!', 5] - - >>> list(intersperse(None, [1, 2, 3, 4, 5], n=2)) - [1, 2, None, 3, 4, None, 5] - - """ - if n == 0: - raise ValueError('n must be > 0') - elif n == 1: - # interleave(repeat(e), iterable) -> e, x_0, e, e, x_1, e, x_2... - # islice(..., 1, None) -> x_0, e, e, x_1, e, x_2... - return islice(interleave(repeat(e), iterable), 1, None) - else: - # interleave(filler, chunks) -> [e], [x_0, x_1], [e], [x_2, x_3]... - # islice(..., 1, None) -> [x_0, x_1], [e], [x_2, x_3]... - # flatten(...) -> x_0, x_1, e, x_2, x_3... - filler = repeat([e]) - chunks = chunked(iterable, n) - return flatten(islice(interleave(filler, chunks), 1, None)) - - -def unique_to_each(*iterables): - """Return the elements from each of the input iterables that aren't in the - other input iterables. - - For example, suppose you have a set of packages, each with a set of - dependencies:: - - {'pkg_1': {'A', 'B'}, 'pkg_2': {'B', 'C'}, 'pkg_3': {'B', 'D'}} - - If you remove one package, which dependencies can also be removed? - - If ``pkg_1`` is removed, then ``A`` is no longer necessary - it is not - associated with ``pkg_2`` or ``pkg_3``. Similarly, ``C`` is only needed for - ``pkg_2``, and ``D`` is only needed for ``pkg_3``:: - - >>> unique_to_each({'A', 'B'}, {'B', 'C'}, {'B', 'D'}) - [['A'], ['C'], ['D']] - - If there are duplicates in one input iterable that aren't in the others - they will be duplicated in the output. Input order is preserved:: - - >>> unique_to_each("mississippi", "missouri") - [['p', 'p'], ['o', 'u', 'r']] - - It is assumed that the elements of each iterable are hashable. - - """ - pool = [list(it) for it in iterables] - counts = Counter(chain.from_iterable(map(set, pool))) - uniques = {element for element in counts if counts[element] == 1} - return [list(filter(uniques.__contains__, it)) for it in pool] - - -def windowed(seq, n, fillvalue=None, step=1): - """Return a sliding window of width *n* over the given iterable. - - >>> all_windows = windowed([1, 2, 3, 4, 5], 3) - >>> list(all_windows) - [(1, 2, 3), (2, 3, 4), (3, 4, 5)] - - When the window is larger than the iterable, *fillvalue* is used in place - of missing values: - - >>> list(windowed([1, 2, 3], 4)) - [(1, 2, 3, None)] - - Each window will advance in increments of *step*: - - >>> list(windowed([1, 2, 3, 4, 5, 6], 3, fillvalue='!', step=2)) - [(1, 2, 3), (3, 4, 5), (5, 6, '!')] - - To slide into the iterable's items, use :func:`chain` to add filler items - to the left: - - >>> iterable = [1, 2, 3, 4] - >>> n = 3 - >>> padding = [None] * (n - 1) - >>> list(windowed(chain(padding, iterable), 3)) - [(None, None, 1), (None, 1, 2), (1, 2, 3), (2, 3, 4)] - """ - if n < 0: - raise ValueError('n must be >= 0') - if n == 0: - yield tuple() - return - if step < 1: - raise ValueError('step must be >= 1') - - window = deque(maxlen=n) - i = n - for _ in map(window.append, seq): - i -= 1 - if not i: - i = step - yield tuple(window) - - size = len(window) - if size < n: - yield tuple(chain(window, repeat(fillvalue, n - size))) - elif 0 < i < min(step, n): - window += (fillvalue,) * i - yield tuple(window) - - -def substrings(iterable): - """Yield all of the substrings of *iterable*. - - >>> [''.join(s) for s in substrings('more')] - ['m', 'o', 'r', 'e', 'mo', 'or', 're', 'mor', 'ore', 'more'] - - Note that non-string iterables can also be subdivided. - - >>> list(substrings([0, 1, 2])) - [(0,), (1,), (2,), (0, 1), (1, 2), (0, 1, 2)] - - """ - # The length-1 substrings - seq = [] - for item in iter(iterable): - seq.append(item) - yield (item,) - seq = tuple(seq) - item_count = len(seq) - - # And the rest - for n in range(2, item_count + 1): - for i in range(item_count - n + 1): - yield seq[i : i + n] - - -def substrings_indexes(seq, reverse=False): - """Yield all substrings and their positions in *seq* - - The items yielded will be a tuple of the form ``(substr, i, j)``, where - ``substr == seq[i:j]``. - - This function only works for iterables that support slicing, such as - ``str`` objects. - - >>> for item in substrings_indexes('more'): - ... print(item) - ('m', 0, 1) - ('o', 1, 2) - ('r', 2, 3) - ('e', 3, 4) - ('mo', 0, 2) - ('or', 1, 3) - ('re', 2, 4) - ('mor', 0, 3) - ('ore', 1, 4) - ('more', 0, 4) - - Set *reverse* to ``True`` to yield the same items in the opposite order. - - - """ - r = range(1, len(seq) + 1) - if reverse: - r = reversed(r) - return ( - (seq[i : i + L], i, i + L) for L in r for i in range(len(seq) - L + 1) - ) - - -class bucket: - """Wrap *iterable* and return an object that buckets it iterable into - child iterables based on a *key* function. - - >>> iterable = ['a1', 'b1', 'c1', 'a2', 'b2', 'c2', 'b3'] - >>> s = bucket(iterable, key=lambda x: x[0]) # Bucket by 1st character - >>> sorted(list(s)) # Get the keys - ['a', 'b', 'c'] - >>> a_iterable = s['a'] - >>> next(a_iterable) - 'a1' - >>> next(a_iterable) - 'a2' - >>> list(s['b']) - ['b1', 'b2', 'b3'] - - The original iterable will be advanced and its items will be cached until - they are used by the child iterables. This may require significant storage. - - By default, attempting to select a bucket to which no items belong will - exhaust the iterable and cache all values. - If you specify a *validator* function, selected buckets will instead be - checked against it. - - >>> from itertools import count - >>> it = count(1, 2) # Infinite sequence of odd numbers - >>> key = lambda x: x % 10 # Bucket by last digit - >>> validator = lambda x: x in {1, 3, 5, 7, 9} # Odd digits only - >>> s = bucket(it, key=key, validator=validator) - >>> 2 in s - False - >>> list(s[2]) - [] - - """ - - def __init__(self, iterable, key, validator=None): - self._it = iter(iterable) - self._key = key - self._cache = defaultdict(deque) - self._validator = validator or (lambda x: True) - - def __contains__(self, value): - if not self._validator(value): - return False - - try: - item = next(self[value]) - except StopIteration: - return False - else: - self._cache[value].appendleft(item) - - return True - - def _get_values(self, value): - """ - Helper to yield items from the parent iterator that match *value*. - Items that don't match are stored in the local cache as they - are encountered. - """ - while True: - # If we've cached some items that match the target value, emit - # the first one and evict it from the cache. - if self._cache[value]: - yield self._cache[value].popleft() - # Otherwise we need to advance the parent iterator to search for - # a matching item, caching the rest. - else: - while True: - try: - item = next(self._it) - except StopIteration: - return - item_value = self._key(item) - if item_value == value: - yield item - break - elif self._validator(item_value): - self._cache[item_value].append(item) - - def __iter__(self): - for item in self._it: - item_value = self._key(item) - if self._validator(item_value): - self._cache[item_value].append(item) - - yield from self._cache.keys() - - def __getitem__(self, value): - if not self._validator(value): - return iter(()) - - return self._get_values(value) - - -def spy(iterable, n=1): - """Return a 2-tuple with a list containing the first *n* elements of - *iterable*, and an iterator with the same items as *iterable*. - This allows you to "look ahead" at the items in the iterable without - advancing it. - - There is one item in the list by default: - - >>> iterable = 'abcdefg' - >>> head, iterable = spy(iterable) - >>> head - ['a'] - >>> list(iterable) - ['a', 'b', 'c', 'd', 'e', 'f', 'g'] - - You may use unpacking to retrieve items instead of lists: - - >>> (head,), iterable = spy('abcdefg') - >>> head - 'a' - >>> (first, second), iterable = spy('abcdefg', 2) - >>> first - 'a' - >>> second - 'b' - - The number of items requested can be larger than the number of items in - the iterable: - - >>> iterable = [1, 2, 3, 4, 5] - >>> head, iterable = spy(iterable, 10) - >>> head - [1, 2, 3, 4, 5] - >>> list(iterable) - [1, 2, 3, 4, 5] - - """ - it = iter(iterable) - head = take(n, it) - - return head.copy(), chain(head, it) - - -def interleave(*iterables): - """Return a new iterable yielding from each iterable in turn, - until the shortest is exhausted. - - >>> list(interleave([1, 2, 3], [4, 5], [6, 7, 8])) - [1, 4, 6, 2, 5, 7] - - For a version that doesn't terminate after the shortest iterable is - exhausted, see :func:`interleave_longest`. - - """ - return chain.from_iterable(zip(*iterables)) - - -def interleave_longest(*iterables): - """Return a new iterable yielding from each iterable in turn, - skipping any that are exhausted. - - >>> list(interleave_longest([1, 2, 3], [4, 5], [6, 7, 8])) - [1, 4, 6, 2, 5, 7, 3, 8] - - This function produces the same output as :func:`roundrobin`, but may - perform better for some inputs (in particular when the number of iterables - is large). - - """ - i = chain.from_iterable(zip_longest(*iterables, fillvalue=_marker)) - return (x for x in i if x is not _marker) - - -def collapse(iterable, base_type=None, levels=None): - """Flatten an iterable with multiple levels of nesting (e.g., a list of - lists of tuples) into non-iterable types. - - >>> iterable = [(1, 2), ([3, 4], [[5], [6]])] - >>> list(collapse(iterable)) - [1, 2, 3, 4, 5, 6] - - Binary and text strings are not considered iterable and - will not be collapsed. - - To avoid collapsing other types, specify *base_type*: - - >>> iterable = ['ab', ('cd', 'ef'), ['gh', 'ij']] - >>> list(collapse(iterable, base_type=tuple)) - ['ab', ('cd', 'ef'), 'gh', 'ij'] - - Specify *levels* to stop flattening after a certain level: - - >>> iterable = [('a', ['b']), ('c', ['d'])] - >>> list(collapse(iterable)) # Fully flattened - ['a', 'b', 'c', 'd'] - >>> list(collapse(iterable, levels=1)) # Only one level flattened - ['a', ['b'], 'c', ['d']] - - """ - - def walk(node, level): - if ( - ((levels is not None) and (level > levels)) - or isinstance(node, (str, bytes)) - or ((base_type is not None) and isinstance(node, base_type)) - ): - yield node - return - - try: - tree = iter(node) - except TypeError: - yield node - return - else: - for child in tree: - yield from walk(child, level + 1) - - yield from walk(iterable, 0) - - -def side_effect(func, iterable, chunk_size=None, before=None, after=None): - """Invoke *func* on each item in *iterable* (or on each *chunk_size* group - of items) before yielding the item. - - `func` must be a function that takes a single argument. Its return value - will be discarded. - - *before* and *after* are optional functions that take no arguments. They - will be executed before iteration starts and after it ends, respectively. - - `side_effect` can be used for logging, updating progress bars, or anything - that is not functionally "pure." - - Emitting a status message: - - >>> from more_itertools import consume - >>> func = lambda item: print('Received {}'.format(item)) - >>> consume(side_effect(func, range(2))) - Received 0 - Received 1 - - Operating on chunks of items: - - >>> pair_sums = [] - >>> func = lambda chunk: pair_sums.append(sum(chunk)) - >>> list(side_effect(func, [0, 1, 2, 3, 4, 5], 2)) - [0, 1, 2, 3, 4, 5] - >>> list(pair_sums) - [1, 5, 9] - - Writing to a file-like object: - - >>> from io import StringIO - >>> from more_itertools import consume - >>> f = StringIO() - >>> func = lambda x: print(x, file=f) - >>> before = lambda: print(u'HEADER', file=f) - >>> after = f.close - >>> it = [u'a', u'b', u'c'] - >>> consume(side_effect(func, it, before=before, after=after)) - >>> f.closed - True - - """ - try: - if before is not None: - before() - - if chunk_size is None: - for item in iterable: - func(item) - yield item - else: - for chunk in chunked(iterable, chunk_size): - func(chunk) - yield from chunk - finally: - if after is not None: - after() - - -def sliced(seq, n, strict=False): - """Yield slices of length *n* from the sequence *seq*. - - >>> list(sliced((1, 2, 3, 4, 5, 6), 3)) - [(1, 2, 3), (4, 5, 6)] - - By the default, the last yielded slice will have fewer than *n* elements - if the length of *seq* is not divisible by *n*: - - >>> list(sliced((1, 2, 3, 4, 5, 6, 7, 8), 3)) - [(1, 2, 3), (4, 5, 6), (7, 8)] - - If the length of *seq* is not divisible by *n* and *strict* is - ``True``, then ``ValueError`` will be raised before the last - slice is yielded. - - This function will only work for iterables that support slicing. - For non-sliceable iterables, see :func:`chunked`. - - """ - iterator = takewhile(len, (seq[i : i + n] for i in count(0, n))) - if strict: - - def ret(): - for _slice in iterator: - if len(_slice) != n: - raise ValueError("seq is not divisible by n.") - yield _slice - - return iter(ret()) - else: - return iterator - - -def split_at(iterable, pred, maxsplit=-1, keep_separator=False): - """Yield lists of items from *iterable*, where each list is delimited by - an item where callable *pred* returns ``True``. - - >>> list(split_at('abcdcba', lambda x: x == 'b')) - [['a'], ['c', 'd', 'c'], ['a']] - - >>> list(split_at(range(10), lambda n: n % 2 == 1)) - [[0], [2], [4], [6], [8], []] - - At most *maxsplit* splits are done. If *maxsplit* is not specified or -1, - then there is no limit on the number of splits: - - >>> list(split_at(range(10), lambda n: n % 2 == 1, maxsplit=2)) - [[0], [2], [4, 5, 6, 7, 8, 9]] - - By default, the delimiting items are not included in the output. - The include them, set *keep_separator* to ``True``. - - >>> list(split_at('abcdcba', lambda x: x == 'b', keep_separator=True)) - [['a'], ['b'], ['c', 'd', 'c'], ['b'], ['a']] - - """ - if maxsplit == 0: - yield list(iterable) - return - - buf = [] - it = iter(iterable) - for item in it: - if pred(item): - yield buf - if keep_separator: - yield [item] - if maxsplit == 1: - yield list(it) - return - buf = [] - maxsplit -= 1 - else: - buf.append(item) - yield buf - - -def split_before(iterable, pred, maxsplit=-1): - """Yield lists of items from *iterable*, where each list ends just before - an item for which callable *pred* returns ``True``: - - >>> list(split_before('OneTwo', lambda s: s.isupper())) - [['O', 'n', 'e'], ['T', 'w', 'o']] - - >>> list(split_before(range(10), lambda n: n % 3 == 0)) - [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]] - - At most *maxsplit* splits are done. If *maxsplit* is not specified or -1, - then there is no limit on the number of splits: - - >>> list(split_before(range(10), lambda n: n % 3 == 0, maxsplit=2)) - [[0, 1, 2], [3, 4, 5], [6, 7, 8, 9]] - """ - if maxsplit == 0: - yield list(iterable) - return - - buf = [] - it = iter(iterable) - for item in it: - if pred(item) and buf: - yield buf - if maxsplit == 1: - yield [item] + list(it) - return - buf = [] - maxsplit -= 1 - buf.append(item) - if buf: - yield buf - - -def split_after(iterable, pred, maxsplit=-1): - """Yield lists of items from *iterable*, where each list ends with an - item where callable *pred* returns ``True``: - - >>> list(split_after('one1two2', lambda s: s.isdigit())) - [['o', 'n', 'e', '1'], ['t', 'w', 'o', '2']] - - >>> list(split_after(range(10), lambda n: n % 3 == 0)) - [[0], [1, 2, 3], [4, 5, 6], [7, 8, 9]] - - At most *maxsplit* splits are done. If *maxsplit* is not specified or -1, - then there is no limit on the number of splits: - - >>> list(split_after(range(10), lambda n: n % 3 == 0, maxsplit=2)) - [[0], [1, 2, 3], [4, 5, 6, 7, 8, 9]] - - """ - if maxsplit == 0: - yield list(iterable) - return - - buf = [] - it = iter(iterable) - for item in it: - buf.append(item) - if pred(item) and buf: - yield buf - if maxsplit == 1: - yield list(it) - return - buf = [] - maxsplit -= 1 - if buf: - yield buf - - -def split_when(iterable, pred, maxsplit=-1): - """Split *iterable* into pieces based on the output of *pred*. - *pred* should be a function that takes successive pairs of items and - returns ``True`` if the iterable should be split in between them. - - For example, to find runs of increasing numbers, split the iterable when - element ``i`` is larger than element ``i + 1``: - - >>> list(split_when([1, 2, 3, 3, 2, 5, 2, 4, 2], lambda x, y: x > y)) - [[1, 2, 3, 3], [2, 5], [2, 4], [2]] - - At most *maxsplit* splits are done. If *maxsplit* is not specified or -1, - then there is no limit on the number of splits: - - >>> list(split_when([1, 2, 3, 3, 2, 5, 2, 4, 2], - ... lambda x, y: x > y, maxsplit=2)) - [[1, 2, 3, 3], [2, 5], [2, 4, 2]] - - """ - if maxsplit == 0: - yield list(iterable) - return - - it = iter(iterable) - try: - cur_item = next(it) - except StopIteration: - return - - buf = [cur_item] - for next_item in it: - if pred(cur_item, next_item): - yield buf - if maxsplit == 1: - yield [next_item] + list(it) - return - buf = [] - maxsplit -= 1 - - buf.append(next_item) - cur_item = next_item - - yield buf - - -def split_into(iterable, sizes): - """Yield a list of sequential items from *iterable* of length 'n' for each - integer 'n' in *sizes*. - - >>> list(split_into([1,2,3,4,5,6], [1,2,3])) - [[1], [2, 3], [4, 5, 6]] - - If the sum of *sizes* is smaller than the length of *iterable*, then the - remaining items of *iterable* will not be returned. - - >>> list(split_into([1,2,3,4,5,6], [2,3])) - [[1, 2], [3, 4, 5]] - - If the sum of *sizes* is larger than the length of *iterable*, fewer items - will be returned in the iteration that overruns *iterable* and further - lists will be empty: - - >>> list(split_into([1,2,3,4], [1,2,3,4])) - [[1], [2, 3], [4], []] - - When a ``None`` object is encountered in *sizes*, the returned list will - contain items up to the end of *iterable* the same way that itertools.slice - does: - - >>> list(split_into([1,2,3,4,5,6,7,8,9,0], [2,3,None])) - [[1, 2], [3, 4, 5], [6, 7, 8, 9, 0]] - - :func:`split_into` can be useful for grouping a series of items where the - sizes of the groups are not uniform. An example would be where in a row - from a table, multiple columns represent elements of the same feature - (e.g. a point represented by x,y,z) but, the format is not the same for - all columns. - """ - # convert the iterable argument into an iterator so its contents can - # be consumed by islice in case it is a generator - it = iter(iterable) - - for size in sizes: - if size is None: - yield list(it) - return - else: - yield list(islice(it, size)) - - -def padded(iterable, fillvalue=None, n=None, next_multiple=False): - """Yield the elements from *iterable*, followed by *fillvalue*, such that - at least *n* items are emitted. - - >>> list(padded([1, 2, 3], '?', 5)) - [1, 2, 3, '?', '?'] - - If *next_multiple* is ``True``, *fillvalue* will be emitted until the - number of items emitted is a multiple of *n*:: - - >>> list(padded([1, 2, 3, 4], n=3, next_multiple=True)) - [1, 2, 3, 4, None, None] - - If *n* is ``None``, *fillvalue* will be emitted indefinitely. - - """ - it = iter(iterable) - if n is None: - yield from chain(it, repeat(fillvalue)) - elif n < 1: - raise ValueError('n must be at least 1') - else: - item_count = 0 - for item in it: - yield item - item_count += 1 - - remaining = (n - item_count) % n if next_multiple else n - item_count - for _ in range(remaining): - yield fillvalue - - -def repeat_last(iterable, default=None): - """After the *iterable* is exhausted, keep yielding its last element. - - >>> list(islice(repeat_last(range(3)), 5)) - [0, 1, 2, 2, 2] - - If the iterable is empty, yield *default* forever:: - - >>> list(islice(repeat_last(range(0), 42), 5)) - [42, 42, 42, 42, 42] - - """ - item = _marker - for item in iterable: - yield item - final = default if item is _marker else item - yield from repeat(final) - - -def distribute(n, iterable): - """Distribute the items from *iterable* among *n* smaller iterables. - - >>> group_1, group_2 = distribute(2, [1, 2, 3, 4, 5, 6]) - >>> list(group_1) - [1, 3, 5] - >>> list(group_2) - [2, 4, 6] - - If the length of *iterable* is not evenly divisible by *n*, then the - length of the returned iterables will not be identical: - - >>> children = distribute(3, [1, 2, 3, 4, 5, 6, 7]) - >>> [list(c) for c in children] - [[1, 4, 7], [2, 5], [3, 6]] - - If the length of *iterable* is smaller than *n*, then the last returned - iterables will be empty: - - >>> children = distribute(5, [1, 2, 3]) - >>> [list(c) for c in children] - [[1], [2], [3], [], []] - - This function uses :func:`itertools.tee` and may require significant - storage. If you need the order items in the smaller iterables to match the - original iterable, see :func:`divide`. - - """ - if n < 1: - raise ValueError('n must be at least 1') - - children = tee(iterable, n) - return [islice(it, index, None, n) for index, it in enumerate(children)] - - -def stagger(iterable, offsets=(-1, 0, 1), longest=False, fillvalue=None): - """Yield tuples whose elements are offset from *iterable*. - The amount by which the `i`-th item in each tuple is offset is given by - the `i`-th item in *offsets*. - - >>> list(stagger([0, 1, 2, 3])) - [(None, 0, 1), (0, 1, 2), (1, 2, 3)] - >>> list(stagger(range(8), offsets=(0, 2, 4))) - [(0, 2, 4), (1, 3, 5), (2, 4, 6), (3, 5, 7)] - - By default, the sequence will end when the final element of a tuple is the - last item in the iterable. To continue until the first element of a tuple - is the last item in the iterable, set *longest* to ``True``:: - - >>> list(stagger([0, 1, 2, 3], longest=True)) - [(None, 0, 1), (0, 1, 2), (1, 2, 3), (2, 3, None), (3, None, None)] - - By default, ``None`` will be used to replace offsets beyond the end of the - sequence. Specify *fillvalue* to use some other value. - - """ - children = tee(iterable, len(offsets)) - - return zip_offset( - *children, offsets=offsets, longest=longest, fillvalue=fillvalue - ) - - -class UnequalIterablesError(ValueError): - def __init__(self, details=None): - msg = 'Iterables have different lengths' - if details is not None: - msg += (': index 0 has length {}; index {} has length {}').format( - *details - ) - - super().__init__(msg) - - -def _zip_equal_generator(iterables): - for combo in zip_longest(*iterables, fillvalue=_marker): - for val in combo: - if val is _marker: - raise UnequalIterablesError() - yield combo - - -def zip_equal(*iterables): - """``zip`` the input *iterables* together, but raise - ``UnequalIterablesError`` if they aren't all the same length. - - >>> it_1 = range(3) - >>> it_2 = iter('abc') - >>> list(zip_equal(it_1, it_2)) - [(0, 'a'), (1, 'b'), (2, 'c')] - - >>> it_1 = range(3) - >>> it_2 = iter('abcd') - >>> list(zip_equal(it_1, it_2)) # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ... - more_itertools.more.UnequalIterablesError: Iterables have different - lengths - - """ - if hexversion >= 0x30A00A6: - warnings.warn( - ( - 'zip_equal will be removed in a future version of ' - 'more-itertools. Use the builtin zip function with ' - 'strict=True instead.' - ), - DeprecationWarning, - ) - # Check whether the iterables are all the same size. - try: - first_size = len(iterables[0]) - for i, it in enumerate(iterables[1:], 1): - size = len(it) - if size != first_size: - break - else: - # If we didn't break out, we can use the built-in zip. - return zip(*iterables) - - # If we did break out, there was a mismatch. - raise UnequalIterablesError(details=(first_size, i, size)) - # If any one of the iterables didn't have a length, start reading - # them until one runs out. - except TypeError: - return _zip_equal_generator(iterables) - - -def zip_offset(*iterables, offsets, longest=False, fillvalue=None): - """``zip`` the input *iterables* together, but offset the `i`-th iterable - by the `i`-th item in *offsets*. - - >>> list(zip_offset('0123', 'abcdef', offsets=(0, 1))) - [('0', 'b'), ('1', 'c'), ('2', 'd'), ('3', 'e')] - - This can be used as a lightweight alternative to SciPy or pandas to analyze - data sets in which some series have a lead or lag relationship. - - By default, the sequence will end when the shortest iterable is exhausted. - To continue until the longest iterable is exhausted, set *longest* to - ``True``. - - >>> list(zip_offset('0123', 'abcdef', offsets=(0, 1), longest=True)) - [('0', 'b'), ('1', 'c'), ('2', 'd'), ('3', 'e'), (None, 'f')] - - By default, ``None`` will be used to replace offsets beyond the end of the - sequence. Specify *fillvalue* to use some other value. - - """ - if len(iterables) != len(offsets): - raise ValueError("Number of iterables and offsets didn't match") - - staggered = [] - for it, n in zip(iterables, offsets): - if n < 0: - staggered.append(chain(repeat(fillvalue, -n), it)) - elif n > 0: - staggered.append(islice(it, n, None)) - else: - staggered.append(it) - - if longest: - return zip_longest(*staggered, fillvalue=fillvalue) - - return zip(*staggered) - - -def sort_together(iterables, key_list=(0,), key=None, reverse=False): - """Return the input iterables sorted together, with *key_list* as the - priority for sorting. All iterables are trimmed to the length of the - shortest one. - - This can be used like the sorting function in a spreadsheet. If each - iterable represents a column of data, the key list determines which - columns are used for sorting. - - By default, all iterables are sorted using the ``0``-th iterable:: - - >>> iterables = [(4, 3, 2, 1), ('a', 'b', 'c', 'd')] - >>> sort_together(iterables) - [(1, 2, 3, 4), ('d', 'c', 'b', 'a')] - - Set a different key list to sort according to another iterable. - Specifying multiple keys dictates how ties are broken:: - - >>> iterables = [(3, 1, 2), (0, 1, 0), ('c', 'b', 'a')] - >>> sort_together(iterables, key_list=(1, 2)) - [(2, 3, 1), (0, 0, 1), ('a', 'c', 'b')] - - To sort by a function of the elements of the iterable, pass a *key* - function. Its arguments are the elements of the iterables corresponding to - the key list:: - - >>> names = ('a', 'b', 'c') - >>> lengths = (1, 2, 3) - >>> widths = (5, 2, 1) - >>> def area(length, width): - ... return length * width - >>> sort_together([names, lengths, widths], key_list=(1, 2), key=area) - [('c', 'b', 'a'), (3, 2, 1), (1, 2, 5)] - - Set *reverse* to ``True`` to sort in descending order. - - >>> sort_together([(1, 2, 3), ('c', 'b', 'a')], reverse=True) - [(3, 2, 1), ('a', 'b', 'c')] - - """ - if key is None: - # if there is no key function, the key argument to sorted is an - # itemgetter - key_argument = itemgetter(*key_list) - else: - # if there is a key function, call it with the items at the offsets - # specified by the key function as arguments - key_list = list(key_list) - if len(key_list) == 1: - # if key_list contains a single item, pass the item at that offset - # as the only argument to the key function - key_offset = key_list[0] - key_argument = lambda zipped_items: key(zipped_items[key_offset]) - else: - # if key_list contains multiple items, use itemgetter to return a - # tuple of items, which we pass as *args to the key function - get_key_items = itemgetter(*key_list) - key_argument = lambda zipped_items: key( - *get_key_items(zipped_items) - ) - - return list( - zip(*sorted(zip(*iterables), key=key_argument, reverse=reverse)) - ) - - -def unzip(iterable): - """The inverse of :func:`zip`, this function disaggregates the elements - of the zipped *iterable*. - - The ``i``-th iterable contains the ``i``-th element from each element - of the zipped iterable. The first element is used to to determine the - length of the remaining elements. - - >>> iterable = [('a', 1), ('b', 2), ('c', 3), ('d', 4)] - >>> letters, numbers = unzip(iterable) - >>> list(letters) - ['a', 'b', 'c', 'd'] - >>> list(numbers) - [1, 2, 3, 4] - - This is similar to using ``zip(*iterable)``, but it avoids reading - *iterable* into memory. Note, however, that this function uses - :func:`itertools.tee` and thus may require significant storage. - - """ - head, iterable = spy(iter(iterable)) - if not head: - # empty iterable, e.g. zip([], [], []) - return () - # spy returns a one-length iterable as head - head = head[0] - iterables = tee(iterable, len(head)) - - def itemgetter(i): - def getter(obj): - try: - return obj[i] - except IndexError: - # basically if we have an iterable like - # iter([(1, 2, 3), (4, 5), (6,)]) - # the second unzipped iterable would fail at the third tuple - # since it would try to access tup[1] - # same with the third unzipped iterable and the second tuple - # to support these "improperly zipped" iterables, - # we create a custom itemgetter - # which just stops the unzipped iterables - # at first length mismatch - raise StopIteration - - return getter - - return tuple(map(itemgetter(i), it) for i, it in enumerate(iterables)) - - -def divide(n, iterable): - """Divide the elements from *iterable* into *n* parts, maintaining - order. - - >>> group_1, group_2 = divide(2, [1, 2, 3, 4, 5, 6]) - >>> list(group_1) - [1, 2, 3] - >>> list(group_2) - [4, 5, 6] - - If the length of *iterable* is not evenly divisible by *n*, then the - length of the returned iterables will not be identical: - - >>> children = divide(3, [1, 2, 3, 4, 5, 6, 7]) - >>> [list(c) for c in children] - [[1, 2, 3], [4, 5], [6, 7]] - - If the length of the iterable is smaller than n, then the last returned - iterables will be empty: - - >>> children = divide(5, [1, 2, 3]) - >>> [list(c) for c in children] - [[1], [2], [3], [], []] - - This function will exhaust the iterable before returning and may require - significant storage. If order is not important, see :func:`distribute`, - which does not first pull the iterable into memory. - - """ - if n < 1: - raise ValueError('n must be at least 1') - - try: - iterable[:0] - except TypeError: - seq = tuple(iterable) - else: - seq = iterable - - q, r = divmod(len(seq), n) - - ret = [] - stop = 0 - for i in range(1, n + 1): - start = stop - stop += q + 1 if i <= r else q - ret.append(iter(seq[start:stop])) - - return ret - - -def always_iterable(obj, base_type=(str, bytes)): - """If *obj* is iterable, return an iterator over its items:: - - >>> obj = (1, 2, 3) - >>> list(always_iterable(obj)) - [1, 2, 3] - - If *obj* is not iterable, return a one-item iterable containing *obj*:: - - >>> obj = 1 - >>> list(always_iterable(obj)) - [1] - - If *obj* is ``None``, return an empty iterable: - - >>> obj = None - >>> list(always_iterable(None)) - [] - - By default, binary and text strings are not considered iterable:: - - >>> obj = 'foo' - >>> list(always_iterable(obj)) - ['foo'] - - If *base_type* is set, objects for which ``isinstance(obj, base_type)`` - returns ``True`` won't be considered iterable. - - >>> obj = {'a': 1} - >>> list(always_iterable(obj)) # Iterate over the dict's keys - ['a'] - >>> list(always_iterable(obj, base_type=dict)) # Treat dicts as a unit - [{'a': 1}] - - Set *base_type* to ``None`` to avoid any special handling and treat objects - Python considers iterable as iterable: - - >>> obj = 'foo' - >>> list(always_iterable(obj, base_type=None)) - ['f', 'o', 'o'] - """ - if obj is None: - return iter(()) - - if (base_type is not None) and isinstance(obj, base_type): - return iter((obj,)) - - try: - return iter(obj) - except TypeError: - return iter((obj,)) - - -def adjacent(predicate, iterable, distance=1): - """Return an iterable over `(bool, item)` tuples where the `item` is - drawn from *iterable* and the `bool` indicates whether - that item satisfies the *predicate* or is adjacent to an item that does. - - For example, to find whether items are adjacent to a ``3``:: - - >>> list(adjacent(lambda x: x == 3, range(6))) - [(False, 0), (False, 1), (True, 2), (True, 3), (True, 4), (False, 5)] - - Set *distance* to change what counts as adjacent. For example, to find - whether items are two places away from a ``3``: - - >>> list(adjacent(lambda x: x == 3, range(6), distance=2)) - [(False, 0), (True, 1), (True, 2), (True, 3), (True, 4), (True, 5)] - - This is useful for contextualizing the results of a search function. - For example, a code comparison tool might want to identify lines that - have changed, but also surrounding lines to give the viewer of the diff - context. - - The predicate function will only be called once for each item in the - iterable. - - See also :func:`groupby_transform`, which can be used with this function - to group ranges of items with the same `bool` value. - - """ - # Allow distance=0 mainly for testing that it reproduces results with map() - if distance < 0: - raise ValueError('distance must be at least 0') - - i1, i2 = tee(iterable) - padding = [False] * distance - selected = chain(padding, map(predicate, i1), padding) - adjacent_to_selected = map(any, windowed(selected, 2 * distance + 1)) - return zip(adjacent_to_selected, i2) - - -def groupby_transform(iterable, keyfunc=None, valuefunc=None, reducefunc=None): - """An extension of :func:`itertools.groupby` that can apply transformations - to the grouped data. - - * *keyfunc* is a function computing a key value for each item in *iterable* - * *valuefunc* is a function that transforms the individual items from - *iterable* after grouping - * *reducefunc* is a function that transforms each group of items - - >>> iterable = 'aAAbBBcCC' - >>> keyfunc = lambda k: k.upper() - >>> valuefunc = lambda v: v.lower() - >>> reducefunc = lambda g: ''.join(g) - >>> list(groupby_transform(iterable, keyfunc, valuefunc, reducefunc)) - [('A', 'aaa'), ('B', 'bbb'), ('C', 'ccc')] - - Each optional argument defaults to an identity function if not specified. - - :func:`groupby_transform` is useful when grouping elements of an iterable - using a separate iterable as the key. To do this, :func:`zip` the iterables - and pass a *keyfunc* that extracts the first element and a *valuefunc* - that extracts the second element:: - - >>> from operator import itemgetter - >>> keys = [0, 0, 1, 1, 1, 2, 2, 2, 3] - >>> values = 'abcdefghi' - >>> iterable = zip(keys, values) - >>> grouper = groupby_transform(iterable, itemgetter(0), itemgetter(1)) - >>> [(k, ''.join(g)) for k, g in grouper] - [(0, 'ab'), (1, 'cde'), (2, 'fgh'), (3, 'i')] - - Note that the order of items in the iterable is significant. - Only adjacent items are grouped together, so if you don't want any - duplicate groups, you should sort the iterable by the key function. - - """ - ret = groupby(iterable, keyfunc) - if valuefunc: - ret = ((k, map(valuefunc, g)) for k, g in ret) - if reducefunc: - ret = ((k, reducefunc(g)) for k, g in ret) - - return ret - - -class numeric_range(abc.Sequence, abc.Hashable): - """An extension of the built-in ``range()`` function whose arguments can - be any orderable numeric type. - - With only *stop* specified, *start* defaults to ``0`` and *step* - defaults to ``1``. The output items will match the type of *stop*: - - >>> list(numeric_range(3.5)) - [0.0, 1.0, 2.0, 3.0] - - With only *start* and *stop* specified, *step* defaults to ``1``. The - output items will match the type of *start*: - - >>> from decimal import Decimal - >>> start = Decimal('2.1') - >>> stop = Decimal('5.1') - >>> list(numeric_range(start, stop)) - [Decimal('2.1'), Decimal('3.1'), Decimal('4.1')] - - With *start*, *stop*, and *step* specified the output items will match - the type of ``start + step``: - - >>> from fractions import Fraction - >>> start = Fraction(1, 2) # Start at 1/2 - >>> stop = Fraction(5, 2) # End at 5/2 - >>> step = Fraction(1, 2) # Count by 1/2 - >>> list(numeric_range(start, stop, step)) - [Fraction(1, 2), Fraction(1, 1), Fraction(3, 2), Fraction(2, 1)] - - If *step* is zero, ``ValueError`` is raised. Negative steps are supported: - - >>> list(numeric_range(3, -1, -1.0)) - [3.0, 2.0, 1.0, 0.0] - - Be aware of the limitations of floating point numbers; the representation - of the yielded numbers may be surprising. - - ``datetime.datetime`` objects can be used for *start* and *stop*, if *step* - is a ``datetime.timedelta`` object: - - >>> import datetime - >>> start = datetime.datetime(2019, 1, 1) - >>> stop = datetime.datetime(2019, 1, 3) - >>> step = datetime.timedelta(days=1) - >>> items = iter(numeric_range(start, stop, step)) - >>> next(items) - datetime.datetime(2019, 1, 1, 0, 0) - >>> next(items) - datetime.datetime(2019, 1, 2, 0, 0) - - """ - - _EMPTY_HASH = hash(range(0, 0)) - - def __init__(self, *args): - argc = len(args) - if argc == 1: - (self._stop,) = args - self._start = type(self._stop)(0) - self._step = type(self._stop - self._start)(1) - elif argc == 2: - self._start, self._stop = args - self._step = type(self._stop - self._start)(1) - elif argc == 3: - self._start, self._stop, self._step = args - elif argc == 0: - raise TypeError( - 'numeric_range expected at least ' - '1 argument, got {}'.format(argc) - ) - else: - raise TypeError( - 'numeric_range expected at most ' - '3 arguments, got {}'.format(argc) - ) - - self._zero = type(self._step)(0) - if self._step == self._zero: - raise ValueError('numeric_range() arg 3 must not be zero') - self._growing = self._step > self._zero - self._init_len() - - def __bool__(self): - if self._growing: - return self._start < self._stop - else: - return self._start > self._stop - - def __contains__(self, elem): - if self._growing: - if self._start <= elem < self._stop: - return (elem - self._start) % self._step == self._zero - else: - if self._start >= elem > self._stop: - return (self._start - elem) % (-self._step) == self._zero - - return False - - def __eq__(self, other): - if isinstance(other, numeric_range): - empty_self = not bool(self) - empty_other = not bool(other) - if empty_self or empty_other: - return empty_self and empty_other # True if both empty - else: - return ( - self._start == other._start - and self._step == other._step - and self._get_by_index(-1) == other._get_by_index(-1) - ) - else: - return False - - def __getitem__(self, key): - if isinstance(key, int): - return self._get_by_index(key) - elif isinstance(key, slice): - step = self._step if key.step is None else key.step * self._step - - if key.start is None or key.start <= -self._len: - start = self._start - elif key.start >= self._len: - start = self._stop - else: # -self._len < key.start < self._len - start = self._get_by_index(key.start) - - if key.stop is None or key.stop >= self._len: - stop = self._stop - elif key.stop <= -self._len: - stop = self._start - else: # -self._len < key.stop < self._len - stop = self._get_by_index(key.stop) - - return numeric_range(start, stop, step) - else: - raise TypeError( - 'numeric range indices must be ' - 'integers or slices, not {}'.format(type(key).__name__) - ) - - def __hash__(self): - if self: - return hash((self._start, self._get_by_index(-1), self._step)) - else: - return self._EMPTY_HASH - - def __iter__(self): - values = (self._start + (n * self._step) for n in count()) - if self._growing: - return takewhile(partial(gt, self._stop), values) - else: - return takewhile(partial(lt, self._stop), values) - - def __len__(self): - return self._len - - def _init_len(self): - if self._growing: - start = self._start - stop = self._stop - step = self._step - else: - start = self._stop - stop = self._start - step = -self._step - distance = stop - start - if distance <= self._zero: - self._len = 0 - else: # distance > 0 and step > 0: regular euclidean division - q, r = divmod(distance, step) - self._len = int(q) + int(r != self._zero) - - def __reduce__(self): - return numeric_range, (self._start, self._stop, self._step) - - def __repr__(self): - if self._step == 1: - return "numeric_range({}, {})".format( - repr(self._start), repr(self._stop) - ) - else: - return "numeric_range({}, {}, {})".format( - repr(self._start), repr(self._stop), repr(self._step) - ) - - def __reversed__(self): - return iter( - numeric_range( - self._get_by_index(-1), self._start - self._step, -self._step - ) - ) - - def count(self, value): - return int(value in self) - - def index(self, value): - if self._growing: - if self._start <= value < self._stop: - q, r = divmod(value - self._start, self._step) - if r == self._zero: - return int(q) - else: - if self._start >= value > self._stop: - q, r = divmod(self._start - value, -self._step) - if r == self._zero: - return int(q) - - raise ValueError("{} is not in numeric range".format(value)) - - def _get_by_index(self, i): - if i < 0: - i += self._len - if i < 0 or i >= self._len: - raise IndexError("numeric range object index out of range") - return self._start + i * self._step - - -def count_cycle(iterable, n=None): - """Cycle through the items from *iterable* up to *n* times, yielding - the number of completed cycles along with each item. If *n* is omitted the - process repeats indefinitely. - - >>> list(count_cycle('AB', 3)) - [(0, 'A'), (0, 'B'), (1, 'A'), (1, 'B'), (2, 'A'), (2, 'B')] - - """ - iterable = tuple(iterable) - if not iterable: - return iter(()) - counter = count() if n is None else range(n) - return ((i, item) for i in counter for item in iterable) - - -def mark_ends(iterable): - """Yield 3-tuples of the form ``(is_first, is_last, item)``. - - >>> list(mark_ends('ABC')) - [(True, False, 'A'), (False, False, 'B'), (False, True, 'C')] - - Use this when looping over an iterable to take special action on its first - and/or last items: - - >>> iterable = ['Header', 100, 200, 'Footer'] - >>> total = 0 - >>> for is_first, is_last, item in mark_ends(iterable): - ... if is_first: - ... continue # Skip the header - ... if is_last: - ... continue # Skip the footer - ... total += item - >>> print(total) - 300 - """ - it = iter(iterable) - - try: - b = next(it) - except StopIteration: - return - - try: - for i in count(): - a = b - b = next(it) - yield i == 0, False, a - - except StopIteration: - yield i == 0, True, a - - -def locate(iterable, pred=bool, window_size=None): - """Yield the index of each item in *iterable* for which *pred* returns - ``True``. - - *pred* defaults to :func:`bool`, which will select truthy items: - - >>> list(locate([0, 1, 1, 0, 1, 0, 0])) - [1, 2, 4] - - Set *pred* to a custom function to, e.g., find the indexes for a particular - item. - - >>> list(locate(['a', 'b', 'c', 'b'], lambda x: x == 'b')) - [1, 3] - - If *window_size* is given, then the *pred* function will be called with - that many items. This enables searching for sub-sequences: - - >>> iterable = [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3] - >>> pred = lambda *args: args == (1, 2, 3) - >>> list(locate(iterable, pred=pred, window_size=3)) - [1, 5, 9] - - Use with :func:`seekable` to find indexes and then retrieve the associated - items: - - >>> from itertools import count - >>> from more_itertools import seekable - >>> source = (3 * n + 1 if (n % 2) else n // 2 for n in count()) - >>> it = seekable(source) - >>> pred = lambda x: x > 100 - >>> indexes = locate(it, pred=pred) - >>> i = next(indexes) - >>> it.seek(i) - >>> next(it) - 106 - - """ - if window_size is None: - return compress(count(), map(pred, iterable)) - - if window_size < 1: - raise ValueError('window size must be at least 1') - - it = windowed(iterable, window_size, fillvalue=_marker) - return compress(count(), starmap(pred, it)) - - -def lstrip(iterable, pred): - """Yield the items from *iterable*, but strip any from the beginning - for which *pred* returns ``True``. - - For example, to remove a set of items from the start of an iterable: - - >>> iterable = (None, False, None, 1, 2, None, 3, False, None) - >>> pred = lambda x: x in {None, False, ''} - >>> list(lstrip(iterable, pred)) - [1, 2, None, 3, False, None] - - This function is analogous to to :func:`str.lstrip`, and is essentially - an wrapper for :func:`itertools.dropwhile`. - - """ - return dropwhile(pred, iterable) - - -def rstrip(iterable, pred): - """Yield the items from *iterable*, but strip any from the end - for which *pred* returns ``True``. - - For example, to remove a set of items from the end of an iterable: - - >>> iterable = (None, False, None, 1, 2, None, 3, False, None) - >>> pred = lambda x: x in {None, False, ''} - >>> list(rstrip(iterable, pred)) - [None, False, None, 1, 2, None, 3] - - This function is analogous to :func:`str.rstrip`. - - """ - cache = [] - cache_append = cache.append - cache_clear = cache.clear - for x in iterable: - if pred(x): - cache_append(x) - else: - yield from cache - cache_clear() - yield x - - -def strip(iterable, pred): - """Yield the items from *iterable*, but strip any from the - beginning and end for which *pred* returns ``True``. - - For example, to remove a set of items from both ends of an iterable: - - >>> iterable = (None, False, None, 1, 2, None, 3, False, None) - >>> pred = lambda x: x in {None, False, ''} - >>> list(strip(iterable, pred)) - [1, 2, None, 3] - - This function is analogous to :func:`str.strip`. - - """ - return rstrip(lstrip(iterable, pred), pred) - - -class islice_extended: - """An extension of :func:`itertools.islice` that supports negative values - for *stop*, *start*, and *step*. - - >>> iterable = iter('abcdefgh') - >>> list(islice_extended(iterable, -4, -1)) - ['e', 'f', 'g'] - - Slices with negative values require some caching of *iterable*, but this - function takes care to minimize the amount of memory required. - - For example, you can use a negative step with an infinite iterator: - - >>> from itertools import count - >>> list(islice_extended(count(), 110, 99, -2)) - [110, 108, 106, 104, 102, 100] - - You can also use slice notation directly: - - >>> iterable = map(str, count()) - >>> it = islice_extended(iterable)[10:20:2] - >>> list(it) - ['10', '12', '14', '16', '18'] - - """ - - def __init__(self, iterable, *args): - it = iter(iterable) - if args: - self._iterable = _islice_helper(it, slice(*args)) - else: - self._iterable = it - - def __iter__(self): - return self - - def __next__(self): - return next(self._iterable) - - def __getitem__(self, key): - if isinstance(key, slice): - return islice_extended(_islice_helper(self._iterable, key)) - - raise TypeError('islice_extended.__getitem__ argument must be a slice') - - -def _islice_helper(it, s): - start = s.start - stop = s.stop - if s.step == 0: - raise ValueError('step argument must be a non-zero integer or None.') - step = s.step or 1 - - if step > 0: - start = 0 if (start is None) else start - - if start < 0: - # Consume all but the last -start items - cache = deque(enumerate(it, 1), maxlen=-start) - len_iter = cache[-1][0] if cache else 0 - - # Adjust start to be positive - i = max(len_iter + start, 0) - - # Adjust stop to be positive - if stop is None: - j = len_iter - elif stop >= 0: - j = min(stop, len_iter) - else: - j = max(len_iter + stop, 0) - - # Slice the cache - n = j - i - if n <= 0: - return - - for index, item in islice(cache, 0, n, step): - yield item - elif (stop is not None) and (stop < 0): - # Advance to the start position - next(islice(it, start, start), None) - - # When stop is negative, we have to carry -stop items while - # iterating - cache = deque(islice(it, -stop), maxlen=-stop) - - for index, item in enumerate(it): - cached_item = cache.popleft() - if index % step == 0: - yield cached_item - cache.append(item) - else: - # When both start and stop are positive we have the normal case - yield from islice(it, start, stop, step) - else: - start = -1 if (start is None) else start - - if (stop is not None) and (stop < 0): - # Consume all but the last items - n = -stop - 1 - cache = deque(enumerate(it, 1), maxlen=n) - len_iter = cache[-1][0] if cache else 0 - - # If start and stop are both negative they are comparable and - # we can just slice. Otherwise we can adjust start to be negative - # and then slice. - if start < 0: - i, j = start, stop - else: - i, j = min(start - len_iter, -1), None - - for index, item in list(cache)[i:j:step]: - yield item - else: - # Advance to the stop position - if stop is not None: - m = stop + 1 - next(islice(it, m, m), None) - - # stop is positive, so if start is negative they are not comparable - # and we need the rest of the items. - if start < 0: - i = start - n = None - # stop is None and start is positive, so we just need items up to - # the start index. - elif stop is None: - i = None - n = start + 1 - # Both stop and start are positive, so they are comparable. - else: - i = None - n = start - stop - if n <= 0: - return - - cache = list(islice(it, n)) - - yield from cache[i::step] - - -def always_reversible(iterable): - """An extension of :func:`reversed` that supports all iterables, not - just those which implement the ``Reversible`` or ``Sequence`` protocols. - - >>> print(*always_reversible(x for x in range(3))) - 2 1 0 - - If the iterable is already reversible, this function returns the - result of :func:`reversed()`. If the iterable is not reversible, - this function will cache the remaining items in the iterable and - yield them in reverse order, which may require significant storage. - """ - try: - return reversed(iterable) - except TypeError: - return reversed(list(iterable)) - - -def consecutive_groups(iterable, ordering=lambda x: x): - """Yield groups of consecutive items using :func:`itertools.groupby`. - The *ordering* function determines whether two items are adjacent by - returning their position. - - By default, the ordering function is the identity function. This is - suitable for finding runs of numbers: - - >>> iterable = [1, 10, 11, 12, 20, 30, 31, 32, 33, 40] - >>> for group in consecutive_groups(iterable): - ... print(list(group)) - [1] - [10, 11, 12] - [20] - [30, 31, 32, 33] - [40] - - For finding runs of adjacent letters, try using the :meth:`index` method - of a string of letters: - - >>> from string import ascii_lowercase - >>> iterable = 'abcdfgilmnop' - >>> ordering = ascii_lowercase.index - >>> for group in consecutive_groups(iterable, ordering): - ... print(list(group)) - ['a', 'b', 'c', 'd'] - ['f', 'g'] - ['i'] - ['l', 'm', 'n', 'o', 'p'] - - Each group of consecutive items is an iterator that shares it source with - *iterable*. When an an output group is advanced, the previous group is - no longer available unless its elements are copied (e.g., into a ``list``). - - >>> iterable = [1, 2, 11, 12, 21, 22] - >>> saved_groups = [] - >>> for group in consecutive_groups(iterable): - ... saved_groups.append(list(group)) # Copy group elements - >>> saved_groups - [[1, 2], [11, 12], [21, 22]] - - """ - for k, g in groupby( - enumerate(iterable), key=lambda x: x[0] - ordering(x[1]) - ): - yield map(itemgetter(1), g) - - -def difference(iterable, func=sub, *, initial=None): - """This function is the inverse of :func:`itertools.accumulate`. By default - it will compute the first difference of *iterable* using - :func:`operator.sub`: - - >>> from itertools import accumulate - >>> iterable = accumulate([0, 1, 2, 3, 4]) # produces 0, 1, 3, 6, 10 - >>> list(difference(iterable)) - [0, 1, 2, 3, 4] - - *func* defaults to :func:`operator.sub`, but other functions can be - specified. They will be applied as follows:: - - A, B, C, D, ... --> A, func(B, A), func(C, B), func(D, C), ... - - For example, to do progressive division: - - >>> iterable = [1, 2, 6, 24, 120] - >>> func = lambda x, y: x // y - >>> list(difference(iterable, func)) - [1, 2, 3, 4, 5] - - If the *initial* keyword is set, the first element will be skipped when - computing successive differences. - - >>> it = [10, 11, 13, 16] # from accumulate([1, 2, 3], initial=10) - >>> list(difference(it, initial=10)) - [1, 2, 3] - - """ - a, b = tee(iterable) - try: - first = [next(b)] - except StopIteration: - return iter([]) - - if initial is not None: - first = [] - - return chain(first, starmap(func, zip(b, a))) - - -class SequenceView(Sequence): - """Return a read-only view of the sequence object *target*. - - :class:`SequenceView` objects are analogous to Python's built-in - "dictionary view" types. They provide a dynamic view of a sequence's items, - meaning that when the sequence updates, so does the view. - - >>> seq = ['0', '1', '2'] - >>> view = SequenceView(seq) - >>> view - SequenceView(['0', '1', '2']) - >>> seq.append('3') - >>> view - SequenceView(['0', '1', '2', '3']) - - Sequence views support indexing, slicing, and length queries. They act - like the underlying sequence, except they don't allow assignment: - - >>> view[1] - '1' - >>> view[1:-1] - ['1', '2'] - >>> len(view) - 4 - - Sequence views are useful as an alternative to copying, as they don't - require (much) extra storage. - - """ - - def __init__(self, target): - if not isinstance(target, Sequence): - raise TypeError - self._target = target - - def __getitem__(self, index): - return self._target[index] - - def __len__(self): - return len(self._target) - - def __repr__(self): - return '{}({})'.format(self.__class__.__name__, repr(self._target)) - - -class seekable: - """Wrap an iterator to allow for seeking backward and forward. This - progressively caches the items in the source iterable so they can be - re-visited. - - Call :meth:`seek` with an index to seek to that position in the source - iterable. - - To "reset" an iterator, seek to ``0``: - - >>> from itertools import count - >>> it = seekable((str(n) for n in count())) - >>> next(it), next(it), next(it) - ('0', '1', '2') - >>> it.seek(0) - >>> next(it), next(it), next(it) - ('0', '1', '2') - >>> next(it) - '3' - - You can also seek forward: - - >>> it = seekable((str(n) for n in range(20))) - >>> it.seek(10) - >>> next(it) - '10' - >>> it.seek(20) # Seeking past the end of the source isn't a problem - >>> list(it) - [] - >>> it.seek(0) # Resetting works even after hitting the end - >>> next(it), next(it), next(it) - ('0', '1', '2') - - Call :meth:`peek` to look ahead one item without advancing the iterator: - - >>> it = seekable('1234') - >>> it.peek() - '1' - >>> list(it) - ['1', '2', '3', '4'] - >>> it.peek(default='empty') - 'empty' - - Before the iterator is at its end, calling :func:`bool` on it will return - ``True``. After it will return ``False``: - - >>> it = seekable('5678') - >>> bool(it) - True - >>> list(it) - ['5', '6', '7', '8'] - >>> bool(it) - False - - You may view the contents of the cache with the :meth:`elements` method. - That returns a :class:`SequenceView`, a view that updates automatically: - - >>> it = seekable((str(n) for n in range(10))) - >>> next(it), next(it), next(it) - ('0', '1', '2') - >>> elements = it.elements() - >>> elements - SequenceView(['0', '1', '2']) - >>> next(it) - '3' - >>> elements - SequenceView(['0', '1', '2', '3']) - - By default, the cache grows as the source iterable progresses, so beware of - wrapping very large or infinite iterables. Supply *maxlen* to limit the - size of the cache (this of course limits how far back you can seek). - - >>> from itertools import count - >>> it = seekable((str(n) for n in count()), maxlen=2) - >>> next(it), next(it), next(it), next(it) - ('0', '1', '2', '3') - >>> list(it.elements()) - ['2', '3'] - >>> it.seek(0) - >>> next(it), next(it), next(it), next(it) - ('2', '3', '4', '5') - >>> next(it) - '6' - - """ - - def __init__(self, iterable, maxlen=None): - self._source = iter(iterable) - if maxlen is None: - self._cache = [] - else: - self._cache = deque([], maxlen) - self._index = None - - def __iter__(self): - return self - - def __next__(self): - if self._index is not None: - try: - item = self._cache[self._index] - except IndexError: - self._index = None - else: - self._index += 1 - return item - - item = next(self._source) - self._cache.append(item) - return item - - def __bool__(self): - try: - self.peek() - except StopIteration: - return False - return True - - def peek(self, default=_marker): - try: - peeked = next(self) - except StopIteration: - if default is _marker: - raise - return default - if self._index is None: - self._index = len(self._cache) - self._index -= 1 - return peeked - - def elements(self): - return SequenceView(self._cache) - - def seek(self, index): - self._index = index - remainder = index - len(self._cache) - if remainder > 0: - consume(self, remainder) - - -class run_length: - """ - :func:`run_length.encode` compresses an iterable with run-length encoding. - It yields groups of repeated items with the count of how many times they - were repeated: - - >>> uncompressed = 'abbcccdddd' - >>> list(run_length.encode(uncompressed)) - [('a', 1), ('b', 2), ('c', 3), ('d', 4)] - - :func:`run_length.decode` decompresses an iterable that was previously - compressed with run-length encoding. It yields the items of the - decompressed iterable: - - >>> compressed = [('a', 1), ('b', 2), ('c', 3), ('d', 4)] - >>> list(run_length.decode(compressed)) - ['a', 'b', 'b', 'c', 'c', 'c', 'd', 'd', 'd', 'd'] - - """ - - @staticmethod - def encode(iterable): - return ((k, ilen(g)) for k, g in groupby(iterable)) - - @staticmethod - def decode(iterable): - return chain.from_iterable(repeat(k, n) for k, n in iterable) - - -def exactly_n(iterable, n, predicate=bool): - """Return ``True`` if exactly ``n`` items in the iterable are ``True`` - according to the *predicate* function. - - >>> exactly_n([True, True, False], 2) - True - >>> exactly_n([True, True, False], 1) - False - >>> exactly_n([0, 1, 2, 3, 4, 5], 3, lambda x: x < 3) - True - - The iterable will be advanced until ``n + 1`` truthy items are encountered, - so avoid calling it on infinite iterables. - - """ - return len(take(n + 1, filter(predicate, iterable))) == n - - -def circular_shifts(iterable): - """Return a list of circular shifts of *iterable*. - - >>> circular_shifts(range(4)) - [(0, 1, 2, 3), (1, 2, 3, 0), (2, 3, 0, 1), (3, 0, 1, 2)] - """ - lst = list(iterable) - return take(len(lst), windowed(cycle(lst), len(lst))) - - -def make_decorator(wrapping_func, result_index=0): - """Return a decorator version of *wrapping_func*, which is a function that - modifies an iterable. *result_index* is the position in that function's - signature where the iterable goes. - - This lets you use itertools on the "production end," i.e. at function - definition. This can augment what the function returns without changing the - function's code. - - For example, to produce a decorator version of :func:`chunked`: - - >>> from more_itertools import chunked - >>> chunker = make_decorator(chunked, result_index=0) - >>> @chunker(3) - ... def iter_range(n): - ... return iter(range(n)) - ... - >>> list(iter_range(9)) - [[0, 1, 2], [3, 4, 5], [6, 7, 8]] - - To only allow truthy items to be returned: - - >>> truth_serum = make_decorator(filter, result_index=1) - >>> @truth_serum(bool) - ... def boolean_test(): - ... return [0, 1, '', ' ', False, True] - ... - >>> list(boolean_test()) - [1, ' ', True] - - The :func:`peekable` and :func:`seekable` wrappers make for practical - decorators: - - >>> from more_itertools import peekable - >>> peekable_function = make_decorator(peekable) - >>> @peekable_function() - ... def str_range(*args): - ... return (str(x) for x in range(*args)) - ... - >>> it = str_range(1, 20, 2) - >>> next(it), next(it), next(it) - ('1', '3', '5') - >>> it.peek() - '7' - >>> next(it) - '7' - - """ - # See https://sites.google.com/site/bbayles/index/decorator_factory for - # notes on how this works. - def decorator(*wrapping_args, **wrapping_kwargs): - def outer_wrapper(f): - def inner_wrapper(*args, **kwargs): - result = f(*args, **kwargs) - wrapping_args_ = list(wrapping_args) - wrapping_args_.insert(result_index, result) - return wrapping_func(*wrapping_args_, **wrapping_kwargs) - - return inner_wrapper - - return outer_wrapper - - return decorator - - -def map_reduce(iterable, keyfunc, valuefunc=None, reducefunc=None): - """Return a dictionary that maps the items in *iterable* to categories - defined by *keyfunc*, transforms them with *valuefunc*, and - then summarizes them by category with *reducefunc*. - - *valuefunc* defaults to the identity function if it is unspecified. - If *reducefunc* is unspecified, no summarization takes place: - - >>> keyfunc = lambda x: x.upper() - >>> result = map_reduce('abbccc', keyfunc) - >>> sorted(result.items()) - [('A', ['a']), ('B', ['b', 'b']), ('C', ['c', 'c', 'c'])] - - Specifying *valuefunc* transforms the categorized items: - - >>> keyfunc = lambda x: x.upper() - >>> valuefunc = lambda x: 1 - >>> result = map_reduce('abbccc', keyfunc, valuefunc) - >>> sorted(result.items()) - [('A', [1]), ('B', [1, 1]), ('C', [1, 1, 1])] - - Specifying *reducefunc* summarizes the categorized items: - - >>> keyfunc = lambda x: x.upper() - >>> valuefunc = lambda x: 1 - >>> reducefunc = sum - >>> result = map_reduce('abbccc', keyfunc, valuefunc, reducefunc) - >>> sorted(result.items()) - [('A', 1), ('B', 2), ('C', 3)] - - You may want to filter the input iterable before applying the map/reduce - procedure: - - >>> all_items = range(30) - >>> items = [x for x in all_items if 10 <= x <= 20] # Filter - >>> keyfunc = lambda x: x % 2 # Evens map to 0; odds to 1 - >>> categories = map_reduce(items, keyfunc=keyfunc) - >>> sorted(categories.items()) - [(0, [10, 12, 14, 16, 18, 20]), (1, [11, 13, 15, 17, 19])] - >>> summaries = map_reduce(items, keyfunc=keyfunc, reducefunc=sum) - >>> sorted(summaries.items()) - [(0, 90), (1, 75)] - - Note that all items in the iterable are gathered into a list before the - summarization step, which may require significant storage. - - The returned object is a :obj:`collections.defaultdict` with the - ``default_factory`` set to ``None``, such that it behaves like a normal - dictionary. - - """ - valuefunc = (lambda x: x) if (valuefunc is None) else valuefunc - - ret = defaultdict(list) - for item in iterable: - key = keyfunc(item) - value = valuefunc(item) - ret[key].append(value) - - if reducefunc is not None: - for key, value_list in ret.items(): - ret[key] = reducefunc(value_list) - - ret.default_factory = None - return ret - - -def rlocate(iterable, pred=bool, window_size=None): - """Yield the index of each item in *iterable* for which *pred* returns - ``True``, starting from the right and moving left. - - *pred* defaults to :func:`bool`, which will select truthy items: - - >>> list(rlocate([0, 1, 1, 0, 1, 0, 0])) # Truthy at 1, 2, and 4 - [4, 2, 1] - - Set *pred* to a custom function to, e.g., find the indexes for a particular - item: - - >>> iterable = iter('abcb') - >>> pred = lambda x: x == 'b' - >>> list(rlocate(iterable, pred)) - [3, 1] - - If *window_size* is given, then the *pred* function will be called with - that many items. This enables searching for sub-sequences: - - >>> iterable = [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3] - >>> pred = lambda *args: args == (1, 2, 3) - >>> list(rlocate(iterable, pred=pred, window_size=3)) - [9, 5, 1] - - Beware, this function won't return anything for infinite iterables. - If *iterable* is reversible, ``rlocate`` will reverse it and search from - the right. Otherwise, it will search from the left and return the results - in reverse order. - - See :func:`locate` to for other example applications. - - """ - if window_size is None: - try: - len_iter = len(iterable) - return (len_iter - i - 1 for i in locate(reversed(iterable), pred)) - except TypeError: - pass - - return reversed(list(locate(iterable, pred, window_size))) - - -def replace(iterable, pred, substitutes, count=None, window_size=1): - """Yield the items from *iterable*, replacing the items for which *pred* - returns ``True`` with the items from the iterable *substitutes*. - - >>> iterable = [1, 1, 0, 1, 1, 0, 1, 1] - >>> pred = lambda x: x == 0 - >>> substitutes = (2, 3) - >>> list(replace(iterable, pred, substitutes)) - [1, 1, 2, 3, 1, 1, 2, 3, 1, 1] - - If *count* is given, the number of replacements will be limited: - - >>> iterable = [1, 1, 0, 1, 1, 0, 1, 1, 0] - >>> pred = lambda x: x == 0 - >>> substitutes = [None] - >>> list(replace(iterable, pred, substitutes, count=2)) - [1, 1, None, 1, 1, None, 1, 1, 0] - - Use *window_size* to control the number of items passed as arguments to - *pred*. This allows for locating and replacing subsequences. - - >>> iterable = [0, 1, 2, 5, 0, 1, 2, 5] - >>> window_size = 3 - >>> pred = lambda *args: args == (0, 1, 2) # 3 items passed to pred - >>> substitutes = [3, 4] # Splice in these items - >>> list(replace(iterable, pred, substitutes, window_size=window_size)) - [3, 4, 5, 3, 4, 5] - - """ - if window_size < 1: - raise ValueError('window_size must be at least 1') - - # Save the substitutes iterable, since it's used more than once - substitutes = tuple(substitutes) - - # Add padding such that the number of windows matches the length of the - # iterable - it = chain(iterable, [_marker] * (window_size - 1)) - windows = windowed(it, window_size) - - n = 0 - for w in windows: - # If the current window matches our predicate (and we haven't hit - # our maximum number of replacements), splice in the substitutes - # and then consume the following windows that overlap with this one. - # For example, if the iterable is (0, 1, 2, 3, 4...) - # and the window size is 2, we have (0, 1), (1, 2), (2, 3)... - # If the predicate matches on (0, 1), we need to zap (0, 1) and (1, 2) - if pred(*w): - if (count is None) or (n < count): - n += 1 - yield from substitutes - consume(windows, window_size - 1) - continue - - # If there was no match (or we've reached the replacement limit), - # yield the first item from the window. - if w and (w[0] is not _marker): - yield w[0] - - -def partitions(iterable): - """Yield all possible order-preserving partitions of *iterable*. - - >>> iterable = 'abc' - >>> for part in partitions(iterable): - ... print([''.join(p) for p in part]) - ['abc'] - ['a', 'bc'] - ['ab', 'c'] - ['a', 'b', 'c'] - - This is unrelated to :func:`partition`. - - """ - sequence = list(iterable) - n = len(sequence) - for i in powerset(range(1, n)): - yield [sequence[i:j] for i, j in zip((0,) + i, i + (n,))] - - -def set_partitions(iterable, k=None): - """ - Yield the set partitions of *iterable* into *k* parts. Set partitions are - not order-preserving. - - >>> iterable = 'abc' - >>> for part in set_partitions(iterable, 2): - ... print([''.join(p) for p in part]) - ['a', 'bc'] - ['ab', 'c'] - ['b', 'ac'] - - - If *k* is not given, every set partition is generated. - - >>> iterable = 'abc' - >>> for part in set_partitions(iterable): - ... print([''.join(p) for p in part]) - ['abc'] - ['a', 'bc'] - ['ab', 'c'] - ['b', 'ac'] - ['a', 'b', 'c'] - - """ - L = list(iterable) - n = len(L) - if k is not None: - if k < 1: - raise ValueError( - "Can't partition in a negative or zero number of groups" - ) - elif k > n: - return - - def set_partitions_helper(L, k): - n = len(L) - if k == 1: - yield [L] - elif n == k: - yield [[s] for s in L] - else: - e, *M = L - for p in set_partitions_helper(M, k - 1): - yield [[e], *p] - for p in set_partitions_helper(M, k): - for i in range(len(p)): - yield p[:i] + [[e] + p[i]] + p[i + 1 :] - - if k is None: - for k in range(1, n + 1): - yield from set_partitions_helper(L, k) - else: - yield from set_partitions_helper(L, k) - - -class time_limited: - """ - Yield items from *iterable* until *limit_seconds* have passed. - If the time limit expires before all items have been yielded, the - ``timed_out`` parameter will be set to ``True``. - - >>> from time import sleep - >>> def generator(): - ... yield 1 - ... yield 2 - ... sleep(0.2) - ... yield 3 - >>> iterable = time_limited(0.1, generator()) - >>> list(iterable) - [1, 2] - >>> iterable.timed_out - True - - Note that the time is checked before each item is yielded, and iteration - stops if the time elapsed is greater than *limit_seconds*. If your time - limit is 1 second, but it takes 2 seconds to generate the first item from - the iterable, the function will run for 2 seconds and not yield anything. - - """ - - def __init__(self, limit_seconds, iterable): - if limit_seconds < 0: - raise ValueError('limit_seconds must be positive') - self.limit_seconds = limit_seconds - self._iterable = iter(iterable) - self._start_time = monotonic() - self.timed_out = False - - def __iter__(self): - return self - - def __next__(self): - item = next(self._iterable) - if monotonic() - self._start_time > self.limit_seconds: - self.timed_out = True - raise StopIteration - - return item - - -def only(iterable, default=None, too_long=None): - """If *iterable* has only one item, return it. - If it has zero items, return *default*. - If it has more than one item, raise the exception given by *too_long*, - which is ``ValueError`` by default. - - >>> only([], default='missing') - 'missing' - >>> only([1]) - 1 - >>> only([1, 2]) # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ... - ValueError: Expected exactly one item in iterable, but got 1, 2, - and perhaps more.' - >>> only([1, 2], too_long=TypeError) # doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ... - TypeError - - Note that :func:`only` attempts to advance *iterable* twice to ensure there - is only one item. See :func:`spy` or :func:`peekable` to check - iterable contents less destructively. - """ - it = iter(iterable) - first_value = next(it, default) - - try: - second_value = next(it) - except StopIteration: - pass - else: - msg = ( - 'Expected exactly one item in iterable, but got {!r}, {!r}, ' - 'and perhaps more.'.format(first_value, second_value) - ) - raise too_long or ValueError(msg) - - return first_value - - -def ichunked(iterable, n): - """Break *iterable* into sub-iterables with *n* elements each. - :func:`ichunked` is like :func:`chunked`, but it yields iterables - instead of lists. - - If the sub-iterables are read in order, the elements of *iterable* - won't be stored in memory. - If they are read out of order, :func:`itertools.tee` is used to cache - elements as necessary. - - >>> from itertools import count - >>> all_chunks = ichunked(count(), 4) - >>> c_1, c_2, c_3 = next(all_chunks), next(all_chunks), next(all_chunks) - >>> list(c_2) # c_1's elements have been cached; c_3's haven't been - [4, 5, 6, 7] - >>> list(c_1) - [0, 1, 2, 3] - >>> list(c_3) - [8, 9, 10, 11] - - """ - source = iter(iterable) - - while True: - # Check to see whether we're at the end of the source iterable - item = next(source, _marker) - if item is _marker: - return - - # Clone the source and yield an n-length slice - source, it = tee(chain([item], source)) - yield islice(it, n) - - # Advance the source iterable - consume(source, n) - - -def distinct_combinations(iterable, r): - """Yield the distinct combinations of *r* items taken from *iterable*. - - >>> list(distinct_combinations([0, 0, 1], 2)) - [(0, 0), (0, 1)] - - Equivalent to ``set(combinations(iterable))``, except duplicates are not - generated and thrown away. For larger input sequences this is much more - efficient. - - """ - if r < 0: - raise ValueError('r must be non-negative') - elif r == 0: - yield () - return - pool = tuple(iterable) - generators = [unique_everseen(enumerate(pool), key=itemgetter(1))] - current_combo = [None] * r - level = 0 - while generators: - try: - cur_idx, p = next(generators[-1]) - except StopIteration: - generators.pop() - level -= 1 - continue - current_combo[level] = p - if level + 1 == r: - yield tuple(current_combo) - else: - generators.append( - unique_everseen( - enumerate(pool[cur_idx + 1 :], cur_idx + 1), - key=itemgetter(1), - ) - ) - level += 1 - - -def filter_except(validator, iterable, *exceptions): - """Yield the items from *iterable* for which the *validator* function does - not raise one of the specified *exceptions*. - - *validator* is called for each item in *iterable*. - It should be a function that accepts one argument and raises an exception - if that item is not valid. - - >>> iterable = ['1', '2', 'three', '4', None] - >>> list(filter_except(int, iterable, ValueError, TypeError)) - ['1', '2', '4'] - - If an exception other than one given by *exceptions* is raised by - *validator*, it is raised like normal. - """ - for item in iterable: - try: - validator(item) - except exceptions: - pass - else: - yield item - - -def map_except(function, iterable, *exceptions): - """Transform each item from *iterable* with *function* and yield the - result, unless *function* raises one of the specified *exceptions*. - - *function* is called to transform each item in *iterable*. - It should be a accept one argument. - - >>> iterable = ['1', '2', 'three', '4', None] - >>> list(map_except(int, iterable, ValueError, TypeError)) - [1, 2, 4] - - If an exception other than one given by *exceptions* is raised by - *function*, it is raised like normal. - """ - for item in iterable: - try: - yield function(item) - except exceptions: - pass - - -def _sample_unweighted(iterable, k): - # Implementation of "Algorithm L" from the 1994 paper by Kim-Hung Li: - # "Reservoir-Sampling Algorithms of Time Complexity O(n(1+log(N/n)))". - - # Fill up the reservoir (collection of samples) with the first `k` samples - reservoir = take(k, iterable) - - # Generate random number that's the largest in a sample of k U(0,1) numbers - # Largest order statistic: https://en.wikipedia.org/wiki/Order_statistic - W = exp(log(random()) / k) - - # The number of elements to skip before changing the reservoir is a random - # number with a geometric distribution. Sample it using random() and logs. - next_index = k + floor(log(random()) / log(1 - W)) - - for index, element in enumerate(iterable, k): - - if index == next_index: - reservoir[randrange(k)] = element - # The new W is the largest in a sample of k U(0, `old_W`) numbers - W *= exp(log(random()) / k) - next_index += floor(log(random()) / log(1 - W)) + 1 - - return reservoir - - -def _sample_weighted(iterable, k, weights): - # Implementation of "A-ExpJ" from the 2006 paper by Efraimidis et al. : - # "Weighted random sampling with a reservoir". - - # Log-transform for numerical stability for weights that are small/large - weight_keys = (log(random()) / weight for weight in weights) - - # Fill up the reservoir (collection of samples) with the first `k` - # weight-keys and elements, then heapify the list. - reservoir = take(k, zip(weight_keys, iterable)) - heapify(reservoir) - - # The number of jumps before changing the reservoir is a random variable - # with an exponential distribution. Sample it using random() and logs. - smallest_weight_key, _ = reservoir[0] - weights_to_skip = log(random()) / smallest_weight_key - - for weight, element in zip(weights, iterable): - if weight >= weights_to_skip: - # The notation here is consistent with the paper, but we store - # the weight-keys in log-space for better numerical stability. - smallest_weight_key, _ = reservoir[0] - t_w = exp(weight * smallest_weight_key) - r_2 = uniform(t_w, 1) # generate U(t_w, 1) - weight_key = log(r_2) / weight - heapreplace(reservoir, (weight_key, element)) - smallest_weight_key, _ = reservoir[0] - weights_to_skip = log(random()) / smallest_weight_key - else: - weights_to_skip -= weight - - # Equivalent to [element for weight_key, element in sorted(reservoir)] - return [heappop(reservoir)[1] for _ in range(k)] - - -def sample(iterable, k, weights=None): - """Return a *k*-length list of elements chosen (without replacement) - from the *iterable*. Like :func:`random.sample`, but works on iterables - of unknown length. - - >>> iterable = range(100) - >>> sample(iterable, 5) # doctest: +SKIP - [81, 60, 96, 16, 4] - - An iterable with *weights* may also be given: - - >>> iterable = range(100) - >>> weights = (i * i + 1 for i in range(100)) - >>> sampled = sample(iterable, 5, weights=weights) # doctest: +SKIP - [79, 67, 74, 66, 78] - - The algorithm can also be used to generate weighted random permutations. - The relative weight of each item determines the probability that it - appears late in the permutation. - - >>> data = "abcdefgh" - >>> weights = range(1, len(data) + 1) - >>> sample(data, k=len(data), weights=weights) # doctest: +SKIP - ['c', 'a', 'b', 'e', 'g', 'd', 'h', 'f'] - """ - if k == 0: - return [] - - iterable = iter(iterable) - if weights is None: - return _sample_unweighted(iterable, k) - else: - weights = iter(weights) - return _sample_weighted(iterable, k, weights) - - -def is_sorted(iterable, key=None, reverse=False): - """Returns ``True`` if the items of iterable are in sorted order, and - ``False`` otherwise. *key* and *reverse* have the same meaning that they do - in the built-in :func:`sorted` function. - - >>> is_sorted(['1', '2', '3', '4', '5'], key=int) - True - >>> is_sorted([5, 4, 3, 1, 2], reverse=True) - False - - The function returns ``False`` after encountering the first out-of-order - item. If there are no out-of-order items, the iterable is exhausted. - """ - - compare = lt if reverse else gt - it = iterable if (key is None) else map(key, iterable) - return not any(starmap(compare, pairwise(it))) - - -class AbortThread(BaseException): - pass - - -class callback_iter: - """Convert a function that uses callbacks to an iterator. - - Let *func* be a function that takes a `callback` keyword argument. - For example: - - >>> def func(callback=None): - ... for i, c in [(1, 'a'), (2, 'b'), (3, 'c')]: - ... if callback: - ... callback(i, c) - ... return 4 - - - Use ``with callback_iter(func)`` to get an iterator over the parameters - that are delivered to the callback. - - >>> with callback_iter(func) as it: - ... for args, kwargs in it: - ... print(args) - (1, 'a') - (2, 'b') - (3, 'c') - - The function will be called in a background thread. The ``done`` property - indicates whether it has completed execution. - - >>> it.done - True - - If it completes successfully, its return value will be available - in the ``result`` property. - - >>> it.result - 4 - - Notes: - - * If the function uses some keyword argument besides ``callback``, supply - *callback_kwd*. - * If it finished executing, but raised an exception, accessing the - ``result`` property will raise the same exception. - * If it hasn't finished executing, accessing the ``result`` - property from within the ``with`` block will raise ``RuntimeError``. - * If it hasn't finished executing, accessing the ``result`` property from - outside the ``with`` block will raise a - ``more_itertools.AbortThread`` exception. - * Provide *wait_seconds* to adjust how frequently the it is polled for - output. - - """ - - def __init__(self, func, callback_kwd='callback', wait_seconds=0.1): - self._func = func - self._callback_kwd = callback_kwd - self._aborted = False - self._future = None - self._wait_seconds = wait_seconds - self._executor = ThreadPoolExecutor(max_workers=1) - self._iterator = self._reader() - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_value, traceback): - self._aborted = True - self._executor.shutdown() - - def __iter__(self): - return self - - def __next__(self): - return next(self._iterator) - - @property - def done(self): - if self._future is None: - return False - return self._future.done() - - @property - def result(self): - if not self.done: - raise RuntimeError('Function has not yet completed') - - return self._future.result() - - def _reader(self): - q = Queue() - - def callback(*args, **kwargs): - if self._aborted: - raise AbortThread('canceled by user') - - q.put((args, kwargs)) - - self._future = self._executor.submit( - self._func, **{self._callback_kwd: callback} - ) - - while True: - try: - item = q.get(timeout=self._wait_seconds) - except Empty: - pass - else: - q.task_done() - yield item - - if self._future.done(): - break - - remaining = [] - while True: - try: - item = q.get_nowait() - except Empty: - break - else: - q.task_done() - remaining.append(item) - q.join() - yield from remaining - - -def windowed_complete(iterable, n): - """ - Yield ``(beginning, middle, end)`` tuples, where: - - * Each ``middle`` has *n* items from *iterable* - * Each ``beginning`` has the items before the ones in ``middle`` - * Each ``end`` has the items after the ones in ``middle`` - - >>> iterable = range(7) - >>> n = 3 - >>> for beginning, middle, end in windowed_complete(iterable, n): - ... print(beginning, middle, end) - () (0, 1, 2) (3, 4, 5, 6) - (0,) (1, 2, 3) (4, 5, 6) - (0, 1) (2, 3, 4) (5, 6) - (0, 1, 2) (3, 4, 5) (6,) - (0, 1, 2, 3) (4, 5, 6) () - - Note that *n* must be at least 0 and most equal to the length of - *iterable*. - - This function will exhaust the iterable and may require significant - storage. - """ - if n < 0: - raise ValueError('n must be >= 0') - - seq = tuple(iterable) - size = len(seq) - - if n > size: - raise ValueError('n must be <= len(seq)') - - for i in range(size - n + 1): - beginning = seq[:i] - middle = seq[i : i + n] - end = seq[i + n :] - yield beginning, middle, end - - -def all_unique(iterable, key=None): - """ - Returns ``True`` if all the elements of *iterable* are unique (no two - elements are equal). - - >>> all_unique('ABCB') - False - - If a *key* function is specified, it will be used to make comparisons. - - >>> all_unique('ABCb') - True - >>> all_unique('ABCb', str.lower) - False - - The function returns as soon as the first non-unique element is - encountered. Iterables with a mix of hashable and unhashable items can - be used, but the function will be slower for unhashable items. - """ - seenset = set() - seenset_add = seenset.add - seenlist = [] - seenlist_add = seenlist.append - for element in map(key, iterable) if key else iterable: - try: - if element in seenset: - return False - seenset_add(element) - except TypeError: - if element in seenlist: - return False - seenlist_add(element) - return True - - -def nth_product(index, *args): - """Equivalent to ``list(product(*args))[index]``. - - The products of *args* can be ordered lexicographically. - :func:`nth_product` computes the product at sort position *index* without - computing the previous products. - - >>> nth_product(8, range(2), range(2), range(2), range(2)) - (1, 0, 0, 0) - - ``IndexError`` will be raised if the given *index* is invalid. - """ - pools = list(map(tuple, reversed(args))) - ns = list(map(len, pools)) - - c = reduce(mul, ns) - - if index < 0: - index += c - - if not 0 <= index < c: - raise IndexError - - result = [] - for pool, n in zip(pools, ns): - result.append(pool[index % n]) - index //= n - - return tuple(reversed(result)) - - -def nth_permutation(iterable, r, index): - """Equivalent to ``list(permutations(iterable, r))[index]``` - - The subsequences of *iterable* that are of length *r* where order is - important can be ordered lexicographically. :func:`nth_permutation` - computes the subsequence at sort position *index* directly, without - computing the previous subsequences. - - >>> nth_permutation('ghijk', 2, 5) - ('h', 'i') - - ``ValueError`` will be raised If *r* is negative or greater than the length - of *iterable*. - ``IndexError`` will be raised if the given *index* is invalid. - """ - pool = list(iterable) - n = len(pool) - - if r is None or r == n: - r, c = n, factorial(n) - elif not 0 <= r < n: - raise ValueError - else: - c = factorial(n) // factorial(n - r) - - if index < 0: - index += c - - if not 0 <= index < c: - raise IndexError - - if c == 0: - return tuple() - - result = [0] * r - q = index * factorial(n) // c if r < n else index - for d in range(1, n + 1): - q, i = divmod(q, d) - if 0 <= n - d < r: - result[n - d] = i - if q == 0: - break - - return tuple(map(pool.pop, result)) - - -def value_chain(*args): - """Yield all arguments passed to the function in the same order in which - they were passed. If an argument itself is iterable then iterate over its - values. - - >>> list(value_chain(1, 2, 3, [4, 5, 6])) - [1, 2, 3, 4, 5, 6] - - Binary and text strings are not considered iterable and are emitted - as-is: - - >>> list(value_chain('12', '34', ['56', '78'])) - ['12', '34', '56', '78'] - - - Multiple levels of nesting are not flattened. - - """ - for value in args: - if isinstance(value, (str, bytes)): - yield value - continue - try: - yield from value - except TypeError: - yield value - - -def product_index(element, *args): - """Equivalent to ``list(product(*args)).index(element)`` - - The products of *args* can be ordered lexicographically. - :func:`product_index` computes the first index of *element* without - computing the previous products. - - >>> product_index([8, 2], range(10), range(5)) - 42 - - ``ValueError`` will be raised if the given *element* isn't in the product - of *args*. - """ - index = 0 - - for x, pool in zip_longest(element, args, fillvalue=_marker): - if x is _marker or pool is _marker: - raise ValueError('element is not a product of args') - - pool = tuple(pool) - index = index * len(pool) + pool.index(x) - - return index - - -def combination_index(element, iterable): - """Equivalent to ``list(combinations(iterable, r)).index(element)`` - - The subsequences of *iterable* that are of length *r* can be ordered - lexicographically. :func:`combination_index` computes the index of the - first *element*, without computing the previous combinations. - - >>> combination_index('adf', 'abcdefg') - 10 - - ``ValueError`` will be raised if the given *element* isn't one of the - combinations of *iterable*. - """ - element = enumerate(element) - k, y = next(element, (None, None)) - if k is None: - return 0 - - indexes = [] - pool = enumerate(iterable) - for n, x in pool: - if x == y: - indexes.append(n) - tmp, y = next(element, (None, None)) - if tmp is None: - break - else: - k = tmp - else: - raise ValueError('element is not a combination of iterable') - - n, _ = last(pool, default=(n, None)) - - # Python versiosn below 3.8 don't have math.comb - index = 1 - for i, j in enumerate(reversed(indexes), start=1): - j = n - j - if i <= j: - index += factorial(j) // (factorial(i) * factorial(j - i)) - - return factorial(n + 1) // (factorial(k + 1) * factorial(n - k)) - index - - -def permutation_index(element, iterable): - """Equivalent to ``list(permutations(iterable, r)).index(element)``` - - The subsequences of *iterable* that are of length *r* where order is - important can be ordered lexicographically. :func:`permutation_index` - computes the index of the first *element* directly, without computing - the previous permutations. - - >>> permutation_index([1, 3, 2], range(5)) - 19 - - ``ValueError`` will be raised if the given *element* isn't one of the - permutations of *iterable*. - """ - index = 0 - pool = list(iterable) - for i, x in zip(range(len(pool), -1, -1), element): - r = pool.index(x) - index = index * i + r - del pool[r] - - return index - - -class countable: - """Wrap *iterable* and keep a count of how many items have been consumed. - - The ``items_seen`` attribute starts at ``0`` and increments as the iterable - is consumed: - - >>> iterable = map(str, range(10)) - >>> it = countable(iterable) - >>> it.items_seen - 0 - >>> next(it), next(it) - ('0', '1') - >>> list(it) - ['2', '3', '4', '5', '6', '7', '8', '9'] - >>> it.items_seen - 10 - """ - - def __init__(self, iterable): - self._it = iter(iterable) - self.items_seen = 0 - - def __iter__(self): - return self - - def __next__(self): - item = next(self._it) - self.items_seen += 1 - - return item diff --git a/tools/vendored.py b/tools/vendored.py index 57e28d53..fc73933d 100644 --- a/tools/vendored.py +++ b/tools/vendored.py @@ -64,6 +64,16 @@ def rewrite_importlib_resources(pkg_files, new_root): file.write_text(text) +def rewrite_more_itertools(pkg_files: Path): + """ + Rewrite more_itertools to remove unused more_itertools.more + """ + (pkg_files / "more.py").remove() + init_file = pkg_files / "__init__.py" + init_text = "".join(ln for ln in init_file.lines() if "from .more " not in ln) + init_file.write_text(init_text) + + def clean(vendor): """ Remove all files out of the vendor directory except the meta @@ -96,6 +106,7 @@ def update_pkg_resources(): rewrite_jaraco_text(vendor / 'jaraco/text', 'pkg_resources.extern') rewrite_jaraco(vendor / 'jaraco', 'pkg_resources.extern') rewrite_importlib_resources(vendor / 'importlib_resources', 'pkg_resources.extern') + rewrite_more_itertools(vendor / "more_itertools") def update_setuptools(): @@ -105,6 +116,7 @@ def update_setuptools(): rewrite_jaraco_text(vendor / 'jaraco/text', 'setuptools.extern') rewrite_jaraco(vendor / 'jaraco', 'setuptools.extern') rewrite_importlib_resources(vendor / 'importlib_resources', 'setuptools.extern') + rewrite_more_itertools(vendor / "more_itertools") __name__ == '__main__' and update_vendored() -- cgit v1.2.1 From aec4367d5146b318c9fb02e128d6e5bfe84fa2f9 Mon Sep 17 00:00:00 2001 From: Maciej Pasternacki Date: Tue, 8 Feb 2022 20:48:39 +0100 Subject: Clean also .pyi of more_itertools --- .coveragerc | 7 - pkg_resources/_vendor/more_itertools/__init__.pyi | 1 - pkg_resources/_vendor/more_itertools/more.pyi | 664 ---------------------- setuptools/_vendor/more_itertools/__init__.pyi | 1 - setuptools/_vendor/more_itertools/more.pyi | 480 ---------------- tools/vendored.py | 9 +- 6 files changed, 5 insertions(+), 1157 deletions(-) delete mode 100644 .coveragerc delete mode 100644 pkg_resources/_vendor/more_itertools/more.pyi delete mode 100644 setuptools/_vendor/more_itertools/more.pyi diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 6a34e662..00000000 --- a/.coveragerc +++ /dev/null @@ -1,7 +0,0 @@ -[run] -omit = - # leading `*/` for pytest-dev/pytest-cov#456 - */.tox/* - -[report] -show_missing = True diff --git a/pkg_resources/_vendor/more_itertools/__init__.pyi b/pkg_resources/_vendor/more_itertools/__init__.pyi index 96f6e36c..f0fe8b5d 100644 --- a/pkg_resources/_vendor/more_itertools/__init__.pyi +++ b/pkg_resources/_vendor/more_itertools/__init__.pyi @@ -1,2 +1 @@ -from .more import * from .recipes import * diff --git a/pkg_resources/_vendor/more_itertools/more.pyi b/pkg_resources/_vendor/more_itertools/more.pyi deleted file mode 100644 index fe7d4bdd..00000000 --- a/pkg_resources/_vendor/more_itertools/more.pyi +++ /dev/null @@ -1,664 +0,0 @@ -"""Stubs for more_itertools.more""" - -from typing import ( - Any, - Callable, - Container, - Dict, - Generic, - Hashable, - Iterable, - Iterator, - List, - Optional, - Reversible, - Sequence, - Sized, - Tuple, - Union, - TypeVar, - type_check_only, -) -from types import TracebackType -from typing_extensions import ContextManager, Protocol, Type, overload - -# Type and type variable definitions -_T = TypeVar('_T') -_T1 = TypeVar('_T1') -_T2 = TypeVar('_T2') -_U = TypeVar('_U') -_V = TypeVar('_V') -_W = TypeVar('_W') -_T_co = TypeVar('_T_co', covariant=True) -_GenFn = TypeVar('_GenFn', bound=Callable[..., Iterator[object]]) -_Raisable = Union[BaseException, 'Type[BaseException]'] - -@type_check_only -class _SizedIterable(Protocol[_T_co], Sized, Iterable[_T_co]): ... - -@type_check_only -class _SizedReversible(Protocol[_T_co], Sized, Reversible[_T_co]): ... - -def chunked( - iterable: Iterable[_T], n: Optional[int], strict: bool = ... -) -> Iterator[List[_T]]: ... -@overload -def first(iterable: Iterable[_T]) -> _T: ... -@overload -def first(iterable: Iterable[_T], default: _U) -> Union[_T, _U]: ... -@overload -def last(iterable: Iterable[_T]) -> _T: ... -@overload -def last(iterable: Iterable[_T], default: _U) -> Union[_T, _U]: ... -@overload -def nth_or_last(iterable: Iterable[_T], n: int) -> _T: ... -@overload -def nth_or_last( - iterable: Iterable[_T], n: int, default: _U -) -> Union[_T, _U]: ... - -class peekable(Generic[_T], Iterator[_T]): - def __init__(self, iterable: Iterable[_T]) -> None: ... - def __iter__(self) -> peekable[_T]: ... - def __bool__(self) -> bool: ... - @overload - def peek(self) -> _T: ... - @overload - def peek(self, default: _U) -> Union[_T, _U]: ... - def prepend(self, *items: _T) -> None: ... - def __next__(self) -> _T: ... - @overload - def __getitem__(self, index: int) -> _T: ... - @overload - def __getitem__(self, index: slice) -> List[_T]: ... - -def collate(*iterables: Iterable[_T], **kwargs: Any) -> Iterable[_T]: ... -def consumer(func: _GenFn) -> _GenFn: ... -def ilen(iterable: Iterable[object]) -> int: ... -def iterate(func: Callable[[_T], _T], start: _T) -> Iterator[_T]: ... -def with_iter( - context_manager: ContextManager[Iterable[_T]], -) -> Iterator[_T]: ... -def one( - iterable: Iterable[_T], - too_short: Optional[_Raisable] = ..., - too_long: Optional[_Raisable] = ..., -) -> _T: ... -def raise_(exception: _Raisable, *args: Any) -> None: ... -def strictly_n( - iterable: Iterable[_T], - n: int, - too_short: Optional[_GenFn] = ..., - too_long: Optional[_GenFn] = ..., -) -> List[_T]: ... -def distinct_permutations( - iterable: Iterable[_T], r: Optional[int] = ... -) -> Iterator[Tuple[_T, ...]]: ... -def intersperse( - e: _U, iterable: Iterable[_T], n: int = ... -) -> Iterator[Union[_T, _U]]: ... -def unique_to_each(*iterables: Iterable[_T]) -> List[List[_T]]: ... -@overload -def windowed( - seq: Iterable[_T], n: int, *, step: int = ... -) -> Iterator[Tuple[Optional[_T], ...]]: ... -@overload -def windowed( - seq: Iterable[_T], n: int, fillvalue: _U, step: int = ... -) -> Iterator[Tuple[Union[_T, _U], ...]]: ... -def substrings(iterable: Iterable[_T]) -> Iterator[Tuple[_T, ...]]: ... -def substrings_indexes( - seq: Sequence[_T], reverse: bool = ... -) -> Iterator[Tuple[Sequence[_T], int, int]]: ... - -class bucket(Generic[_T, _U], Container[_U]): - def __init__( - self, - iterable: Iterable[_T], - key: Callable[[_T], _U], - validator: Optional[Callable[[object], object]] = ..., - ) -> None: ... - def __contains__(self, value: object) -> bool: ... - def __iter__(self) -> Iterator[_U]: ... - def __getitem__(self, value: object) -> Iterator[_T]: ... - -def spy( - iterable: Iterable[_T], n: int = ... -) -> Tuple[List[_T], Iterator[_T]]: ... -def interleave(*iterables: Iterable[_T]) -> Iterator[_T]: ... -def interleave_longest(*iterables: Iterable[_T]) -> Iterator[_T]: ... -def interleave_evenly( - iterables: List[Iterable[_T]], lengths: Optional[List[int]] = ... -) -> Iterator[_T]: ... -def collapse( - iterable: Iterable[Any], - base_type: Optional[type] = ..., - levels: Optional[int] = ..., -) -> Iterator[Any]: ... -@overload -def side_effect( - func: Callable[[_T], object], - iterable: Iterable[_T], - chunk_size: None = ..., - before: Optional[Callable[[], object]] = ..., - after: Optional[Callable[[], object]] = ..., -) -> Iterator[_T]: ... -@overload -def side_effect( - func: Callable[[List[_T]], object], - iterable: Iterable[_T], - chunk_size: int, - before: Optional[Callable[[], object]] = ..., - after: Optional[Callable[[], object]] = ..., -) -> Iterator[_T]: ... -def sliced( - seq: Sequence[_T], n: int, strict: bool = ... -) -> Iterator[Sequence[_T]]: ... -def split_at( - iterable: Iterable[_T], - pred: Callable[[_T], object], - maxsplit: int = ..., - keep_separator: bool = ..., -) -> Iterator[List[_T]]: ... -def split_before( - iterable: Iterable[_T], pred: Callable[[_T], object], maxsplit: int = ... -) -> Iterator[List[_T]]: ... -def split_after( - iterable: Iterable[_T], pred: Callable[[_T], object], maxsplit: int = ... -) -> Iterator[List[_T]]: ... -def split_when( - iterable: Iterable[_T], - pred: Callable[[_T, _T], object], - maxsplit: int = ..., -) -> Iterator[List[_T]]: ... -def split_into( - iterable: Iterable[_T], sizes: Iterable[Optional[int]] -) -> Iterator[List[_T]]: ... -@overload -def padded( - iterable: Iterable[_T], - *, - n: Optional[int] = ..., - next_multiple: bool = ... -) -> Iterator[Optional[_T]]: ... -@overload -def padded( - iterable: Iterable[_T], - fillvalue: _U, - n: Optional[int] = ..., - next_multiple: bool = ..., -) -> Iterator[Union[_T, _U]]: ... -@overload -def repeat_last(iterable: Iterable[_T]) -> Iterator[_T]: ... -@overload -def repeat_last( - iterable: Iterable[_T], default: _U -) -> Iterator[Union[_T, _U]]: ... -def distribute(n: int, iterable: Iterable[_T]) -> List[Iterator[_T]]: ... -@overload -def stagger( - iterable: Iterable[_T], - offsets: _SizedIterable[int] = ..., - longest: bool = ..., -) -> Iterator[Tuple[Optional[_T], ...]]: ... -@overload -def stagger( - iterable: Iterable[_T], - offsets: _SizedIterable[int] = ..., - longest: bool = ..., - fillvalue: _U = ..., -) -> Iterator[Tuple[Union[_T, _U], ...]]: ... - -class UnequalIterablesError(ValueError): - def __init__( - self, details: Optional[Tuple[int, int, int]] = ... - ) -> None: ... - -@overload -def zip_equal(__iter1: Iterable[_T1]) -> Iterator[Tuple[_T1]]: ... -@overload -def zip_equal( - __iter1: Iterable[_T1], __iter2: Iterable[_T2] -) -> Iterator[Tuple[_T1, _T2]]: ... -@overload -def zip_equal( - __iter1: Iterable[_T], - __iter2: Iterable[_T], - __iter3: Iterable[_T], - *iterables: Iterable[_T] -) -> Iterator[Tuple[_T, ...]]: ... -@overload -def zip_offset( - __iter1: Iterable[_T1], - *, - offsets: _SizedIterable[int], - longest: bool = ..., - fillvalue: None = None -) -> Iterator[Tuple[Optional[_T1]]]: ... -@overload -def zip_offset( - __iter1: Iterable[_T1], - __iter2: Iterable[_T2], - *, - offsets: _SizedIterable[int], - longest: bool = ..., - fillvalue: None = None -) -> Iterator[Tuple[Optional[_T1], Optional[_T2]]]: ... -@overload -def zip_offset( - __iter1: Iterable[_T], - __iter2: Iterable[_T], - __iter3: Iterable[_T], - *iterables: Iterable[_T], - offsets: _SizedIterable[int], - longest: bool = ..., - fillvalue: None = None -) -> Iterator[Tuple[Optional[_T], ...]]: ... -@overload -def zip_offset( - __iter1: Iterable[_T1], - *, - offsets: _SizedIterable[int], - longest: bool = ..., - fillvalue: _U, -) -> Iterator[Tuple[Union[_T1, _U]]]: ... -@overload -def zip_offset( - __iter1: Iterable[_T1], - __iter2: Iterable[_T2], - *, - offsets: _SizedIterable[int], - longest: bool = ..., - fillvalue: _U, -) -> Iterator[Tuple[Union[_T1, _U], Union[_T2, _U]]]: ... -@overload -def zip_offset( - __iter1: Iterable[_T], - __iter2: Iterable[_T], - __iter3: Iterable[_T], - *iterables: Iterable[_T], - offsets: _SizedIterable[int], - longest: bool = ..., - fillvalue: _U, -) -> Iterator[Tuple[Union[_T, _U], ...]]: ... -def sort_together( - iterables: Iterable[Iterable[_T]], - key_list: Iterable[int] = ..., - key: Optional[Callable[..., Any]] = ..., - reverse: bool = ..., -) -> List[Tuple[_T, ...]]: ... -def unzip(iterable: Iterable[Sequence[_T]]) -> Tuple[Iterator[_T], ...]: ... -def divide(n: int, iterable: Iterable[_T]) -> List[Iterator[_T]]: ... -def always_iterable( - obj: object, - base_type: Union[ - type, Tuple[Union[type, Tuple[Any, ...]], ...], None - ] = ..., -) -> Iterator[Any]: ... -def adjacent( - predicate: Callable[[_T], bool], - iterable: Iterable[_T], - distance: int = ..., -) -> Iterator[Tuple[bool, _T]]: ... -@overload -def groupby_transform( - iterable: Iterable[_T], - keyfunc: None = None, - valuefunc: None = None, - reducefunc: None = None, -) -> Iterator[Tuple[_T, Iterator[_T]]]: ... -@overload -def groupby_transform( - iterable: Iterable[_T], - keyfunc: Callable[[_T], _U], - valuefunc: None, - reducefunc: None, -) -> Iterator[Tuple[_U, Iterator[_T]]]: ... -@overload -def groupby_transform( - iterable: Iterable[_T], - keyfunc: None, - valuefunc: Callable[[_T], _V], - reducefunc: None, -) -> Iterable[Tuple[_T, Iterable[_V]]]: ... -@overload -def groupby_transform( - iterable: Iterable[_T], - keyfunc: Callable[[_T], _U], - valuefunc: Callable[[_T], _V], - reducefunc: None, -) -> Iterable[Tuple[_U, Iterator[_V]]]: ... -@overload -def groupby_transform( - iterable: Iterable[_T], - keyfunc: None, - valuefunc: None, - reducefunc: Callable[[Iterator[_T]], _W], -) -> Iterable[Tuple[_T, _W]]: ... -@overload -def groupby_transform( - iterable: Iterable[_T], - keyfunc: Callable[[_T], _U], - valuefunc: None, - reducefunc: Callable[[Iterator[_T]], _W], -) -> Iterable[Tuple[_U, _W]]: ... -@overload -def groupby_transform( - iterable: Iterable[_T], - keyfunc: None, - valuefunc: Callable[[_T], _V], - reducefunc: Callable[[Iterable[_V]], _W], -) -> Iterable[Tuple[_T, _W]]: ... -@overload -def groupby_transform( - iterable: Iterable[_T], - keyfunc: Callable[[_T], _U], - valuefunc: Callable[[_T], _V], - reducefunc: Callable[[Iterable[_V]], _W], -) -> Iterable[Tuple[_U, _W]]: ... - -class numeric_range(Generic[_T, _U], Sequence[_T], Hashable, Reversible[_T]): - @overload - def __init__(self, __stop: _T) -> None: ... - @overload - def __init__(self, __start: _T, __stop: _T) -> None: ... - @overload - def __init__(self, __start: _T, __stop: _T, __step: _U) -> None: ... - def __bool__(self) -> bool: ... - def __contains__(self, elem: object) -> bool: ... - def __eq__(self, other: object) -> bool: ... - @overload - def __getitem__(self, key: int) -> _T: ... - @overload - def __getitem__(self, key: slice) -> numeric_range[_T, _U]: ... - def __hash__(self) -> int: ... - def __iter__(self) -> Iterator[_T]: ... - def __len__(self) -> int: ... - def __reduce__( - self, - ) -> Tuple[Type[numeric_range[_T, _U]], Tuple[_T, _T, _U]]: ... - def __repr__(self) -> str: ... - def __reversed__(self) -> Iterator[_T]: ... - def count(self, value: _T) -> int: ... - def index(self, value: _T) -> int: ... # type: ignore - -def count_cycle( - iterable: Iterable[_T], n: Optional[int] = ... -) -> Iterable[Tuple[int, _T]]: ... -def mark_ends( - iterable: Iterable[_T], -) -> Iterable[Tuple[bool, bool, _T]]: ... -def locate( - iterable: Iterable[object], - pred: Callable[..., Any] = ..., - window_size: Optional[int] = ..., -) -> Iterator[int]: ... -def lstrip( - iterable: Iterable[_T], pred: Callable[[_T], object] -) -> Iterator[_T]: ... -def rstrip( - iterable: Iterable[_T], pred: Callable[[_T], object] -) -> Iterator[_T]: ... -def strip( - iterable: Iterable[_T], pred: Callable[[_T], object] -) -> Iterator[_T]: ... - -class islice_extended(Generic[_T], Iterator[_T]): - def __init__( - self, iterable: Iterable[_T], *args: Optional[int] - ) -> None: ... - def __iter__(self) -> islice_extended[_T]: ... - def __next__(self) -> _T: ... - def __getitem__(self, index: slice) -> islice_extended[_T]: ... - -def always_reversible(iterable: Iterable[_T]) -> Iterator[_T]: ... -def consecutive_groups( - iterable: Iterable[_T], ordering: Callable[[_T], int] = ... -) -> Iterator[Iterator[_T]]: ... -@overload -def difference( - iterable: Iterable[_T], - func: Callable[[_T, _T], _U] = ..., - *, - initial: None = ... -) -> Iterator[Union[_T, _U]]: ... -@overload -def difference( - iterable: Iterable[_T], func: Callable[[_T, _T], _U] = ..., *, initial: _U -) -> Iterator[_U]: ... - -class SequenceView(Generic[_T], Sequence[_T]): - def __init__(self, target: Sequence[_T]) -> None: ... - @overload - def __getitem__(self, index: int) -> _T: ... - @overload - def __getitem__(self, index: slice) -> Sequence[_T]: ... - def __len__(self) -> int: ... - -class seekable(Generic[_T], Iterator[_T]): - def __init__( - self, iterable: Iterable[_T], maxlen: Optional[int] = ... - ) -> None: ... - def __iter__(self) -> seekable[_T]: ... - def __next__(self) -> _T: ... - def __bool__(self) -> bool: ... - @overload - def peek(self) -> _T: ... - @overload - def peek(self, default: _U) -> Union[_T, _U]: ... - def elements(self) -> SequenceView[_T]: ... - def seek(self, index: int) -> None: ... - -class run_length: - @staticmethod - def encode(iterable: Iterable[_T]) -> Iterator[Tuple[_T, int]]: ... - @staticmethod - def decode(iterable: Iterable[Tuple[_T, int]]) -> Iterator[_T]: ... - -def exactly_n( - iterable: Iterable[_T], n: int, predicate: Callable[[_T], object] = ... -) -> bool: ... -def circular_shifts(iterable: Iterable[_T]) -> List[Tuple[_T, ...]]: ... -def make_decorator( - wrapping_func: Callable[..., _U], result_index: int = ... -) -> Callable[..., Callable[[Callable[..., Any]], Callable[..., _U]]]: ... -@overload -def map_reduce( - iterable: Iterable[_T], - keyfunc: Callable[[_T], _U], - valuefunc: None = ..., - reducefunc: None = ..., -) -> Dict[_U, List[_T]]: ... -@overload -def map_reduce( - iterable: Iterable[_T], - keyfunc: Callable[[_T], _U], - valuefunc: Callable[[_T], _V], - reducefunc: None = ..., -) -> Dict[_U, List[_V]]: ... -@overload -def map_reduce( - iterable: Iterable[_T], - keyfunc: Callable[[_T], _U], - valuefunc: None = ..., - reducefunc: Callable[[List[_T]], _W] = ..., -) -> Dict[_U, _W]: ... -@overload -def map_reduce( - iterable: Iterable[_T], - keyfunc: Callable[[_T], _U], - valuefunc: Callable[[_T], _V], - reducefunc: Callable[[List[_V]], _W], -) -> Dict[_U, _W]: ... -def rlocate( - iterable: Iterable[_T], - pred: Callable[..., object] = ..., - window_size: Optional[int] = ..., -) -> Iterator[int]: ... -def replace( - iterable: Iterable[_T], - pred: Callable[..., object], - substitutes: Iterable[_U], - count: Optional[int] = ..., - window_size: int = ..., -) -> Iterator[Union[_T, _U]]: ... -def partitions(iterable: Iterable[_T]) -> Iterator[List[List[_T]]]: ... -def set_partitions( - iterable: Iterable[_T], k: Optional[int] = ... -) -> Iterator[List[List[_T]]]: ... - -class time_limited(Generic[_T], Iterator[_T]): - def __init__( - self, limit_seconds: float, iterable: Iterable[_T] - ) -> None: ... - def __iter__(self) -> islice_extended[_T]: ... - def __next__(self) -> _T: ... - -@overload -def only( - iterable: Iterable[_T], *, too_long: Optional[_Raisable] = ... -) -> Optional[_T]: ... -@overload -def only( - iterable: Iterable[_T], default: _U, too_long: Optional[_Raisable] = ... -) -> Union[_T, _U]: ... -def ichunked(iterable: Iterable[_T], n: int) -> Iterator[Iterator[_T]]: ... -def distinct_combinations( - iterable: Iterable[_T], r: int -) -> Iterator[Tuple[_T, ...]]: ... -def filter_except( - validator: Callable[[Any], object], - iterable: Iterable[_T], - *exceptions: Type[BaseException] -) -> Iterator[_T]: ... -def map_except( - function: Callable[[Any], _U], - iterable: Iterable[_T], - *exceptions: Type[BaseException] -) -> Iterator[_U]: ... -def map_if( - iterable: Iterable[Any], - pred: Callable[[Any], bool], - func: Callable[[Any], Any], - func_else: Optional[Callable[[Any], Any]] = ..., -) -> Iterator[Any]: ... -def sample( - iterable: Iterable[_T], - k: int, - weights: Optional[Iterable[float]] = ..., -) -> List[_T]: ... -def is_sorted( - iterable: Iterable[_T], - key: Optional[Callable[[_T], _U]] = ..., - reverse: bool = False, - strict: bool = False, -) -> bool: ... - -class AbortThread(BaseException): - pass - -class callback_iter(Generic[_T], Iterator[_T]): - def __init__( - self, - func: Callable[..., Any], - callback_kwd: str = ..., - wait_seconds: float = ..., - ) -> None: ... - def __enter__(self) -> callback_iter[_T]: ... - def __exit__( - self, - exc_type: Optional[Type[BaseException]], - exc_value: Optional[BaseException], - traceback: Optional[TracebackType], - ) -> Optional[bool]: ... - def __iter__(self) -> callback_iter[_T]: ... - def __next__(self) -> _T: ... - def _reader(self) -> Iterator[_T]: ... - @property - def done(self) -> bool: ... - @property - def result(self) -> Any: ... - -def windowed_complete( - iterable: Iterable[_T], n: int -) -> Iterator[Tuple[_T, ...]]: ... -def all_unique( - iterable: Iterable[_T], key: Optional[Callable[[_T], _U]] = ... -) -> bool: ... -def nth_product(index: int, *args: Iterable[_T]) -> Tuple[_T, ...]: ... -def nth_permutation( - iterable: Iterable[_T], r: int, index: int -) -> Tuple[_T, ...]: ... -def value_chain(*args: Union[_T, Iterable[_T]]) -> Iterable[_T]: ... -def product_index(element: Iterable[_T], *args: Iterable[_T]) -> int: ... -def combination_index( - element: Iterable[_T], iterable: Iterable[_T] -) -> int: ... -def permutation_index( - element: Iterable[_T], iterable: Iterable[_T] -) -> int: ... -def repeat_each(iterable: Iterable[_T], n: int = ...) -> Iterator[_T]: ... - -class countable(Generic[_T], Iterator[_T]): - def __init__(self, iterable: Iterable[_T]) -> None: ... - def __iter__(self) -> countable[_T]: ... - def __next__(self) -> _T: ... - -def chunked_even(iterable: Iterable[_T], n: int) -> Iterator[List[_T]]: ... -def zip_broadcast( - *objects: Union[_T, Iterable[_T]], - scalar_types: Union[ - type, Tuple[Union[type, Tuple[Any, ...]], ...], None - ] = ..., - strict: bool = ... -) -> Iterable[Tuple[_T, ...]]: ... -def unique_in_window( - iterable: Iterable[_T], n: int, key: Optional[Callable[[_T], _U]] = ... -) -> Iterator[_T]: ... -def duplicates_everseen( - iterable: Iterable[_T], key: Optional[Callable[[_T], _U]] = ... -) -> Iterator[_T]: ... -def duplicates_justseen( - iterable: Iterable[_T], key: Optional[Callable[[_T], _U]] = ... -) -> Iterator[_T]: ... - -class _SupportsLessThan(Protocol): - def __lt__(self, __other: Any) -> bool: ... - -_SupportsLessThanT = TypeVar("_SupportsLessThanT", bound=_SupportsLessThan) - -@overload -def minmax( - iterable_or_value: Iterable[_SupportsLessThanT], *, key: None = None -) -> Tuple[_SupportsLessThanT, _SupportsLessThanT]: ... -@overload -def minmax( - iterable_or_value: Iterable[_T], *, key: Callable[[_T], _SupportsLessThan] -) -> Tuple[_T, _T]: ... -@overload -def minmax( - iterable_or_value: Iterable[_SupportsLessThanT], - *, - key: None = None, - default: _U -) -> Union[_U, Tuple[_SupportsLessThanT, _SupportsLessThanT]]: ... -@overload -def minmax( - iterable_or_value: Iterable[_T], - *, - key: Callable[[_T], _SupportsLessThan], - default: _U, -) -> Union[_U, Tuple[_T, _T]]: ... -@overload -def minmax( - iterable_or_value: _SupportsLessThanT, - __other: _SupportsLessThanT, - *others: _SupportsLessThanT -) -> Tuple[_SupportsLessThanT, _SupportsLessThanT]: ... -@overload -def minmax( - iterable_or_value: _T, - __other: _T, - *others: _T, - key: Callable[[_T], _SupportsLessThan] -) -> Tuple[_T, _T]: ... diff --git a/setuptools/_vendor/more_itertools/__init__.pyi b/setuptools/_vendor/more_itertools/__init__.pyi index 96f6e36c..f0fe8b5d 100644 --- a/setuptools/_vendor/more_itertools/__init__.pyi +++ b/setuptools/_vendor/more_itertools/__init__.pyi @@ -1,2 +1 @@ -from .more import * from .recipes import * diff --git a/setuptools/_vendor/more_itertools/more.pyi b/setuptools/_vendor/more_itertools/more.pyi deleted file mode 100644 index 2fba9cb3..00000000 --- a/setuptools/_vendor/more_itertools/more.pyi +++ /dev/null @@ -1,480 +0,0 @@ -"""Stubs for more_itertools.more""" - -from typing import ( - Any, - Callable, - Container, - Dict, - Generic, - Hashable, - Iterable, - Iterator, - List, - Optional, - Reversible, - Sequence, - Sized, - Tuple, - Union, - TypeVar, - type_check_only, -) -from types import TracebackType -from typing_extensions import ContextManager, Protocol, Type, overload - -# Type and type variable definitions -_T = TypeVar('_T') -_U = TypeVar('_U') -_V = TypeVar('_V') -_W = TypeVar('_W') -_T_co = TypeVar('_T_co', covariant=True) -_GenFn = TypeVar('_GenFn', bound=Callable[..., Iterator[object]]) -_Raisable = Union[BaseException, 'Type[BaseException]'] - -@type_check_only -class _SizedIterable(Protocol[_T_co], Sized, Iterable[_T_co]): ... - -@type_check_only -class _SizedReversible(Protocol[_T_co], Sized, Reversible[_T_co]): ... - -def chunked( - iterable: Iterable[_T], n: int, strict: bool = ... -) -> Iterator[List[_T]]: ... -@overload -def first(iterable: Iterable[_T]) -> _T: ... -@overload -def first(iterable: Iterable[_T], default: _U) -> Union[_T, _U]: ... -@overload -def last(iterable: Iterable[_T]) -> _T: ... -@overload -def last(iterable: Iterable[_T], default: _U) -> Union[_T, _U]: ... -@overload -def nth_or_last(iterable: Iterable[_T], n: int) -> _T: ... -@overload -def nth_or_last( - iterable: Iterable[_T], n: int, default: _U -) -> Union[_T, _U]: ... - -class peekable(Generic[_T], Iterator[_T]): - def __init__(self, iterable: Iterable[_T]) -> None: ... - def __iter__(self) -> peekable[_T]: ... - def __bool__(self) -> bool: ... - @overload - def peek(self) -> _T: ... - @overload - def peek(self, default: _U) -> Union[_T, _U]: ... - def prepend(self, *items: _T) -> None: ... - def __next__(self) -> _T: ... - @overload - def __getitem__(self, index: int) -> _T: ... - @overload - def __getitem__(self, index: slice) -> List[_T]: ... - -def collate(*iterables: Iterable[_T], **kwargs: Any) -> Iterable[_T]: ... -def consumer(func: _GenFn) -> _GenFn: ... -def ilen(iterable: Iterable[object]) -> int: ... -def iterate(func: Callable[[_T], _T], start: _T) -> Iterator[_T]: ... -def with_iter( - context_manager: ContextManager[Iterable[_T]], -) -> Iterator[_T]: ... -def one( - iterable: Iterable[_T], - too_short: Optional[_Raisable] = ..., - too_long: Optional[_Raisable] = ..., -) -> _T: ... -def distinct_permutations( - iterable: Iterable[_T], r: Optional[int] = ... -) -> Iterator[Tuple[_T, ...]]: ... -def intersperse( - e: _U, iterable: Iterable[_T], n: int = ... -) -> Iterator[Union[_T, _U]]: ... -def unique_to_each(*iterables: Iterable[_T]) -> List[List[_T]]: ... -@overload -def windowed( - seq: Iterable[_T], n: int, *, step: int = ... -) -> Iterator[Tuple[Optional[_T], ...]]: ... -@overload -def windowed( - seq: Iterable[_T], n: int, fillvalue: _U, step: int = ... -) -> Iterator[Tuple[Union[_T, _U], ...]]: ... -def substrings(iterable: Iterable[_T]) -> Iterator[Tuple[_T, ...]]: ... -def substrings_indexes( - seq: Sequence[_T], reverse: bool = ... -) -> Iterator[Tuple[Sequence[_T], int, int]]: ... - -class bucket(Generic[_T, _U], Container[_U]): - def __init__( - self, - iterable: Iterable[_T], - key: Callable[[_T], _U], - validator: Optional[Callable[[object], object]] = ..., - ) -> None: ... - def __contains__(self, value: object) -> bool: ... - def __iter__(self) -> Iterator[_U]: ... - def __getitem__(self, value: object) -> Iterator[_T]: ... - -def spy( - iterable: Iterable[_T], n: int = ... -) -> Tuple[List[_T], Iterator[_T]]: ... -def interleave(*iterables: Iterable[_T]) -> Iterator[_T]: ... -def interleave_longest(*iterables: Iterable[_T]) -> Iterator[_T]: ... -def collapse( - iterable: Iterable[Any], - base_type: Optional[type] = ..., - levels: Optional[int] = ..., -) -> Iterator[Any]: ... -@overload -def side_effect( - func: Callable[[_T], object], - iterable: Iterable[_T], - chunk_size: None = ..., - before: Optional[Callable[[], object]] = ..., - after: Optional[Callable[[], object]] = ..., -) -> Iterator[_T]: ... -@overload -def side_effect( - func: Callable[[List[_T]], object], - iterable: Iterable[_T], - chunk_size: int, - before: Optional[Callable[[], object]] = ..., - after: Optional[Callable[[], object]] = ..., -) -> Iterator[_T]: ... -def sliced( - seq: Sequence[_T], n: int, strict: bool = ... -) -> Iterator[Sequence[_T]]: ... -def split_at( - iterable: Iterable[_T], - pred: Callable[[_T], object], - maxsplit: int = ..., - keep_separator: bool = ..., -) -> Iterator[List[_T]]: ... -def split_before( - iterable: Iterable[_T], pred: Callable[[_T], object], maxsplit: int = ... -) -> Iterator[List[_T]]: ... -def split_after( - iterable: Iterable[_T], pred: Callable[[_T], object], maxsplit: int = ... -) -> Iterator[List[_T]]: ... -def split_when( - iterable: Iterable[_T], - pred: Callable[[_T, _T], object], - maxsplit: int = ..., -) -> Iterator[List[_T]]: ... -def split_into( - iterable: Iterable[_T], sizes: Iterable[Optional[int]] -) -> Iterator[List[_T]]: ... -@overload -def padded( - iterable: Iterable[_T], - *, - n: Optional[int] = ..., - next_multiple: bool = ... -) -> Iterator[Optional[_T]]: ... -@overload -def padded( - iterable: Iterable[_T], - fillvalue: _U, - n: Optional[int] = ..., - next_multiple: bool = ..., -) -> Iterator[Union[_T, _U]]: ... -@overload -def repeat_last(iterable: Iterable[_T]) -> Iterator[_T]: ... -@overload -def repeat_last( - iterable: Iterable[_T], default: _U -) -> Iterator[Union[_T, _U]]: ... -def distribute(n: int, iterable: Iterable[_T]) -> List[Iterator[_T]]: ... -@overload -def stagger( - iterable: Iterable[_T], - offsets: _SizedIterable[int] = ..., - longest: bool = ..., -) -> Iterator[Tuple[Optional[_T], ...]]: ... -@overload -def stagger( - iterable: Iterable[_T], - offsets: _SizedIterable[int] = ..., - longest: bool = ..., - fillvalue: _U = ..., -) -> Iterator[Tuple[Union[_T, _U], ...]]: ... - -class UnequalIterablesError(ValueError): - def __init__( - self, details: Optional[Tuple[int, int, int]] = ... - ) -> None: ... - -def zip_equal(*iterables: Iterable[_T]) -> Iterator[Tuple[_T, ...]]: ... -@overload -def zip_offset( - *iterables: Iterable[_T], offsets: _SizedIterable[int], longest: bool = ... -) -> Iterator[Tuple[Optional[_T], ...]]: ... -@overload -def zip_offset( - *iterables: Iterable[_T], - offsets: _SizedIterable[int], - longest: bool = ..., - fillvalue: _U -) -> Iterator[Tuple[Union[_T, _U], ...]]: ... -def sort_together( - iterables: Iterable[Iterable[_T]], - key_list: Iterable[int] = ..., - key: Optional[Callable[..., Any]] = ..., - reverse: bool = ..., -) -> List[Tuple[_T, ...]]: ... -def unzip(iterable: Iterable[Sequence[_T]]) -> Tuple[Iterator[_T], ...]: ... -def divide(n: int, iterable: Iterable[_T]) -> List[Iterator[_T]]: ... -def always_iterable( - obj: object, - base_type: Union[ - type, Tuple[Union[type, Tuple[Any, ...]], ...], None - ] = ..., -) -> Iterator[Any]: ... -def adjacent( - predicate: Callable[[_T], bool], - iterable: Iterable[_T], - distance: int = ..., -) -> Iterator[Tuple[bool, _T]]: ... -def groupby_transform( - iterable: Iterable[_T], - keyfunc: Optional[Callable[[_T], _U]] = ..., - valuefunc: Optional[Callable[[_T], _V]] = ..., - reducefunc: Optional[Callable[..., _W]] = ..., -) -> Iterator[Tuple[_T, _W]]: ... - -class numeric_range(Generic[_T, _U], Sequence[_T], Hashable, Reversible[_T]): - @overload - def __init__(self, __stop: _T) -> None: ... - @overload - def __init__(self, __start: _T, __stop: _T) -> None: ... - @overload - def __init__(self, __start: _T, __stop: _T, __step: _U) -> None: ... - def __bool__(self) -> bool: ... - def __contains__(self, elem: object) -> bool: ... - def __eq__(self, other: object) -> bool: ... - @overload - def __getitem__(self, key: int) -> _T: ... - @overload - def __getitem__(self, key: slice) -> numeric_range[_T, _U]: ... - def __hash__(self) -> int: ... - def __iter__(self) -> Iterator[_T]: ... - def __len__(self) -> int: ... - def __reduce__( - self, - ) -> Tuple[Type[numeric_range[_T, _U]], Tuple[_T, _T, _U]]: ... - def __repr__(self) -> str: ... - def __reversed__(self) -> Iterator[_T]: ... - def count(self, value: _T) -> int: ... - def index(self, value: _T) -> int: ... # type: ignore - -def count_cycle( - iterable: Iterable[_T], n: Optional[int] = ... -) -> Iterable[Tuple[int, _T]]: ... -def mark_ends( - iterable: Iterable[_T], -) -> Iterable[Tuple[bool, bool, _T]]: ... -def locate( - iterable: Iterable[object], - pred: Callable[..., Any] = ..., - window_size: Optional[int] = ..., -) -> Iterator[int]: ... -def lstrip( - iterable: Iterable[_T], pred: Callable[[_T], object] -) -> Iterator[_T]: ... -def rstrip( - iterable: Iterable[_T], pred: Callable[[_T], object] -) -> Iterator[_T]: ... -def strip( - iterable: Iterable[_T], pred: Callable[[_T], object] -) -> Iterator[_T]: ... - -class islice_extended(Generic[_T], Iterator[_T]): - def __init__( - self, iterable: Iterable[_T], *args: Optional[int] - ) -> None: ... - def __iter__(self) -> islice_extended[_T]: ... - def __next__(self) -> _T: ... - def __getitem__(self, index: slice) -> islice_extended[_T]: ... - -def always_reversible(iterable: Iterable[_T]) -> Iterator[_T]: ... -def consecutive_groups( - iterable: Iterable[_T], ordering: Callable[[_T], int] = ... -) -> Iterator[Iterator[_T]]: ... -@overload -def difference( - iterable: Iterable[_T], - func: Callable[[_T, _T], _U] = ..., - *, - initial: None = ... -) -> Iterator[Union[_T, _U]]: ... -@overload -def difference( - iterable: Iterable[_T], func: Callable[[_T, _T], _U] = ..., *, initial: _U -) -> Iterator[_U]: ... - -class SequenceView(Generic[_T], Sequence[_T]): - def __init__(self, target: Sequence[_T]) -> None: ... - @overload - def __getitem__(self, index: int) -> _T: ... - @overload - def __getitem__(self, index: slice) -> Sequence[_T]: ... - def __len__(self) -> int: ... - -class seekable(Generic[_T], Iterator[_T]): - def __init__( - self, iterable: Iterable[_T], maxlen: Optional[int] = ... - ) -> None: ... - def __iter__(self) -> seekable[_T]: ... - def __next__(self) -> _T: ... - def __bool__(self) -> bool: ... - @overload - def peek(self) -> _T: ... - @overload - def peek(self, default: _U) -> Union[_T, _U]: ... - def elements(self) -> SequenceView[_T]: ... - def seek(self, index: int) -> None: ... - -class run_length: - @staticmethod - def encode(iterable: Iterable[_T]) -> Iterator[Tuple[_T, int]]: ... - @staticmethod - def decode(iterable: Iterable[Tuple[_T, int]]) -> Iterator[_T]: ... - -def exactly_n( - iterable: Iterable[_T], n: int, predicate: Callable[[_T], object] = ... -) -> bool: ... -def circular_shifts(iterable: Iterable[_T]) -> List[Tuple[_T, ...]]: ... -def make_decorator( - wrapping_func: Callable[..., _U], result_index: int = ... -) -> Callable[..., Callable[[Callable[..., Any]], Callable[..., _U]]]: ... -@overload -def map_reduce( - iterable: Iterable[_T], - keyfunc: Callable[[_T], _U], - valuefunc: None = ..., - reducefunc: None = ..., -) -> Dict[_U, List[_T]]: ... -@overload -def map_reduce( - iterable: Iterable[_T], - keyfunc: Callable[[_T], _U], - valuefunc: Callable[[_T], _V], - reducefunc: None = ..., -) -> Dict[_U, List[_V]]: ... -@overload -def map_reduce( - iterable: Iterable[_T], - keyfunc: Callable[[_T], _U], - valuefunc: None = ..., - reducefunc: Callable[[List[_T]], _W] = ..., -) -> Dict[_U, _W]: ... -@overload -def map_reduce( - iterable: Iterable[_T], - keyfunc: Callable[[_T], _U], - valuefunc: Callable[[_T], _V], - reducefunc: Callable[[List[_V]], _W], -) -> Dict[_U, _W]: ... -def rlocate( - iterable: Iterable[_T], - pred: Callable[..., object] = ..., - window_size: Optional[int] = ..., -) -> Iterator[int]: ... -def replace( - iterable: Iterable[_T], - pred: Callable[..., object], - substitutes: Iterable[_U], - count: Optional[int] = ..., - window_size: int = ..., -) -> Iterator[Union[_T, _U]]: ... -def partitions(iterable: Iterable[_T]) -> Iterator[List[List[_T]]]: ... -def set_partitions( - iterable: Iterable[_T], k: Optional[int] = ... -) -> Iterator[List[List[_T]]]: ... - -class time_limited(Generic[_T], Iterator[_T]): - def __init__( - self, limit_seconds: float, iterable: Iterable[_T] - ) -> None: ... - def __iter__(self) -> islice_extended[_T]: ... - def __next__(self) -> _T: ... - -@overload -def only( - iterable: Iterable[_T], *, too_long: Optional[_Raisable] = ... -) -> Optional[_T]: ... -@overload -def only( - iterable: Iterable[_T], default: _U, too_long: Optional[_Raisable] = ... -) -> Union[_T, _U]: ... -def ichunked(iterable: Iterable[_T], n: int) -> Iterator[Iterator[_T]]: ... -def distinct_combinations( - iterable: Iterable[_T], r: int -) -> Iterator[Tuple[_T, ...]]: ... -def filter_except( - validator: Callable[[Any], object], - iterable: Iterable[_T], - *exceptions: Type[BaseException] -) -> Iterator[_T]: ... -def map_except( - function: Callable[[Any], _U], - iterable: Iterable[_T], - *exceptions: Type[BaseException] -) -> Iterator[_U]: ... -def sample( - iterable: Iterable[_T], - k: int, - weights: Optional[Iterable[float]] = ..., -) -> List[_T]: ... -def is_sorted( - iterable: Iterable[_T], - key: Optional[Callable[[_T], _U]] = ..., - reverse: bool = False, -) -> bool: ... - -class AbortThread(BaseException): - pass - -class callback_iter(Generic[_T], Iterator[_T]): - def __init__( - self, - func: Callable[..., Any], - callback_kwd: str = ..., - wait_seconds: float = ..., - ) -> None: ... - def __enter__(self) -> callback_iter[_T]: ... - def __exit__( - self, - exc_type: Optional[Type[BaseException]], - exc_value: Optional[BaseException], - traceback: Optional[TracebackType], - ) -> Optional[bool]: ... - def __iter__(self) -> callback_iter[_T]: ... - def __next__(self) -> _T: ... - def _reader(self) -> Iterator[_T]: ... - @property - def done(self) -> bool: ... - @property - def result(self) -> Any: ... - -def windowed_complete( - iterable: Iterable[_T], n: int -) -> Iterator[Tuple[_T, ...]]: ... -def all_unique( - iterable: Iterable[_T], key: Optional[Callable[[_T], _U]] = ... -) -> bool: ... -def nth_product(index: int, *args: Iterable[_T]) -> Tuple[_T, ...]: ... -def nth_permutation( - iterable: Iterable[_T], r: int, index: int -) -> Tuple[_T, ...]: ... -def value_chain(*args: Union[_T, Iterable[_T]]) -> Iterable[_T]: ... -def product_index(element: Iterable[_T], *args: Iterable[_T]) -> int: ... -def combination_index( - element: Iterable[_T], iterable: Iterable[_T] -) -> int: ... -def permutation_index( - element: Iterable[_T], iterable: Iterable[_T] -) -> int: ... - -class countable(Generic[_T], Iterator[_T]): - def __init__(self, iterable: Iterable[_T]) -> None: ... - def __iter__(self) -> countable[_T]: ... - def __next__(self) -> _T: ... diff --git a/tools/vendored.py b/tools/vendored.py index fc73933d..9428e752 100644 --- a/tools/vendored.py +++ b/tools/vendored.py @@ -68,10 +68,11 @@ def rewrite_more_itertools(pkg_files: Path): """ Rewrite more_itertools to remove unused more_itertools.more """ - (pkg_files / "more.py").remove() - init_file = pkg_files / "__init__.py" - init_text = "".join(ln for ln in init_file.lines() if "from .more " not in ln) - init_file.write_text(init_text) + for more_file in pkg_files.glob("more.py*"): + more_file.remove() + for init_file in pkg_files.glob("__init__.py*"): + text = "".join(ln for ln in init_file.lines() if "from .more " not in ln) + init_file.write_text(text) def clean(vendor): -- cgit v1.2.1 From 0047264f88cfc8c15591f8c053e563f84f4445a7 Mon Sep 17 00:00:00 2001 From: Maciej Pasternacki Date: Tue, 8 Feb 2022 20:59:42 +0100 Subject: Add changelog --- changelog.d/3091.misc.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelog.d/3091.misc.rst diff --git a/changelog.d/3091.misc.rst b/changelog.d/3091.misc.rst new file mode 100644 index 00000000..ebe9ba4f --- /dev/null +++ b/changelog.d/3091.misc.rst @@ -0,0 +1,4 @@ +Removed unused more_itertools.more module from vendored more_itertools +package to avoid importing threading as a side effect (which caused +gevent/gevent#1865). +-- by :user:`maciejp-ro` -- cgit v1.2.1 From dc25988c6cbc9883500075184153e941a4c7a0a6 Mon Sep 17 00:00:00 2001 From: Maciej Pasternacki Date: Tue, 8 Feb 2022 21:10:30 +0100 Subject: Restore accidentally deleted .coveragerc --- .coveragerc | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..6a34e662 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,7 @@ +[run] +omit = + # leading `*/` for pytest-dev/pytest-cov#456 + */.tox/* + +[report] +show_missing = True -- cgit v1.2.1 From cbac6933012c963ddbf9d3da1358005b4dcf1d79 Mon Sep 17 00:00:00 2001 From: Maciej Pasternacki <52241383+maciejp-ro@users.noreply.github.com> Date: Tue, 8 Feb 2022 21:11:45 +0100 Subject: Apply suggestions from code review Co-authored-by: Sviatoslav Sydorenko --- changelog.d/3091.misc.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.d/3091.misc.rst b/changelog.d/3091.misc.rst index ebe9ba4f..59967af0 100644 --- a/changelog.d/3091.misc.rst +++ b/changelog.d/3091.misc.rst @@ -1,4 +1,4 @@ -Removed unused more_itertools.more module from vendored more_itertools +Removed unused ``more_itertools.more`` module from vendored ``more_itertools`` package to avoid importing threading as a side effect (which caused -gevent/gevent#1865). +`gevent/gevent#1865 `__). -- by :user:`maciejp-ro` -- cgit v1.2.1 From 44b39e0df56b553aed045049ec30839fcb06cdd3 Mon Sep 17 00:00:00 2001 From: Maciej Pasternacki <52241383+maciejp-ro@users.noreply.github.com> Date: Tue, 8 Feb 2022 23:24:15 +0100 Subject: Restore more_itertools.more, make importing `concurrent.futures` lazy Co-authored-by: Jason R. Coombs --- changelog.d/3091.misc.rst | 4 +- pkg_resources/_vendor/more_itertools/__init__.py | 1 + pkg_resources/_vendor/more_itertools/__init__.pyi | 1 + pkg_resources/_vendor/more_itertools/more.py | 4316 +++++++++++++++++++++ pkg_resources/_vendor/more_itertools/more.pyi | 664 ++++ setuptools/_vendor/more_itertools/__init__.py | 1 + setuptools/_vendor/more_itertools/__init__.pyi | 1 + setuptools/_vendor/more_itertools/more.py | 3824 ++++++++++++++++++ setuptools/_vendor/more_itertools/more.pyi | 480 +++ tools/vendored.py | 16 +- 10 files changed, 9300 insertions(+), 8 deletions(-) create mode 100644 pkg_resources/_vendor/more_itertools/more.py create mode 100644 pkg_resources/_vendor/more_itertools/more.pyi create mode 100644 setuptools/_vendor/more_itertools/more.py create mode 100644 setuptools/_vendor/more_itertools/more.pyi diff --git a/changelog.d/3091.misc.rst b/changelog.d/3091.misc.rst index 59967af0..d6664125 100644 --- a/changelog.d/3091.misc.rst +++ b/changelog.d/3091.misc.rst @@ -1,4 +1,4 @@ -Removed unused ``more_itertools.more`` module from vendored ``more_itertools`` -package to avoid importing threading as a side effect (which caused +Make ``concurrent.futures`` import lazy in vendored ``more_itertools`` +package to a avoid importing threading as a side effect (which caused `gevent/gevent#1865 `__). -- by :user:`maciejp-ro` diff --git a/pkg_resources/_vendor/more_itertools/__init__.py b/pkg_resources/_vendor/more_itertools/__init__.py index 64bd1018..ea38bef1 100644 --- a/pkg_resources/_vendor/more_itertools/__init__.py +++ b/pkg_resources/_vendor/more_itertools/__init__.py @@ -1,3 +1,4 @@ +from .more import * # noqa from .recipes import * # noqa __version__ = '8.12.0' diff --git a/pkg_resources/_vendor/more_itertools/__init__.pyi b/pkg_resources/_vendor/more_itertools/__init__.pyi index f0fe8b5d..96f6e36c 100644 --- a/pkg_resources/_vendor/more_itertools/__init__.pyi +++ b/pkg_resources/_vendor/more_itertools/__init__.pyi @@ -1 +1,2 @@ +from .more import * from .recipes import * diff --git a/pkg_resources/_vendor/more_itertools/more.py b/pkg_resources/_vendor/more_itertools/more.py new file mode 100644 index 00000000..6b6a5cab --- /dev/null +++ b/pkg_resources/_vendor/more_itertools/more.py @@ -0,0 +1,4316 @@ +import warnings + +from collections import Counter, defaultdict, deque, abc +from collections.abc import Sequence +from functools import partial, reduce, wraps +from heapq import merge, heapify, heapreplace, heappop +from itertools import ( + chain, + compress, + count, + cycle, + dropwhile, + groupby, + islice, + repeat, + starmap, + takewhile, + tee, + zip_longest, +) +from math import exp, factorial, floor, log +from queue import Empty, Queue +from random import random, randrange, uniform +from operator import itemgetter, mul, sub, gt, lt, ge, le +from sys import hexversion, maxsize +from time import monotonic + +from .recipes import ( + consume, + flatten, + pairwise, + powerset, + take, + unique_everseen, +) + +__all__ = [ + 'AbortThread', + 'SequenceView', + 'UnequalIterablesError', + 'adjacent', + 'all_unique', + 'always_iterable', + 'always_reversible', + 'bucket', + 'callback_iter', + 'chunked', + 'chunked_even', + 'circular_shifts', + 'collapse', + 'collate', + 'combination_index', + 'consecutive_groups', + 'consumer', + 'count_cycle', + 'countable', + 'difference', + 'distinct_combinations', + 'distinct_permutations', + 'distribute', + 'divide', + 'duplicates_everseen', + 'duplicates_justseen', + 'exactly_n', + 'filter_except', + 'first', + 'groupby_transform', + 'ichunked', + 'ilen', + 'interleave', + 'interleave_evenly', + 'interleave_longest', + 'intersperse', + 'is_sorted', + 'islice_extended', + 'iterate', + 'last', + 'locate', + 'lstrip', + 'make_decorator', + 'map_except', + 'map_if', + 'map_reduce', + 'mark_ends', + 'minmax', + 'nth_or_last', + 'nth_permutation', + 'nth_product', + 'numeric_range', + 'one', + 'only', + 'padded', + 'partitions', + 'peekable', + 'permutation_index', + 'product_index', + 'raise_', + 'repeat_each', + 'repeat_last', + 'replace', + 'rlocate', + 'rstrip', + 'run_length', + 'sample', + 'seekable', + 'set_partitions', + 'side_effect', + 'sliced', + 'sort_together', + 'split_after', + 'split_at', + 'split_before', + 'split_into', + 'split_when', + 'spy', + 'stagger', + 'strip', + 'strictly_n', + 'substrings', + 'substrings_indexes', + 'time_limited', + 'unique_in_window', + 'unique_to_each', + 'unzip', + 'value_chain', + 'windowed', + 'windowed_complete', + 'with_iter', + 'zip_broadcast', + 'zip_equal', + 'zip_offset', +] + + +_marker = object() + + +def chunked(iterable, n, strict=False): + """Break *iterable* into lists of length *n*: + + >>> list(chunked([1, 2, 3, 4, 5, 6], 3)) + [[1, 2, 3], [4, 5, 6]] + + By the default, the last yielded list will have fewer than *n* elements + if the length of *iterable* is not divisible by *n*: + + >>> list(chunked([1, 2, 3, 4, 5, 6, 7, 8], 3)) + [[1, 2, 3], [4, 5, 6], [7, 8]] + + To use a fill-in value instead, see the :func:`grouper` recipe. + + If the length of *iterable* is not divisible by *n* and *strict* is + ``True``, then ``ValueError`` will be raised before the last + list is yielded. + + """ + iterator = iter(partial(take, n, iter(iterable)), []) + if strict: + if n is None: + raise ValueError('n must not be None when using strict mode.') + + def ret(): + for chunk in iterator: + if len(chunk) != n: + raise ValueError('iterable is not divisible by n.') + yield chunk + + return iter(ret()) + else: + return iterator + + +def first(iterable, default=_marker): + """Return the first item of *iterable*, or *default* if *iterable* is + empty. + + >>> first([0, 1, 2, 3]) + 0 + >>> first([], 'some default') + 'some default' + + If *default* is not provided and there are no items in the iterable, + raise ``ValueError``. + + :func:`first` is useful when you have a generator of expensive-to-retrieve + values and want any arbitrary one. It is marginally shorter than + ``next(iter(iterable), default)``. + + """ + try: + return next(iter(iterable)) + except StopIteration as e: + if default is _marker: + raise ValueError( + 'first() was called on an empty iterable, and no ' + 'default value was provided.' + ) from e + return default + + +def last(iterable, default=_marker): + """Return the last item of *iterable*, or *default* if *iterable* is + empty. + + >>> last([0, 1, 2, 3]) + 3 + >>> last([], 'some default') + 'some default' + + If *default* is not provided and there are no items in the iterable, + raise ``ValueError``. + """ + try: + if isinstance(iterable, Sequence): + return iterable[-1] + # Work around https://bugs.python.org/issue38525 + elif hasattr(iterable, '__reversed__') and (hexversion != 0x030800F0): + return next(reversed(iterable)) + else: + return deque(iterable, maxlen=1)[-1] + except (IndexError, TypeError, StopIteration): + if default is _marker: + raise ValueError( + 'last() was called on an empty iterable, and no default was ' + 'provided.' + ) + return default + + +def nth_or_last(iterable, n, default=_marker): + """Return the nth or the last item of *iterable*, + or *default* if *iterable* is empty. + + >>> nth_or_last([0, 1, 2, 3], 2) + 2 + >>> nth_or_last([0, 1], 2) + 1 + >>> nth_or_last([], 0, 'some default') + 'some default' + + If *default* is not provided and there are no items in the iterable, + raise ``ValueError``. + """ + return last(islice(iterable, n + 1), default=default) + + +class peekable: + """Wrap an iterator to allow lookahead and prepending elements. + + Call :meth:`peek` on the result to get the value that will be returned + by :func:`next`. This won't advance the iterator: + + >>> p = peekable(['a', 'b']) + >>> p.peek() + 'a' + >>> next(p) + 'a' + + Pass :meth:`peek` a default value to return that instead of raising + ``StopIteration`` when the iterator is exhausted. + + >>> p = peekable([]) + >>> p.peek('hi') + 'hi' + + peekables also offer a :meth:`prepend` method, which "inserts" items + at the head of the iterable: + + >>> p = peekable([1, 2, 3]) + >>> p.prepend(10, 11, 12) + >>> next(p) + 10 + >>> p.peek() + 11 + >>> list(p) + [11, 12, 1, 2, 3] + + peekables can be indexed. Index 0 is the item that will be returned by + :func:`next`, index 1 is the item after that, and so on: + The values up to the given index will be cached. + + >>> p = peekable(['a', 'b', 'c', 'd']) + >>> p[0] + 'a' + >>> p[1] + 'b' + >>> next(p) + 'a' + + Negative indexes are supported, but be aware that they will cache the + remaining items in the source iterator, which may require significant + storage. + + To check whether a peekable is exhausted, check its truth value: + + >>> p = peekable(['a', 'b']) + >>> if p: # peekable has items + ... list(p) + ['a', 'b'] + >>> if not p: # peekable is exhausted + ... list(p) + [] + + """ + + def __init__(self, iterable): + self._it = iter(iterable) + self._cache = deque() + + def __iter__(self): + return self + + def __bool__(self): + try: + self.peek() + except StopIteration: + return False + return True + + def peek(self, default=_marker): + """Return the item that will be next returned from ``next()``. + + Return ``default`` if there are no items left. If ``default`` is not + provided, raise ``StopIteration``. + + """ + if not self._cache: + try: + self._cache.append(next(self._it)) + except StopIteration: + if default is _marker: + raise + return default + return self._cache[0] + + def prepend(self, *items): + """Stack up items to be the next ones returned from ``next()`` or + ``self.peek()``. The items will be returned in + first in, first out order:: + + >>> p = peekable([1, 2, 3]) + >>> p.prepend(10, 11, 12) + >>> next(p) + 10 + >>> list(p) + [11, 12, 1, 2, 3] + + It is possible, by prepending items, to "resurrect" a peekable that + previously raised ``StopIteration``. + + >>> p = peekable([]) + >>> next(p) + Traceback (most recent call last): + ... + StopIteration + >>> p.prepend(1) + >>> next(p) + 1 + >>> next(p) + Traceback (most recent call last): + ... + StopIteration + + """ + self._cache.extendleft(reversed(items)) + + def __next__(self): + if self._cache: + return self._cache.popleft() + + return next(self._it) + + def _get_slice(self, index): + # Normalize the slice's arguments + step = 1 if (index.step is None) else index.step + if step > 0: + start = 0 if (index.start is None) else index.start + stop = maxsize if (index.stop is None) else index.stop + elif step < 0: + start = -1 if (index.start is None) else index.start + stop = (-maxsize - 1) if (index.stop is None) else index.stop + else: + raise ValueError('slice step cannot be zero') + + # If either the start or stop index is negative, we'll need to cache + # the rest of the iterable in order to slice from the right side. + if (start < 0) or (stop < 0): + self._cache.extend(self._it) + # Otherwise we'll need to find the rightmost index and cache to that + # point. + else: + n = min(max(start, stop) + 1, maxsize) + cache_len = len(self._cache) + if n >= cache_len: + self._cache.extend(islice(self._it, n - cache_len)) + + return list(self._cache)[index] + + def __getitem__(self, index): + if isinstance(index, slice): + return self._get_slice(index) + + cache_len = len(self._cache) + if index < 0: + self._cache.extend(self._it) + elif index >= cache_len: + self._cache.extend(islice(self._it, index + 1 - cache_len)) + + return self._cache[index] + + +def collate(*iterables, **kwargs): + """Return a sorted merge of the items from each of several already-sorted + *iterables*. + + >>> list(collate('ACDZ', 'AZ', 'JKL')) + ['A', 'A', 'C', 'D', 'J', 'K', 'L', 'Z', 'Z'] + + Works lazily, keeping only the next value from each iterable in memory. Use + :func:`collate` to, for example, perform a n-way mergesort of items that + don't fit in memory. + + If a *key* function is specified, the iterables will be sorted according + to its result: + + >>> key = lambda s: int(s) # Sort by numeric value, not by string + >>> list(collate(['1', '10'], ['2', '11'], key=key)) + ['1', '2', '10', '11'] + + + If the *iterables* are sorted in descending order, set *reverse* to + ``True``: + + >>> list(collate([5, 3, 1], [4, 2, 0], reverse=True)) + [5, 4, 3, 2, 1, 0] + + If the elements of the passed-in iterables are out of order, you might get + unexpected results. + + On Python 3.5+, this function is an alias for :func:`heapq.merge`. + + """ + warnings.warn( + "collate is no longer part of more_itertools, use heapq.merge", + DeprecationWarning, + ) + return merge(*iterables, **kwargs) + + +def consumer(func): + """Decorator that automatically advances a PEP-342-style "reverse iterator" + to its first yield point so you don't have to call ``next()`` on it + manually. + + >>> @consumer + ... def tally(): + ... i = 0 + ... while True: + ... print('Thing number %s is %s.' % (i, (yield))) + ... i += 1 + ... + >>> t = tally() + >>> t.send('red') + Thing number 0 is red. + >>> t.send('fish') + Thing number 1 is fish. + + Without the decorator, you would have to call ``next(t)`` before + ``t.send()`` could be used. + + """ + + @wraps(func) + def wrapper(*args, **kwargs): + gen = func(*args, **kwargs) + next(gen) + return gen + + return wrapper + + +def ilen(iterable): + """Return the number of items in *iterable*. + + >>> ilen(x for x in range(1000000) if x % 3 == 0) + 333334 + + This consumes the iterable, so handle with care. + + """ + # This approach was selected because benchmarks showed it's likely the + # fastest of the known implementations at the time of writing. + # See GitHub tracker: #236, #230. + counter = count() + deque(zip(iterable, counter), maxlen=0) + return next(counter) + + +def iterate(func, start): + """Return ``start``, ``func(start)``, ``func(func(start))``, ... + + >>> from itertools import islice + >>> list(islice(iterate(lambda x: 2*x, 1), 10)) + [1, 2, 4, 8, 16, 32, 64, 128, 256, 512] + + """ + while True: + yield start + start = func(start) + + +def with_iter(context_manager): + """Wrap an iterable in a ``with`` statement, so it closes once exhausted. + + For example, this will close the file when the iterator is exhausted:: + + upper_lines = (line.upper() for line in with_iter(open('foo'))) + + Any context manager which returns an iterable is a candidate for + ``with_iter``. + + """ + with context_manager as iterable: + yield from iterable + + +def one(iterable, too_short=None, too_long=None): + """Return the first item from *iterable*, which is expected to contain only + that item. Raise an exception if *iterable* is empty or has more than one + item. + + :func:`one` is useful for ensuring that an iterable contains only one item. + For example, it can be used to retrieve the result of a database query + that is expected to return a single row. + + If *iterable* is empty, ``ValueError`` will be raised. You may specify a + different exception with the *too_short* keyword: + + >>> it = [] + >>> one(it) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ValueError: too many items in iterable (expected 1)' + >>> too_short = IndexError('too few items') + >>> one(it, too_short=too_short) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + IndexError: too few items + + Similarly, if *iterable* contains more than one item, ``ValueError`` will + be raised. You may specify a different exception with the *too_long* + keyword: + + >>> it = ['too', 'many'] + >>> one(it) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ValueError: Expected exactly one item in iterable, but got 'too', + 'many', and perhaps more. + >>> too_long = RuntimeError + >>> one(it, too_long=too_long) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + RuntimeError + + Note that :func:`one` attempts to advance *iterable* twice to ensure there + is only one item. See :func:`spy` or :func:`peekable` to check iterable + contents less destructively. + + """ + it = iter(iterable) + + try: + first_value = next(it) + except StopIteration as e: + raise ( + too_short or ValueError('too few items in iterable (expected 1)') + ) from e + + try: + second_value = next(it) + except StopIteration: + pass + else: + msg = ( + 'Expected exactly one item in iterable, but got {!r}, {!r}, ' + 'and perhaps more.'.format(first_value, second_value) + ) + raise too_long or ValueError(msg) + + return first_value + + +def raise_(exception, *args): + raise exception(*args) + + +def strictly_n(iterable, n, too_short=None, too_long=None): + """Validate that *iterable* has exactly *n* items and return them if + it does. If it has fewer than *n* items, call function *too_short* + with those items. If it has more than *n* items, call function + *too_long* with the first ``n + 1`` items. + + >>> iterable = ['a', 'b', 'c', 'd'] + >>> n = 4 + >>> list(strictly_n(iterable, n)) + ['a', 'b', 'c', 'd'] + + By default, *too_short* and *too_long* are functions that raise + ``ValueError``. + + >>> list(strictly_n('ab', 3)) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ValueError: too few items in iterable (got 2) + + >>> list(strictly_n('abc', 2)) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ValueError: too many items in iterable (got at least 3) + + You can instead supply functions that do something else. + *too_short* will be called with the number of items in *iterable*. + *too_long* will be called with `n + 1`. + + >>> def too_short(item_count): + ... raise RuntimeError + >>> it = strictly_n('abcd', 6, too_short=too_short) + >>> list(it) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + RuntimeError + + >>> def too_long(item_count): + ... print('The boss is going to hear about this') + >>> it = strictly_n('abcdef', 4, too_long=too_long) + >>> list(it) + The boss is going to hear about this + ['a', 'b', 'c', 'd'] + + """ + if too_short is None: + too_short = lambda item_count: raise_( + ValueError, + 'Too few items in iterable (got {})'.format(item_count), + ) + + if too_long is None: + too_long = lambda item_count: raise_( + ValueError, + 'Too many items in iterable (got at least {})'.format(item_count), + ) + + it = iter(iterable) + for i in range(n): + try: + item = next(it) + except StopIteration: + too_short(i) + return + else: + yield item + + try: + next(it) + except StopIteration: + pass + else: + too_long(n + 1) + + +def distinct_permutations(iterable, r=None): + """Yield successive distinct permutations of the elements in *iterable*. + + >>> sorted(distinct_permutations([1, 0, 1])) + [(0, 1, 1), (1, 0, 1), (1, 1, 0)] + + Equivalent to ``set(permutations(iterable))``, except duplicates are not + generated and thrown away. For larger input sequences this is much more + efficient. + + Duplicate permutations arise when there are duplicated elements in the + input iterable. The number of items returned is + `n! / (x_1! * x_2! * ... * x_n!)`, where `n` is the total number of + items input, and each `x_i` is the count of a distinct item in the input + sequence. + + If *r* is given, only the *r*-length permutations are yielded. + + >>> sorted(distinct_permutations([1, 0, 1], r=2)) + [(0, 1), (1, 0), (1, 1)] + >>> sorted(distinct_permutations(range(3), r=2)) + [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)] + + """ + # Algorithm: https://w.wiki/Qai + def _full(A): + while True: + # Yield the permutation we have + yield tuple(A) + + # Find the largest index i such that A[i] < A[i + 1] + for i in range(size - 2, -1, -1): + if A[i] < A[i + 1]: + break + # If no such index exists, this permutation is the last one + else: + return + + # Find the largest index j greater than j such that A[i] < A[j] + for j in range(size - 1, i, -1): + if A[i] < A[j]: + break + + # Swap the value of A[i] with that of A[j], then reverse the + # sequence from A[i + 1] to form the new permutation + A[i], A[j] = A[j], A[i] + A[i + 1 :] = A[: i - size : -1] # A[i + 1:][::-1] + + # Algorithm: modified from the above + def _partial(A, r): + # Split A into the first r items and the last r items + head, tail = A[:r], A[r:] + right_head_indexes = range(r - 1, -1, -1) + left_tail_indexes = range(len(tail)) + + while True: + # Yield the permutation we have + yield tuple(head) + + # Starting from the right, find the first index of the head with + # value smaller than the maximum value of the tail - call it i. + pivot = tail[-1] + for i in right_head_indexes: + if head[i] < pivot: + break + pivot = head[i] + else: + return + + # Starting from the left, find the first value of the tail + # with a value greater than head[i] and swap. + for j in left_tail_indexes: + if tail[j] > head[i]: + head[i], tail[j] = tail[j], head[i] + break + # If we didn't find one, start from the right and find the first + # index of the head with a value greater than head[i] and swap. + else: + for j in right_head_indexes: + if head[j] > head[i]: + head[i], head[j] = head[j], head[i] + break + + # Reverse head[i + 1:] and swap it with tail[:r - (i + 1)] + tail += head[: i - r : -1] # head[i + 1:][::-1] + i += 1 + head[i:], tail[:] = tail[: r - i], tail[r - i :] + + items = sorted(iterable) + + size = len(items) + if r is None: + r = size + + if 0 < r <= size: + return _full(items) if (r == size) else _partial(items, r) + + return iter(() if r else ((),)) + + +def intersperse(e, iterable, n=1): + """Intersperse filler element *e* among the items in *iterable*, leaving + *n* items between each filler element. + + >>> list(intersperse('!', [1, 2, 3, 4, 5])) + [1, '!', 2, '!', 3, '!', 4, '!', 5] + + >>> list(intersperse(None, [1, 2, 3, 4, 5], n=2)) + [1, 2, None, 3, 4, None, 5] + + """ + if n == 0: + raise ValueError('n must be > 0') + elif n == 1: + # interleave(repeat(e), iterable) -> e, x_0, e, x_1, e, x_2... + # islice(..., 1, None) -> x_0, e, x_1, e, x_2... + return islice(interleave(repeat(e), iterable), 1, None) + else: + # interleave(filler, chunks) -> [e], [x_0, x_1], [e], [x_2, x_3]... + # islice(..., 1, None) -> [x_0, x_1], [e], [x_2, x_3]... + # flatten(...) -> x_0, x_1, e, x_2, x_3... + filler = repeat([e]) + chunks = chunked(iterable, n) + return flatten(islice(interleave(filler, chunks), 1, None)) + + +def unique_to_each(*iterables): + """Return the elements from each of the input iterables that aren't in the + other input iterables. + + For example, suppose you have a set of packages, each with a set of + dependencies:: + + {'pkg_1': {'A', 'B'}, 'pkg_2': {'B', 'C'}, 'pkg_3': {'B', 'D'}} + + If you remove one package, which dependencies can also be removed? + + If ``pkg_1`` is removed, then ``A`` is no longer necessary - it is not + associated with ``pkg_2`` or ``pkg_3``. Similarly, ``C`` is only needed for + ``pkg_2``, and ``D`` is only needed for ``pkg_3``:: + + >>> unique_to_each({'A', 'B'}, {'B', 'C'}, {'B', 'D'}) + [['A'], ['C'], ['D']] + + If there are duplicates in one input iterable that aren't in the others + they will be duplicated in the output. Input order is preserved:: + + >>> unique_to_each("mississippi", "missouri") + [['p', 'p'], ['o', 'u', 'r']] + + It is assumed that the elements of each iterable are hashable. + + """ + pool = [list(it) for it in iterables] + counts = Counter(chain.from_iterable(map(set, pool))) + uniques = {element for element in counts if counts[element] == 1} + return [list(filter(uniques.__contains__, it)) for it in pool] + + +def windowed(seq, n, fillvalue=None, step=1): + """Return a sliding window of width *n* over the given iterable. + + >>> all_windows = windowed([1, 2, 3, 4, 5], 3) + >>> list(all_windows) + [(1, 2, 3), (2, 3, 4), (3, 4, 5)] + + When the window is larger than the iterable, *fillvalue* is used in place + of missing values: + + >>> list(windowed([1, 2, 3], 4)) + [(1, 2, 3, None)] + + Each window will advance in increments of *step*: + + >>> list(windowed([1, 2, 3, 4, 5, 6], 3, fillvalue='!', step=2)) + [(1, 2, 3), (3, 4, 5), (5, 6, '!')] + + To slide into the iterable's items, use :func:`chain` to add filler items + to the left: + + >>> iterable = [1, 2, 3, 4] + >>> n = 3 + >>> padding = [None] * (n - 1) + >>> list(windowed(chain(padding, iterable), 3)) + [(None, None, 1), (None, 1, 2), (1, 2, 3), (2, 3, 4)] + """ + if n < 0: + raise ValueError('n must be >= 0') + if n == 0: + yield tuple() + return + if step < 1: + raise ValueError('step must be >= 1') + + window = deque(maxlen=n) + i = n + for _ in map(window.append, seq): + i -= 1 + if not i: + i = step + yield tuple(window) + + size = len(window) + if size < n: + yield tuple(chain(window, repeat(fillvalue, n - size))) + elif 0 < i < min(step, n): + window += (fillvalue,) * i + yield tuple(window) + + +def substrings(iterable): + """Yield all of the substrings of *iterable*. + + >>> [''.join(s) for s in substrings('more')] + ['m', 'o', 'r', 'e', 'mo', 'or', 're', 'mor', 'ore', 'more'] + + Note that non-string iterables can also be subdivided. + + >>> list(substrings([0, 1, 2])) + [(0,), (1,), (2,), (0, 1), (1, 2), (0, 1, 2)] + + """ + # The length-1 substrings + seq = [] + for item in iter(iterable): + seq.append(item) + yield (item,) + seq = tuple(seq) + item_count = len(seq) + + # And the rest + for n in range(2, item_count + 1): + for i in range(item_count - n + 1): + yield seq[i : i + n] + + +def substrings_indexes(seq, reverse=False): + """Yield all substrings and their positions in *seq* + + The items yielded will be a tuple of the form ``(substr, i, j)``, where + ``substr == seq[i:j]``. + + This function only works for iterables that support slicing, such as + ``str`` objects. + + >>> for item in substrings_indexes('more'): + ... print(item) + ('m', 0, 1) + ('o', 1, 2) + ('r', 2, 3) + ('e', 3, 4) + ('mo', 0, 2) + ('or', 1, 3) + ('re', 2, 4) + ('mor', 0, 3) + ('ore', 1, 4) + ('more', 0, 4) + + Set *reverse* to ``True`` to yield the same items in the opposite order. + + + """ + r = range(1, len(seq) + 1) + if reverse: + r = reversed(r) + return ( + (seq[i : i + L], i, i + L) for L in r for i in range(len(seq) - L + 1) + ) + + +class bucket: + """Wrap *iterable* and return an object that buckets it iterable into + child iterables based on a *key* function. + + >>> iterable = ['a1', 'b1', 'c1', 'a2', 'b2', 'c2', 'b3'] + >>> s = bucket(iterable, key=lambda x: x[0]) # Bucket by 1st character + >>> sorted(list(s)) # Get the keys + ['a', 'b', 'c'] + >>> a_iterable = s['a'] + >>> next(a_iterable) + 'a1' + >>> next(a_iterable) + 'a2' + >>> list(s['b']) + ['b1', 'b2', 'b3'] + + The original iterable will be advanced and its items will be cached until + they are used by the child iterables. This may require significant storage. + + By default, attempting to select a bucket to which no items belong will + exhaust the iterable and cache all values. + If you specify a *validator* function, selected buckets will instead be + checked against it. + + >>> from itertools import count + >>> it = count(1, 2) # Infinite sequence of odd numbers + >>> key = lambda x: x % 10 # Bucket by last digit + >>> validator = lambda x: x in {1, 3, 5, 7, 9} # Odd digits only + >>> s = bucket(it, key=key, validator=validator) + >>> 2 in s + False + >>> list(s[2]) + [] + + """ + + def __init__(self, iterable, key, validator=None): + self._it = iter(iterable) + self._key = key + self._cache = defaultdict(deque) + self._validator = validator or (lambda x: True) + + def __contains__(self, value): + if not self._validator(value): + return False + + try: + item = next(self[value]) + except StopIteration: + return False + else: + self._cache[value].appendleft(item) + + return True + + def _get_values(self, value): + """ + Helper to yield items from the parent iterator that match *value*. + Items that don't match are stored in the local cache as they + are encountered. + """ + while True: + # If we've cached some items that match the target value, emit + # the first one and evict it from the cache. + if self._cache[value]: + yield self._cache[value].popleft() + # Otherwise we need to advance the parent iterator to search for + # a matching item, caching the rest. + else: + while True: + try: + item = next(self._it) + except StopIteration: + return + item_value = self._key(item) + if item_value == value: + yield item + break + elif self._validator(item_value): + self._cache[item_value].append(item) + + def __iter__(self): + for item in self._it: + item_value = self._key(item) + if self._validator(item_value): + self._cache[item_value].append(item) + + yield from self._cache.keys() + + def __getitem__(self, value): + if not self._validator(value): + return iter(()) + + return self._get_values(value) + + +def spy(iterable, n=1): + """Return a 2-tuple with a list containing the first *n* elements of + *iterable*, and an iterator with the same items as *iterable*. + This allows you to "look ahead" at the items in the iterable without + advancing it. + + There is one item in the list by default: + + >>> iterable = 'abcdefg' + >>> head, iterable = spy(iterable) + >>> head + ['a'] + >>> list(iterable) + ['a', 'b', 'c', 'd', 'e', 'f', 'g'] + + You may use unpacking to retrieve items instead of lists: + + >>> (head,), iterable = spy('abcdefg') + >>> head + 'a' + >>> (first, second), iterable = spy('abcdefg', 2) + >>> first + 'a' + >>> second + 'b' + + The number of items requested can be larger than the number of items in + the iterable: + + >>> iterable = [1, 2, 3, 4, 5] + >>> head, iterable = spy(iterable, 10) + >>> head + [1, 2, 3, 4, 5] + >>> list(iterable) + [1, 2, 3, 4, 5] + + """ + it = iter(iterable) + head = take(n, it) + + return head.copy(), chain(head, it) + + +def interleave(*iterables): + """Return a new iterable yielding from each iterable in turn, + until the shortest is exhausted. + + >>> list(interleave([1, 2, 3], [4, 5], [6, 7, 8])) + [1, 4, 6, 2, 5, 7] + + For a version that doesn't terminate after the shortest iterable is + exhausted, see :func:`interleave_longest`. + + """ + return chain.from_iterable(zip(*iterables)) + + +def interleave_longest(*iterables): + """Return a new iterable yielding from each iterable in turn, + skipping any that are exhausted. + + >>> list(interleave_longest([1, 2, 3], [4, 5], [6, 7, 8])) + [1, 4, 6, 2, 5, 7, 3, 8] + + This function produces the same output as :func:`roundrobin`, but may + perform better for some inputs (in particular when the number of iterables + is large). + + """ + i = chain.from_iterable(zip_longest(*iterables, fillvalue=_marker)) + return (x for x in i if x is not _marker) + + +def interleave_evenly(iterables, lengths=None): + """ + Interleave multiple iterables so that their elements are evenly distributed + throughout the output sequence. + + >>> iterables = [1, 2, 3, 4, 5], ['a', 'b'] + >>> list(interleave_evenly(iterables)) + [1, 2, 'a', 3, 4, 'b', 5] + + >>> iterables = [[1, 2, 3], [4, 5], [6, 7, 8]] + >>> list(interleave_evenly(iterables)) + [1, 6, 4, 2, 7, 3, 8, 5] + + This function requires iterables of known length. Iterables without + ``__len__()`` can be used by manually specifying lengths with *lengths*: + + >>> from itertools import combinations, repeat + >>> iterables = [combinations(range(4), 2), ['a', 'b', 'c']] + >>> lengths = [4 * (4 - 1) // 2, 3] + >>> list(interleave_evenly(iterables, lengths=lengths)) + [(0, 1), (0, 2), 'a', (0, 3), (1, 2), 'b', (1, 3), (2, 3), 'c'] + + Based on Bresenham's algorithm. + """ + if lengths is None: + try: + lengths = [len(it) for it in iterables] + except TypeError: + raise ValueError( + 'Iterable lengths could not be determined automatically. ' + 'Specify them with the lengths keyword.' + ) + elif len(iterables) != len(lengths): + raise ValueError('Mismatching number of iterables and lengths.') + + dims = len(lengths) + + # sort iterables by length, descending + lengths_permute = sorted( + range(dims), key=lambda i: lengths[i], reverse=True + ) + lengths_desc = [lengths[i] for i in lengths_permute] + iters_desc = [iter(iterables[i]) for i in lengths_permute] + + # the longest iterable is the primary one (Bresenham: the longest + # distance along an axis) + delta_primary, deltas_secondary = lengths_desc[0], lengths_desc[1:] + iter_primary, iters_secondary = iters_desc[0], iters_desc[1:] + errors = [delta_primary // dims] * len(deltas_secondary) + + to_yield = sum(lengths) + while to_yield: + yield next(iter_primary) + to_yield -= 1 + # update errors for each secondary iterable + errors = [e - delta for e, delta in zip(errors, deltas_secondary)] + + # those iterables for which the error is negative are yielded + # ("diagonal step" in Bresenham) + for i, e in enumerate(errors): + if e < 0: + yield next(iters_secondary[i]) + to_yield -= 1 + errors[i] += delta_primary + + +def collapse(iterable, base_type=None, levels=None): + """Flatten an iterable with multiple levels of nesting (e.g., a list of + lists of tuples) into non-iterable types. + + >>> iterable = [(1, 2), ([3, 4], [[5], [6]])] + >>> list(collapse(iterable)) + [1, 2, 3, 4, 5, 6] + + Binary and text strings are not considered iterable and + will not be collapsed. + + To avoid collapsing other types, specify *base_type*: + + >>> iterable = ['ab', ('cd', 'ef'), ['gh', 'ij']] + >>> list(collapse(iterable, base_type=tuple)) + ['ab', ('cd', 'ef'), 'gh', 'ij'] + + Specify *levels* to stop flattening after a certain level: + + >>> iterable = [('a', ['b']), ('c', ['d'])] + >>> list(collapse(iterable)) # Fully flattened + ['a', 'b', 'c', 'd'] + >>> list(collapse(iterable, levels=1)) # Only one level flattened + ['a', ['b'], 'c', ['d']] + + """ + + def walk(node, level): + if ( + ((levels is not None) and (level > levels)) + or isinstance(node, (str, bytes)) + or ((base_type is not None) and isinstance(node, base_type)) + ): + yield node + return + + try: + tree = iter(node) + except TypeError: + yield node + return + else: + for child in tree: + yield from walk(child, level + 1) + + yield from walk(iterable, 0) + + +def side_effect(func, iterable, chunk_size=None, before=None, after=None): + """Invoke *func* on each item in *iterable* (or on each *chunk_size* group + of items) before yielding the item. + + `func` must be a function that takes a single argument. Its return value + will be discarded. + + *before* and *after* are optional functions that take no arguments. They + will be executed before iteration starts and after it ends, respectively. + + `side_effect` can be used for logging, updating progress bars, or anything + that is not functionally "pure." + + Emitting a status message: + + >>> from more_itertools import consume + >>> func = lambda item: print('Received {}'.format(item)) + >>> consume(side_effect(func, range(2))) + Received 0 + Received 1 + + Operating on chunks of items: + + >>> pair_sums = [] + >>> func = lambda chunk: pair_sums.append(sum(chunk)) + >>> list(side_effect(func, [0, 1, 2, 3, 4, 5], 2)) + [0, 1, 2, 3, 4, 5] + >>> list(pair_sums) + [1, 5, 9] + + Writing to a file-like object: + + >>> from io import StringIO + >>> from more_itertools import consume + >>> f = StringIO() + >>> func = lambda x: print(x, file=f) + >>> before = lambda: print(u'HEADER', file=f) + >>> after = f.close + >>> it = [u'a', u'b', u'c'] + >>> consume(side_effect(func, it, before=before, after=after)) + >>> f.closed + True + + """ + try: + if before is not None: + before() + + if chunk_size is None: + for item in iterable: + func(item) + yield item + else: + for chunk in chunked(iterable, chunk_size): + func(chunk) + yield from chunk + finally: + if after is not None: + after() + + +def sliced(seq, n, strict=False): + """Yield slices of length *n* from the sequence *seq*. + + >>> list(sliced((1, 2, 3, 4, 5, 6), 3)) + [(1, 2, 3), (4, 5, 6)] + + By the default, the last yielded slice will have fewer than *n* elements + if the length of *seq* is not divisible by *n*: + + >>> list(sliced((1, 2, 3, 4, 5, 6, 7, 8), 3)) + [(1, 2, 3), (4, 5, 6), (7, 8)] + + If the length of *seq* is not divisible by *n* and *strict* is + ``True``, then ``ValueError`` will be raised before the last + slice is yielded. + + This function will only work for iterables that support slicing. + For non-sliceable iterables, see :func:`chunked`. + + """ + iterator = takewhile(len, (seq[i : i + n] for i in count(0, n))) + if strict: + + def ret(): + for _slice in iterator: + if len(_slice) != n: + raise ValueError("seq is not divisible by n.") + yield _slice + + return iter(ret()) + else: + return iterator + + +def split_at(iterable, pred, maxsplit=-1, keep_separator=False): + """Yield lists of items from *iterable*, where each list is delimited by + an item where callable *pred* returns ``True``. + + >>> list(split_at('abcdcba', lambda x: x == 'b')) + [['a'], ['c', 'd', 'c'], ['a']] + + >>> list(split_at(range(10), lambda n: n % 2 == 1)) + [[0], [2], [4], [6], [8], []] + + At most *maxsplit* splits are done. If *maxsplit* is not specified or -1, + then there is no limit on the number of splits: + + >>> list(split_at(range(10), lambda n: n % 2 == 1, maxsplit=2)) + [[0], [2], [4, 5, 6, 7, 8, 9]] + + By default, the delimiting items are not included in the output. + The include them, set *keep_separator* to ``True``. + + >>> list(split_at('abcdcba', lambda x: x == 'b', keep_separator=True)) + [['a'], ['b'], ['c', 'd', 'c'], ['b'], ['a']] + + """ + if maxsplit == 0: + yield list(iterable) + return + + buf = [] + it = iter(iterable) + for item in it: + if pred(item): + yield buf + if keep_separator: + yield [item] + if maxsplit == 1: + yield list(it) + return + buf = [] + maxsplit -= 1 + else: + buf.append(item) + yield buf + + +def split_before(iterable, pred, maxsplit=-1): + """Yield lists of items from *iterable*, where each list ends just before + an item for which callable *pred* returns ``True``: + + >>> list(split_before('OneTwo', lambda s: s.isupper())) + [['O', 'n', 'e'], ['T', 'w', 'o']] + + >>> list(split_before(range(10), lambda n: n % 3 == 0)) + [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]] + + At most *maxsplit* splits are done. If *maxsplit* is not specified or -1, + then there is no limit on the number of splits: + + >>> list(split_before(range(10), lambda n: n % 3 == 0, maxsplit=2)) + [[0, 1, 2], [3, 4, 5], [6, 7, 8, 9]] + """ + if maxsplit == 0: + yield list(iterable) + return + + buf = [] + it = iter(iterable) + for item in it: + if pred(item) and buf: + yield buf + if maxsplit == 1: + yield [item] + list(it) + return + buf = [] + maxsplit -= 1 + buf.append(item) + if buf: + yield buf + + +def split_after(iterable, pred, maxsplit=-1): + """Yield lists of items from *iterable*, where each list ends with an + item where callable *pred* returns ``True``: + + >>> list(split_after('one1two2', lambda s: s.isdigit())) + [['o', 'n', 'e', '1'], ['t', 'w', 'o', '2']] + + >>> list(split_after(range(10), lambda n: n % 3 == 0)) + [[0], [1, 2, 3], [4, 5, 6], [7, 8, 9]] + + At most *maxsplit* splits are done. If *maxsplit* is not specified or -1, + then there is no limit on the number of splits: + + >>> list(split_after(range(10), lambda n: n % 3 == 0, maxsplit=2)) + [[0], [1, 2, 3], [4, 5, 6, 7, 8, 9]] + + """ + if maxsplit == 0: + yield list(iterable) + return + + buf = [] + it = iter(iterable) + for item in it: + buf.append(item) + if pred(item) and buf: + yield buf + if maxsplit == 1: + yield list(it) + return + buf = [] + maxsplit -= 1 + if buf: + yield buf + + +def split_when(iterable, pred, maxsplit=-1): + """Split *iterable* into pieces based on the output of *pred*. + *pred* should be a function that takes successive pairs of items and + returns ``True`` if the iterable should be split in between them. + + For example, to find runs of increasing numbers, split the iterable when + element ``i`` is larger than element ``i + 1``: + + >>> list(split_when([1, 2, 3, 3, 2, 5, 2, 4, 2], lambda x, y: x > y)) + [[1, 2, 3, 3], [2, 5], [2, 4], [2]] + + At most *maxsplit* splits are done. If *maxsplit* is not specified or -1, + then there is no limit on the number of splits: + + >>> list(split_when([1, 2, 3, 3, 2, 5, 2, 4, 2], + ... lambda x, y: x > y, maxsplit=2)) + [[1, 2, 3, 3], [2, 5], [2, 4, 2]] + + """ + if maxsplit == 0: + yield list(iterable) + return + + it = iter(iterable) + try: + cur_item = next(it) + except StopIteration: + return + + buf = [cur_item] + for next_item in it: + if pred(cur_item, next_item): + yield buf + if maxsplit == 1: + yield [next_item] + list(it) + return + buf = [] + maxsplit -= 1 + + buf.append(next_item) + cur_item = next_item + + yield buf + + +def split_into(iterable, sizes): + """Yield a list of sequential items from *iterable* of length 'n' for each + integer 'n' in *sizes*. + + >>> list(split_into([1,2,3,4,5,6], [1,2,3])) + [[1], [2, 3], [4, 5, 6]] + + If the sum of *sizes* is smaller than the length of *iterable*, then the + remaining items of *iterable* will not be returned. + + >>> list(split_into([1,2,3,4,5,6], [2,3])) + [[1, 2], [3, 4, 5]] + + If the sum of *sizes* is larger than the length of *iterable*, fewer items + will be returned in the iteration that overruns *iterable* and further + lists will be empty: + + >>> list(split_into([1,2,3,4], [1,2,3,4])) + [[1], [2, 3], [4], []] + + When a ``None`` object is encountered in *sizes*, the returned list will + contain items up to the end of *iterable* the same way that itertools.slice + does: + + >>> list(split_into([1,2,3,4,5,6,7,8,9,0], [2,3,None])) + [[1, 2], [3, 4, 5], [6, 7, 8, 9, 0]] + + :func:`split_into` can be useful for grouping a series of items where the + sizes of the groups are not uniform. An example would be where in a row + from a table, multiple columns represent elements of the same feature + (e.g. a point represented by x,y,z) but, the format is not the same for + all columns. + """ + # convert the iterable argument into an iterator so its contents can + # be consumed by islice in case it is a generator + it = iter(iterable) + + for size in sizes: + if size is None: + yield list(it) + return + else: + yield list(islice(it, size)) + + +def padded(iterable, fillvalue=None, n=None, next_multiple=False): + """Yield the elements from *iterable*, followed by *fillvalue*, such that + at least *n* items are emitted. + + >>> list(padded([1, 2, 3], '?', 5)) + [1, 2, 3, '?', '?'] + + If *next_multiple* is ``True``, *fillvalue* will be emitted until the + number of items emitted is a multiple of *n*:: + + >>> list(padded([1, 2, 3, 4], n=3, next_multiple=True)) + [1, 2, 3, 4, None, None] + + If *n* is ``None``, *fillvalue* will be emitted indefinitely. + + """ + it = iter(iterable) + if n is None: + yield from chain(it, repeat(fillvalue)) + elif n < 1: + raise ValueError('n must be at least 1') + else: + item_count = 0 + for item in it: + yield item + item_count += 1 + + remaining = (n - item_count) % n if next_multiple else n - item_count + for _ in range(remaining): + yield fillvalue + + +def repeat_each(iterable, n=2): + """Repeat each element in *iterable* *n* times. + + >>> list(repeat_each('ABC', 3)) + ['A', 'A', 'A', 'B', 'B', 'B', 'C', 'C', 'C'] + """ + return chain.from_iterable(map(repeat, iterable, repeat(n))) + + +def repeat_last(iterable, default=None): + """After the *iterable* is exhausted, keep yielding its last element. + + >>> list(islice(repeat_last(range(3)), 5)) + [0, 1, 2, 2, 2] + + If the iterable is empty, yield *default* forever:: + + >>> list(islice(repeat_last(range(0), 42), 5)) + [42, 42, 42, 42, 42] + + """ + item = _marker + for item in iterable: + yield item + final = default if item is _marker else item + yield from repeat(final) + + +def distribute(n, iterable): + """Distribute the items from *iterable* among *n* smaller iterables. + + >>> group_1, group_2 = distribute(2, [1, 2, 3, 4, 5, 6]) + >>> list(group_1) + [1, 3, 5] + >>> list(group_2) + [2, 4, 6] + + If the length of *iterable* is not evenly divisible by *n*, then the + length of the returned iterables will not be identical: + + >>> children = distribute(3, [1, 2, 3, 4, 5, 6, 7]) + >>> [list(c) for c in children] + [[1, 4, 7], [2, 5], [3, 6]] + + If the length of *iterable* is smaller than *n*, then the last returned + iterables will be empty: + + >>> children = distribute(5, [1, 2, 3]) + >>> [list(c) for c in children] + [[1], [2], [3], [], []] + + This function uses :func:`itertools.tee` and may require significant + storage. If you need the order items in the smaller iterables to match the + original iterable, see :func:`divide`. + + """ + if n < 1: + raise ValueError('n must be at least 1') + + children = tee(iterable, n) + return [islice(it, index, None, n) for index, it in enumerate(children)] + + +def stagger(iterable, offsets=(-1, 0, 1), longest=False, fillvalue=None): + """Yield tuples whose elements are offset from *iterable*. + The amount by which the `i`-th item in each tuple is offset is given by + the `i`-th item in *offsets*. + + >>> list(stagger([0, 1, 2, 3])) + [(None, 0, 1), (0, 1, 2), (1, 2, 3)] + >>> list(stagger(range(8), offsets=(0, 2, 4))) + [(0, 2, 4), (1, 3, 5), (2, 4, 6), (3, 5, 7)] + + By default, the sequence will end when the final element of a tuple is the + last item in the iterable. To continue until the first element of a tuple + is the last item in the iterable, set *longest* to ``True``:: + + >>> list(stagger([0, 1, 2, 3], longest=True)) + [(None, 0, 1), (0, 1, 2), (1, 2, 3), (2, 3, None), (3, None, None)] + + By default, ``None`` will be used to replace offsets beyond the end of the + sequence. Specify *fillvalue* to use some other value. + + """ + children = tee(iterable, len(offsets)) + + return zip_offset( + *children, offsets=offsets, longest=longest, fillvalue=fillvalue + ) + + +class UnequalIterablesError(ValueError): + def __init__(self, details=None): + msg = 'Iterables have different lengths' + if details is not None: + msg += (': index 0 has length {}; index {} has length {}').format( + *details + ) + + super().__init__(msg) + + +def _zip_equal_generator(iterables): + for combo in zip_longest(*iterables, fillvalue=_marker): + for val in combo: + if val is _marker: + raise UnequalIterablesError() + yield combo + + +def _zip_equal(*iterables): + # Check whether the iterables are all the same size. + try: + first_size = len(iterables[0]) + for i, it in enumerate(iterables[1:], 1): + size = len(it) + if size != first_size: + break + else: + # If we didn't break out, we can use the built-in zip. + return zip(*iterables) + + # If we did break out, there was a mismatch. + raise UnequalIterablesError(details=(first_size, i, size)) + # If any one of the iterables didn't have a length, start reading + # them until one runs out. + except TypeError: + return _zip_equal_generator(iterables) + + +def zip_equal(*iterables): + """``zip`` the input *iterables* together, but raise + ``UnequalIterablesError`` if they aren't all the same length. + + >>> it_1 = range(3) + >>> it_2 = iter('abc') + >>> list(zip_equal(it_1, it_2)) + [(0, 'a'), (1, 'b'), (2, 'c')] + + >>> it_1 = range(3) + >>> it_2 = iter('abcd') + >>> list(zip_equal(it_1, it_2)) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + more_itertools.more.UnequalIterablesError: Iterables have different + lengths + + """ + if hexversion >= 0x30A00A6: + warnings.warn( + ( + 'zip_equal will be removed in a future version of ' + 'more-itertools. Use the builtin zip function with ' + 'strict=True instead.' + ), + DeprecationWarning, + ) + + return _zip_equal(*iterables) + + +def zip_offset(*iterables, offsets, longest=False, fillvalue=None): + """``zip`` the input *iterables* together, but offset the `i`-th iterable + by the `i`-th item in *offsets*. + + >>> list(zip_offset('0123', 'abcdef', offsets=(0, 1))) + [('0', 'b'), ('1', 'c'), ('2', 'd'), ('3', 'e')] + + This can be used as a lightweight alternative to SciPy or pandas to analyze + data sets in which some series have a lead or lag relationship. + + By default, the sequence will end when the shortest iterable is exhausted. + To continue until the longest iterable is exhausted, set *longest* to + ``True``. + + >>> list(zip_offset('0123', 'abcdef', offsets=(0, 1), longest=True)) + [('0', 'b'), ('1', 'c'), ('2', 'd'), ('3', 'e'), (None, 'f')] + + By default, ``None`` will be used to replace offsets beyond the end of the + sequence. Specify *fillvalue* to use some other value. + + """ + if len(iterables) != len(offsets): + raise ValueError("Number of iterables and offsets didn't match") + + staggered = [] + for it, n in zip(iterables, offsets): + if n < 0: + staggered.append(chain(repeat(fillvalue, -n), it)) + elif n > 0: + staggered.append(islice(it, n, None)) + else: + staggered.append(it) + + if longest: + return zip_longest(*staggered, fillvalue=fillvalue) + + return zip(*staggered) + + +def sort_together(iterables, key_list=(0,), key=None, reverse=False): + """Return the input iterables sorted together, with *key_list* as the + priority for sorting. All iterables are trimmed to the length of the + shortest one. + + This can be used like the sorting function in a spreadsheet. If each + iterable represents a column of data, the key list determines which + columns are used for sorting. + + By default, all iterables are sorted using the ``0``-th iterable:: + + >>> iterables = [(4, 3, 2, 1), ('a', 'b', 'c', 'd')] + >>> sort_together(iterables) + [(1, 2, 3, 4), ('d', 'c', 'b', 'a')] + + Set a different key list to sort according to another iterable. + Specifying multiple keys dictates how ties are broken:: + + >>> iterables = [(3, 1, 2), (0, 1, 0), ('c', 'b', 'a')] + >>> sort_together(iterables, key_list=(1, 2)) + [(2, 3, 1), (0, 0, 1), ('a', 'c', 'b')] + + To sort by a function of the elements of the iterable, pass a *key* + function. Its arguments are the elements of the iterables corresponding to + the key list:: + + >>> names = ('a', 'b', 'c') + >>> lengths = (1, 2, 3) + >>> widths = (5, 2, 1) + >>> def area(length, width): + ... return length * width + >>> sort_together([names, lengths, widths], key_list=(1, 2), key=area) + [('c', 'b', 'a'), (3, 2, 1), (1, 2, 5)] + + Set *reverse* to ``True`` to sort in descending order. + + >>> sort_together([(1, 2, 3), ('c', 'b', 'a')], reverse=True) + [(3, 2, 1), ('a', 'b', 'c')] + + """ + if key is None: + # if there is no key function, the key argument to sorted is an + # itemgetter + key_argument = itemgetter(*key_list) + else: + # if there is a key function, call it with the items at the offsets + # specified by the key function as arguments + key_list = list(key_list) + if len(key_list) == 1: + # if key_list contains a single item, pass the item at that offset + # as the only argument to the key function + key_offset = key_list[0] + key_argument = lambda zipped_items: key(zipped_items[key_offset]) + else: + # if key_list contains multiple items, use itemgetter to return a + # tuple of items, which we pass as *args to the key function + get_key_items = itemgetter(*key_list) + key_argument = lambda zipped_items: key( + *get_key_items(zipped_items) + ) + + return list( + zip(*sorted(zip(*iterables), key=key_argument, reverse=reverse)) + ) + + +def unzip(iterable): + """The inverse of :func:`zip`, this function disaggregates the elements + of the zipped *iterable*. + + The ``i``-th iterable contains the ``i``-th element from each element + of the zipped iterable. The first element is used to to determine the + length of the remaining elements. + + >>> iterable = [('a', 1), ('b', 2), ('c', 3), ('d', 4)] + >>> letters, numbers = unzip(iterable) + >>> list(letters) + ['a', 'b', 'c', 'd'] + >>> list(numbers) + [1, 2, 3, 4] + + This is similar to using ``zip(*iterable)``, but it avoids reading + *iterable* into memory. Note, however, that this function uses + :func:`itertools.tee` and thus may require significant storage. + + """ + head, iterable = spy(iter(iterable)) + if not head: + # empty iterable, e.g. zip([], [], []) + return () + # spy returns a one-length iterable as head + head = head[0] + iterables = tee(iterable, len(head)) + + def itemgetter(i): + def getter(obj): + try: + return obj[i] + except IndexError: + # basically if we have an iterable like + # iter([(1, 2, 3), (4, 5), (6,)]) + # the second unzipped iterable would fail at the third tuple + # since it would try to access tup[1] + # same with the third unzipped iterable and the second tuple + # to support these "improperly zipped" iterables, + # we create a custom itemgetter + # which just stops the unzipped iterables + # at first length mismatch + raise StopIteration + + return getter + + return tuple(map(itemgetter(i), it) for i, it in enumerate(iterables)) + + +def divide(n, iterable): + """Divide the elements from *iterable* into *n* parts, maintaining + order. + + >>> group_1, group_2 = divide(2, [1, 2, 3, 4, 5, 6]) + >>> list(group_1) + [1, 2, 3] + >>> list(group_2) + [4, 5, 6] + + If the length of *iterable* is not evenly divisible by *n*, then the + length of the returned iterables will not be identical: + + >>> children = divide(3, [1, 2, 3, 4, 5, 6, 7]) + >>> [list(c) for c in children] + [[1, 2, 3], [4, 5], [6, 7]] + + If the length of the iterable is smaller than n, then the last returned + iterables will be empty: + + >>> children = divide(5, [1, 2, 3]) + >>> [list(c) for c in children] + [[1], [2], [3], [], []] + + This function will exhaust the iterable before returning and may require + significant storage. If order is not important, see :func:`distribute`, + which does not first pull the iterable into memory. + + """ + if n < 1: + raise ValueError('n must be at least 1') + + try: + iterable[:0] + except TypeError: + seq = tuple(iterable) + else: + seq = iterable + + q, r = divmod(len(seq), n) + + ret = [] + stop = 0 + for i in range(1, n + 1): + start = stop + stop += q + 1 if i <= r else q + ret.append(iter(seq[start:stop])) + + return ret + + +def always_iterable(obj, base_type=(str, bytes)): + """If *obj* is iterable, return an iterator over its items:: + + >>> obj = (1, 2, 3) + >>> list(always_iterable(obj)) + [1, 2, 3] + + If *obj* is not iterable, return a one-item iterable containing *obj*:: + + >>> obj = 1 + >>> list(always_iterable(obj)) + [1] + + If *obj* is ``None``, return an empty iterable: + + >>> obj = None + >>> list(always_iterable(None)) + [] + + By default, binary and text strings are not considered iterable:: + + >>> obj = 'foo' + >>> list(always_iterable(obj)) + ['foo'] + + If *base_type* is set, objects for which ``isinstance(obj, base_type)`` + returns ``True`` won't be considered iterable. + + >>> obj = {'a': 1} + >>> list(always_iterable(obj)) # Iterate over the dict's keys + ['a'] + >>> list(always_iterable(obj, base_type=dict)) # Treat dicts as a unit + [{'a': 1}] + + Set *base_type* to ``None`` to avoid any special handling and treat objects + Python considers iterable as iterable: + + >>> obj = 'foo' + >>> list(always_iterable(obj, base_type=None)) + ['f', 'o', 'o'] + """ + if obj is None: + return iter(()) + + if (base_type is not None) and isinstance(obj, base_type): + return iter((obj,)) + + try: + return iter(obj) + except TypeError: + return iter((obj,)) + + +def adjacent(predicate, iterable, distance=1): + """Return an iterable over `(bool, item)` tuples where the `item` is + drawn from *iterable* and the `bool` indicates whether + that item satisfies the *predicate* or is adjacent to an item that does. + + For example, to find whether items are adjacent to a ``3``:: + + >>> list(adjacent(lambda x: x == 3, range(6))) + [(False, 0), (False, 1), (True, 2), (True, 3), (True, 4), (False, 5)] + + Set *distance* to change what counts as adjacent. For example, to find + whether items are two places away from a ``3``: + + >>> list(adjacent(lambda x: x == 3, range(6), distance=2)) + [(False, 0), (True, 1), (True, 2), (True, 3), (True, 4), (True, 5)] + + This is useful for contextualizing the results of a search function. + For example, a code comparison tool might want to identify lines that + have changed, but also surrounding lines to give the viewer of the diff + context. + + The predicate function will only be called once for each item in the + iterable. + + See also :func:`groupby_transform`, which can be used with this function + to group ranges of items with the same `bool` value. + + """ + # Allow distance=0 mainly for testing that it reproduces results with map() + if distance < 0: + raise ValueError('distance must be at least 0') + + i1, i2 = tee(iterable) + padding = [False] * distance + selected = chain(padding, map(predicate, i1), padding) + adjacent_to_selected = map(any, windowed(selected, 2 * distance + 1)) + return zip(adjacent_to_selected, i2) + + +def groupby_transform(iterable, keyfunc=None, valuefunc=None, reducefunc=None): + """An extension of :func:`itertools.groupby` that can apply transformations + to the grouped data. + + * *keyfunc* is a function computing a key value for each item in *iterable* + * *valuefunc* is a function that transforms the individual items from + *iterable* after grouping + * *reducefunc* is a function that transforms each group of items + + >>> iterable = 'aAAbBBcCC' + >>> keyfunc = lambda k: k.upper() + >>> valuefunc = lambda v: v.lower() + >>> reducefunc = lambda g: ''.join(g) + >>> list(groupby_transform(iterable, keyfunc, valuefunc, reducefunc)) + [('A', 'aaa'), ('B', 'bbb'), ('C', 'ccc')] + + Each optional argument defaults to an identity function if not specified. + + :func:`groupby_transform` is useful when grouping elements of an iterable + using a separate iterable as the key. To do this, :func:`zip` the iterables + and pass a *keyfunc* that extracts the first element and a *valuefunc* + that extracts the second element:: + + >>> from operator import itemgetter + >>> keys = [0, 0, 1, 1, 1, 2, 2, 2, 3] + >>> values = 'abcdefghi' + >>> iterable = zip(keys, values) + >>> grouper = groupby_transform(iterable, itemgetter(0), itemgetter(1)) + >>> [(k, ''.join(g)) for k, g in grouper] + [(0, 'ab'), (1, 'cde'), (2, 'fgh'), (3, 'i')] + + Note that the order of items in the iterable is significant. + Only adjacent items are grouped together, so if you don't want any + duplicate groups, you should sort the iterable by the key function. + + """ + ret = groupby(iterable, keyfunc) + if valuefunc: + ret = ((k, map(valuefunc, g)) for k, g in ret) + if reducefunc: + ret = ((k, reducefunc(g)) for k, g in ret) + + return ret + + +class numeric_range(abc.Sequence, abc.Hashable): + """An extension of the built-in ``range()`` function whose arguments can + be any orderable numeric type. + + With only *stop* specified, *start* defaults to ``0`` and *step* + defaults to ``1``. The output items will match the type of *stop*: + + >>> list(numeric_range(3.5)) + [0.0, 1.0, 2.0, 3.0] + + With only *start* and *stop* specified, *step* defaults to ``1``. The + output items will match the type of *start*: + + >>> from decimal import Decimal + >>> start = Decimal('2.1') + >>> stop = Decimal('5.1') + >>> list(numeric_range(start, stop)) + [Decimal('2.1'), Decimal('3.1'), Decimal('4.1')] + + With *start*, *stop*, and *step* specified the output items will match + the type of ``start + step``: + + >>> from fractions import Fraction + >>> start = Fraction(1, 2) # Start at 1/2 + >>> stop = Fraction(5, 2) # End at 5/2 + >>> step = Fraction(1, 2) # Count by 1/2 + >>> list(numeric_range(start, stop, step)) + [Fraction(1, 2), Fraction(1, 1), Fraction(3, 2), Fraction(2, 1)] + + If *step* is zero, ``ValueError`` is raised. Negative steps are supported: + + >>> list(numeric_range(3, -1, -1.0)) + [3.0, 2.0, 1.0, 0.0] + + Be aware of the limitations of floating point numbers; the representation + of the yielded numbers may be surprising. + + ``datetime.datetime`` objects can be used for *start* and *stop*, if *step* + is a ``datetime.timedelta`` object: + + >>> import datetime + >>> start = datetime.datetime(2019, 1, 1) + >>> stop = datetime.datetime(2019, 1, 3) + >>> step = datetime.timedelta(days=1) + >>> items = iter(numeric_range(start, stop, step)) + >>> next(items) + datetime.datetime(2019, 1, 1, 0, 0) + >>> next(items) + datetime.datetime(2019, 1, 2, 0, 0) + + """ + + _EMPTY_HASH = hash(range(0, 0)) + + def __init__(self, *args): + argc = len(args) + if argc == 1: + (self._stop,) = args + self._start = type(self._stop)(0) + self._step = type(self._stop - self._start)(1) + elif argc == 2: + self._start, self._stop = args + self._step = type(self._stop - self._start)(1) + elif argc == 3: + self._start, self._stop, self._step = args + elif argc == 0: + raise TypeError( + 'numeric_range expected at least ' + '1 argument, got {}'.format(argc) + ) + else: + raise TypeError( + 'numeric_range expected at most ' + '3 arguments, got {}'.format(argc) + ) + + self._zero = type(self._step)(0) + if self._step == self._zero: + raise ValueError('numeric_range() arg 3 must not be zero') + self._growing = self._step > self._zero + self._init_len() + + def __bool__(self): + if self._growing: + return self._start < self._stop + else: + return self._start > self._stop + + def __contains__(self, elem): + if self._growing: + if self._start <= elem < self._stop: + return (elem - self._start) % self._step == self._zero + else: + if self._start >= elem > self._stop: + return (self._start - elem) % (-self._step) == self._zero + + return False + + def __eq__(self, other): + if isinstance(other, numeric_range): + empty_self = not bool(self) + empty_other = not bool(other) + if empty_self or empty_other: + return empty_self and empty_other # True if both empty + else: + return ( + self._start == other._start + and self._step == other._step + and self._get_by_index(-1) == other._get_by_index(-1) + ) + else: + return False + + def __getitem__(self, key): + if isinstance(key, int): + return self._get_by_index(key) + elif isinstance(key, slice): + step = self._step if key.step is None else key.step * self._step + + if key.start is None or key.start <= -self._len: + start = self._start + elif key.start >= self._len: + start = self._stop + else: # -self._len < key.start < self._len + start = self._get_by_index(key.start) + + if key.stop is None or key.stop >= self._len: + stop = self._stop + elif key.stop <= -self._len: + stop = self._start + else: # -self._len < key.stop < self._len + stop = self._get_by_index(key.stop) + + return numeric_range(start, stop, step) + else: + raise TypeError( + 'numeric range indices must be ' + 'integers or slices, not {}'.format(type(key).__name__) + ) + + def __hash__(self): + if self: + return hash((self._start, self._get_by_index(-1), self._step)) + else: + return self._EMPTY_HASH + + def __iter__(self): + values = (self._start + (n * self._step) for n in count()) + if self._growing: + return takewhile(partial(gt, self._stop), values) + else: + return takewhile(partial(lt, self._stop), values) + + def __len__(self): + return self._len + + def _init_len(self): + if self._growing: + start = self._start + stop = self._stop + step = self._step + else: + start = self._stop + stop = self._start + step = -self._step + distance = stop - start + if distance <= self._zero: + self._len = 0 + else: # distance > 0 and step > 0: regular euclidean division + q, r = divmod(distance, step) + self._len = int(q) + int(r != self._zero) + + def __reduce__(self): + return numeric_range, (self._start, self._stop, self._step) + + def __repr__(self): + if self._step == 1: + return "numeric_range({}, {})".format( + repr(self._start), repr(self._stop) + ) + else: + return "numeric_range({}, {}, {})".format( + repr(self._start), repr(self._stop), repr(self._step) + ) + + def __reversed__(self): + return iter( + numeric_range( + self._get_by_index(-1), self._start - self._step, -self._step + ) + ) + + def count(self, value): + return int(value in self) + + def index(self, value): + if self._growing: + if self._start <= value < self._stop: + q, r = divmod(value - self._start, self._step) + if r == self._zero: + return int(q) + else: + if self._start >= value > self._stop: + q, r = divmod(self._start - value, -self._step) + if r == self._zero: + return int(q) + + raise ValueError("{} is not in numeric range".format(value)) + + def _get_by_index(self, i): + if i < 0: + i += self._len + if i < 0 or i >= self._len: + raise IndexError("numeric range object index out of range") + return self._start + i * self._step + + +def count_cycle(iterable, n=None): + """Cycle through the items from *iterable* up to *n* times, yielding + the number of completed cycles along with each item. If *n* is omitted the + process repeats indefinitely. + + >>> list(count_cycle('AB', 3)) + [(0, 'A'), (0, 'B'), (1, 'A'), (1, 'B'), (2, 'A'), (2, 'B')] + + """ + iterable = tuple(iterable) + if not iterable: + return iter(()) + counter = count() if n is None else range(n) + return ((i, item) for i in counter for item in iterable) + + +def mark_ends(iterable): + """Yield 3-tuples of the form ``(is_first, is_last, item)``. + + >>> list(mark_ends('ABC')) + [(True, False, 'A'), (False, False, 'B'), (False, True, 'C')] + + Use this when looping over an iterable to take special action on its first + and/or last items: + + >>> iterable = ['Header', 100, 200, 'Footer'] + >>> total = 0 + >>> for is_first, is_last, item in mark_ends(iterable): + ... if is_first: + ... continue # Skip the header + ... if is_last: + ... continue # Skip the footer + ... total += item + >>> print(total) + 300 + """ + it = iter(iterable) + + try: + b = next(it) + except StopIteration: + return + + try: + for i in count(): + a = b + b = next(it) + yield i == 0, False, a + + except StopIteration: + yield i == 0, True, a + + +def locate(iterable, pred=bool, window_size=None): + """Yield the index of each item in *iterable* for which *pred* returns + ``True``. + + *pred* defaults to :func:`bool`, which will select truthy items: + + >>> list(locate([0, 1, 1, 0, 1, 0, 0])) + [1, 2, 4] + + Set *pred* to a custom function to, e.g., find the indexes for a particular + item. + + >>> list(locate(['a', 'b', 'c', 'b'], lambda x: x == 'b')) + [1, 3] + + If *window_size* is given, then the *pred* function will be called with + that many items. This enables searching for sub-sequences: + + >>> iterable = [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3] + >>> pred = lambda *args: args == (1, 2, 3) + >>> list(locate(iterable, pred=pred, window_size=3)) + [1, 5, 9] + + Use with :func:`seekable` to find indexes and then retrieve the associated + items: + + >>> from itertools import count + >>> from more_itertools import seekable + >>> source = (3 * n + 1 if (n % 2) else n // 2 for n in count()) + >>> it = seekable(source) + >>> pred = lambda x: x > 100 + >>> indexes = locate(it, pred=pred) + >>> i = next(indexes) + >>> it.seek(i) + >>> next(it) + 106 + + """ + if window_size is None: + return compress(count(), map(pred, iterable)) + + if window_size < 1: + raise ValueError('window size must be at least 1') + + it = windowed(iterable, window_size, fillvalue=_marker) + return compress(count(), starmap(pred, it)) + + +def lstrip(iterable, pred): + """Yield the items from *iterable*, but strip any from the beginning + for which *pred* returns ``True``. + + For example, to remove a set of items from the start of an iterable: + + >>> iterable = (None, False, None, 1, 2, None, 3, False, None) + >>> pred = lambda x: x in {None, False, ''} + >>> list(lstrip(iterable, pred)) + [1, 2, None, 3, False, None] + + This function is analogous to to :func:`str.lstrip`, and is essentially + an wrapper for :func:`itertools.dropwhile`. + + """ + return dropwhile(pred, iterable) + + +def rstrip(iterable, pred): + """Yield the items from *iterable*, but strip any from the end + for which *pred* returns ``True``. + + For example, to remove a set of items from the end of an iterable: + + >>> iterable = (None, False, None, 1, 2, None, 3, False, None) + >>> pred = lambda x: x in {None, False, ''} + >>> list(rstrip(iterable, pred)) + [None, False, None, 1, 2, None, 3] + + This function is analogous to :func:`str.rstrip`. + + """ + cache = [] + cache_append = cache.append + cache_clear = cache.clear + for x in iterable: + if pred(x): + cache_append(x) + else: + yield from cache + cache_clear() + yield x + + +def strip(iterable, pred): + """Yield the items from *iterable*, but strip any from the + beginning and end for which *pred* returns ``True``. + + For example, to remove a set of items from both ends of an iterable: + + >>> iterable = (None, False, None, 1, 2, None, 3, False, None) + >>> pred = lambda x: x in {None, False, ''} + >>> list(strip(iterable, pred)) + [1, 2, None, 3] + + This function is analogous to :func:`str.strip`. + + """ + return rstrip(lstrip(iterable, pred), pred) + + +class islice_extended: + """An extension of :func:`itertools.islice` that supports negative values + for *stop*, *start*, and *step*. + + >>> iterable = iter('abcdefgh') + >>> list(islice_extended(iterable, -4, -1)) + ['e', 'f', 'g'] + + Slices with negative values require some caching of *iterable*, but this + function takes care to minimize the amount of memory required. + + For example, you can use a negative step with an infinite iterator: + + >>> from itertools import count + >>> list(islice_extended(count(), 110, 99, -2)) + [110, 108, 106, 104, 102, 100] + + You can also use slice notation directly: + + >>> iterable = map(str, count()) + >>> it = islice_extended(iterable)[10:20:2] + >>> list(it) + ['10', '12', '14', '16', '18'] + + """ + + def __init__(self, iterable, *args): + it = iter(iterable) + if args: + self._iterable = _islice_helper(it, slice(*args)) + else: + self._iterable = it + + def __iter__(self): + return self + + def __next__(self): + return next(self._iterable) + + def __getitem__(self, key): + if isinstance(key, slice): + return islice_extended(_islice_helper(self._iterable, key)) + + raise TypeError('islice_extended.__getitem__ argument must be a slice') + + +def _islice_helper(it, s): + start = s.start + stop = s.stop + if s.step == 0: + raise ValueError('step argument must be a non-zero integer or None.') + step = s.step or 1 + + if step > 0: + start = 0 if (start is None) else start + + if start < 0: + # Consume all but the last -start items + cache = deque(enumerate(it, 1), maxlen=-start) + len_iter = cache[-1][0] if cache else 0 + + # Adjust start to be positive + i = max(len_iter + start, 0) + + # Adjust stop to be positive + if stop is None: + j = len_iter + elif stop >= 0: + j = min(stop, len_iter) + else: + j = max(len_iter + stop, 0) + + # Slice the cache + n = j - i + if n <= 0: + return + + for index, item in islice(cache, 0, n, step): + yield item + elif (stop is not None) and (stop < 0): + # Advance to the start position + next(islice(it, start, start), None) + + # When stop is negative, we have to carry -stop items while + # iterating + cache = deque(islice(it, -stop), maxlen=-stop) + + for index, item in enumerate(it): + cached_item = cache.popleft() + if index % step == 0: + yield cached_item + cache.append(item) + else: + # When both start and stop are positive we have the normal case + yield from islice(it, start, stop, step) + else: + start = -1 if (start is None) else start + + if (stop is not None) and (stop < 0): + # Consume all but the last items + n = -stop - 1 + cache = deque(enumerate(it, 1), maxlen=n) + len_iter = cache[-1][0] if cache else 0 + + # If start and stop are both negative they are comparable and + # we can just slice. Otherwise we can adjust start to be negative + # and then slice. + if start < 0: + i, j = start, stop + else: + i, j = min(start - len_iter, -1), None + + for index, item in list(cache)[i:j:step]: + yield item + else: + # Advance to the stop position + if stop is not None: + m = stop + 1 + next(islice(it, m, m), None) + + # stop is positive, so if start is negative they are not comparable + # and we need the rest of the items. + if start < 0: + i = start + n = None + # stop is None and start is positive, so we just need items up to + # the start index. + elif stop is None: + i = None + n = start + 1 + # Both stop and start are positive, so they are comparable. + else: + i = None + n = start - stop + if n <= 0: + return + + cache = list(islice(it, n)) + + yield from cache[i::step] + + +def always_reversible(iterable): + """An extension of :func:`reversed` that supports all iterables, not + just those which implement the ``Reversible`` or ``Sequence`` protocols. + + >>> print(*always_reversible(x for x in range(3))) + 2 1 0 + + If the iterable is already reversible, this function returns the + result of :func:`reversed()`. If the iterable is not reversible, + this function will cache the remaining items in the iterable and + yield them in reverse order, which may require significant storage. + """ + try: + return reversed(iterable) + except TypeError: + return reversed(list(iterable)) + + +def consecutive_groups(iterable, ordering=lambda x: x): + """Yield groups of consecutive items using :func:`itertools.groupby`. + The *ordering* function determines whether two items are adjacent by + returning their position. + + By default, the ordering function is the identity function. This is + suitable for finding runs of numbers: + + >>> iterable = [1, 10, 11, 12, 20, 30, 31, 32, 33, 40] + >>> for group in consecutive_groups(iterable): + ... print(list(group)) + [1] + [10, 11, 12] + [20] + [30, 31, 32, 33] + [40] + + For finding runs of adjacent letters, try using the :meth:`index` method + of a string of letters: + + >>> from string import ascii_lowercase + >>> iterable = 'abcdfgilmnop' + >>> ordering = ascii_lowercase.index + >>> for group in consecutive_groups(iterable, ordering): + ... print(list(group)) + ['a', 'b', 'c', 'd'] + ['f', 'g'] + ['i'] + ['l', 'm', 'n', 'o', 'p'] + + Each group of consecutive items is an iterator that shares it source with + *iterable*. When an an output group is advanced, the previous group is + no longer available unless its elements are copied (e.g., into a ``list``). + + >>> iterable = [1, 2, 11, 12, 21, 22] + >>> saved_groups = [] + >>> for group in consecutive_groups(iterable): + ... saved_groups.append(list(group)) # Copy group elements + >>> saved_groups + [[1, 2], [11, 12], [21, 22]] + + """ + for k, g in groupby( + enumerate(iterable), key=lambda x: x[0] - ordering(x[1]) + ): + yield map(itemgetter(1), g) + + +def difference(iterable, func=sub, *, initial=None): + """This function is the inverse of :func:`itertools.accumulate`. By default + it will compute the first difference of *iterable* using + :func:`operator.sub`: + + >>> from itertools import accumulate + >>> iterable = accumulate([0, 1, 2, 3, 4]) # produces 0, 1, 3, 6, 10 + >>> list(difference(iterable)) + [0, 1, 2, 3, 4] + + *func* defaults to :func:`operator.sub`, but other functions can be + specified. They will be applied as follows:: + + A, B, C, D, ... --> A, func(B, A), func(C, B), func(D, C), ... + + For example, to do progressive division: + + >>> iterable = [1, 2, 6, 24, 120] + >>> func = lambda x, y: x // y + >>> list(difference(iterable, func)) + [1, 2, 3, 4, 5] + + If the *initial* keyword is set, the first element will be skipped when + computing successive differences. + + >>> it = [10, 11, 13, 16] # from accumulate([1, 2, 3], initial=10) + >>> list(difference(it, initial=10)) + [1, 2, 3] + + """ + a, b = tee(iterable) + try: + first = [next(b)] + except StopIteration: + return iter([]) + + if initial is not None: + first = [] + + return chain(first, starmap(func, zip(b, a))) + + +class SequenceView(Sequence): + """Return a read-only view of the sequence object *target*. + + :class:`SequenceView` objects are analogous to Python's built-in + "dictionary view" types. They provide a dynamic view of a sequence's items, + meaning that when the sequence updates, so does the view. + + >>> seq = ['0', '1', '2'] + >>> view = SequenceView(seq) + >>> view + SequenceView(['0', '1', '2']) + >>> seq.append('3') + >>> view + SequenceView(['0', '1', '2', '3']) + + Sequence views support indexing, slicing, and length queries. They act + like the underlying sequence, except they don't allow assignment: + + >>> view[1] + '1' + >>> view[1:-1] + ['1', '2'] + >>> len(view) + 4 + + Sequence views are useful as an alternative to copying, as they don't + require (much) extra storage. + + """ + + def __init__(self, target): + if not isinstance(target, Sequence): + raise TypeError + self._target = target + + def __getitem__(self, index): + return self._target[index] + + def __len__(self): + return len(self._target) + + def __repr__(self): + return '{}({})'.format(self.__class__.__name__, repr(self._target)) + + +class seekable: + """Wrap an iterator to allow for seeking backward and forward. This + progressively caches the items in the source iterable so they can be + re-visited. + + Call :meth:`seek` with an index to seek to that position in the source + iterable. + + To "reset" an iterator, seek to ``0``: + + >>> from itertools import count + >>> it = seekable((str(n) for n in count())) + >>> next(it), next(it), next(it) + ('0', '1', '2') + >>> it.seek(0) + >>> next(it), next(it), next(it) + ('0', '1', '2') + >>> next(it) + '3' + + You can also seek forward: + + >>> it = seekable((str(n) for n in range(20))) + >>> it.seek(10) + >>> next(it) + '10' + >>> it.seek(20) # Seeking past the end of the source isn't a problem + >>> list(it) + [] + >>> it.seek(0) # Resetting works even after hitting the end + >>> next(it), next(it), next(it) + ('0', '1', '2') + + Call :meth:`peek` to look ahead one item without advancing the iterator: + + >>> it = seekable('1234') + >>> it.peek() + '1' + >>> list(it) + ['1', '2', '3', '4'] + >>> it.peek(default='empty') + 'empty' + + Before the iterator is at its end, calling :func:`bool` on it will return + ``True``. After it will return ``False``: + + >>> it = seekable('5678') + >>> bool(it) + True + >>> list(it) + ['5', '6', '7', '8'] + >>> bool(it) + False + + You may view the contents of the cache with the :meth:`elements` method. + That returns a :class:`SequenceView`, a view that updates automatically: + + >>> it = seekable((str(n) for n in range(10))) + >>> next(it), next(it), next(it) + ('0', '1', '2') + >>> elements = it.elements() + >>> elements + SequenceView(['0', '1', '2']) + >>> next(it) + '3' + >>> elements + SequenceView(['0', '1', '2', '3']) + + By default, the cache grows as the source iterable progresses, so beware of + wrapping very large or infinite iterables. Supply *maxlen* to limit the + size of the cache (this of course limits how far back you can seek). + + >>> from itertools import count + >>> it = seekable((str(n) for n in count()), maxlen=2) + >>> next(it), next(it), next(it), next(it) + ('0', '1', '2', '3') + >>> list(it.elements()) + ['2', '3'] + >>> it.seek(0) + >>> next(it), next(it), next(it), next(it) + ('2', '3', '4', '5') + >>> next(it) + '6' + + """ + + def __init__(self, iterable, maxlen=None): + self._source = iter(iterable) + if maxlen is None: + self._cache = [] + else: + self._cache = deque([], maxlen) + self._index = None + + def __iter__(self): + return self + + def __next__(self): + if self._index is not None: + try: + item = self._cache[self._index] + except IndexError: + self._index = None + else: + self._index += 1 + return item + + item = next(self._source) + self._cache.append(item) + return item + + def __bool__(self): + try: + self.peek() + except StopIteration: + return False + return True + + def peek(self, default=_marker): + try: + peeked = next(self) + except StopIteration: + if default is _marker: + raise + return default + if self._index is None: + self._index = len(self._cache) + self._index -= 1 + return peeked + + def elements(self): + return SequenceView(self._cache) + + def seek(self, index): + self._index = index + remainder = index - len(self._cache) + if remainder > 0: + consume(self, remainder) + + +class run_length: + """ + :func:`run_length.encode` compresses an iterable with run-length encoding. + It yields groups of repeated items with the count of how many times they + were repeated: + + >>> uncompressed = 'abbcccdddd' + >>> list(run_length.encode(uncompressed)) + [('a', 1), ('b', 2), ('c', 3), ('d', 4)] + + :func:`run_length.decode` decompresses an iterable that was previously + compressed with run-length encoding. It yields the items of the + decompressed iterable: + + >>> compressed = [('a', 1), ('b', 2), ('c', 3), ('d', 4)] + >>> list(run_length.decode(compressed)) + ['a', 'b', 'b', 'c', 'c', 'c', 'd', 'd', 'd', 'd'] + + """ + + @staticmethod + def encode(iterable): + return ((k, ilen(g)) for k, g in groupby(iterable)) + + @staticmethod + def decode(iterable): + return chain.from_iterable(repeat(k, n) for k, n in iterable) + + +def exactly_n(iterable, n, predicate=bool): + """Return ``True`` if exactly ``n`` items in the iterable are ``True`` + according to the *predicate* function. + + >>> exactly_n([True, True, False], 2) + True + >>> exactly_n([True, True, False], 1) + False + >>> exactly_n([0, 1, 2, 3, 4, 5], 3, lambda x: x < 3) + True + + The iterable will be advanced until ``n + 1`` truthy items are encountered, + so avoid calling it on infinite iterables. + + """ + return len(take(n + 1, filter(predicate, iterable))) == n + + +def circular_shifts(iterable): + """Return a list of circular shifts of *iterable*. + + >>> circular_shifts(range(4)) + [(0, 1, 2, 3), (1, 2, 3, 0), (2, 3, 0, 1), (3, 0, 1, 2)] + """ + lst = list(iterable) + return take(len(lst), windowed(cycle(lst), len(lst))) + + +def make_decorator(wrapping_func, result_index=0): + """Return a decorator version of *wrapping_func*, which is a function that + modifies an iterable. *result_index* is the position in that function's + signature where the iterable goes. + + This lets you use itertools on the "production end," i.e. at function + definition. This can augment what the function returns without changing the + function's code. + + For example, to produce a decorator version of :func:`chunked`: + + >>> from more_itertools import chunked + >>> chunker = make_decorator(chunked, result_index=0) + >>> @chunker(3) + ... def iter_range(n): + ... return iter(range(n)) + ... + >>> list(iter_range(9)) + [[0, 1, 2], [3, 4, 5], [6, 7, 8]] + + To only allow truthy items to be returned: + + >>> truth_serum = make_decorator(filter, result_index=1) + >>> @truth_serum(bool) + ... def boolean_test(): + ... return [0, 1, '', ' ', False, True] + ... + >>> list(boolean_test()) + [1, ' ', True] + + The :func:`peekable` and :func:`seekable` wrappers make for practical + decorators: + + >>> from more_itertools import peekable + >>> peekable_function = make_decorator(peekable) + >>> @peekable_function() + ... def str_range(*args): + ... return (str(x) for x in range(*args)) + ... + >>> it = str_range(1, 20, 2) + >>> next(it), next(it), next(it) + ('1', '3', '5') + >>> it.peek() + '7' + >>> next(it) + '7' + + """ + # See https://sites.google.com/site/bbayles/index/decorator_factory for + # notes on how this works. + def decorator(*wrapping_args, **wrapping_kwargs): + def outer_wrapper(f): + def inner_wrapper(*args, **kwargs): + result = f(*args, **kwargs) + wrapping_args_ = list(wrapping_args) + wrapping_args_.insert(result_index, result) + return wrapping_func(*wrapping_args_, **wrapping_kwargs) + + return inner_wrapper + + return outer_wrapper + + return decorator + + +def map_reduce(iterable, keyfunc, valuefunc=None, reducefunc=None): + """Return a dictionary that maps the items in *iterable* to categories + defined by *keyfunc*, transforms them with *valuefunc*, and + then summarizes them by category with *reducefunc*. + + *valuefunc* defaults to the identity function if it is unspecified. + If *reducefunc* is unspecified, no summarization takes place: + + >>> keyfunc = lambda x: x.upper() + >>> result = map_reduce('abbccc', keyfunc) + >>> sorted(result.items()) + [('A', ['a']), ('B', ['b', 'b']), ('C', ['c', 'c', 'c'])] + + Specifying *valuefunc* transforms the categorized items: + + >>> keyfunc = lambda x: x.upper() + >>> valuefunc = lambda x: 1 + >>> result = map_reduce('abbccc', keyfunc, valuefunc) + >>> sorted(result.items()) + [('A', [1]), ('B', [1, 1]), ('C', [1, 1, 1])] + + Specifying *reducefunc* summarizes the categorized items: + + >>> keyfunc = lambda x: x.upper() + >>> valuefunc = lambda x: 1 + >>> reducefunc = sum + >>> result = map_reduce('abbccc', keyfunc, valuefunc, reducefunc) + >>> sorted(result.items()) + [('A', 1), ('B', 2), ('C', 3)] + + You may want to filter the input iterable before applying the map/reduce + procedure: + + >>> all_items = range(30) + >>> items = [x for x in all_items if 10 <= x <= 20] # Filter + >>> keyfunc = lambda x: x % 2 # Evens map to 0; odds to 1 + >>> categories = map_reduce(items, keyfunc=keyfunc) + >>> sorted(categories.items()) + [(0, [10, 12, 14, 16, 18, 20]), (1, [11, 13, 15, 17, 19])] + >>> summaries = map_reduce(items, keyfunc=keyfunc, reducefunc=sum) + >>> sorted(summaries.items()) + [(0, 90), (1, 75)] + + Note that all items in the iterable are gathered into a list before the + summarization step, which may require significant storage. + + The returned object is a :obj:`collections.defaultdict` with the + ``default_factory`` set to ``None``, such that it behaves like a normal + dictionary. + + """ + valuefunc = (lambda x: x) if (valuefunc is None) else valuefunc + + ret = defaultdict(list) + for item in iterable: + key = keyfunc(item) + value = valuefunc(item) + ret[key].append(value) + + if reducefunc is not None: + for key, value_list in ret.items(): + ret[key] = reducefunc(value_list) + + ret.default_factory = None + return ret + + +def rlocate(iterable, pred=bool, window_size=None): + """Yield the index of each item in *iterable* for which *pred* returns + ``True``, starting from the right and moving left. + + *pred* defaults to :func:`bool`, which will select truthy items: + + >>> list(rlocate([0, 1, 1, 0, 1, 0, 0])) # Truthy at 1, 2, and 4 + [4, 2, 1] + + Set *pred* to a custom function to, e.g., find the indexes for a particular + item: + + >>> iterable = iter('abcb') + >>> pred = lambda x: x == 'b' + >>> list(rlocate(iterable, pred)) + [3, 1] + + If *window_size* is given, then the *pred* function will be called with + that many items. This enables searching for sub-sequences: + + >>> iterable = [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3] + >>> pred = lambda *args: args == (1, 2, 3) + >>> list(rlocate(iterable, pred=pred, window_size=3)) + [9, 5, 1] + + Beware, this function won't return anything for infinite iterables. + If *iterable* is reversible, ``rlocate`` will reverse it and search from + the right. Otherwise, it will search from the left and return the results + in reverse order. + + See :func:`locate` to for other example applications. + + """ + if window_size is None: + try: + len_iter = len(iterable) + return (len_iter - i - 1 for i in locate(reversed(iterable), pred)) + except TypeError: + pass + + return reversed(list(locate(iterable, pred, window_size))) + + +def replace(iterable, pred, substitutes, count=None, window_size=1): + """Yield the items from *iterable*, replacing the items for which *pred* + returns ``True`` with the items from the iterable *substitutes*. + + >>> iterable = [1, 1, 0, 1, 1, 0, 1, 1] + >>> pred = lambda x: x == 0 + >>> substitutes = (2, 3) + >>> list(replace(iterable, pred, substitutes)) + [1, 1, 2, 3, 1, 1, 2, 3, 1, 1] + + If *count* is given, the number of replacements will be limited: + + >>> iterable = [1, 1, 0, 1, 1, 0, 1, 1, 0] + >>> pred = lambda x: x == 0 + >>> substitutes = [None] + >>> list(replace(iterable, pred, substitutes, count=2)) + [1, 1, None, 1, 1, None, 1, 1, 0] + + Use *window_size* to control the number of items passed as arguments to + *pred*. This allows for locating and replacing subsequences. + + >>> iterable = [0, 1, 2, 5, 0, 1, 2, 5] + >>> window_size = 3 + >>> pred = lambda *args: args == (0, 1, 2) # 3 items passed to pred + >>> substitutes = [3, 4] # Splice in these items + >>> list(replace(iterable, pred, substitutes, window_size=window_size)) + [3, 4, 5, 3, 4, 5] + + """ + if window_size < 1: + raise ValueError('window_size must be at least 1') + + # Save the substitutes iterable, since it's used more than once + substitutes = tuple(substitutes) + + # Add padding such that the number of windows matches the length of the + # iterable + it = chain(iterable, [_marker] * (window_size - 1)) + windows = windowed(it, window_size) + + n = 0 + for w in windows: + # If the current window matches our predicate (and we haven't hit + # our maximum number of replacements), splice in the substitutes + # and then consume the following windows that overlap with this one. + # For example, if the iterable is (0, 1, 2, 3, 4...) + # and the window size is 2, we have (0, 1), (1, 2), (2, 3)... + # If the predicate matches on (0, 1), we need to zap (0, 1) and (1, 2) + if pred(*w): + if (count is None) or (n < count): + n += 1 + yield from substitutes + consume(windows, window_size - 1) + continue + + # If there was no match (or we've reached the replacement limit), + # yield the first item from the window. + if w and (w[0] is not _marker): + yield w[0] + + +def partitions(iterable): + """Yield all possible order-preserving partitions of *iterable*. + + >>> iterable = 'abc' + >>> for part in partitions(iterable): + ... print([''.join(p) for p in part]) + ['abc'] + ['a', 'bc'] + ['ab', 'c'] + ['a', 'b', 'c'] + + This is unrelated to :func:`partition`. + + """ + sequence = list(iterable) + n = len(sequence) + for i in powerset(range(1, n)): + yield [sequence[i:j] for i, j in zip((0,) + i, i + (n,))] + + +def set_partitions(iterable, k=None): + """ + Yield the set partitions of *iterable* into *k* parts. Set partitions are + not order-preserving. + + >>> iterable = 'abc' + >>> for part in set_partitions(iterable, 2): + ... print([''.join(p) for p in part]) + ['a', 'bc'] + ['ab', 'c'] + ['b', 'ac'] + + + If *k* is not given, every set partition is generated. + + >>> iterable = 'abc' + >>> for part in set_partitions(iterable): + ... print([''.join(p) for p in part]) + ['abc'] + ['a', 'bc'] + ['ab', 'c'] + ['b', 'ac'] + ['a', 'b', 'c'] + + """ + L = list(iterable) + n = len(L) + if k is not None: + if k < 1: + raise ValueError( + "Can't partition in a negative or zero number of groups" + ) + elif k > n: + return + + def set_partitions_helper(L, k): + n = len(L) + if k == 1: + yield [L] + elif n == k: + yield [[s] for s in L] + else: + e, *M = L + for p in set_partitions_helper(M, k - 1): + yield [[e], *p] + for p in set_partitions_helper(M, k): + for i in range(len(p)): + yield p[:i] + [[e] + p[i]] + p[i + 1 :] + + if k is None: + for k in range(1, n + 1): + yield from set_partitions_helper(L, k) + else: + yield from set_partitions_helper(L, k) + + +class time_limited: + """ + Yield items from *iterable* until *limit_seconds* have passed. + If the time limit expires before all items have been yielded, the + ``timed_out`` parameter will be set to ``True``. + + >>> from time import sleep + >>> def generator(): + ... yield 1 + ... yield 2 + ... sleep(0.2) + ... yield 3 + >>> iterable = time_limited(0.1, generator()) + >>> list(iterable) + [1, 2] + >>> iterable.timed_out + True + + Note that the time is checked before each item is yielded, and iteration + stops if the time elapsed is greater than *limit_seconds*. If your time + limit is 1 second, but it takes 2 seconds to generate the first item from + the iterable, the function will run for 2 seconds and not yield anything. + + """ + + def __init__(self, limit_seconds, iterable): + if limit_seconds < 0: + raise ValueError('limit_seconds must be positive') + self.limit_seconds = limit_seconds + self._iterable = iter(iterable) + self._start_time = monotonic() + self.timed_out = False + + def __iter__(self): + return self + + def __next__(self): + item = next(self._iterable) + if monotonic() - self._start_time > self.limit_seconds: + self.timed_out = True + raise StopIteration + + return item + + +def only(iterable, default=None, too_long=None): + """If *iterable* has only one item, return it. + If it has zero items, return *default*. + If it has more than one item, raise the exception given by *too_long*, + which is ``ValueError`` by default. + + >>> only([], default='missing') + 'missing' + >>> only([1]) + 1 + >>> only([1, 2]) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ValueError: Expected exactly one item in iterable, but got 1, 2, + and perhaps more.' + >>> only([1, 2], too_long=TypeError) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + TypeError + + Note that :func:`only` attempts to advance *iterable* twice to ensure there + is only one item. See :func:`spy` or :func:`peekable` to check + iterable contents less destructively. + """ + it = iter(iterable) + first_value = next(it, default) + + try: + second_value = next(it) + except StopIteration: + pass + else: + msg = ( + 'Expected exactly one item in iterable, but got {!r}, {!r}, ' + 'and perhaps more.'.format(first_value, second_value) + ) + raise too_long or ValueError(msg) + + return first_value + + +def ichunked(iterable, n): + """Break *iterable* into sub-iterables with *n* elements each. + :func:`ichunked` is like :func:`chunked`, but it yields iterables + instead of lists. + + If the sub-iterables are read in order, the elements of *iterable* + won't be stored in memory. + If they are read out of order, :func:`itertools.tee` is used to cache + elements as necessary. + + >>> from itertools import count + >>> all_chunks = ichunked(count(), 4) + >>> c_1, c_2, c_3 = next(all_chunks), next(all_chunks), next(all_chunks) + >>> list(c_2) # c_1's elements have been cached; c_3's haven't been + [4, 5, 6, 7] + >>> list(c_1) + [0, 1, 2, 3] + >>> list(c_3) + [8, 9, 10, 11] + + """ + source = iter(iterable) + + while True: + # Check to see whether we're at the end of the source iterable + item = next(source, _marker) + if item is _marker: + return + + # Clone the source and yield an n-length slice + source, it = tee(chain([item], source)) + yield islice(it, n) + + # Advance the source iterable + consume(source, n) + + +def distinct_combinations(iterable, r): + """Yield the distinct combinations of *r* items taken from *iterable*. + + >>> list(distinct_combinations([0, 0, 1], 2)) + [(0, 0), (0, 1)] + + Equivalent to ``set(combinations(iterable))``, except duplicates are not + generated and thrown away. For larger input sequences this is much more + efficient. + + """ + if r < 0: + raise ValueError('r must be non-negative') + elif r == 0: + yield () + return + pool = tuple(iterable) + generators = [unique_everseen(enumerate(pool), key=itemgetter(1))] + current_combo = [None] * r + level = 0 + while generators: + try: + cur_idx, p = next(generators[-1]) + except StopIteration: + generators.pop() + level -= 1 + continue + current_combo[level] = p + if level + 1 == r: + yield tuple(current_combo) + else: + generators.append( + unique_everseen( + enumerate(pool[cur_idx + 1 :], cur_idx + 1), + key=itemgetter(1), + ) + ) + level += 1 + + +def filter_except(validator, iterable, *exceptions): + """Yield the items from *iterable* for which the *validator* function does + not raise one of the specified *exceptions*. + + *validator* is called for each item in *iterable*. + It should be a function that accepts one argument and raises an exception + if that item is not valid. + + >>> iterable = ['1', '2', 'three', '4', None] + >>> list(filter_except(int, iterable, ValueError, TypeError)) + ['1', '2', '4'] + + If an exception other than one given by *exceptions* is raised by + *validator*, it is raised like normal. + """ + for item in iterable: + try: + validator(item) + except exceptions: + pass + else: + yield item + + +def map_except(function, iterable, *exceptions): + """Transform each item from *iterable* with *function* and yield the + result, unless *function* raises one of the specified *exceptions*. + + *function* is called to transform each item in *iterable*. + It should accept one argument. + + >>> iterable = ['1', '2', 'three', '4', None] + >>> list(map_except(int, iterable, ValueError, TypeError)) + [1, 2, 4] + + If an exception other than one given by *exceptions* is raised by + *function*, it is raised like normal. + """ + for item in iterable: + try: + yield function(item) + except exceptions: + pass + + +def map_if(iterable, pred, func, func_else=lambda x: x): + """Evaluate each item from *iterable* using *pred*. If the result is + equivalent to ``True``, transform the item with *func* and yield it. + Otherwise, transform the item with *func_else* and yield it. + + *pred*, *func*, and *func_else* should each be functions that accept + one argument. By default, *func_else* is the identity function. + + >>> from math import sqrt + >>> iterable = list(range(-5, 5)) + >>> iterable + [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4] + >>> list(map_if(iterable, lambda x: x > 3, lambda x: 'toobig')) + [-5, -4, -3, -2, -1, 0, 1, 2, 3, 'toobig'] + >>> list(map_if(iterable, lambda x: x >= 0, + ... lambda x: f'{sqrt(x):.2f}', lambda x: None)) + [None, None, None, None, None, '0.00', '1.00', '1.41', '1.73', '2.00'] + """ + for item in iterable: + yield func(item) if pred(item) else func_else(item) + + +def _sample_unweighted(iterable, k): + # Implementation of "Algorithm L" from the 1994 paper by Kim-Hung Li: + # "Reservoir-Sampling Algorithms of Time Complexity O(n(1+log(N/n)))". + + # Fill up the reservoir (collection of samples) with the first `k` samples + reservoir = take(k, iterable) + + # Generate random number that's the largest in a sample of k U(0,1) numbers + # Largest order statistic: https://en.wikipedia.org/wiki/Order_statistic + W = exp(log(random()) / k) + + # The number of elements to skip before changing the reservoir is a random + # number with a geometric distribution. Sample it using random() and logs. + next_index = k + floor(log(random()) / log(1 - W)) + + for index, element in enumerate(iterable, k): + + if index == next_index: + reservoir[randrange(k)] = element + # The new W is the largest in a sample of k U(0, `old_W`) numbers + W *= exp(log(random()) / k) + next_index += floor(log(random()) / log(1 - W)) + 1 + + return reservoir + + +def _sample_weighted(iterable, k, weights): + # Implementation of "A-ExpJ" from the 2006 paper by Efraimidis et al. : + # "Weighted random sampling with a reservoir". + + # Log-transform for numerical stability for weights that are small/large + weight_keys = (log(random()) / weight for weight in weights) + + # Fill up the reservoir (collection of samples) with the first `k` + # weight-keys and elements, then heapify the list. + reservoir = take(k, zip(weight_keys, iterable)) + heapify(reservoir) + + # The number of jumps before changing the reservoir is a random variable + # with an exponential distribution. Sample it using random() and logs. + smallest_weight_key, _ = reservoir[0] + weights_to_skip = log(random()) / smallest_weight_key + + for weight, element in zip(weights, iterable): + if weight >= weights_to_skip: + # The notation here is consistent with the paper, but we store + # the weight-keys in log-space for better numerical stability. + smallest_weight_key, _ = reservoir[0] + t_w = exp(weight * smallest_weight_key) + r_2 = uniform(t_w, 1) # generate U(t_w, 1) + weight_key = log(r_2) / weight + heapreplace(reservoir, (weight_key, element)) + smallest_weight_key, _ = reservoir[0] + weights_to_skip = log(random()) / smallest_weight_key + else: + weights_to_skip -= weight + + # Equivalent to [element for weight_key, element in sorted(reservoir)] + return [heappop(reservoir)[1] for _ in range(k)] + + +def sample(iterable, k, weights=None): + """Return a *k*-length list of elements chosen (without replacement) + from the *iterable*. Like :func:`random.sample`, but works on iterables + of unknown length. + + >>> iterable = range(100) + >>> sample(iterable, 5) # doctest: +SKIP + [81, 60, 96, 16, 4] + + An iterable with *weights* may also be given: + + >>> iterable = range(100) + >>> weights = (i * i + 1 for i in range(100)) + >>> sampled = sample(iterable, 5, weights=weights) # doctest: +SKIP + [79, 67, 74, 66, 78] + + The algorithm can also be used to generate weighted random permutations. + The relative weight of each item determines the probability that it + appears late in the permutation. + + >>> data = "abcdefgh" + >>> weights = range(1, len(data) + 1) + >>> sample(data, k=len(data), weights=weights) # doctest: +SKIP + ['c', 'a', 'b', 'e', 'g', 'd', 'h', 'f'] + """ + if k == 0: + return [] + + iterable = iter(iterable) + if weights is None: + return _sample_unweighted(iterable, k) + else: + weights = iter(weights) + return _sample_weighted(iterable, k, weights) + + +def is_sorted(iterable, key=None, reverse=False, strict=False): + """Returns ``True`` if the items of iterable are in sorted order, and + ``False`` otherwise. *key* and *reverse* have the same meaning that they do + in the built-in :func:`sorted` function. + + >>> is_sorted(['1', '2', '3', '4', '5'], key=int) + True + >>> is_sorted([5, 4, 3, 1, 2], reverse=True) + False + + If *strict*, tests for strict sorting, that is, returns ``False`` if equal + elements are found: + + >>> is_sorted([1, 2, 2]) + True + >>> is_sorted([1, 2, 2], strict=True) + False + + The function returns ``False`` after encountering the first out-of-order + item. If there are no out-of-order items, the iterable is exhausted. + """ + + compare = (le if reverse else ge) if strict else (lt if reverse else gt) + it = iterable if key is None else map(key, iterable) + return not any(starmap(compare, pairwise(it))) + + +class AbortThread(BaseException): + pass + + +class callback_iter: + """Convert a function that uses callbacks to an iterator. + + Let *func* be a function that takes a `callback` keyword argument. + For example: + + >>> def func(callback=None): + ... for i, c in [(1, 'a'), (2, 'b'), (3, 'c')]: + ... if callback: + ... callback(i, c) + ... return 4 + + + Use ``with callback_iter(func)`` to get an iterator over the parameters + that are delivered to the callback. + + >>> with callback_iter(func) as it: + ... for args, kwargs in it: + ... print(args) + (1, 'a') + (2, 'b') + (3, 'c') + + The function will be called in a background thread. The ``done`` property + indicates whether it has completed execution. + + >>> it.done + True + + If it completes successfully, its return value will be available + in the ``result`` property. + + >>> it.result + 4 + + Notes: + + * If the function uses some keyword argument besides ``callback``, supply + *callback_kwd*. + * If it finished executing, but raised an exception, accessing the + ``result`` property will raise the same exception. + * If it hasn't finished executing, accessing the ``result`` + property from within the ``with`` block will raise ``RuntimeError``. + * If it hasn't finished executing, accessing the ``result`` property from + outside the ``with`` block will raise a + ``more_itertools.AbortThread`` exception. + * Provide *wait_seconds* to adjust how frequently the it is polled for + output. + + """ + + def __init__(self, func, callback_kwd='callback', wait_seconds=0.1): + self._func = func + self._callback_kwd = callback_kwd + self._aborted = False + self._future = None + self._wait_seconds = wait_seconds + self._executor = __import__("concurrent.futures").futures.ThreadPoolExecutor(max_workers=1) + self._iterator = self._reader() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self._aborted = True + self._executor.shutdown() + + def __iter__(self): + return self + + def __next__(self): + return next(self._iterator) + + @property + def done(self): + if self._future is None: + return False + return self._future.done() + + @property + def result(self): + if not self.done: + raise RuntimeError('Function has not yet completed') + + return self._future.result() + + def _reader(self): + q = Queue() + + def callback(*args, **kwargs): + if self._aborted: + raise AbortThread('canceled by user') + + q.put((args, kwargs)) + + self._future = self._executor.submit( + self._func, **{self._callback_kwd: callback} + ) + + while True: + try: + item = q.get(timeout=self._wait_seconds) + except Empty: + pass + else: + q.task_done() + yield item + + if self._future.done(): + break + + remaining = [] + while True: + try: + item = q.get_nowait() + except Empty: + break + else: + q.task_done() + remaining.append(item) + q.join() + yield from remaining + + +def windowed_complete(iterable, n): + """ + Yield ``(beginning, middle, end)`` tuples, where: + + * Each ``middle`` has *n* items from *iterable* + * Each ``beginning`` has the items before the ones in ``middle`` + * Each ``end`` has the items after the ones in ``middle`` + + >>> iterable = range(7) + >>> n = 3 + >>> for beginning, middle, end in windowed_complete(iterable, n): + ... print(beginning, middle, end) + () (0, 1, 2) (3, 4, 5, 6) + (0,) (1, 2, 3) (4, 5, 6) + (0, 1) (2, 3, 4) (5, 6) + (0, 1, 2) (3, 4, 5) (6,) + (0, 1, 2, 3) (4, 5, 6) () + + Note that *n* must be at least 0 and most equal to the length of + *iterable*. + + This function will exhaust the iterable and may require significant + storage. + """ + if n < 0: + raise ValueError('n must be >= 0') + + seq = tuple(iterable) + size = len(seq) + + if n > size: + raise ValueError('n must be <= len(seq)') + + for i in range(size - n + 1): + beginning = seq[:i] + middle = seq[i : i + n] + end = seq[i + n :] + yield beginning, middle, end + + +def all_unique(iterable, key=None): + """ + Returns ``True`` if all the elements of *iterable* are unique (no two + elements are equal). + + >>> all_unique('ABCB') + False + + If a *key* function is specified, it will be used to make comparisons. + + >>> all_unique('ABCb') + True + >>> all_unique('ABCb', str.lower) + False + + The function returns as soon as the first non-unique element is + encountered. Iterables with a mix of hashable and unhashable items can + be used, but the function will be slower for unhashable items. + """ + seenset = set() + seenset_add = seenset.add + seenlist = [] + seenlist_add = seenlist.append + for element in map(key, iterable) if key else iterable: + try: + if element in seenset: + return False + seenset_add(element) + except TypeError: + if element in seenlist: + return False + seenlist_add(element) + return True + + +def nth_product(index, *args): + """Equivalent to ``list(product(*args))[index]``. + + The products of *args* can be ordered lexicographically. + :func:`nth_product` computes the product at sort position *index* without + computing the previous products. + + >>> nth_product(8, range(2), range(2), range(2), range(2)) + (1, 0, 0, 0) + + ``IndexError`` will be raised if the given *index* is invalid. + """ + pools = list(map(tuple, reversed(args))) + ns = list(map(len, pools)) + + c = reduce(mul, ns) + + if index < 0: + index += c + + if not 0 <= index < c: + raise IndexError + + result = [] + for pool, n in zip(pools, ns): + result.append(pool[index % n]) + index //= n + + return tuple(reversed(result)) + + +def nth_permutation(iterable, r, index): + """Equivalent to ``list(permutations(iterable, r))[index]``` + + The subsequences of *iterable* that are of length *r* where order is + important can be ordered lexicographically. :func:`nth_permutation` + computes the subsequence at sort position *index* directly, without + computing the previous subsequences. + + >>> nth_permutation('ghijk', 2, 5) + ('h', 'i') + + ``ValueError`` will be raised If *r* is negative or greater than the length + of *iterable*. + ``IndexError`` will be raised if the given *index* is invalid. + """ + pool = list(iterable) + n = len(pool) + + if r is None or r == n: + r, c = n, factorial(n) + elif not 0 <= r < n: + raise ValueError + else: + c = factorial(n) // factorial(n - r) + + if index < 0: + index += c + + if not 0 <= index < c: + raise IndexError + + if c == 0: + return tuple() + + result = [0] * r + q = index * factorial(n) // c if r < n else index + for d in range(1, n + 1): + q, i = divmod(q, d) + if 0 <= n - d < r: + result[n - d] = i + if q == 0: + break + + return tuple(map(pool.pop, result)) + + +def value_chain(*args): + """Yield all arguments passed to the function in the same order in which + they were passed. If an argument itself is iterable then iterate over its + values. + + >>> list(value_chain(1, 2, 3, [4, 5, 6])) + [1, 2, 3, 4, 5, 6] + + Binary and text strings are not considered iterable and are emitted + as-is: + + >>> list(value_chain('12', '34', ['56', '78'])) + ['12', '34', '56', '78'] + + + Multiple levels of nesting are not flattened. + + """ + for value in args: + if isinstance(value, (str, bytes)): + yield value + continue + try: + yield from value + except TypeError: + yield value + + +def product_index(element, *args): + """Equivalent to ``list(product(*args)).index(element)`` + + The products of *args* can be ordered lexicographically. + :func:`product_index` computes the first index of *element* without + computing the previous products. + + >>> product_index([8, 2], range(10), range(5)) + 42 + + ``ValueError`` will be raised if the given *element* isn't in the product + of *args*. + """ + index = 0 + + for x, pool in zip_longest(element, args, fillvalue=_marker): + if x is _marker or pool is _marker: + raise ValueError('element is not a product of args') + + pool = tuple(pool) + index = index * len(pool) + pool.index(x) + + return index + + +def combination_index(element, iterable): + """Equivalent to ``list(combinations(iterable, r)).index(element)`` + + The subsequences of *iterable* that are of length *r* can be ordered + lexicographically. :func:`combination_index` computes the index of the + first *element*, without computing the previous combinations. + + >>> combination_index('adf', 'abcdefg') + 10 + + ``ValueError`` will be raised if the given *element* isn't one of the + combinations of *iterable*. + """ + element = enumerate(element) + k, y = next(element, (None, None)) + if k is None: + return 0 + + indexes = [] + pool = enumerate(iterable) + for n, x in pool: + if x == y: + indexes.append(n) + tmp, y = next(element, (None, None)) + if tmp is None: + break + else: + k = tmp + else: + raise ValueError('element is not a combination of iterable') + + n, _ = last(pool, default=(n, None)) + + # Python versiosn below 3.8 don't have math.comb + index = 1 + for i, j in enumerate(reversed(indexes), start=1): + j = n - j + if i <= j: + index += factorial(j) // (factorial(i) * factorial(j - i)) + + return factorial(n + 1) // (factorial(k + 1) * factorial(n - k)) - index + + +def permutation_index(element, iterable): + """Equivalent to ``list(permutations(iterable, r)).index(element)``` + + The subsequences of *iterable* that are of length *r* where order is + important can be ordered lexicographically. :func:`permutation_index` + computes the index of the first *element* directly, without computing + the previous permutations. + + >>> permutation_index([1, 3, 2], range(5)) + 19 + + ``ValueError`` will be raised if the given *element* isn't one of the + permutations of *iterable*. + """ + index = 0 + pool = list(iterable) + for i, x in zip(range(len(pool), -1, -1), element): + r = pool.index(x) + index = index * i + r + del pool[r] + + return index + + +class countable: + """Wrap *iterable* and keep a count of how many items have been consumed. + + The ``items_seen`` attribute starts at ``0`` and increments as the iterable + is consumed: + + >>> iterable = map(str, range(10)) + >>> it = countable(iterable) + >>> it.items_seen + 0 + >>> next(it), next(it) + ('0', '1') + >>> list(it) + ['2', '3', '4', '5', '6', '7', '8', '9'] + >>> it.items_seen + 10 + """ + + def __init__(self, iterable): + self._it = iter(iterable) + self.items_seen = 0 + + def __iter__(self): + return self + + def __next__(self): + item = next(self._it) + self.items_seen += 1 + + return item + + +def chunked_even(iterable, n): + """Break *iterable* into lists of approximately length *n*. + Items are distributed such the lengths of the lists differ by at most + 1 item. + + >>> iterable = [1, 2, 3, 4, 5, 6, 7] + >>> n = 3 + >>> list(chunked_even(iterable, n)) # List lengths: 3, 2, 2 + [[1, 2, 3], [4, 5], [6, 7]] + >>> list(chunked(iterable, n)) # List lengths: 3, 3, 1 + [[1, 2, 3], [4, 5, 6], [7]] + + """ + + len_method = getattr(iterable, '__len__', None) + + if len_method is None: + return _chunked_even_online(iterable, n) + else: + return _chunked_even_finite(iterable, len_method(), n) + + +def _chunked_even_online(iterable, n): + buffer = [] + maxbuf = n + (n - 2) * (n - 1) + for x in iterable: + buffer.append(x) + if len(buffer) == maxbuf: + yield buffer[:n] + buffer = buffer[n:] + yield from _chunked_even_finite(buffer, len(buffer), n) + + +def _chunked_even_finite(iterable, N, n): + if N < 1: + return + + # Lists are either size `full_size <= n` or `partial_size = full_size - 1` + q, r = divmod(N, n) + num_lists = q + (1 if r > 0 else 0) + q, r = divmod(N, num_lists) + full_size = q + (1 if r > 0 else 0) + partial_size = full_size - 1 + num_full = N - partial_size * num_lists + num_partial = num_lists - num_full + + buffer = [] + iterator = iter(iterable) + + # Yield num_full lists of full_size + for x in iterator: + buffer.append(x) + if len(buffer) == full_size: + yield buffer + buffer = [] + num_full -= 1 + if num_full <= 0: + break + + # Yield num_partial lists of partial_size + for x in iterator: + buffer.append(x) + if len(buffer) == partial_size: + yield buffer + buffer = [] + num_partial -= 1 + + +def zip_broadcast(*objects, scalar_types=(str, bytes), strict=False): + """A version of :func:`zip` that "broadcasts" any scalar + (i.e., non-iterable) items into output tuples. + + >>> iterable_1 = [1, 2, 3] + >>> iterable_2 = ['a', 'b', 'c'] + >>> scalar = '_' + >>> list(zip_broadcast(iterable_1, iterable_2, scalar)) + [(1, 'a', '_'), (2, 'b', '_'), (3, 'c', '_')] + + The *scalar_types* keyword argument determines what types are considered + scalar. It is set to ``(str, bytes)`` by default. Set it to ``None`` to + treat strings and byte strings as iterable: + + >>> list(zip_broadcast('abc', 0, 'xyz', scalar_types=None)) + [('a', 0, 'x'), ('b', 0, 'y'), ('c', 0, 'z')] + + If the *strict* keyword argument is ``True``, then + ``UnequalIterablesError`` will be raised if any of the iterables have + different lengthss. + """ + + def is_scalar(obj): + if scalar_types and isinstance(obj, scalar_types): + return True + try: + iter(obj) + except TypeError: + return True + else: + return False + + size = len(objects) + if not size: + return + + iterables, iterable_positions = [], [] + scalars, scalar_positions = [], [] + for i, obj in enumerate(objects): + if is_scalar(obj): + scalars.append(obj) + scalar_positions.append(i) + else: + iterables.append(iter(obj)) + iterable_positions.append(i) + + if len(scalars) == size: + yield tuple(objects) + return + + zipper = _zip_equal if strict else zip + for item in zipper(*iterables): + new_item = [None] * size + + for i, elem in zip(iterable_positions, item): + new_item[i] = elem + + for i, elem in zip(scalar_positions, scalars): + new_item[i] = elem + + yield tuple(new_item) + + +def unique_in_window(iterable, n, key=None): + """Yield the items from *iterable* that haven't been seen recently. + *n* is the size of the lookback window. + + >>> iterable = [0, 1, 0, 2, 3, 0] + >>> n = 3 + >>> list(unique_in_window(iterable, n)) + [0, 1, 2, 3, 0] + + The *key* function, if provided, will be used to determine uniqueness: + + >>> list(unique_in_window('abAcda', 3, key=lambda x: x.lower())) + ['a', 'b', 'c', 'd', 'a'] + + The items in *iterable* must be hashable. + + """ + if n <= 0: + raise ValueError('n must be greater than 0') + + window = deque(maxlen=n) + uniques = set() + use_key = key is not None + + for item in iterable: + k = key(item) if use_key else item + if k in uniques: + continue + + if len(uniques) == n: + uniques.discard(window[0]) + + uniques.add(k) + window.append(k) + + yield item + + +def duplicates_everseen(iterable, key=None): + """Yield duplicate elements after their first appearance. + + >>> list(duplicates_everseen('mississippi')) + ['s', 'i', 's', 's', 'i', 'p', 'i'] + >>> list(duplicates_everseen('AaaBbbCccAaa', str.lower)) + ['a', 'a', 'b', 'b', 'c', 'c', 'A', 'a', 'a'] + + This function is analagous to :func:`unique_everseen` and is subject to + the same performance considerations. + + """ + seen_set = set() + seen_list = [] + use_key = key is not None + + for element in iterable: + k = key(element) if use_key else element + try: + if k not in seen_set: + seen_set.add(k) + else: + yield element + except TypeError: + if k not in seen_list: + seen_list.append(k) + else: + yield element + + +def duplicates_justseen(iterable, key=None): + """Yields serially-duplicate elements after their first appearance. + + >>> list(duplicates_justseen('mississippi')) + ['s', 's', 'p'] + >>> list(duplicates_justseen('AaaBbbCccAaa', str.lower)) + ['a', 'a', 'b', 'b', 'c', 'c', 'a', 'a'] + + This function is analagous to :func:`unique_justseen`. + + """ + return flatten( + map( + lambda group_tuple: islice_extended(group_tuple[1])[1:], + groupby(iterable, key), + ) + ) + + +def minmax(iterable_or_value, *others, key=None, default=_marker): + """Returns both the smallest and largest items in an iterable + or the largest of two or more arguments. + + >>> minmax([3, 1, 5]) + (1, 5) + + >>> minmax(4, 2, 6) + (2, 6) + + If a *key* function is provided, it will be used to transform the input + items for comparison. + + >>> minmax([5, 30], key=str) # '30' sorts before '5' + (30, 5) + + If a *default* value is provided, it will be returned if there are no + input items. + + >>> minmax([], default=(0, 0)) + (0, 0) + + Otherwise ``ValueError`` is raised. + + This function is based on the + `recipe `__ by + Raymond Hettinger and takes care to minimize the number of comparisons + performed. + """ + iterable = (iterable_or_value, *others) if others else iterable_or_value + + it = iter(iterable) + + try: + lo = hi = next(it) + except StopIteration as e: + if default is _marker: + raise ValueError( + '`minmax()` argument is an empty iterable. ' + 'Provide a `default` value to suppress this error.' + ) from e + return default + + # Different branches depending on the presence of key. This saves a lot + # of unimportant copies which would slow the "key=None" branch + # significantly down. + if key is None: + for x, y in zip_longest(it, it, fillvalue=lo): + if y < x: + x, y = y, x + if x < lo: + lo = x + if hi < y: + hi = y + + else: + lo_key = hi_key = key(lo) + + for x, y in zip_longest(it, it, fillvalue=lo): + + x_key, y_key = key(x), key(y) + + if y_key < x_key: + x, y, x_key, y_key = y, x, y_key, x_key + if x_key < lo_key: + lo, lo_key = x, x_key + if hi_key < y_key: + hi, hi_key = y, y_key + + return lo, hi diff --git a/pkg_resources/_vendor/more_itertools/more.pyi b/pkg_resources/_vendor/more_itertools/more.pyi new file mode 100644 index 00000000..fe7d4bdd --- /dev/null +++ b/pkg_resources/_vendor/more_itertools/more.pyi @@ -0,0 +1,664 @@ +"""Stubs for more_itertools.more""" + +from typing import ( + Any, + Callable, + Container, + Dict, + Generic, + Hashable, + Iterable, + Iterator, + List, + Optional, + Reversible, + Sequence, + Sized, + Tuple, + Union, + TypeVar, + type_check_only, +) +from types import TracebackType +from typing_extensions import ContextManager, Protocol, Type, overload + +# Type and type variable definitions +_T = TypeVar('_T') +_T1 = TypeVar('_T1') +_T2 = TypeVar('_T2') +_U = TypeVar('_U') +_V = TypeVar('_V') +_W = TypeVar('_W') +_T_co = TypeVar('_T_co', covariant=True) +_GenFn = TypeVar('_GenFn', bound=Callable[..., Iterator[object]]) +_Raisable = Union[BaseException, 'Type[BaseException]'] + +@type_check_only +class _SizedIterable(Protocol[_T_co], Sized, Iterable[_T_co]): ... + +@type_check_only +class _SizedReversible(Protocol[_T_co], Sized, Reversible[_T_co]): ... + +def chunked( + iterable: Iterable[_T], n: Optional[int], strict: bool = ... +) -> Iterator[List[_T]]: ... +@overload +def first(iterable: Iterable[_T]) -> _T: ... +@overload +def first(iterable: Iterable[_T], default: _U) -> Union[_T, _U]: ... +@overload +def last(iterable: Iterable[_T]) -> _T: ... +@overload +def last(iterable: Iterable[_T], default: _U) -> Union[_T, _U]: ... +@overload +def nth_or_last(iterable: Iterable[_T], n: int) -> _T: ... +@overload +def nth_or_last( + iterable: Iterable[_T], n: int, default: _U +) -> Union[_T, _U]: ... + +class peekable(Generic[_T], Iterator[_T]): + def __init__(self, iterable: Iterable[_T]) -> None: ... + def __iter__(self) -> peekable[_T]: ... + def __bool__(self) -> bool: ... + @overload + def peek(self) -> _T: ... + @overload + def peek(self, default: _U) -> Union[_T, _U]: ... + def prepend(self, *items: _T) -> None: ... + def __next__(self) -> _T: ... + @overload + def __getitem__(self, index: int) -> _T: ... + @overload + def __getitem__(self, index: slice) -> List[_T]: ... + +def collate(*iterables: Iterable[_T], **kwargs: Any) -> Iterable[_T]: ... +def consumer(func: _GenFn) -> _GenFn: ... +def ilen(iterable: Iterable[object]) -> int: ... +def iterate(func: Callable[[_T], _T], start: _T) -> Iterator[_T]: ... +def with_iter( + context_manager: ContextManager[Iterable[_T]], +) -> Iterator[_T]: ... +def one( + iterable: Iterable[_T], + too_short: Optional[_Raisable] = ..., + too_long: Optional[_Raisable] = ..., +) -> _T: ... +def raise_(exception: _Raisable, *args: Any) -> None: ... +def strictly_n( + iterable: Iterable[_T], + n: int, + too_short: Optional[_GenFn] = ..., + too_long: Optional[_GenFn] = ..., +) -> List[_T]: ... +def distinct_permutations( + iterable: Iterable[_T], r: Optional[int] = ... +) -> Iterator[Tuple[_T, ...]]: ... +def intersperse( + e: _U, iterable: Iterable[_T], n: int = ... +) -> Iterator[Union[_T, _U]]: ... +def unique_to_each(*iterables: Iterable[_T]) -> List[List[_T]]: ... +@overload +def windowed( + seq: Iterable[_T], n: int, *, step: int = ... +) -> Iterator[Tuple[Optional[_T], ...]]: ... +@overload +def windowed( + seq: Iterable[_T], n: int, fillvalue: _U, step: int = ... +) -> Iterator[Tuple[Union[_T, _U], ...]]: ... +def substrings(iterable: Iterable[_T]) -> Iterator[Tuple[_T, ...]]: ... +def substrings_indexes( + seq: Sequence[_T], reverse: bool = ... +) -> Iterator[Tuple[Sequence[_T], int, int]]: ... + +class bucket(Generic[_T, _U], Container[_U]): + def __init__( + self, + iterable: Iterable[_T], + key: Callable[[_T], _U], + validator: Optional[Callable[[object], object]] = ..., + ) -> None: ... + def __contains__(self, value: object) -> bool: ... + def __iter__(self) -> Iterator[_U]: ... + def __getitem__(self, value: object) -> Iterator[_T]: ... + +def spy( + iterable: Iterable[_T], n: int = ... +) -> Tuple[List[_T], Iterator[_T]]: ... +def interleave(*iterables: Iterable[_T]) -> Iterator[_T]: ... +def interleave_longest(*iterables: Iterable[_T]) -> Iterator[_T]: ... +def interleave_evenly( + iterables: List[Iterable[_T]], lengths: Optional[List[int]] = ... +) -> Iterator[_T]: ... +def collapse( + iterable: Iterable[Any], + base_type: Optional[type] = ..., + levels: Optional[int] = ..., +) -> Iterator[Any]: ... +@overload +def side_effect( + func: Callable[[_T], object], + iterable: Iterable[_T], + chunk_size: None = ..., + before: Optional[Callable[[], object]] = ..., + after: Optional[Callable[[], object]] = ..., +) -> Iterator[_T]: ... +@overload +def side_effect( + func: Callable[[List[_T]], object], + iterable: Iterable[_T], + chunk_size: int, + before: Optional[Callable[[], object]] = ..., + after: Optional[Callable[[], object]] = ..., +) -> Iterator[_T]: ... +def sliced( + seq: Sequence[_T], n: int, strict: bool = ... +) -> Iterator[Sequence[_T]]: ... +def split_at( + iterable: Iterable[_T], + pred: Callable[[_T], object], + maxsplit: int = ..., + keep_separator: bool = ..., +) -> Iterator[List[_T]]: ... +def split_before( + iterable: Iterable[_T], pred: Callable[[_T], object], maxsplit: int = ... +) -> Iterator[List[_T]]: ... +def split_after( + iterable: Iterable[_T], pred: Callable[[_T], object], maxsplit: int = ... +) -> Iterator[List[_T]]: ... +def split_when( + iterable: Iterable[_T], + pred: Callable[[_T, _T], object], + maxsplit: int = ..., +) -> Iterator[List[_T]]: ... +def split_into( + iterable: Iterable[_T], sizes: Iterable[Optional[int]] +) -> Iterator[List[_T]]: ... +@overload +def padded( + iterable: Iterable[_T], + *, + n: Optional[int] = ..., + next_multiple: bool = ... +) -> Iterator[Optional[_T]]: ... +@overload +def padded( + iterable: Iterable[_T], + fillvalue: _U, + n: Optional[int] = ..., + next_multiple: bool = ..., +) -> Iterator[Union[_T, _U]]: ... +@overload +def repeat_last(iterable: Iterable[_T]) -> Iterator[_T]: ... +@overload +def repeat_last( + iterable: Iterable[_T], default: _U +) -> Iterator[Union[_T, _U]]: ... +def distribute(n: int, iterable: Iterable[_T]) -> List[Iterator[_T]]: ... +@overload +def stagger( + iterable: Iterable[_T], + offsets: _SizedIterable[int] = ..., + longest: bool = ..., +) -> Iterator[Tuple[Optional[_T], ...]]: ... +@overload +def stagger( + iterable: Iterable[_T], + offsets: _SizedIterable[int] = ..., + longest: bool = ..., + fillvalue: _U = ..., +) -> Iterator[Tuple[Union[_T, _U], ...]]: ... + +class UnequalIterablesError(ValueError): + def __init__( + self, details: Optional[Tuple[int, int, int]] = ... + ) -> None: ... + +@overload +def zip_equal(__iter1: Iterable[_T1]) -> Iterator[Tuple[_T1]]: ... +@overload +def zip_equal( + __iter1: Iterable[_T1], __iter2: Iterable[_T2] +) -> Iterator[Tuple[_T1, _T2]]: ... +@overload +def zip_equal( + __iter1: Iterable[_T], + __iter2: Iterable[_T], + __iter3: Iterable[_T], + *iterables: Iterable[_T] +) -> Iterator[Tuple[_T, ...]]: ... +@overload +def zip_offset( + __iter1: Iterable[_T1], + *, + offsets: _SizedIterable[int], + longest: bool = ..., + fillvalue: None = None +) -> Iterator[Tuple[Optional[_T1]]]: ... +@overload +def zip_offset( + __iter1: Iterable[_T1], + __iter2: Iterable[_T2], + *, + offsets: _SizedIterable[int], + longest: bool = ..., + fillvalue: None = None +) -> Iterator[Tuple[Optional[_T1], Optional[_T2]]]: ... +@overload +def zip_offset( + __iter1: Iterable[_T], + __iter2: Iterable[_T], + __iter3: Iterable[_T], + *iterables: Iterable[_T], + offsets: _SizedIterable[int], + longest: bool = ..., + fillvalue: None = None +) -> Iterator[Tuple[Optional[_T], ...]]: ... +@overload +def zip_offset( + __iter1: Iterable[_T1], + *, + offsets: _SizedIterable[int], + longest: bool = ..., + fillvalue: _U, +) -> Iterator[Tuple[Union[_T1, _U]]]: ... +@overload +def zip_offset( + __iter1: Iterable[_T1], + __iter2: Iterable[_T2], + *, + offsets: _SizedIterable[int], + longest: bool = ..., + fillvalue: _U, +) -> Iterator[Tuple[Union[_T1, _U], Union[_T2, _U]]]: ... +@overload +def zip_offset( + __iter1: Iterable[_T], + __iter2: Iterable[_T], + __iter3: Iterable[_T], + *iterables: Iterable[_T], + offsets: _SizedIterable[int], + longest: bool = ..., + fillvalue: _U, +) -> Iterator[Tuple[Union[_T, _U], ...]]: ... +def sort_together( + iterables: Iterable[Iterable[_T]], + key_list: Iterable[int] = ..., + key: Optional[Callable[..., Any]] = ..., + reverse: bool = ..., +) -> List[Tuple[_T, ...]]: ... +def unzip(iterable: Iterable[Sequence[_T]]) -> Tuple[Iterator[_T], ...]: ... +def divide(n: int, iterable: Iterable[_T]) -> List[Iterator[_T]]: ... +def always_iterable( + obj: object, + base_type: Union[ + type, Tuple[Union[type, Tuple[Any, ...]], ...], None + ] = ..., +) -> Iterator[Any]: ... +def adjacent( + predicate: Callable[[_T], bool], + iterable: Iterable[_T], + distance: int = ..., +) -> Iterator[Tuple[bool, _T]]: ... +@overload +def groupby_transform( + iterable: Iterable[_T], + keyfunc: None = None, + valuefunc: None = None, + reducefunc: None = None, +) -> Iterator[Tuple[_T, Iterator[_T]]]: ... +@overload +def groupby_transform( + iterable: Iterable[_T], + keyfunc: Callable[[_T], _U], + valuefunc: None, + reducefunc: None, +) -> Iterator[Tuple[_U, Iterator[_T]]]: ... +@overload +def groupby_transform( + iterable: Iterable[_T], + keyfunc: None, + valuefunc: Callable[[_T], _V], + reducefunc: None, +) -> Iterable[Tuple[_T, Iterable[_V]]]: ... +@overload +def groupby_transform( + iterable: Iterable[_T], + keyfunc: Callable[[_T], _U], + valuefunc: Callable[[_T], _V], + reducefunc: None, +) -> Iterable[Tuple[_U, Iterator[_V]]]: ... +@overload +def groupby_transform( + iterable: Iterable[_T], + keyfunc: None, + valuefunc: None, + reducefunc: Callable[[Iterator[_T]], _W], +) -> Iterable[Tuple[_T, _W]]: ... +@overload +def groupby_transform( + iterable: Iterable[_T], + keyfunc: Callable[[_T], _U], + valuefunc: None, + reducefunc: Callable[[Iterator[_T]], _W], +) -> Iterable[Tuple[_U, _W]]: ... +@overload +def groupby_transform( + iterable: Iterable[_T], + keyfunc: None, + valuefunc: Callable[[_T], _V], + reducefunc: Callable[[Iterable[_V]], _W], +) -> Iterable[Tuple[_T, _W]]: ... +@overload +def groupby_transform( + iterable: Iterable[_T], + keyfunc: Callable[[_T], _U], + valuefunc: Callable[[_T], _V], + reducefunc: Callable[[Iterable[_V]], _W], +) -> Iterable[Tuple[_U, _W]]: ... + +class numeric_range(Generic[_T, _U], Sequence[_T], Hashable, Reversible[_T]): + @overload + def __init__(self, __stop: _T) -> None: ... + @overload + def __init__(self, __start: _T, __stop: _T) -> None: ... + @overload + def __init__(self, __start: _T, __stop: _T, __step: _U) -> None: ... + def __bool__(self) -> bool: ... + def __contains__(self, elem: object) -> bool: ... + def __eq__(self, other: object) -> bool: ... + @overload + def __getitem__(self, key: int) -> _T: ... + @overload + def __getitem__(self, key: slice) -> numeric_range[_T, _U]: ... + def __hash__(self) -> int: ... + def __iter__(self) -> Iterator[_T]: ... + def __len__(self) -> int: ... + def __reduce__( + self, + ) -> Tuple[Type[numeric_range[_T, _U]], Tuple[_T, _T, _U]]: ... + def __repr__(self) -> str: ... + def __reversed__(self) -> Iterator[_T]: ... + def count(self, value: _T) -> int: ... + def index(self, value: _T) -> int: ... # type: ignore + +def count_cycle( + iterable: Iterable[_T], n: Optional[int] = ... +) -> Iterable[Tuple[int, _T]]: ... +def mark_ends( + iterable: Iterable[_T], +) -> Iterable[Tuple[bool, bool, _T]]: ... +def locate( + iterable: Iterable[object], + pred: Callable[..., Any] = ..., + window_size: Optional[int] = ..., +) -> Iterator[int]: ... +def lstrip( + iterable: Iterable[_T], pred: Callable[[_T], object] +) -> Iterator[_T]: ... +def rstrip( + iterable: Iterable[_T], pred: Callable[[_T], object] +) -> Iterator[_T]: ... +def strip( + iterable: Iterable[_T], pred: Callable[[_T], object] +) -> Iterator[_T]: ... + +class islice_extended(Generic[_T], Iterator[_T]): + def __init__( + self, iterable: Iterable[_T], *args: Optional[int] + ) -> None: ... + def __iter__(self) -> islice_extended[_T]: ... + def __next__(self) -> _T: ... + def __getitem__(self, index: slice) -> islice_extended[_T]: ... + +def always_reversible(iterable: Iterable[_T]) -> Iterator[_T]: ... +def consecutive_groups( + iterable: Iterable[_T], ordering: Callable[[_T], int] = ... +) -> Iterator[Iterator[_T]]: ... +@overload +def difference( + iterable: Iterable[_T], + func: Callable[[_T, _T], _U] = ..., + *, + initial: None = ... +) -> Iterator[Union[_T, _U]]: ... +@overload +def difference( + iterable: Iterable[_T], func: Callable[[_T, _T], _U] = ..., *, initial: _U +) -> Iterator[_U]: ... + +class SequenceView(Generic[_T], Sequence[_T]): + def __init__(self, target: Sequence[_T]) -> None: ... + @overload + def __getitem__(self, index: int) -> _T: ... + @overload + def __getitem__(self, index: slice) -> Sequence[_T]: ... + def __len__(self) -> int: ... + +class seekable(Generic[_T], Iterator[_T]): + def __init__( + self, iterable: Iterable[_T], maxlen: Optional[int] = ... + ) -> None: ... + def __iter__(self) -> seekable[_T]: ... + def __next__(self) -> _T: ... + def __bool__(self) -> bool: ... + @overload + def peek(self) -> _T: ... + @overload + def peek(self, default: _U) -> Union[_T, _U]: ... + def elements(self) -> SequenceView[_T]: ... + def seek(self, index: int) -> None: ... + +class run_length: + @staticmethod + def encode(iterable: Iterable[_T]) -> Iterator[Tuple[_T, int]]: ... + @staticmethod + def decode(iterable: Iterable[Tuple[_T, int]]) -> Iterator[_T]: ... + +def exactly_n( + iterable: Iterable[_T], n: int, predicate: Callable[[_T], object] = ... +) -> bool: ... +def circular_shifts(iterable: Iterable[_T]) -> List[Tuple[_T, ...]]: ... +def make_decorator( + wrapping_func: Callable[..., _U], result_index: int = ... +) -> Callable[..., Callable[[Callable[..., Any]], Callable[..., _U]]]: ... +@overload +def map_reduce( + iterable: Iterable[_T], + keyfunc: Callable[[_T], _U], + valuefunc: None = ..., + reducefunc: None = ..., +) -> Dict[_U, List[_T]]: ... +@overload +def map_reduce( + iterable: Iterable[_T], + keyfunc: Callable[[_T], _U], + valuefunc: Callable[[_T], _V], + reducefunc: None = ..., +) -> Dict[_U, List[_V]]: ... +@overload +def map_reduce( + iterable: Iterable[_T], + keyfunc: Callable[[_T], _U], + valuefunc: None = ..., + reducefunc: Callable[[List[_T]], _W] = ..., +) -> Dict[_U, _W]: ... +@overload +def map_reduce( + iterable: Iterable[_T], + keyfunc: Callable[[_T], _U], + valuefunc: Callable[[_T], _V], + reducefunc: Callable[[List[_V]], _W], +) -> Dict[_U, _W]: ... +def rlocate( + iterable: Iterable[_T], + pred: Callable[..., object] = ..., + window_size: Optional[int] = ..., +) -> Iterator[int]: ... +def replace( + iterable: Iterable[_T], + pred: Callable[..., object], + substitutes: Iterable[_U], + count: Optional[int] = ..., + window_size: int = ..., +) -> Iterator[Union[_T, _U]]: ... +def partitions(iterable: Iterable[_T]) -> Iterator[List[List[_T]]]: ... +def set_partitions( + iterable: Iterable[_T], k: Optional[int] = ... +) -> Iterator[List[List[_T]]]: ... + +class time_limited(Generic[_T], Iterator[_T]): + def __init__( + self, limit_seconds: float, iterable: Iterable[_T] + ) -> None: ... + def __iter__(self) -> islice_extended[_T]: ... + def __next__(self) -> _T: ... + +@overload +def only( + iterable: Iterable[_T], *, too_long: Optional[_Raisable] = ... +) -> Optional[_T]: ... +@overload +def only( + iterable: Iterable[_T], default: _U, too_long: Optional[_Raisable] = ... +) -> Union[_T, _U]: ... +def ichunked(iterable: Iterable[_T], n: int) -> Iterator[Iterator[_T]]: ... +def distinct_combinations( + iterable: Iterable[_T], r: int +) -> Iterator[Tuple[_T, ...]]: ... +def filter_except( + validator: Callable[[Any], object], + iterable: Iterable[_T], + *exceptions: Type[BaseException] +) -> Iterator[_T]: ... +def map_except( + function: Callable[[Any], _U], + iterable: Iterable[_T], + *exceptions: Type[BaseException] +) -> Iterator[_U]: ... +def map_if( + iterable: Iterable[Any], + pred: Callable[[Any], bool], + func: Callable[[Any], Any], + func_else: Optional[Callable[[Any], Any]] = ..., +) -> Iterator[Any]: ... +def sample( + iterable: Iterable[_T], + k: int, + weights: Optional[Iterable[float]] = ..., +) -> List[_T]: ... +def is_sorted( + iterable: Iterable[_T], + key: Optional[Callable[[_T], _U]] = ..., + reverse: bool = False, + strict: bool = False, +) -> bool: ... + +class AbortThread(BaseException): + pass + +class callback_iter(Generic[_T], Iterator[_T]): + def __init__( + self, + func: Callable[..., Any], + callback_kwd: str = ..., + wait_seconds: float = ..., + ) -> None: ... + def __enter__(self) -> callback_iter[_T]: ... + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> Optional[bool]: ... + def __iter__(self) -> callback_iter[_T]: ... + def __next__(self) -> _T: ... + def _reader(self) -> Iterator[_T]: ... + @property + def done(self) -> bool: ... + @property + def result(self) -> Any: ... + +def windowed_complete( + iterable: Iterable[_T], n: int +) -> Iterator[Tuple[_T, ...]]: ... +def all_unique( + iterable: Iterable[_T], key: Optional[Callable[[_T], _U]] = ... +) -> bool: ... +def nth_product(index: int, *args: Iterable[_T]) -> Tuple[_T, ...]: ... +def nth_permutation( + iterable: Iterable[_T], r: int, index: int +) -> Tuple[_T, ...]: ... +def value_chain(*args: Union[_T, Iterable[_T]]) -> Iterable[_T]: ... +def product_index(element: Iterable[_T], *args: Iterable[_T]) -> int: ... +def combination_index( + element: Iterable[_T], iterable: Iterable[_T] +) -> int: ... +def permutation_index( + element: Iterable[_T], iterable: Iterable[_T] +) -> int: ... +def repeat_each(iterable: Iterable[_T], n: int = ...) -> Iterator[_T]: ... + +class countable(Generic[_T], Iterator[_T]): + def __init__(self, iterable: Iterable[_T]) -> None: ... + def __iter__(self) -> countable[_T]: ... + def __next__(self) -> _T: ... + +def chunked_even(iterable: Iterable[_T], n: int) -> Iterator[List[_T]]: ... +def zip_broadcast( + *objects: Union[_T, Iterable[_T]], + scalar_types: Union[ + type, Tuple[Union[type, Tuple[Any, ...]], ...], None + ] = ..., + strict: bool = ... +) -> Iterable[Tuple[_T, ...]]: ... +def unique_in_window( + iterable: Iterable[_T], n: int, key: Optional[Callable[[_T], _U]] = ... +) -> Iterator[_T]: ... +def duplicates_everseen( + iterable: Iterable[_T], key: Optional[Callable[[_T], _U]] = ... +) -> Iterator[_T]: ... +def duplicates_justseen( + iterable: Iterable[_T], key: Optional[Callable[[_T], _U]] = ... +) -> Iterator[_T]: ... + +class _SupportsLessThan(Protocol): + def __lt__(self, __other: Any) -> bool: ... + +_SupportsLessThanT = TypeVar("_SupportsLessThanT", bound=_SupportsLessThan) + +@overload +def minmax( + iterable_or_value: Iterable[_SupportsLessThanT], *, key: None = None +) -> Tuple[_SupportsLessThanT, _SupportsLessThanT]: ... +@overload +def minmax( + iterable_or_value: Iterable[_T], *, key: Callable[[_T], _SupportsLessThan] +) -> Tuple[_T, _T]: ... +@overload +def minmax( + iterable_or_value: Iterable[_SupportsLessThanT], + *, + key: None = None, + default: _U +) -> Union[_U, Tuple[_SupportsLessThanT, _SupportsLessThanT]]: ... +@overload +def minmax( + iterable_or_value: Iterable[_T], + *, + key: Callable[[_T], _SupportsLessThan], + default: _U, +) -> Union[_U, Tuple[_T, _T]]: ... +@overload +def minmax( + iterable_or_value: _SupportsLessThanT, + __other: _SupportsLessThanT, + *others: _SupportsLessThanT +) -> Tuple[_SupportsLessThanT, _SupportsLessThanT]: ... +@overload +def minmax( + iterable_or_value: _T, + __other: _T, + *others: _T, + key: Callable[[_T], _SupportsLessThan] +) -> Tuple[_T, _T]: ... diff --git a/setuptools/_vendor/more_itertools/__init__.py b/setuptools/_vendor/more_itertools/__init__.py index 53cf238c..19a169fc 100644 --- a/setuptools/_vendor/more_itertools/__init__.py +++ b/setuptools/_vendor/more_itertools/__init__.py @@ -1,3 +1,4 @@ +from .more import * # noqa from .recipes import * # noqa __version__ = '8.8.0' diff --git a/setuptools/_vendor/more_itertools/__init__.pyi b/setuptools/_vendor/more_itertools/__init__.pyi index f0fe8b5d..96f6e36c 100644 --- a/setuptools/_vendor/more_itertools/__init__.pyi +++ b/setuptools/_vendor/more_itertools/__init__.pyi @@ -1 +1,2 @@ +from .more import * from .recipes import * diff --git a/setuptools/_vendor/more_itertools/more.py b/setuptools/_vendor/more_itertools/more.py new file mode 100644 index 00000000..e6fca4d4 --- /dev/null +++ b/setuptools/_vendor/more_itertools/more.py @@ -0,0 +1,3824 @@ +import warnings + +from collections import Counter, defaultdict, deque, abc +from collections.abc import Sequence +from functools import partial, reduce, wraps +from heapq import merge, heapify, heapreplace, heappop +from itertools import ( + chain, + compress, + count, + cycle, + dropwhile, + groupby, + islice, + repeat, + starmap, + takewhile, + tee, + zip_longest, +) +from math import exp, factorial, floor, log +from queue import Empty, Queue +from random import random, randrange, uniform +from operator import itemgetter, mul, sub, gt, lt +from sys import hexversion, maxsize +from time import monotonic + +from .recipes import ( + consume, + flatten, + pairwise, + powerset, + take, + unique_everseen, +) + +__all__ = [ + 'AbortThread', + 'adjacent', + 'always_iterable', + 'always_reversible', + 'bucket', + 'callback_iter', + 'chunked', + 'circular_shifts', + 'collapse', + 'collate', + 'consecutive_groups', + 'consumer', + 'countable', + 'count_cycle', + 'mark_ends', + 'difference', + 'distinct_combinations', + 'distinct_permutations', + 'distribute', + 'divide', + 'exactly_n', + 'filter_except', + 'first', + 'groupby_transform', + 'ilen', + 'interleave_longest', + 'interleave', + 'intersperse', + 'islice_extended', + 'iterate', + 'ichunked', + 'is_sorted', + 'last', + 'locate', + 'lstrip', + 'make_decorator', + 'map_except', + 'map_reduce', + 'nth_or_last', + 'nth_permutation', + 'nth_product', + 'numeric_range', + 'one', + 'only', + 'padded', + 'partitions', + 'set_partitions', + 'peekable', + 'repeat_last', + 'replace', + 'rlocate', + 'rstrip', + 'run_length', + 'sample', + 'seekable', + 'SequenceView', + 'side_effect', + 'sliced', + 'sort_together', + 'split_at', + 'split_after', + 'split_before', + 'split_when', + 'split_into', + 'spy', + 'stagger', + 'strip', + 'substrings', + 'substrings_indexes', + 'time_limited', + 'unique_to_each', + 'unzip', + 'windowed', + 'with_iter', + 'UnequalIterablesError', + 'zip_equal', + 'zip_offset', + 'windowed_complete', + 'all_unique', + 'value_chain', + 'product_index', + 'combination_index', + 'permutation_index', +] + +_marker = object() + + +def chunked(iterable, n, strict=False): + """Break *iterable* into lists of length *n*: + + >>> list(chunked([1, 2, 3, 4, 5, 6], 3)) + [[1, 2, 3], [4, 5, 6]] + + By the default, the last yielded list will have fewer than *n* elements + if the length of *iterable* is not divisible by *n*: + + >>> list(chunked([1, 2, 3, 4, 5, 6, 7, 8], 3)) + [[1, 2, 3], [4, 5, 6], [7, 8]] + + To use a fill-in value instead, see the :func:`grouper` recipe. + + If the length of *iterable* is not divisible by *n* and *strict* is + ``True``, then ``ValueError`` will be raised before the last + list is yielded. + + """ + iterator = iter(partial(take, n, iter(iterable)), []) + if strict: + + def ret(): + for chunk in iterator: + if len(chunk) != n: + raise ValueError('iterable is not divisible by n.') + yield chunk + + return iter(ret()) + else: + return iterator + + +def first(iterable, default=_marker): + """Return the first item of *iterable*, or *default* if *iterable* is + empty. + + >>> first([0, 1, 2, 3]) + 0 + >>> first([], 'some default') + 'some default' + + If *default* is not provided and there are no items in the iterable, + raise ``ValueError``. + + :func:`first` is useful when you have a generator of expensive-to-retrieve + values and want any arbitrary one. It is marginally shorter than + ``next(iter(iterable), default)``. + + """ + try: + return next(iter(iterable)) + except StopIteration as e: + if default is _marker: + raise ValueError( + 'first() was called on an empty iterable, and no ' + 'default value was provided.' + ) from e + return default + + +def last(iterable, default=_marker): + """Return the last item of *iterable*, or *default* if *iterable* is + empty. + + >>> last([0, 1, 2, 3]) + 3 + >>> last([], 'some default') + 'some default' + + If *default* is not provided and there are no items in the iterable, + raise ``ValueError``. + """ + try: + if isinstance(iterable, Sequence): + return iterable[-1] + # Work around https://bugs.python.org/issue38525 + elif hasattr(iterable, '__reversed__') and (hexversion != 0x030800F0): + return next(reversed(iterable)) + else: + return deque(iterable, maxlen=1)[-1] + except (IndexError, TypeError, StopIteration): + if default is _marker: + raise ValueError( + 'last() was called on an empty iterable, and no default was ' + 'provided.' + ) + return default + + +def nth_or_last(iterable, n, default=_marker): + """Return the nth or the last item of *iterable*, + or *default* if *iterable* is empty. + + >>> nth_or_last([0, 1, 2, 3], 2) + 2 + >>> nth_or_last([0, 1], 2) + 1 + >>> nth_or_last([], 0, 'some default') + 'some default' + + If *default* is not provided and there are no items in the iterable, + raise ``ValueError``. + """ + return last(islice(iterable, n + 1), default=default) + + +class peekable: + """Wrap an iterator to allow lookahead and prepending elements. + + Call :meth:`peek` on the result to get the value that will be returned + by :func:`next`. This won't advance the iterator: + + >>> p = peekable(['a', 'b']) + >>> p.peek() + 'a' + >>> next(p) + 'a' + + Pass :meth:`peek` a default value to return that instead of raising + ``StopIteration`` when the iterator is exhausted. + + >>> p = peekable([]) + >>> p.peek('hi') + 'hi' + + peekables also offer a :meth:`prepend` method, which "inserts" items + at the head of the iterable: + + >>> p = peekable([1, 2, 3]) + >>> p.prepend(10, 11, 12) + >>> next(p) + 10 + >>> p.peek() + 11 + >>> list(p) + [11, 12, 1, 2, 3] + + peekables can be indexed. Index 0 is the item that will be returned by + :func:`next`, index 1 is the item after that, and so on: + The values up to the given index will be cached. + + >>> p = peekable(['a', 'b', 'c', 'd']) + >>> p[0] + 'a' + >>> p[1] + 'b' + >>> next(p) + 'a' + + Negative indexes are supported, but be aware that they will cache the + remaining items in the source iterator, which may require significant + storage. + + To check whether a peekable is exhausted, check its truth value: + + >>> p = peekable(['a', 'b']) + >>> if p: # peekable has items + ... list(p) + ['a', 'b'] + >>> if not p: # peekable is exhausted + ... list(p) + [] + + """ + + def __init__(self, iterable): + self._it = iter(iterable) + self._cache = deque() + + def __iter__(self): + return self + + def __bool__(self): + try: + self.peek() + except StopIteration: + return False + return True + + def peek(self, default=_marker): + """Return the item that will be next returned from ``next()``. + + Return ``default`` if there are no items left. If ``default`` is not + provided, raise ``StopIteration``. + + """ + if not self._cache: + try: + self._cache.append(next(self._it)) + except StopIteration: + if default is _marker: + raise + return default + return self._cache[0] + + def prepend(self, *items): + """Stack up items to be the next ones returned from ``next()`` or + ``self.peek()``. The items will be returned in + first in, first out order:: + + >>> p = peekable([1, 2, 3]) + >>> p.prepend(10, 11, 12) + >>> next(p) + 10 + >>> list(p) + [11, 12, 1, 2, 3] + + It is possible, by prepending items, to "resurrect" a peekable that + previously raised ``StopIteration``. + + >>> p = peekable([]) + >>> next(p) + Traceback (most recent call last): + ... + StopIteration + >>> p.prepend(1) + >>> next(p) + 1 + >>> next(p) + Traceback (most recent call last): + ... + StopIteration + + """ + self._cache.extendleft(reversed(items)) + + def __next__(self): + if self._cache: + return self._cache.popleft() + + return next(self._it) + + def _get_slice(self, index): + # Normalize the slice's arguments + step = 1 if (index.step is None) else index.step + if step > 0: + start = 0 if (index.start is None) else index.start + stop = maxsize if (index.stop is None) else index.stop + elif step < 0: + start = -1 if (index.start is None) else index.start + stop = (-maxsize - 1) if (index.stop is None) else index.stop + else: + raise ValueError('slice step cannot be zero') + + # If either the start or stop index is negative, we'll need to cache + # the rest of the iterable in order to slice from the right side. + if (start < 0) or (stop < 0): + self._cache.extend(self._it) + # Otherwise we'll need to find the rightmost index and cache to that + # point. + else: + n = min(max(start, stop) + 1, maxsize) + cache_len = len(self._cache) + if n >= cache_len: + self._cache.extend(islice(self._it, n - cache_len)) + + return list(self._cache)[index] + + def __getitem__(self, index): + if isinstance(index, slice): + return self._get_slice(index) + + cache_len = len(self._cache) + if index < 0: + self._cache.extend(self._it) + elif index >= cache_len: + self._cache.extend(islice(self._it, index + 1 - cache_len)) + + return self._cache[index] + + +def collate(*iterables, **kwargs): + """Return a sorted merge of the items from each of several already-sorted + *iterables*. + + >>> list(collate('ACDZ', 'AZ', 'JKL')) + ['A', 'A', 'C', 'D', 'J', 'K', 'L', 'Z', 'Z'] + + Works lazily, keeping only the next value from each iterable in memory. Use + :func:`collate` to, for example, perform a n-way mergesort of items that + don't fit in memory. + + If a *key* function is specified, the iterables will be sorted according + to its result: + + >>> key = lambda s: int(s) # Sort by numeric value, not by string + >>> list(collate(['1', '10'], ['2', '11'], key=key)) + ['1', '2', '10', '11'] + + + If the *iterables* are sorted in descending order, set *reverse* to + ``True``: + + >>> list(collate([5, 3, 1], [4, 2, 0], reverse=True)) + [5, 4, 3, 2, 1, 0] + + If the elements of the passed-in iterables are out of order, you might get + unexpected results. + + On Python 3.5+, this function is an alias for :func:`heapq.merge`. + + """ + warnings.warn( + "collate is no longer part of more_itertools, use heapq.merge", + DeprecationWarning, + ) + return merge(*iterables, **kwargs) + + +def consumer(func): + """Decorator that automatically advances a PEP-342-style "reverse iterator" + to its first yield point so you don't have to call ``next()`` on it + manually. + + >>> @consumer + ... def tally(): + ... i = 0 + ... while True: + ... print('Thing number %s is %s.' % (i, (yield))) + ... i += 1 + ... + >>> t = tally() + >>> t.send('red') + Thing number 0 is red. + >>> t.send('fish') + Thing number 1 is fish. + + Without the decorator, you would have to call ``next(t)`` before + ``t.send()`` could be used. + + """ + + @wraps(func) + def wrapper(*args, **kwargs): + gen = func(*args, **kwargs) + next(gen) + return gen + + return wrapper + + +def ilen(iterable): + """Return the number of items in *iterable*. + + >>> ilen(x for x in range(1000000) if x % 3 == 0) + 333334 + + This consumes the iterable, so handle with care. + + """ + # This approach was selected because benchmarks showed it's likely the + # fastest of the known implementations at the time of writing. + # See GitHub tracker: #236, #230. + counter = count() + deque(zip(iterable, counter), maxlen=0) + return next(counter) + + +def iterate(func, start): + """Return ``start``, ``func(start)``, ``func(func(start))``, ... + + >>> from itertools import islice + >>> list(islice(iterate(lambda x: 2*x, 1), 10)) + [1, 2, 4, 8, 16, 32, 64, 128, 256, 512] + + """ + while True: + yield start + start = func(start) + + +def with_iter(context_manager): + """Wrap an iterable in a ``with`` statement, so it closes once exhausted. + + For example, this will close the file when the iterator is exhausted:: + + upper_lines = (line.upper() for line in with_iter(open('foo'))) + + Any context manager which returns an iterable is a candidate for + ``with_iter``. + + """ + with context_manager as iterable: + yield from iterable + + +def one(iterable, too_short=None, too_long=None): + """Return the first item from *iterable*, which is expected to contain only + that item. Raise an exception if *iterable* is empty or has more than one + item. + + :func:`one` is useful for ensuring that an iterable contains only one item. + For example, it can be used to retrieve the result of a database query + that is expected to return a single row. + + If *iterable* is empty, ``ValueError`` will be raised. You may specify a + different exception with the *too_short* keyword: + + >>> it = [] + >>> one(it) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ValueError: too many items in iterable (expected 1)' + >>> too_short = IndexError('too few items') + >>> one(it, too_short=too_short) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + IndexError: too few items + + Similarly, if *iterable* contains more than one item, ``ValueError`` will + be raised. You may specify a different exception with the *too_long* + keyword: + + >>> it = ['too', 'many'] + >>> one(it) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ValueError: Expected exactly one item in iterable, but got 'too', + 'many', and perhaps more. + >>> too_long = RuntimeError + >>> one(it, too_long=too_long) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + RuntimeError + + Note that :func:`one` attempts to advance *iterable* twice to ensure there + is only one item. See :func:`spy` or :func:`peekable` to check iterable + contents less destructively. + + """ + it = iter(iterable) + + try: + first_value = next(it) + except StopIteration as e: + raise ( + too_short or ValueError('too few items in iterable (expected 1)') + ) from e + + try: + second_value = next(it) + except StopIteration: + pass + else: + msg = ( + 'Expected exactly one item in iterable, but got {!r}, {!r}, ' + 'and perhaps more.'.format(first_value, second_value) + ) + raise too_long or ValueError(msg) + + return first_value + + +def distinct_permutations(iterable, r=None): + """Yield successive distinct permutations of the elements in *iterable*. + + >>> sorted(distinct_permutations([1, 0, 1])) + [(0, 1, 1), (1, 0, 1), (1, 1, 0)] + + Equivalent to ``set(permutations(iterable))``, except duplicates are not + generated and thrown away. For larger input sequences this is much more + efficient. + + Duplicate permutations arise when there are duplicated elements in the + input iterable. The number of items returned is + `n! / (x_1! * x_2! * ... * x_n!)`, where `n` is the total number of + items input, and each `x_i` is the count of a distinct item in the input + sequence. + + If *r* is given, only the *r*-length permutations are yielded. + + >>> sorted(distinct_permutations([1, 0, 1], r=2)) + [(0, 1), (1, 0), (1, 1)] + >>> sorted(distinct_permutations(range(3), r=2)) + [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)] + + """ + # Algorithm: https://w.wiki/Qai + def _full(A): + while True: + # Yield the permutation we have + yield tuple(A) + + # Find the largest index i such that A[i] < A[i + 1] + for i in range(size - 2, -1, -1): + if A[i] < A[i + 1]: + break + # If no such index exists, this permutation is the last one + else: + return + + # Find the largest index j greater than j such that A[i] < A[j] + for j in range(size - 1, i, -1): + if A[i] < A[j]: + break + + # Swap the value of A[i] with that of A[j], then reverse the + # sequence from A[i + 1] to form the new permutation + A[i], A[j] = A[j], A[i] + A[i + 1 :] = A[: i - size : -1] # A[i + 1:][::-1] + + # Algorithm: modified from the above + def _partial(A, r): + # Split A into the first r items and the last r items + head, tail = A[:r], A[r:] + right_head_indexes = range(r - 1, -1, -1) + left_tail_indexes = range(len(tail)) + + while True: + # Yield the permutation we have + yield tuple(head) + + # Starting from the right, find the first index of the head with + # value smaller than the maximum value of the tail - call it i. + pivot = tail[-1] + for i in right_head_indexes: + if head[i] < pivot: + break + pivot = head[i] + else: + return + + # Starting from the left, find the first value of the tail + # with a value greater than head[i] and swap. + for j in left_tail_indexes: + if tail[j] > head[i]: + head[i], tail[j] = tail[j], head[i] + break + # If we didn't find one, start from the right and find the first + # index of the head with a value greater than head[i] and swap. + else: + for j in right_head_indexes: + if head[j] > head[i]: + head[i], head[j] = head[j], head[i] + break + + # Reverse head[i + 1:] and swap it with tail[:r - (i + 1)] + tail += head[: i - r : -1] # head[i + 1:][::-1] + i += 1 + head[i:], tail[:] = tail[: r - i], tail[r - i :] + + items = sorted(iterable) + + size = len(items) + if r is None: + r = size + + if 0 < r <= size: + return _full(items) if (r == size) else _partial(items, r) + + return iter(() if r else ((),)) + + +def intersperse(e, iterable, n=1): + """Intersperse filler element *e* among the items in *iterable*, leaving + *n* items between each filler element. + + >>> list(intersperse('!', [1, 2, 3, 4, 5])) + [1, '!', 2, '!', 3, '!', 4, '!', 5] + + >>> list(intersperse(None, [1, 2, 3, 4, 5], n=2)) + [1, 2, None, 3, 4, None, 5] + + """ + if n == 0: + raise ValueError('n must be > 0') + elif n == 1: + # interleave(repeat(e), iterable) -> e, x_0, e, e, x_1, e, x_2... + # islice(..., 1, None) -> x_0, e, e, x_1, e, x_2... + return islice(interleave(repeat(e), iterable), 1, None) + else: + # interleave(filler, chunks) -> [e], [x_0, x_1], [e], [x_2, x_3]... + # islice(..., 1, None) -> [x_0, x_1], [e], [x_2, x_3]... + # flatten(...) -> x_0, x_1, e, x_2, x_3... + filler = repeat([e]) + chunks = chunked(iterable, n) + return flatten(islice(interleave(filler, chunks), 1, None)) + + +def unique_to_each(*iterables): + """Return the elements from each of the input iterables that aren't in the + other input iterables. + + For example, suppose you have a set of packages, each with a set of + dependencies:: + + {'pkg_1': {'A', 'B'}, 'pkg_2': {'B', 'C'}, 'pkg_3': {'B', 'D'}} + + If you remove one package, which dependencies can also be removed? + + If ``pkg_1`` is removed, then ``A`` is no longer necessary - it is not + associated with ``pkg_2`` or ``pkg_3``. Similarly, ``C`` is only needed for + ``pkg_2``, and ``D`` is only needed for ``pkg_3``:: + + >>> unique_to_each({'A', 'B'}, {'B', 'C'}, {'B', 'D'}) + [['A'], ['C'], ['D']] + + If there are duplicates in one input iterable that aren't in the others + they will be duplicated in the output. Input order is preserved:: + + >>> unique_to_each("mississippi", "missouri") + [['p', 'p'], ['o', 'u', 'r']] + + It is assumed that the elements of each iterable are hashable. + + """ + pool = [list(it) for it in iterables] + counts = Counter(chain.from_iterable(map(set, pool))) + uniques = {element for element in counts if counts[element] == 1} + return [list(filter(uniques.__contains__, it)) for it in pool] + + +def windowed(seq, n, fillvalue=None, step=1): + """Return a sliding window of width *n* over the given iterable. + + >>> all_windows = windowed([1, 2, 3, 4, 5], 3) + >>> list(all_windows) + [(1, 2, 3), (2, 3, 4), (3, 4, 5)] + + When the window is larger than the iterable, *fillvalue* is used in place + of missing values: + + >>> list(windowed([1, 2, 3], 4)) + [(1, 2, 3, None)] + + Each window will advance in increments of *step*: + + >>> list(windowed([1, 2, 3, 4, 5, 6], 3, fillvalue='!', step=2)) + [(1, 2, 3), (3, 4, 5), (5, 6, '!')] + + To slide into the iterable's items, use :func:`chain` to add filler items + to the left: + + >>> iterable = [1, 2, 3, 4] + >>> n = 3 + >>> padding = [None] * (n - 1) + >>> list(windowed(chain(padding, iterable), 3)) + [(None, None, 1), (None, 1, 2), (1, 2, 3), (2, 3, 4)] + """ + if n < 0: + raise ValueError('n must be >= 0') + if n == 0: + yield tuple() + return + if step < 1: + raise ValueError('step must be >= 1') + + window = deque(maxlen=n) + i = n + for _ in map(window.append, seq): + i -= 1 + if not i: + i = step + yield tuple(window) + + size = len(window) + if size < n: + yield tuple(chain(window, repeat(fillvalue, n - size))) + elif 0 < i < min(step, n): + window += (fillvalue,) * i + yield tuple(window) + + +def substrings(iterable): + """Yield all of the substrings of *iterable*. + + >>> [''.join(s) for s in substrings('more')] + ['m', 'o', 'r', 'e', 'mo', 'or', 're', 'mor', 'ore', 'more'] + + Note that non-string iterables can also be subdivided. + + >>> list(substrings([0, 1, 2])) + [(0,), (1,), (2,), (0, 1), (1, 2), (0, 1, 2)] + + """ + # The length-1 substrings + seq = [] + for item in iter(iterable): + seq.append(item) + yield (item,) + seq = tuple(seq) + item_count = len(seq) + + # And the rest + for n in range(2, item_count + 1): + for i in range(item_count - n + 1): + yield seq[i : i + n] + + +def substrings_indexes(seq, reverse=False): + """Yield all substrings and their positions in *seq* + + The items yielded will be a tuple of the form ``(substr, i, j)``, where + ``substr == seq[i:j]``. + + This function only works for iterables that support slicing, such as + ``str`` objects. + + >>> for item in substrings_indexes('more'): + ... print(item) + ('m', 0, 1) + ('o', 1, 2) + ('r', 2, 3) + ('e', 3, 4) + ('mo', 0, 2) + ('or', 1, 3) + ('re', 2, 4) + ('mor', 0, 3) + ('ore', 1, 4) + ('more', 0, 4) + + Set *reverse* to ``True`` to yield the same items in the opposite order. + + + """ + r = range(1, len(seq) + 1) + if reverse: + r = reversed(r) + return ( + (seq[i : i + L], i, i + L) for L in r for i in range(len(seq) - L + 1) + ) + + +class bucket: + """Wrap *iterable* and return an object that buckets it iterable into + child iterables based on a *key* function. + + >>> iterable = ['a1', 'b1', 'c1', 'a2', 'b2', 'c2', 'b3'] + >>> s = bucket(iterable, key=lambda x: x[0]) # Bucket by 1st character + >>> sorted(list(s)) # Get the keys + ['a', 'b', 'c'] + >>> a_iterable = s['a'] + >>> next(a_iterable) + 'a1' + >>> next(a_iterable) + 'a2' + >>> list(s['b']) + ['b1', 'b2', 'b3'] + + The original iterable will be advanced and its items will be cached until + they are used by the child iterables. This may require significant storage. + + By default, attempting to select a bucket to which no items belong will + exhaust the iterable and cache all values. + If you specify a *validator* function, selected buckets will instead be + checked against it. + + >>> from itertools import count + >>> it = count(1, 2) # Infinite sequence of odd numbers + >>> key = lambda x: x % 10 # Bucket by last digit + >>> validator = lambda x: x in {1, 3, 5, 7, 9} # Odd digits only + >>> s = bucket(it, key=key, validator=validator) + >>> 2 in s + False + >>> list(s[2]) + [] + + """ + + def __init__(self, iterable, key, validator=None): + self._it = iter(iterable) + self._key = key + self._cache = defaultdict(deque) + self._validator = validator or (lambda x: True) + + def __contains__(self, value): + if not self._validator(value): + return False + + try: + item = next(self[value]) + except StopIteration: + return False + else: + self._cache[value].appendleft(item) + + return True + + def _get_values(self, value): + """ + Helper to yield items from the parent iterator that match *value*. + Items that don't match are stored in the local cache as they + are encountered. + """ + while True: + # If we've cached some items that match the target value, emit + # the first one and evict it from the cache. + if self._cache[value]: + yield self._cache[value].popleft() + # Otherwise we need to advance the parent iterator to search for + # a matching item, caching the rest. + else: + while True: + try: + item = next(self._it) + except StopIteration: + return + item_value = self._key(item) + if item_value == value: + yield item + break + elif self._validator(item_value): + self._cache[item_value].append(item) + + def __iter__(self): + for item in self._it: + item_value = self._key(item) + if self._validator(item_value): + self._cache[item_value].append(item) + + yield from self._cache.keys() + + def __getitem__(self, value): + if not self._validator(value): + return iter(()) + + return self._get_values(value) + + +def spy(iterable, n=1): + """Return a 2-tuple with a list containing the first *n* elements of + *iterable*, and an iterator with the same items as *iterable*. + This allows you to "look ahead" at the items in the iterable without + advancing it. + + There is one item in the list by default: + + >>> iterable = 'abcdefg' + >>> head, iterable = spy(iterable) + >>> head + ['a'] + >>> list(iterable) + ['a', 'b', 'c', 'd', 'e', 'f', 'g'] + + You may use unpacking to retrieve items instead of lists: + + >>> (head,), iterable = spy('abcdefg') + >>> head + 'a' + >>> (first, second), iterable = spy('abcdefg', 2) + >>> first + 'a' + >>> second + 'b' + + The number of items requested can be larger than the number of items in + the iterable: + + >>> iterable = [1, 2, 3, 4, 5] + >>> head, iterable = spy(iterable, 10) + >>> head + [1, 2, 3, 4, 5] + >>> list(iterable) + [1, 2, 3, 4, 5] + + """ + it = iter(iterable) + head = take(n, it) + + return head.copy(), chain(head, it) + + +def interleave(*iterables): + """Return a new iterable yielding from each iterable in turn, + until the shortest is exhausted. + + >>> list(interleave([1, 2, 3], [4, 5], [6, 7, 8])) + [1, 4, 6, 2, 5, 7] + + For a version that doesn't terminate after the shortest iterable is + exhausted, see :func:`interleave_longest`. + + """ + return chain.from_iterable(zip(*iterables)) + + +def interleave_longest(*iterables): + """Return a new iterable yielding from each iterable in turn, + skipping any that are exhausted. + + >>> list(interleave_longest([1, 2, 3], [4, 5], [6, 7, 8])) + [1, 4, 6, 2, 5, 7, 3, 8] + + This function produces the same output as :func:`roundrobin`, but may + perform better for some inputs (in particular when the number of iterables + is large). + + """ + i = chain.from_iterable(zip_longest(*iterables, fillvalue=_marker)) + return (x for x in i if x is not _marker) + + +def collapse(iterable, base_type=None, levels=None): + """Flatten an iterable with multiple levels of nesting (e.g., a list of + lists of tuples) into non-iterable types. + + >>> iterable = [(1, 2), ([3, 4], [[5], [6]])] + >>> list(collapse(iterable)) + [1, 2, 3, 4, 5, 6] + + Binary and text strings are not considered iterable and + will not be collapsed. + + To avoid collapsing other types, specify *base_type*: + + >>> iterable = ['ab', ('cd', 'ef'), ['gh', 'ij']] + >>> list(collapse(iterable, base_type=tuple)) + ['ab', ('cd', 'ef'), 'gh', 'ij'] + + Specify *levels* to stop flattening after a certain level: + + >>> iterable = [('a', ['b']), ('c', ['d'])] + >>> list(collapse(iterable)) # Fully flattened + ['a', 'b', 'c', 'd'] + >>> list(collapse(iterable, levels=1)) # Only one level flattened + ['a', ['b'], 'c', ['d']] + + """ + + def walk(node, level): + if ( + ((levels is not None) and (level > levels)) + or isinstance(node, (str, bytes)) + or ((base_type is not None) and isinstance(node, base_type)) + ): + yield node + return + + try: + tree = iter(node) + except TypeError: + yield node + return + else: + for child in tree: + yield from walk(child, level + 1) + + yield from walk(iterable, 0) + + +def side_effect(func, iterable, chunk_size=None, before=None, after=None): + """Invoke *func* on each item in *iterable* (or on each *chunk_size* group + of items) before yielding the item. + + `func` must be a function that takes a single argument. Its return value + will be discarded. + + *before* and *after* are optional functions that take no arguments. They + will be executed before iteration starts and after it ends, respectively. + + `side_effect` can be used for logging, updating progress bars, or anything + that is not functionally "pure." + + Emitting a status message: + + >>> from more_itertools import consume + >>> func = lambda item: print('Received {}'.format(item)) + >>> consume(side_effect(func, range(2))) + Received 0 + Received 1 + + Operating on chunks of items: + + >>> pair_sums = [] + >>> func = lambda chunk: pair_sums.append(sum(chunk)) + >>> list(side_effect(func, [0, 1, 2, 3, 4, 5], 2)) + [0, 1, 2, 3, 4, 5] + >>> list(pair_sums) + [1, 5, 9] + + Writing to a file-like object: + + >>> from io import StringIO + >>> from more_itertools import consume + >>> f = StringIO() + >>> func = lambda x: print(x, file=f) + >>> before = lambda: print(u'HEADER', file=f) + >>> after = f.close + >>> it = [u'a', u'b', u'c'] + >>> consume(side_effect(func, it, before=before, after=after)) + >>> f.closed + True + + """ + try: + if before is not None: + before() + + if chunk_size is None: + for item in iterable: + func(item) + yield item + else: + for chunk in chunked(iterable, chunk_size): + func(chunk) + yield from chunk + finally: + if after is not None: + after() + + +def sliced(seq, n, strict=False): + """Yield slices of length *n* from the sequence *seq*. + + >>> list(sliced((1, 2, 3, 4, 5, 6), 3)) + [(1, 2, 3), (4, 5, 6)] + + By the default, the last yielded slice will have fewer than *n* elements + if the length of *seq* is not divisible by *n*: + + >>> list(sliced((1, 2, 3, 4, 5, 6, 7, 8), 3)) + [(1, 2, 3), (4, 5, 6), (7, 8)] + + If the length of *seq* is not divisible by *n* and *strict* is + ``True``, then ``ValueError`` will be raised before the last + slice is yielded. + + This function will only work for iterables that support slicing. + For non-sliceable iterables, see :func:`chunked`. + + """ + iterator = takewhile(len, (seq[i : i + n] for i in count(0, n))) + if strict: + + def ret(): + for _slice in iterator: + if len(_slice) != n: + raise ValueError("seq is not divisible by n.") + yield _slice + + return iter(ret()) + else: + return iterator + + +def split_at(iterable, pred, maxsplit=-1, keep_separator=False): + """Yield lists of items from *iterable*, where each list is delimited by + an item where callable *pred* returns ``True``. + + >>> list(split_at('abcdcba', lambda x: x == 'b')) + [['a'], ['c', 'd', 'c'], ['a']] + + >>> list(split_at(range(10), lambda n: n % 2 == 1)) + [[0], [2], [4], [6], [8], []] + + At most *maxsplit* splits are done. If *maxsplit* is not specified or -1, + then there is no limit on the number of splits: + + >>> list(split_at(range(10), lambda n: n % 2 == 1, maxsplit=2)) + [[0], [2], [4, 5, 6, 7, 8, 9]] + + By default, the delimiting items are not included in the output. + The include them, set *keep_separator* to ``True``. + + >>> list(split_at('abcdcba', lambda x: x == 'b', keep_separator=True)) + [['a'], ['b'], ['c', 'd', 'c'], ['b'], ['a']] + + """ + if maxsplit == 0: + yield list(iterable) + return + + buf = [] + it = iter(iterable) + for item in it: + if pred(item): + yield buf + if keep_separator: + yield [item] + if maxsplit == 1: + yield list(it) + return + buf = [] + maxsplit -= 1 + else: + buf.append(item) + yield buf + + +def split_before(iterable, pred, maxsplit=-1): + """Yield lists of items from *iterable*, where each list ends just before + an item for which callable *pred* returns ``True``: + + >>> list(split_before('OneTwo', lambda s: s.isupper())) + [['O', 'n', 'e'], ['T', 'w', 'o']] + + >>> list(split_before(range(10), lambda n: n % 3 == 0)) + [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]] + + At most *maxsplit* splits are done. If *maxsplit* is not specified or -1, + then there is no limit on the number of splits: + + >>> list(split_before(range(10), lambda n: n % 3 == 0, maxsplit=2)) + [[0, 1, 2], [3, 4, 5], [6, 7, 8, 9]] + """ + if maxsplit == 0: + yield list(iterable) + return + + buf = [] + it = iter(iterable) + for item in it: + if pred(item) and buf: + yield buf + if maxsplit == 1: + yield [item] + list(it) + return + buf = [] + maxsplit -= 1 + buf.append(item) + if buf: + yield buf + + +def split_after(iterable, pred, maxsplit=-1): + """Yield lists of items from *iterable*, where each list ends with an + item where callable *pred* returns ``True``: + + >>> list(split_after('one1two2', lambda s: s.isdigit())) + [['o', 'n', 'e', '1'], ['t', 'w', 'o', '2']] + + >>> list(split_after(range(10), lambda n: n % 3 == 0)) + [[0], [1, 2, 3], [4, 5, 6], [7, 8, 9]] + + At most *maxsplit* splits are done. If *maxsplit* is not specified or -1, + then there is no limit on the number of splits: + + >>> list(split_after(range(10), lambda n: n % 3 == 0, maxsplit=2)) + [[0], [1, 2, 3], [4, 5, 6, 7, 8, 9]] + + """ + if maxsplit == 0: + yield list(iterable) + return + + buf = [] + it = iter(iterable) + for item in it: + buf.append(item) + if pred(item) and buf: + yield buf + if maxsplit == 1: + yield list(it) + return + buf = [] + maxsplit -= 1 + if buf: + yield buf + + +def split_when(iterable, pred, maxsplit=-1): + """Split *iterable* into pieces based on the output of *pred*. + *pred* should be a function that takes successive pairs of items and + returns ``True`` if the iterable should be split in between them. + + For example, to find runs of increasing numbers, split the iterable when + element ``i`` is larger than element ``i + 1``: + + >>> list(split_when([1, 2, 3, 3, 2, 5, 2, 4, 2], lambda x, y: x > y)) + [[1, 2, 3, 3], [2, 5], [2, 4], [2]] + + At most *maxsplit* splits are done. If *maxsplit* is not specified or -1, + then there is no limit on the number of splits: + + >>> list(split_when([1, 2, 3, 3, 2, 5, 2, 4, 2], + ... lambda x, y: x > y, maxsplit=2)) + [[1, 2, 3, 3], [2, 5], [2, 4, 2]] + + """ + if maxsplit == 0: + yield list(iterable) + return + + it = iter(iterable) + try: + cur_item = next(it) + except StopIteration: + return + + buf = [cur_item] + for next_item in it: + if pred(cur_item, next_item): + yield buf + if maxsplit == 1: + yield [next_item] + list(it) + return + buf = [] + maxsplit -= 1 + + buf.append(next_item) + cur_item = next_item + + yield buf + + +def split_into(iterable, sizes): + """Yield a list of sequential items from *iterable* of length 'n' for each + integer 'n' in *sizes*. + + >>> list(split_into([1,2,3,4,5,6], [1,2,3])) + [[1], [2, 3], [4, 5, 6]] + + If the sum of *sizes* is smaller than the length of *iterable*, then the + remaining items of *iterable* will not be returned. + + >>> list(split_into([1,2,3,4,5,6], [2,3])) + [[1, 2], [3, 4, 5]] + + If the sum of *sizes* is larger than the length of *iterable*, fewer items + will be returned in the iteration that overruns *iterable* and further + lists will be empty: + + >>> list(split_into([1,2,3,4], [1,2,3,4])) + [[1], [2, 3], [4], []] + + When a ``None`` object is encountered in *sizes*, the returned list will + contain items up to the end of *iterable* the same way that itertools.slice + does: + + >>> list(split_into([1,2,3,4,5,6,7,8,9,0], [2,3,None])) + [[1, 2], [3, 4, 5], [6, 7, 8, 9, 0]] + + :func:`split_into` can be useful for grouping a series of items where the + sizes of the groups are not uniform. An example would be where in a row + from a table, multiple columns represent elements of the same feature + (e.g. a point represented by x,y,z) but, the format is not the same for + all columns. + """ + # convert the iterable argument into an iterator so its contents can + # be consumed by islice in case it is a generator + it = iter(iterable) + + for size in sizes: + if size is None: + yield list(it) + return + else: + yield list(islice(it, size)) + + +def padded(iterable, fillvalue=None, n=None, next_multiple=False): + """Yield the elements from *iterable*, followed by *fillvalue*, such that + at least *n* items are emitted. + + >>> list(padded([1, 2, 3], '?', 5)) + [1, 2, 3, '?', '?'] + + If *next_multiple* is ``True``, *fillvalue* will be emitted until the + number of items emitted is a multiple of *n*:: + + >>> list(padded([1, 2, 3, 4], n=3, next_multiple=True)) + [1, 2, 3, 4, None, None] + + If *n* is ``None``, *fillvalue* will be emitted indefinitely. + + """ + it = iter(iterable) + if n is None: + yield from chain(it, repeat(fillvalue)) + elif n < 1: + raise ValueError('n must be at least 1') + else: + item_count = 0 + for item in it: + yield item + item_count += 1 + + remaining = (n - item_count) % n if next_multiple else n - item_count + for _ in range(remaining): + yield fillvalue + + +def repeat_last(iterable, default=None): + """After the *iterable* is exhausted, keep yielding its last element. + + >>> list(islice(repeat_last(range(3)), 5)) + [0, 1, 2, 2, 2] + + If the iterable is empty, yield *default* forever:: + + >>> list(islice(repeat_last(range(0), 42), 5)) + [42, 42, 42, 42, 42] + + """ + item = _marker + for item in iterable: + yield item + final = default if item is _marker else item + yield from repeat(final) + + +def distribute(n, iterable): + """Distribute the items from *iterable* among *n* smaller iterables. + + >>> group_1, group_2 = distribute(2, [1, 2, 3, 4, 5, 6]) + >>> list(group_1) + [1, 3, 5] + >>> list(group_2) + [2, 4, 6] + + If the length of *iterable* is not evenly divisible by *n*, then the + length of the returned iterables will not be identical: + + >>> children = distribute(3, [1, 2, 3, 4, 5, 6, 7]) + >>> [list(c) for c in children] + [[1, 4, 7], [2, 5], [3, 6]] + + If the length of *iterable* is smaller than *n*, then the last returned + iterables will be empty: + + >>> children = distribute(5, [1, 2, 3]) + >>> [list(c) for c in children] + [[1], [2], [3], [], []] + + This function uses :func:`itertools.tee` and may require significant + storage. If you need the order items in the smaller iterables to match the + original iterable, see :func:`divide`. + + """ + if n < 1: + raise ValueError('n must be at least 1') + + children = tee(iterable, n) + return [islice(it, index, None, n) for index, it in enumerate(children)] + + +def stagger(iterable, offsets=(-1, 0, 1), longest=False, fillvalue=None): + """Yield tuples whose elements are offset from *iterable*. + The amount by which the `i`-th item in each tuple is offset is given by + the `i`-th item in *offsets*. + + >>> list(stagger([0, 1, 2, 3])) + [(None, 0, 1), (0, 1, 2), (1, 2, 3)] + >>> list(stagger(range(8), offsets=(0, 2, 4))) + [(0, 2, 4), (1, 3, 5), (2, 4, 6), (3, 5, 7)] + + By default, the sequence will end when the final element of a tuple is the + last item in the iterable. To continue until the first element of a tuple + is the last item in the iterable, set *longest* to ``True``:: + + >>> list(stagger([0, 1, 2, 3], longest=True)) + [(None, 0, 1), (0, 1, 2), (1, 2, 3), (2, 3, None), (3, None, None)] + + By default, ``None`` will be used to replace offsets beyond the end of the + sequence. Specify *fillvalue* to use some other value. + + """ + children = tee(iterable, len(offsets)) + + return zip_offset( + *children, offsets=offsets, longest=longest, fillvalue=fillvalue + ) + + +class UnequalIterablesError(ValueError): + def __init__(self, details=None): + msg = 'Iterables have different lengths' + if details is not None: + msg += (': index 0 has length {}; index {} has length {}').format( + *details + ) + + super().__init__(msg) + + +def _zip_equal_generator(iterables): + for combo in zip_longest(*iterables, fillvalue=_marker): + for val in combo: + if val is _marker: + raise UnequalIterablesError() + yield combo + + +def zip_equal(*iterables): + """``zip`` the input *iterables* together, but raise + ``UnequalIterablesError`` if they aren't all the same length. + + >>> it_1 = range(3) + >>> it_2 = iter('abc') + >>> list(zip_equal(it_1, it_2)) + [(0, 'a'), (1, 'b'), (2, 'c')] + + >>> it_1 = range(3) + >>> it_2 = iter('abcd') + >>> list(zip_equal(it_1, it_2)) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + more_itertools.more.UnequalIterablesError: Iterables have different + lengths + + """ + if hexversion >= 0x30A00A6: + warnings.warn( + ( + 'zip_equal will be removed in a future version of ' + 'more-itertools. Use the builtin zip function with ' + 'strict=True instead.' + ), + DeprecationWarning, + ) + # Check whether the iterables are all the same size. + try: + first_size = len(iterables[0]) + for i, it in enumerate(iterables[1:], 1): + size = len(it) + if size != first_size: + break + else: + # If we didn't break out, we can use the built-in zip. + return zip(*iterables) + + # If we did break out, there was a mismatch. + raise UnequalIterablesError(details=(first_size, i, size)) + # If any one of the iterables didn't have a length, start reading + # them until one runs out. + except TypeError: + return _zip_equal_generator(iterables) + + +def zip_offset(*iterables, offsets, longest=False, fillvalue=None): + """``zip`` the input *iterables* together, but offset the `i`-th iterable + by the `i`-th item in *offsets*. + + >>> list(zip_offset('0123', 'abcdef', offsets=(0, 1))) + [('0', 'b'), ('1', 'c'), ('2', 'd'), ('3', 'e')] + + This can be used as a lightweight alternative to SciPy or pandas to analyze + data sets in which some series have a lead or lag relationship. + + By default, the sequence will end when the shortest iterable is exhausted. + To continue until the longest iterable is exhausted, set *longest* to + ``True``. + + >>> list(zip_offset('0123', 'abcdef', offsets=(0, 1), longest=True)) + [('0', 'b'), ('1', 'c'), ('2', 'd'), ('3', 'e'), (None, 'f')] + + By default, ``None`` will be used to replace offsets beyond the end of the + sequence. Specify *fillvalue* to use some other value. + + """ + if len(iterables) != len(offsets): + raise ValueError("Number of iterables and offsets didn't match") + + staggered = [] + for it, n in zip(iterables, offsets): + if n < 0: + staggered.append(chain(repeat(fillvalue, -n), it)) + elif n > 0: + staggered.append(islice(it, n, None)) + else: + staggered.append(it) + + if longest: + return zip_longest(*staggered, fillvalue=fillvalue) + + return zip(*staggered) + + +def sort_together(iterables, key_list=(0,), key=None, reverse=False): + """Return the input iterables sorted together, with *key_list* as the + priority for sorting. All iterables are trimmed to the length of the + shortest one. + + This can be used like the sorting function in a spreadsheet. If each + iterable represents a column of data, the key list determines which + columns are used for sorting. + + By default, all iterables are sorted using the ``0``-th iterable:: + + >>> iterables = [(4, 3, 2, 1), ('a', 'b', 'c', 'd')] + >>> sort_together(iterables) + [(1, 2, 3, 4), ('d', 'c', 'b', 'a')] + + Set a different key list to sort according to another iterable. + Specifying multiple keys dictates how ties are broken:: + + >>> iterables = [(3, 1, 2), (0, 1, 0), ('c', 'b', 'a')] + >>> sort_together(iterables, key_list=(1, 2)) + [(2, 3, 1), (0, 0, 1), ('a', 'c', 'b')] + + To sort by a function of the elements of the iterable, pass a *key* + function. Its arguments are the elements of the iterables corresponding to + the key list:: + + >>> names = ('a', 'b', 'c') + >>> lengths = (1, 2, 3) + >>> widths = (5, 2, 1) + >>> def area(length, width): + ... return length * width + >>> sort_together([names, lengths, widths], key_list=(1, 2), key=area) + [('c', 'b', 'a'), (3, 2, 1), (1, 2, 5)] + + Set *reverse* to ``True`` to sort in descending order. + + >>> sort_together([(1, 2, 3), ('c', 'b', 'a')], reverse=True) + [(3, 2, 1), ('a', 'b', 'c')] + + """ + if key is None: + # if there is no key function, the key argument to sorted is an + # itemgetter + key_argument = itemgetter(*key_list) + else: + # if there is a key function, call it with the items at the offsets + # specified by the key function as arguments + key_list = list(key_list) + if len(key_list) == 1: + # if key_list contains a single item, pass the item at that offset + # as the only argument to the key function + key_offset = key_list[0] + key_argument = lambda zipped_items: key(zipped_items[key_offset]) + else: + # if key_list contains multiple items, use itemgetter to return a + # tuple of items, which we pass as *args to the key function + get_key_items = itemgetter(*key_list) + key_argument = lambda zipped_items: key( + *get_key_items(zipped_items) + ) + + return list( + zip(*sorted(zip(*iterables), key=key_argument, reverse=reverse)) + ) + + +def unzip(iterable): + """The inverse of :func:`zip`, this function disaggregates the elements + of the zipped *iterable*. + + The ``i``-th iterable contains the ``i``-th element from each element + of the zipped iterable. The first element is used to to determine the + length of the remaining elements. + + >>> iterable = [('a', 1), ('b', 2), ('c', 3), ('d', 4)] + >>> letters, numbers = unzip(iterable) + >>> list(letters) + ['a', 'b', 'c', 'd'] + >>> list(numbers) + [1, 2, 3, 4] + + This is similar to using ``zip(*iterable)``, but it avoids reading + *iterable* into memory. Note, however, that this function uses + :func:`itertools.tee` and thus may require significant storage. + + """ + head, iterable = spy(iter(iterable)) + if not head: + # empty iterable, e.g. zip([], [], []) + return () + # spy returns a one-length iterable as head + head = head[0] + iterables = tee(iterable, len(head)) + + def itemgetter(i): + def getter(obj): + try: + return obj[i] + except IndexError: + # basically if we have an iterable like + # iter([(1, 2, 3), (4, 5), (6,)]) + # the second unzipped iterable would fail at the third tuple + # since it would try to access tup[1] + # same with the third unzipped iterable and the second tuple + # to support these "improperly zipped" iterables, + # we create a custom itemgetter + # which just stops the unzipped iterables + # at first length mismatch + raise StopIteration + + return getter + + return tuple(map(itemgetter(i), it) for i, it in enumerate(iterables)) + + +def divide(n, iterable): + """Divide the elements from *iterable* into *n* parts, maintaining + order. + + >>> group_1, group_2 = divide(2, [1, 2, 3, 4, 5, 6]) + >>> list(group_1) + [1, 2, 3] + >>> list(group_2) + [4, 5, 6] + + If the length of *iterable* is not evenly divisible by *n*, then the + length of the returned iterables will not be identical: + + >>> children = divide(3, [1, 2, 3, 4, 5, 6, 7]) + >>> [list(c) for c in children] + [[1, 2, 3], [4, 5], [6, 7]] + + If the length of the iterable is smaller than n, then the last returned + iterables will be empty: + + >>> children = divide(5, [1, 2, 3]) + >>> [list(c) for c in children] + [[1], [2], [3], [], []] + + This function will exhaust the iterable before returning and may require + significant storage. If order is not important, see :func:`distribute`, + which does not first pull the iterable into memory. + + """ + if n < 1: + raise ValueError('n must be at least 1') + + try: + iterable[:0] + except TypeError: + seq = tuple(iterable) + else: + seq = iterable + + q, r = divmod(len(seq), n) + + ret = [] + stop = 0 + for i in range(1, n + 1): + start = stop + stop += q + 1 if i <= r else q + ret.append(iter(seq[start:stop])) + + return ret + + +def always_iterable(obj, base_type=(str, bytes)): + """If *obj* is iterable, return an iterator over its items:: + + >>> obj = (1, 2, 3) + >>> list(always_iterable(obj)) + [1, 2, 3] + + If *obj* is not iterable, return a one-item iterable containing *obj*:: + + >>> obj = 1 + >>> list(always_iterable(obj)) + [1] + + If *obj* is ``None``, return an empty iterable: + + >>> obj = None + >>> list(always_iterable(None)) + [] + + By default, binary and text strings are not considered iterable:: + + >>> obj = 'foo' + >>> list(always_iterable(obj)) + ['foo'] + + If *base_type* is set, objects for which ``isinstance(obj, base_type)`` + returns ``True`` won't be considered iterable. + + >>> obj = {'a': 1} + >>> list(always_iterable(obj)) # Iterate over the dict's keys + ['a'] + >>> list(always_iterable(obj, base_type=dict)) # Treat dicts as a unit + [{'a': 1}] + + Set *base_type* to ``None`` to avoid any special handling and treat objects + Python considers iterable as iterable: + + >>> obj = 'foo' + >>> list(always_iterable(obj, base_type=None)) + ['f', 'o', 'o'] + """ + if obj is None: + return iter(()) + + if (base_type is not None) and isinstance(obj, base_type): + return iter((obj,)) + + try: + return iter(obj) + except TypeError: + return iter((obj,)) + + +def adjacent(predicate, iterable, distance=1): + """Return an iterable over `(bool, item)` tuples where the `item` is + drawn from *iterable* and the `bool` indicates whether + that item satisfies the *predicate* or is adjacent to an item that does. + + For example, to find whether items are adjacent to a ``3``:: + + >>> list(adjacent(lambda x: x == 3, range(6))) + [(False, 0), (False, 1), (True, 2), (True, 3), (True, 4), (False, 5)] + + Set *distance* to change what counts as adjacent. For example, to find + whether items are two places away from a ``3``: + + >>> list(adjacent(lambda x: x == 3, range(6), distance=2)) + [(False, 0), (True, 1), (True, 2), (True, 3), (True, 4), (True, 5)] + + This is useful for contextualizing the results of a search function. + For example, a code comparison tool might want to identify lines that + have changed, but also surrounding lines to give the viewer of the diff + context. + + The predicate function will only be called once for each item in the + iterable. + + See also :func:`groupby_transform`, which can be used with this function + to group ranges of items with the same `bool` value. + + """ + # Allow distance=0 mainly for testing that it reproduces results with map() + if distance < 0: + raise ValueError('distance must be at least 0') + + i1, i2 = tee(iterable) + padding = [False] * distance + selected = chain(padding, map(predicate, i1), padding) + adjacent_to_selected = map(any, windowed(selected, 2 * distance + 1)) + return zip(adjacent_to_selected, i2) + + +def groupby_transform(iterable, keyfunc=None, valuefunc=None, reducefunc=None): + """An extension of :func:`itertools.groupby` that can apply transformations + to the grouped data. + + * *keyfunc* is a function computing a key value for each item in *iterable* + * *valuefunc* is a function that transforms the individual items from + *iterable* after grouping + * *reducefunc* is a function that transforms each group of items + + >>> iterable = 'aAAbBBcCC' + >>> keyfunc = lambda k: k.upper() + >>> valuefunc = lambda v: v.lower() + >>> reducefunc = lambda g: ''.join(g) + >>> list(groupby_transform(iterable, keyfunc, valuefunc, reducefunc)) + [('A', 'aaa'), ('B', 'bbb'), ('C', 'ccc')] + + Each optional argument defaults to an identity function if not specified. + + :func:`groupby_transform` is useful when grouping elements of an iterable + using a separate iterable as the key. To do this, :func:`zip` the iterables + and pass a *keyfunc* that extracts the first element and a *valuefunc* + that extracts the second element:: + + >>> from operator import itemgetter + >>> keys = [0, 0, 1, 1, 1, 2, 2, 2, 3] + >>> values = 'abcdefghi' + >>> iterable = zip(keys, values) + >>> grouper = groupby_transform(iterable, itemgetter(0), itemgetter(1)) + >>> [(k, ''.join(g)) for k, g in grouper] + [(0, 'ab'), (1, 'cde'), (2, 'fgh'), (3, 'i')] + + Note that the order of items in the iterable is significant. + Only adjacent items are grouped together, so if you don't want any + duplicate groups, you should sort the iterable by the key function. + + """ + ret = groupby(iterable, keyfunc) + if valuefunc: + ret = ((k, map(valuefunc, g)) for k, g in ret) + if reducefunc: + ret = ((k, reducefunc(g)) for k, g in ret) + + return ret + + +class numeric_range(abc.Sequence, abc.Hashable): + """An extension of the built-in ``range()`` function whose arguments can + be any orderable numeric type. + + With only *stop* specified, *start* defaults to ``0`` and *step* + defaults to ``1``. The output items will match the type of *stop*: + + >>> list(numeric_range(3.5)) + [0.0, 1.0, 2.0, 3.0] + + With only *start* and *stop* specified, *step* defaults to ``1``. The + output items will match the type of *start*: + + >>> from decimal import Decimal + >>> start = Decimal('2.1') + >>> stop = Decimal('5.1') + >>> list(numeric_range(start, stop)) + [Decimal('2.1'), Decimal('3.1'), Decimal('4.1')] + + With *start*, *stop*, and *step* specified the output items will match + the type of ``start + step``: + + >>> from fractions import Fraction + >>> start = Fraction(1, 2) # Start at 1/2 + >>> stop = Fraction(5, 2) # End at 5/2 + >>> step = Fraction(1, 2) # Count by 1/2 + >>> list(numeric_range(start, stop, step)) + [Fraction(1, 2), Fraction(1, 1), Fraction(3, 2), Fraction(2, 1)] + + If *step* is zero, ``ValueError`` is raised. Negative steps are supported: + + >>> list(numeric_range(3, -1, -1.0)) + [3.0, 2.0, 1.0, 0.0] + + Be aware of the limitations of floating point numbers; the representation + of the yielded numbers may be surprising. + + ``datetime.datetime`` objects can be used for *start* and *stop*, if *step* + is a ``datetime.timedelta`` object: + + >>> import datetime + >>> start = datetime.datetime(2019, 1, 1) + >>> stop = datetime.datetime(2019, 1, 3) + >>> step = datetime.timedelta(days=1) + >>> items = iter(numeric_range(start, stop, step)) + >>> next(items) + datetime.datetime(2019, 1, 1, 0, 0) + >>> next(items) + datetime.datetime(2019, 1, 2, 0, 0) + + """ + + _EMPTY_HASH = hash(range(0, 0)) + + def __init__(self, *args): + argc = len(args) + if argc == 1: + (self._stop,) = args + self._start = type(self._stop)(0) + self._step = type(self._stop - self._start)(1) + elif argc == 2: + self._start, self._stop = args + self._step = type(self._stop - self._start)(1) + elif argc == 3: + self._start, self._stop, self._step = args + elif argc == 0: + raise TypeError( + 'numeric_range expected at least ' + '1 argument, got {}'.format(argc) + ) + else: + raise TypeError( + 'numeric_range expected at most ' + '3 arguments, got {}'.format(argc) + ) + + self._zero = type(self._step)(0) + if self._step == self._zero: + raise ValueError('numeric_range() arg 3 must not be zero') + self._growing = self._step > self._zero + self._init_len() + + def __bool__(self): + if self._growing: + return self._start < self._stop + else: + return self._start > self._stop + + def __contains__(self, elem): + if self._growing: + if self._start <= elem < self._stop: + return (elem - self._start) % self._step == self._zero + else: + if self._start >= elem > self._stop: + return (self._start - elem) % (-self._step) == self._zero + + return False + + def __eq__(self, other): + if isinstance(other, numeric_range): + empty_self = not bool(self) + empty_other = not bool(other) + if empty_self or empty_other: + return empty_self and empty_other # True if both empty + else: + return ( + self._start == other._start + and self._step == other._step + and self._get_by_index(-1) == other._get_by_index(-1) + ) + else: + return False + + def __getitem__(self, key): + if isinstance(key, int): + return self._get_by_index(key) + elif isinstance(key, slice): + step = self._step if key.step is None else key.step * self._step + + if key.start is None or key.start <= -self._len: + start = self._start + elif key.start >= self._len: + start = self._stop + else: # -self._len < key.start < self._len + start = self._get_by_index(key.start) + + if key.stop is None or key.stop >= self._len: + stop = self._stop + elif key.stop <= -self._len: + stop = self._start + else: # -self._len < key.stop < self._len + stop = self._get_by_index(key.stop) + + return numeric_range(start, stop, step) + else: + raise TypeError( + 'numeric range indices must be ' + 'integers or slices, not {}'.format(type(key).__name__) + ) + + def __hash__(self): + if self: + return hash((self._start, self._get_by_index(-1), self._step)) + else: + return self._EMPTY_HASH + + def __iter__(self): + values = (self._start + (n * self._step) for n in count()) + if self._growing: + return takewhile(partial(gt, self._stop), values) + else: + return takewhile(partial(lt, self._stop), values) + + def __len__(self): + return self._len + + def _init_len(self): + if self._growing: + start = self._start + stop = self._stop + step = self._step + else: + start = self._stop + stop = self._start + step = -self._step + distance = stop - start + if distance <= self._zero: + self._len = 0 + else: # distance > 0 and step > 0: regular euclidean division + q, r = divmod(distance, step) + self._len = int(q) + int(r != self._zero) + + def __reduce__(self): + return numeric_range, (self._start, self._stop, self._step) + + def __repr__(self): + if self._step == 1: + return "numeric_range({}, {})".format( + repr(self._start), repr(self._stop) + ) + else: + return "numeric_range({}, {}, {})".format( + repr(self._start), repr(self._stop), repr(self._step) + ) + + def __reversed__(self): + return iter( + numeric_range( + self._get_by_index(-1), self._start - self._step, -self._step + ) + ) + + def count(self, value): + return int(value in self) + + def index(self, value): + if self._growing: + if self._start <= value < self._stop: + q, r = divmod(value - self._start, self._step) + if r == self._zero: + return int(q) + else: + if self._start >= value > self._stop: + q, r = divmod(self._start - value, -self._step) + if r == self._zero: + return int(q) + + raise ValueError("{} is not in numeric range".format(value)) + + def _get_by_index(self, i): + if i < 0: + i += self._len + if i < 0 or i >= self._len: + raise IndexError("numeric range object index out of range") + return self._start + i * self._step + + +def count_cycle(iterable, n=None): + """Cycle through the items from *iterable* up to *n* times, yielding + the number of completed cycles along with each item. If *n* is omitted the + process repeats indefinitely. + + >>> list(count_cycle('AB', 3)) + [(0, 'A'), (0, 'B'), (1, 'A'), (1, 'B'), (2, 'A'), (2, 'B')] + + """ + iterable = tuple(iterable) + if not iterable: + return iter(()) + counter = count() if n is None else range(n) + return ((i, item) for i in counter for item in iterable) + + +def mark_ends(iterable): + """Yield 3-tuples of the form ``(is_first, is_last, item)``. + + >>> list(mark_ends('ABC')) + [(True, False, 'A'), (False, False, 'B'), (False, True, 'C')] + + Use this when looping over an iterable to take special action on its first + and/or last items: + + >>> iterable = ['Header', 100, 200, 'Footer'] + >>> total = 0 + >>> for is_first, is_last, item in mark_ends(iterable): + ... if is_first: + ... continue # Skip the header + ... if is_last: + ... continue # Skip the footer + ... total += item + >>> print(total) + 300 + """ + it = iter(iterable) + + try: + b = next(it) + except StopIteration: + return + + try: + for i in count(): + a = b + b = next(it) + yield i == 0, False, a + + except StopIteration: + yield i == 0, True, a + + +def locate(iterable, pred=bool, window_size=None): + """Yield the index of each item in *iterable* for which *pred* returns + ``True``. + + *pred* defaults to :func:`bool`, which will select truthy items: + + >>> list(locate([0, 1, 1, 0, 1, 0, 0])) + [1, 2, 4] + + Set *pred* to a custom function to, e.g., find the indexes for a particular + item. + + >>> list(locate(['a', 'b', 'c', 'b'], lambda x: x == 'b')) + [1, 3] + + If *window_size* is given, then the *pred* function will be called with + that many items. This enables searching for sub-sequences: + + >>> iterable = [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3] + >>> pred = lambda *args: args == (1, 2, 3) + >>> list(locate(iterable, pred=pred, window_size=3)) + [1, 5, 9] + + Use with :func:`seekable` to find indexes and then retrieve the associated + items: + + >>> from itertools import count + >>> from more_itertools import seekable + >>> source = (3 * n + 1 if (n % 2) else n // 2 for n in count()) + >>> it = seekable(source) + >>> pred = lambda x: x > 100 + >>> indexes = locate(it, pred=pred) + >>> i = next(indexes) + >>> it.seek(i) + >>> next(it) + 106 + + """ + if window_size is None: + return compress(count(), map(pred, iterable)) + + if window_size < 1: + raise ValueError('window size must be at least 1') + + it = windowed(iterable, window_size, fillvalue=_marker) + return compress(count(), starmap(pred, it)) + + +def lstrip(iterable, pred): + """Yield the items from *iterable*, but strip any from the beginning + for which *pred* returns ``True``. + + For example, to remove a set of items from the start of an iterable: + + >>> iterable = (None, False, None, 1, 2, None, 3, False, None) + >>> pred = lambda x: x in {None, False, ''} + >>> list(lstrip(iterable, pred)) + [1, 2, None, 3, False, None] + + This function is analogous to to :func:`str.lstrip`, and is essentially + an wrapper for :func:`itertools.dropwhile`. + + """ + return dropwhile(pred, iterable) + + +def rstrip(iterable, pred): + """Yield the items from *iterable*, but strip any from the end + for which *pred* returns ``True``. + + For example, to remove a set of items from the end of an iterable: + + >>> iterable = (None, False, None, 1, 2, None, 3, False, None) + >>> pred = lambda x: x in {None, False, ''} + >>> list(rstrip(iterable, pred)) + [None, False, None, 1, 2, None, 3] + + This function is analogous to :func:`str.rstrip`. + + """ + cache = [] + cache_append = cache.append + cache_clear = cache.clear + for x in iterable: + if pred(x): + cache_append(x) + else: + yield from cache + cache_clear() + yield x + + +def strip(iterable, pred): + """Yield the items from *iterable*, but strip any from the + beginning and end for which *pred* returns ``True``. + + For example, to remove a set of items from both ends of an iterable: + + >>> iterable = (None, False, None, 1, 2, None, 3, False, None) + >>> pred = lambda x: x in {None, False, ''} + >>> list(strip(iterable, pred)) + [1, 2, None, 3] + + This function is analogous to :func:`str.strip`. + + """ + return rstrip(lstrip(iterable, pred), pred) + + +class islice_extended: + """An extension of :func:`itertools.islice` that supports negative values + for *stop*, *start*, and *step*. + + >>> iterable = iter('abcdefgh') + >>> list(islice_extended(iterable, -4, -1)) + ['e', 'f', 'g'] + + Slices with negative values require some caching of *iterable*, but this + function takes care to minimize the amount of memory required. + + For example, you can use a negative step with an infinite iterator: + + >>> from itertools import count + >>> list(islice_extended(count(), 110, 99, -2)) + [110, 108, 106, 104, 102, 100] + + You can also use slice notation directly: + + >>> iterable = map(str, count()) + >>> it = islice_extended(iterable)[10:20:2] + >>> list(it) + ['10', '12', '14', '16', '18'] + + """ + + def __init__(self, iterable, *args): + it = iter(iterable) + if args: + self._iterable = _islice_helper(it, slice(*args)) + else: + self._iterable = it + + def __iter__(self): + return self + + def __next__(self): + return next(self._iterable) + + def __getitem__(self, key): + if isinstance(key, slice): + return islice_extended(_islice_helper(self._iterable, key)) + + raise TypeError('islice_extended.__getitem__ argument must be a slice') + + +def _islice_helper(it, s): + start = s.start + stop = s.stop + if s.step == 0: + raise ValueError('step argument must be a non-zero integer or None.') + step = s.step or 1 + + if step > 0: + start = 0 if (start is None) else start + + if start < 0: + # Consume all but the last -start items + cache = deque(enumerate(it, 1), maxlen=-start) + len_iter = cache[-1][0] if cache else 0 + + # Adjust start to be positive + i = max(len_iter + start, 0) + + # Adjust stop to be positive + if stop is None: + j = len_iter + elif stop >= 0: + j = min(stop, len_iter) + else: + j = max(len_iter + stop, 0) + + # Slice the cache + n = j - i + if n <= 0: + return + + for index, item in islice(cache, 0, n, step): + yield item + elif (stop is not None) and (stop < 0): + # Advance to the start position + next(islice(it, start, start), None) + + # When stop is negative, we have to carry -stop items while + # iterating + cache = deque(islice(it, -stop), maxlen=-stop) + + for index, item in enumerate(it): + cached_item = cache.popleft() + if index % step == 0: + yield cached_item + cache.append(item) + else: + # When both start and stop are positive we have the normal case + yield from islice(it, start, stop, step) + else: + start = -1 if (start is None) else start + + if (stop is not None) and (stop < 0): + # Consume all but the last items + n = -stop - 1 + cache = deque(enumerate(it, 1), maxlen=n) + len_iter = cache[-1][0] if cache else 0 + + # If start and stop are both negative they are comparable and + # we can just slice. Otherwise we can adjust start to be negative + # and then slice. + if start < 0: + i, j = start, stop + else: + i, j = min(start - len_iter, -1), None + + for index, item in list(cache)[i:j:step]: + yield item + else: + # Advance to the stop position + if stop is not None: + m = stop + 1 + next(islice(it, m, m), None) + + # stop is positive, so if start is negative they are not comparable + # and we need the rest of the items. + if start < 0: + i = start + n = None + # stop is None and start is positive, so we just need items up to + # the start index. + elif stop is None: + i = None + n = start + 1 + # Both stop and start are positive, so they are comparable. + else: + i = None + n = start - stop + if n <= 0: + return + + cache = list(islice(it, n)) + + yield from cache[i::step] + + +def always_reversible(iterable): + """An extension of :func:`reversed` that supports all iterables, not + just those which implement the ``Reversible`` or ``Sequence`` protocols. + + >>> print(*always_reversible(x for x in range(3))) + 2 1 0 + + If the iterable is already reversible, this function returns the + result of :func:`reversed()`. If the iterable is not reversible, + this function will cache the remaining items in the iterable and + yield them in reverse order, which may require significant storage. + """ + try: + return reversed(iterable) + except TypeError: + return reversed(list(iterable)) + + +def consecutive_groups(iterable, ordering=lambda x: x): + """Yield groups of consecutive items using :func:`itertools.groupby`. + The *ordering* function determines whether two items are adjacent by + returning their position. + + By default, the ordering function is the identity function. This is + suitable for finding runs of numbers: + + >>> iterable = [1, 10, 11, 12, 20, 30, 31, 32, 33, 40] + >>> for group in consecutive_groups(iterable): + ... print(list(group)) + [1] + [10, 11, 12] + [20] + [30, 31, 32, 33] + [40] + + For finding runs of adjacent letters, try using the :meth:`index` method + of a string of letters: + + >>> from string import ascii_lowercase + >>> iterable = 'abcdfgilmnop' + >>> ordering = ascii_lowercase.index + >>> for group in consecutive_groups(iterable, ordering): + ... print(list(group)) + ['a', 'b', 'c', 'd'] + ['f', 'g'] + ['i'] + ['l', 'm', 'n', 'o', 'p'] + + Each group of consecutive items is an iterator that shares it source with + *iterable*. When an an output group is advanced, the previous group is + no longer available unless its elements are copied (e.g., into a ``list``). + + >>> iterable = [1, 2, 11, 12, 21, 22] + >>> saved_groups = [] + >>> for group in consecutive_groups(iterable): + ... saved_groups.append(list(group)) # Copy group elements + >>> saved_groups + [[1, 2], [11, 12], [21, 22]] + + """ + for k, g in groupby( + enumerate(iterable), key=lambda x: x[0] - ordering(x[1]) + ): + yield map(itemgetter(1), g) + + +def difference(iterable, func=sub, *, initial=None): + """This function is the inverse of :func:`itertools.accumulate`. By default + it will compute the first difference of *iterable* using + :func:`operator.sub`: + + >>> from itertools import accumulate + >>> iterable = accumulate([0, 1, 2, 3, 4]) # produces 0, 1, 3, 6, 10 + >>> list(difference(iterable)) + [0, 1, 2, 3, 4] + + *func* defaults to :func:`operator.sub`, but other functions can be + specified. They will be applied as follows:: + + A, B, C, D, ... --> A, func(B, A), func(C, B), func(D, C), ... + + For example, to do progressive division: + + >>> iterable = [1, 2, 6, 24, 120] + >>> func = lambda x, y: x // y + >>> list(difference(iterable, func)) + [1, 2, 3, 4, 5] + + If the *initial* keyword is set, the first element will be skipped when + computing successive differences. + + >>> it = [10, 11, 13, 16] # from accumulate([1, 2, 3], initial=10) + >>> list(difference(it, initial=10)) + [1, 2, 3] + + """ + a, b = tee(iterable) + try: + first = [next(b)] + except StopIteration: + return iter([]) + + if initial is not None: + first = [] + + return chain(first, starmap(func, zip(b, a))) + + +class SequenceView(Sequence): + """Return a read-only view of the sequence object *target*. + + :class:`SequenceView` objects are analogous to Python's built-in + "dictionary view" types. They provide a dynamic view of a sequence's items, + meaning that when the sequence updates, so does the view. + + >>> seq = ['0', '1', '2'] + >>> view = SequenceView(seq) + >>> view + SequenceView(['0', '1', '2']) + >>> seq.append('3') + >>> view + SequenceView(['0', '1', '2', '3']) + + Sequence views support indexing, slicing, and length queries. They act + like the underlying sequence, except they don't allow assignment: + + >>> view[1] + '1' + >>> view[1:-1] + ['1', '2'] + >>> len(view) + 4 + + Sequence views are useful as an alternative to copying, as they don't + require (much) extra storage. + + """ + + def __init__(self, target): + if not isinstance(target, Sequence): + raise TypeError + self._target = target + + def __getitem__(self, index): + return self._target[index] + + def __len__(self): + return len(self._target) + + def __repr__(self): + return '{}({})'.format(self.__class__.__name__, repr(self._target)) + + +class seekable: + """Wrap an iterator to allow for seeking backward and forward. This + progressively caches the items in the source iterable so they can be + re-visited. + + Call :meth:`seek` with an index to seek to that position in the source + iterable. + + To "reset" an iterator, seek to ``0``: + + >>> from itertools import count + >>> it = seekable((str(n) for n in count())) + >>> next(it), next(it), next(it) + ('0', '1', '2') + >>> it.seek(0) + >>> next(it), next(it), next(it) + ('0', '1', '2') + >>> next(it) + '3' + + You can also seek forward: + + >>> it = seekable((str(n) for n in range(20))) + >>> it.seek(10) + >>> next(it) + '10' + >>> it.seek(20) # Seeking past the end of the source isn't a problem + >>> list(it) + [] + >>> it.seek(0) # Resetting works even after hitting the end + >>> next(it), next(it), next(it) + ('0', '1', '2') + + Call :meth:`peek` to look ahead one item without advancing the iterator: + + >>> it = seekable('1234') + >>> it.peek() + '1' + >>> list(it) + ['1', '2', '3', '4'] + >>> it.peek(default='empty') + 'empty' + + Before the iterator is at its end, calling :func:`bool` on it will return + ``True``. After it will return ``False``: + + >>> it = seekable('5678') + >>> bool(it) + True + >>> list(it) + ['5', '6', '7', '8'] + >>> bool(it) + False + + You may view the contents of the cache with the :meth:`elements` method. + That returns a :class:`SequenceView`, a view that updates automatically: + + >>> it = seekable((str(n) for n in range(10))) + >>> next(it), next(it), next(it) + ('0', '1', '2') + >>> elements = it.elements() + >>> elements + SequenceView(['0', '1', '2']) + >>> next(it) + '3' + >>> elements + SequenceView(['0', '1', '2', '3']) + + By default, the cache grows as the source iterable progresses, so beware of + wrapping very large or infinite iterables. Supply *maxlen* to limit the + size of the cache (this of course limits how far back you can seek). + + >>> from itertools import count + >>> it = seekable((str(n) for n in count()), maxlen=2) + >>> next(it), next(it), next(it), next(it) + ('0', '1', '2', '3') + >>> list(it.elements()) + ['2', '3'] + >>> it.seek(0) + >>> next(it), next(it), next(it), next(it) + ('2', '3', '4', '5') + >>> next(it) + '6' + + """ + + def __init__(self, iterable, maxlen=None): + self._source = iter(iterable) + if maxlen is None: + self._cache = [] + else: + self._cache = deque([], maxlen) + self._index = None + + def __iter__(self): + return self + + def __next__(self): + if self._index is not None: + try: + item = self._cache[self._index] + except IndexError: + self._index = None + else: + self._index += 1 + return item + + item = next(self._source) + self._cache.append(item) + return item + + def __bool__(self): + try: + self.peek() + except StopIteration: + return False + return True + + def peek(self, default=_marker): + try: + peeked = next(self) + except StopIteration: + if default is _marker: + raise + return default + if self._index is None: + self._index = len(self._cache) + self._index -= 1 + return peeked + + def elements(self): + return SequenceView(self._cache) + + def seek(self, index): + self._index = index + remainder = index - len(self._cache) + if remainder > 0: + consume(self, remainder) + + +class run_length: + """ + :func:`run_length.encode` compresses an iterable with run-length encoding. + It yields groups of repeated items with the count of how many times they + were repeated: + + >>> uncompressed = 'abbcccdddd' + >>> list(run_length.encode(uncompressed)) + [('a', 1), ('b', 2), ('c', 3), ('d', 4)] + + :func:`run_length.decode` decompresses an iterable that was previously + compressed with run-length encoding. It yields the items of the + decompressed iterable: + + >>> compressed = [('a', 1), ('b', 2), ('c', 3), ('d', 4)] + >>> list(run_length.decode(compressed)) + ['a', 'b', 'b', 'c', 'c', 'c', 'd', 'd', 'd', 'd'] + + """ + + @staticmethod + def encode(iterable): + return ((k, ilen(g)) for k, g in groupby(iterable)) + + @staticmethod + def decode(iterable): + return chain.from_iterable(repeat(k, n) for k, n in iterable) + + +def exactly_n(iterable, n, predicate=bool): + """Return ``True`` if exactly ``n`` items in the iterable are ``True`` + according to the *predicate* function. + + >>> exactly_n([True, True, False], 2) + True + >>> exactly_n([True, True, False], 1) + False + >>> exactly_n([0, 1, 2, 3, 4, 5], 3, lambda x: x < 3) + True + + The iterable will be advanced until ``n + 1`` truthy items are encountered, + so avoid calling it on infinite iterables. + + """ + return len(take(n + 1, filter(predicate, iterable))) == n + + +def circular_shifts(iterable): + """Return a list of circular shifts of *iterable*. + + >>> circular_shifts(range(4)) + [(0, 1, 2, 3), (1, 2, 3, 0), (2, 3, 0, 1), (3, 0, 1, 2)] + """ + lst = list(iterable) + return take(len(lst), windowed(cycle(lst), len(lst))) + + +def make_decorator(wrapping_func, result_index=0): + """Return a decorator version of *wrapping_func*, which is a function that + modifies an iterable. *result_index* is the position in that function's + signature where the iterable goes. + + This lets you use itertools on the "production end," i.e. at function + definition. This can augment what the function returns without changing the + function's code. + + For example, to produce a decorator version of :func:`chunked`: + + >>> from more_itertools import chunked + >>> chunker = make_decorator(chunked, result_index=0) + >>> @chunker(3) + ... def iter_range(n): + ... return iter(range(n)) + ... + >>> list(iter_range(9)) + [[0, 1, 2], [3, 4, 5], [6, 7, 8]] + + To only allow truthy items to be returned: + + >>> truth_serum = make_decorator(filter, result_index=1) + >>> @truth_serum(bool) + ... def boolean_test(): + ... return [0, 1, '', ' ', False, True] + ... + >>> list(boolean_test()) + [1, ' ', True] + + The :func:`peekable` and :func:`seekable` wrappers make for practical + decorators: + + >>> from more_itertools import peekable + >>> peekable_function = make_decorator(peekable) + >>> @peekable_function() + ... def str_range(*args): + ... return (str(x) for x in range(*args)) + ... + >>> it = str_range(1, 20, 2) + >>> next(it), next(it), next(it) + ('1', '3', '5') + >>> it.peek() + '7' + >>> next(it) + '7' + + """ + # See https://sites.google.com/site/bbayles/index/decorator_factory for + # notes on how this works. + def decorator(*wrapping_args, **wrapping_kwargs): + def outer_wrapper(f): + def inner_wrapper(*args, **kwargs): + result = f(*args, **kwargs) + wrapping_args_ = list(wrapping_args) + wrapping_args_.insert(result_index, result) + return wrapping_func(*wrapping_args_, **wrapping_kwargs) + + return inner_wrapper + + return outer_wrapper + + return decorator + + +def map_reduce(iterable, keyfunc, valuefunc=None, reducefunc=None): + """Return a dictionary that maps the items in *iterable* to categories + defined by *keyfunc*, transforms them with *valuefunc*, and + then summarizes them by category with *reducefunc*. + + *valuefunc* defaults to the identity function if it is unspecified. + If *reducefunc* is unspecified, no summarization takes place: + + >>> keyfunc = lambda x: x.upper() + >>> result = map_reduce('abbccc', keyfunc) + >>> sorted(result.items()) + [('A', ['a']), ('B', ['b', 'b']), ('C', ['c', 'c', 'c'])] + + Specifying *valuefunc* transforms the categorized items: + + >>> keyfunc = lambda x: x.upper() + >>> valuefunc = lambda x: 1 + >>> result = map_reduce('abbccc', keyfunc, valuefunc) + >>> sorted(result.items()) + [('A', [1]), ('B', [1, 1]), ('C', [1, 1, 1])] + + Specifying *reducefunc* summarizes the categorized items: + + >>> keyfunc = lambda x: x.upper() + >>> valuefunc = lambda x: 1 + >>> reducefunc = sum + >>> result = map_reduce('abbccc', keyfunc, valuefunc, reducefunc) + >>> sorted(result.items()) + [('A', 1), ('B', 2), ('C', 3)] + + You may want to filter the input iterable before applying the map/reduce + procedure: + + >>> all_items = range(30) + >>> items = [x for x in all_items if 10 <= x <= 20] # Filter + >>> keyfunc = lambda x: x % 2 # Evens map to 0; odds to 1 + >>> categories = map_reduce(items, keyfunc=keyfunc) + >>> sorted(categories.items()) + [(0, [10, 12, 14, 16, 18, 20]), (1, [11, 13, 15, 17, 19])] + >>> summaries = map_reduce(items, keyfunc=keyfunc, reducefunc=sum) + >>> sorted(summaries.items()) + [(0, 90), (1, 75)] + + Note that all items in the iterable are gathered into a list before the + summarization step, which may require significant storage. + + The returned object is a :obj:`collections.defaultdict` with the + ``default_factory`` set to ``None``, such that it behaves like a normal + dictionary. + + """ + valuefunc = (lambda x: x) if (valuefunc is None) else valuefunc + + ret = defaultdict(list) + for item in iterable: + key = keyfunc(item) + value = valuefunc(item) + ret[key].append(value) + + if reducefunc is not None: + for key, value_list in ret.items(): + ret[key] = reducefunc(value_list) + + ret.default_factory = None + return ret + + +def rlocate(iterable, pred=bool, window_size=None): + """Yield the index of each item in *iterable* for which *pred* returns + ``True``, starting from the right and moving left. + + *pred* defaults to :func:`bool`, which will select truthy items: + + >>> list(rlocate([0, 1, 1, 0, 1, 0, 0])) # Truthy at 1, 2, and 4 + [4, 2, 1] + + Set *pred* to a custom function to, e.g., find the indexes for a particular + item: + + >>> iterable = iter('abcb') + >>> pred = lambda x: x == 'b' + >>> list(rlocate(iterable, pred)) + [3, 1] + + If *window_size* is given, then the *pred* function will be called with + that many items. This enables searching for sub-sequences: + + >>> iterable = [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3] + >>> pred = lambda *args: args == (1, 2, 3) + >>> list(rlocate(iterable, pred=pred, window_size=3)) + [9, 5, 1] + + Beware, this function won't return anything for infinite iterables. + If *iterable* is reversible, ``rlocate`` will reverse it and search from + the right. Otherwise, it will search from the left and return the results + in reverse order. + + See :func:`locate` to for other example applications. + + """ + if window_size is None: + try: + len_iter = len(iterable) + return (len_iter - i - 1 for i in locate(reversed(iterable), pred)) + except TypeError: + pass + + return reversed(list(locate(iterable, pred, window_size))) + + +def replace(iterable, pred, substitutes, count=None, window_size=1): + """Yield the items from *iterable*, replacing the items for which *pred* + returns ``True`` with the items from the iterable *substitutes*. + + >>> iterable = [1, 1, 0, 1, 1, 0, 1, 1] + >>> pred = lambda x: x == 0 + >>> substitutes = (2, 3) + >>> list(replace(iterable, pred, substitutes)) + [1, 1, 2, 3, 1, 1, 2, 3, 1, 1] + + If *count* is given, the number of replacements will be limited: + + >>> iterable = [1, 1, 0, 1, 1, 0, 1, 1, 0] + >>> pred = lambda x: x == 0 + >>> substitutes = [None] + >>> list(replace(iterable, pred, substitutes, count=2)) + [1, 1, None, 1, 1, None, 1, 1, 0] + + Use *window_size* to control the number of items passed as arguments to + *pred*. This allows for locating and replacing subsequences. + + >>> iterable = [0, 1, 2, 5, 0, 1, 2, 5] + >>> window_size = 3 + >>> pred = lambda *args: args == (0, 1, 2) # 3 items passed to pred + >>> substitutes = [3, 4] # Splice in these items + >>> list(replace(iterable, pred, substitutes, window_size=window_size)) + [3, 4, 5, 3, 4, 5] + + """ + if window_size < 1: + raise ValueError('window_size must be at least 1') + + # Save the substitutes iterable, since it's used more than once + substitutes = tuple(substitutes) + + # Add padding such that the number of windows matches the length of the + # iterable + it = chain(iterable, [_marker] * (window_size - 1)) + windows = windowed(it, window_size) + + n = 0 + for w in windows: + # If the current window matches our predicate (and we haven't hit + # our maximum number of replacements), splice in the substitutes + # and then consume the following windows that overlap with this one. + # For example, if the iterable is (0, 1, 2, 3, 4...) + # and the window size is 2, we have (0, 1), (1, 2), (2, 3)... + # If the predicate matches on (0, 1), we need to zap (0, 1) and (1, 2) + if pred(*w): + if (count is None) or (n < count): + n += 1 + yield from substitutes + consume(windows, window_size - 1) + continue + + # If there was no match (or we've reached the replacement limit), + # yield the first item from the window. + if w and (w[0] is not _marker): + yield w[0] + + +def partitions(iterable): + """Yield all possible order-preserving partitions of *iterable*. + + >>> iterable = 'abc' + >>> for part in partitions(iterable): + ... print([''.join(p) for p in part]) + ['abc'] + ['a', 'bc'] + ['ab', 'c'] + ['a', 'b', 'c'] + + This is unrelated to :func:`partition`. + + """ + sequence = list(iterable) + n = len(sequence) + for i in powerset(range(1, n)): + yield [sequence[i:j] for i, j in zip((0,) + i, i + (n,))] + + +def set_partitions(iterable, k=None): + """ + Yield the set partitions of *iterable* into *k* parts. Set partitions are + not order-preserving. + + >>> iterable = 'abc' + >>> for part in set_partitions(iterable, 2): + ... print([''.join(p) for p in part]) + ['a', 'bc'] + ['ab', 'c'] + ['b', 'ac'] + + + If *k* is not given, every set partition is generated. + + >>> iterable = 'abc' + >>> for part in set_partitions(iterable): + ... print([''.join(p) for p in part]) + ['abc'] + ['a', 'bc'] + ['ab', 'c'] + ['b', 'ac'] + ['a', 'b', 'c'] + + """ + L = list(iterable) + n = len(L) + if k is not None: + if k < 1: + raise ValueError( + "Can't partition in a negative or zero number of groups" + ) + elif k > n: + return + + def set_partitions_helper(L, k): + n = len(L) + if k == 1: + yield [L] + elif n == k: + yield [[s] for s in L] + else: + e, *M = L + for p in set_partitions_helper(M, k - 1): + yield [[e], *p] + for p in set_partitions_helper(M, k): + for i in range(len(p)): + yield p[:i] + [[e] + p[i]] + p[i + 1 :] + + if k is None: + for k in range(1, n + 1): + yield from set_partitions_helper(L, k) + else: + yield from set_partitions_helper(L, k) + + +class time_limited: + """ + Yield items from *iterable* until *limit_seconds* have passed. + If the time limit expires before all items have been yielded, the + ``timed_out`` parameter will be set to ``True``. + + >>> from time import sleep + >>> def generator(): + ... yield 1 + ... yield 2 + ... sleep(0.2) + ... yield 3 + >>> iterable = time_limited(0.1, generator()) + >>> list(iterable) + [1, 2] + >>> iterable.timed_out + True + + Note that the time is checked before each item is yielded, and iteration + stops if the time elapsed is greater than *limit_seconds*. If your time + limit is 1 second, but it takes 2 seconds to generate the first item from + the iterable, the function will run for 2 seconds and not yield anything. + + """ + + def __init__(self, limit_seconds, iterable): + if limit_seconds < 0: + raise ValueError('limit_seconds must be positive') + self.limit_seconds = limit_seconds + self._iterable = iter(iterable) + self._start_time = monotonic() + self.timed_out = False + + def __iter__(self): + return self + + def __next__(self): + item = next(self._iterable) + if monotonic() - self._start_time > self.limit_seconds: + self.timed_out = True + raise StopIteration + + return item + + +def only(iterable, default=None, too_long=None): + """If *iterable* has only one item, return it. + If it has zero items, return *default*. + If it has more than one item, raise the exception given by *too_long*, + which is ``ValueError`` by default. + + >>> only([], default='missing') + 'missing' + >>> only([1]) + 1 + >>> only([1, 2]) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ValueError: Expected exactly one item in iterable, but got 1, 2, + and perhaps more.' + >>> only([1, 2], too_long=TypeError) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + TypeError + + Note that :func:`only` attempts to advance *iterable* twice to ensure there + is only one item. See :func:`spy` or :func:`peekable` to check + iterable contents less destructively. + """ + it = iter(iterable) + first_value = next(it, default) + + try: + second_value = next(it) + except StopIteration: + pass + else: + msg = ( + 'Expected exactly one item in iterable, but got {!r}, {!r}, ' + 'and perhaps more.'.format(first_value, second_value) + ) + raise too_long or ValueError(msg) + + return first_value + + +def ichunked(iterable, n): + """Break *iterable* into sub-iterables with *n* elements each. + :func:`ichunked` is like :func:`chunked`, but it yields iterables + instead of lists. + + If the sub-iterables are read in order, the elements of *iterable* + won't be stored in memory. + If they are read out of order, :func:`itertools.tee` is used to cache + elements as necessary. + + >>> from itertools import count + >>> all_chunks = ichunked(count(), 4) + >>> c_1, c_2, c_3 = next(all_chunks), next(all_chunks), next(all_chunks) + >>> list(c_2) # c_1's elements have been cached; c_3's haven't been + [4, 5, 6, 7] + >>> list(c_1) + [0, 1, 2, 3] + >>> list(c_3) + [8, 9, 10, 11] + + """ + source = iter(iterable) + + while True: + # Check to see whether we're at the end of the source iterable + item = next(source, _marker) + if item is _marker: + return + + # Clone the source and yield an n-length slice + source, it = tee(chain([item], source)) + yield islice(it, n) + + # Advance the source iterable + consume(source, n) + + +def distinct_combinations(iterable, r): + """Yield the distinct combinations of *r* items taken from *iterable*. + + >>> list(distinct_combinations([0, 0, 1], 2)) + [(0, 0), (0, 1)] + + Equivalent to ``set(combinations(iterable))``, except duplicates are not + generated and thrown away. For larger input sequences this is much more + efficient. + + """ + if r < 0: + raise ValueError('r must be non-negative') + elif r == 0: + yield () + return + pool = tuple(iterable) + generators = [unique_everseen(enumerate(pool), key=itemgetter(1))] + current_combo = [None] * r + level = 0 + while generators: + try: + cur_idx, p = next(generators[-1]) + except StopIteration: + generators.pop() + level -= 1 + continue + current_combo[level] = p + if level + 1 == r: + yield tuple(current_combo) + else: + generators.append( + unique_everseen( + enumerate(pool[cur_idx + 1 :], cur_idx + 1), + key=itemgetter(1), + ) + ) + level += 1 + + +def filter_except(validator, iterable, *exceptions): + """Yield the items from *iterable* for which the *validator* function does + not raise one of the specified *exceptions*. + + *validator* is called for each item in *iterable*. + It should be a function that accepts one argument and raises an exception + if that item is not valid. + + >>> iterable = ['1', '2', 'three', '4', None] + >>> list(filter_except(int, iterable, ValueError, TypeError)) + ['1', '2', '4'] + + If an exception other than one given by *exceptions* is raised by + *validator*, it is raised like normal. + """ + for item in iterable: + try: + validator(item) + except exceptions: + pass + else: + yield item + + +def map_except(function, iterable, *exceptions): + """Transform each item from *iterable* with *function* and yield the + result, unless *function* raises one of the specified *exceptions*. + + *function* is called to transform each item in *iterable*. + It should be a accept one argument. + + >>> iterable = ['1', '2', 'three', '4', None] + >>> list(map_except(int, iterable, ValueError, TypeError)) + [1, 2, 4] + + If an exception other than one given by *exceptions* is raised by + *function*, it is raised like normal. + """ + for item in iterable: + try: + yield function(item) + except exceptions: + pass + + +def _sample_unweighted(iterable, k): + # Implementation of "Algorithm L" from the 1994 paper by Kim-Hung Li: + # "Reservoir-Sampling Algorithms of Time Complexity O(n(1+log(N/n)))". + + # Fill up the reservoir (collection of samples) with the first `k` samples + reservoir = take(k, iterable) + + # Generate random number that's the largest in a sample of k U(0,1) numbers + # Largest order statistic: https://en.wikipedia.org/wiki/Order_statistic + W = exp(log(random()) / k) + + # The number of elements to skip before changing the reservoir is a random + # number with a geometric distribution. Sample it using random() and logs. + next_index = k + floor(log(random()) / log(1 - W)) + + for index, element in enumerate(iterable, k): + + if index == next_index: + reservoir[randrange(k)] = element + # The new W is the largest in a sample of k U(0, `old_W`) numbers + W *= exp(log(random()) / k) + next_index += floor(log(random()) / log(1 - W)) + 1 + + return reservoir + + +def _sample_weighted(iterable, k, weights): + # Implementation of "A-ExpJ" from the 2006 paper by Efraimidis et al. : + # "Weighted random sampling with a reservoir". + + # Log-transform for numerical stability for weights that are small/large + weight_keys = (log(random()) / weight for weight in weights) + + # Fill up the reservoir (collection of samples) with the first `k` + # weight-keys and elements, then heapify the list. + reservoir = take(k, zip(weight_keys, iterable)) + heapify(reservoir) + + # The number of jumps before changing the reservoir is a random variable + # with an exponential distribution. Sample it using random() and logs. + smallest_weight_key, _ = reservoir[0] + weights_to_skip = log(random()) / smallest_weight_key + + for weight, element in zip(weights, iterable): + if weight >= weights_to_skip: + # The notation here is consistent with the paper, but we store + # the weight-keys in log-space for better numerical stability. + smallest_weight_key, _ = reservoir[0] + t_w = exp(weight * smallest_weight_key) + r_2 = uniform(t_w, 1) # generate U(t_w, 1) + weight_key = log(r_2) / weight + heapreplace(reservoir, (weight_key, element)) + smallest_weight_key, _ = reservoir[0] + weights_to_skip = log(random()) / smallest_weight_key + else: + weights_to_skip -= weight + + # Equivalent to [element for weight_key, element in sorted(reservoir)] + return [heappop(reservoir)[1] for _ in range(k)] + + +def sample(iterable, k, weights=None): + """Return a *k*-length list of elements chosen (without replacement) + from the *iterable*. Like :func:`random.sample`, but works on iterables + of unknown length. + + >>> iterable = range(100) + >>> sample(iterable, 5) # doctest: +SKIP + [81, 60, 96, 16, 4] + + An iterable with *weights* may also be given: + + >>> iterable = range(100) + >>> weights = (i * i + 1 for i in range(100)) + >>> sampled = sample(iterable, 5, weights=weights) # doctest: +SKIP + [79, 67, 74, 66, 78] + + The algorithm can also be used to generate weighted random permutations. + The relative weight of each item determines the probability that it + appears late in the permutation. + + >>> data = "abcdefgh" + >>> weights = range(1, len(data) + 1) + >>> sample(data, k=len(data), weights=weights) # doctest: +SKIP + ['c', 'a', 'b', 'e', 'g', 'd', 'h', 'f'] + """ + if k == 0: + return [] + + iterable = iter(iterable) + if weights is None: + return _sample_unweighted(iterable, k) + else: + weights = iter(weights) + return _sample_weighted(iterable, k, weights) + + +def is_sorted(iterable, key=None, reverse=False): + """Returns ``True`` if the items of iterable are in sorted order, and + ``False`` otherwise. *key* and *reverse* have the same meaning that they do + in the built-in :func:`sorted` function. + + >>> is_sorted(['1', '2', '3', '4', '5'], key=int) + True + >>> is_sorted([5, 4, 3, 1, 2], reverse=True) + False + + The function returns ``False`` after encountering the first out-of-order + item. If there are no out-of-order items, the iterable is exhausted. + """ + + compare = lt if reverse else gt + it = iterable if (key is None) else map(key, iterable) + return not any(starmap(compare, pairwise(it))) + + +class AbortThread(BaseException): + pass + + +class callback_iter: + """Convert a function that uses callbacks to an iterator. + + Let *func* be a function that takes a `callback` keyword argument. + For example: + + >>> def func(callback=None): + ... for i, c in [(1, 'a'), (2, 'b'), (3, 'c')]: + ... if callback: + ... callback(i, c) + ... return 4 + + + Use ``with callback_iter(func)`` to get an iterator over the parameters + that are delivered to the callback. + + >>> with callback_iter(func) as it: + ... for args, kwargs in it: + ... print(args) + (1, 'a') + (2, 'b') + (3, 'c') + + The function will be called in a background thread. The ``done`` property + indicates whether it has completed execution. + + >>> it.done + True + + If it completes successfully, its return value will be available + in the ``result`` property. + + >>> it.result + 4 + + Notes: + + * If the function uses some keyword argument besides ``callback``, supply + *callback_kwd*. + * If it finished executing, but raised an exception, accessing the + ``result`` property will raise the same exception. + * If it hasn't finished executing, accessing the ``result`` + property from within the ``with`` block will raise ``RuntimeError``. + * If it hasn't finished executing, accessing the ``result`` property from + outside the ``with`` block will raise a + ``more_itertools.AbortThread`` exception. + * Provide *wait_seconds* to adjust how frequently the it is polled for + output. + + """ + + def __init__(self, func, callback_kwd='callback', wait_seconds=0.1): + self._func = func + self._callback_kwd = callback_kwd + self._aborted = False + self._future = None + self._wait_seconds = wait_seconds + self._executor = __import__("concurrent.futures").futures.ThreadPoolExecutor(max_workers=1) + self._iterator = self._reader() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self._aborted = True + self._executor.shutdown() + + def __iter__(self): + return self + + def __next__(self): + return next(self._iterator) + + @property + def done(self): + if self._future is None: + return False + return self._future.done() + + @property + def result(self): + if not self.done: + raise RuntimeError('Function has not yet completed') + + return self._future.result() + + def _reader(self): + q = Queue() + + def callback(*args, **kwargs): + if self._aborted: + raise AbortThread('canceled by user') + + q.put((args, kwargs)) + + self._future = self._executor.submit( + self._func, **{self._callback_kwd: callback} + ) + + while True: + try: + item = q.get(timeout=self._wait_seconds) + except Empty: + pass + else: + q.task_done() + yield item + + if self._future.done(): + break + + remaining = [] + while True: + try: + item = q.get_nowait() + except Empty: + break + else: + q.task_done() + remaining.append(item) + q.join() + yield from remaining + + +def windowed_complete(iterable, n): + """ + Yield ``(beginning, middle, end)`` tuples, where: + + * Each ``middle`` has *n* items from *iterable* + * Each ``beginning`` has the items before the ones in ``middle`` + * Each ``end`` has the items after the ones in ``middle`` + + >>> iterable = range(7) + >>> n = 3 + >>> for beginning, middle, end in windowed_complete(iterable, n): + ... print(beginning, middle, end) + () (0, 1, 2) (3, 4, 5, 6) + (0,) (1, 2, 3) (4, 5, 6) + (0, 1) (2, 3, 4) (5, 6) + (0, 1, 2) (3, 4, 5) (6,) + (0, 1, 2, 3) (4, 5, 6) () + + Note that *n* must be at least 0 and most equal to the length of + *iterable*. + + This function will exhaust the iterable and may require significant + storage. + """ + if n < 0: + raise ValueError('n must be >= 0') + + seq = tuple(iterable) + size = len(seq) + + if n > size: + raise ValueError('n must be <= len(seq)') + + for i in range(size - n + 1): + beginning = seq[:i] + middle = seq[i : i + n] + end = seq[i + n :] + yield beginning, middle, end + + +def all_unique(iterable, key=None): + """ + Returns ``True`` if all the elements of *iterable* are unique (no two + elements are equal). + + >>> all_unique('ABCB') + False + + If a *key* function is specified, it will be used to make comparisons. + + >>> all_unique('ABCb') + True + >>> all_unique('ABCb', str.lower) + False + + The function returns as soon as the first non-unique element is + encountered. Iterables with a mix of hashable and unhashable items can + be used, but the function will be slower for unhashable items. + """ + seenset = set() + seenset_add = seenset.add + seenlist = [] + seenlist_add = seenlist.append + for element in map(key, iterable) if key else iterable: + try: + if element in seenset: + return False + seenset_add(element) + except TypeError: + if element in seenlist: + return False + seenlist_add(element) + return True + + +def nth_product(index, *args): + """Equivalent to ``list(product(*args))[index]``. + + The products of *args* can be ordered lexicographically. + :func:`nth_product` computes the product at sort position *index* without + computing the previous products. + + >>> nth_product(8, range(2), range(2), range(2), range(2)) + (1, 0, 0, 0) + + ``IndexError`` will be raised if the given *index* is invalid. + """ + pools = list(map(tuple, reversed(args))) + ns = list(map(len, pools)) + + c = reduce(mul, ns) + + if index < 0: + index += c + + if not 0 <= index < c: + raise IndexError + + result = [] + for pool, n in zip(pools, ns): + result.append(pool[index % n]) + index //= n + + return tuple(reversed(result)) + + +def nth_permutation(iterable, r, index): + """Equivalent to ``list(permutations(iterable, r))[index]``` + + The subsequences of *iterable* that are of length *r* where order is + important can be ordered lexicographically. :func:`nth_permutation` + computes the subsequence at sort position *index* directly, without + computing the previous subsequences. + + >>> nth_permutation('ghijk', 2, 5) + ('h', 'i') + + ``ValueError`` will be raised If *r* is negative or greater than the length + of *iterable*. + ``IndexError`` will be raised if the given *index* is invalid. + """ + pool = list(iterable) + n = len(pool) + + if r is None or r == n: + r, c = n, factorial(n) + elif not 0 <= r < n: + raise ValueError + else: + c = factorial(n) // factorial(n - r) + + if index < 0: + index += c + + if not 0 <= index < c: + raise IndexError + + if c == 0: + return tuple() + + result = [0] * r + q = index * factorial(n) // c if r < n else index + for d in range(1, n + 1): + q, i = divmod(q, d) + if 0 <= n - d < r: + result[n - d] = i + if q == 0: + break + + return tuple(map(pool.pop, result)) + + +def value_chain(*args): + """Yield all arguments passed to the function in the same order in which + they were passed. If an argument itself is iterable then iterate over its + values. + + >>> list(value_chain(1, 2, 3, [4, 5, 6])) + [1, 2, 3, 4, 5, 6] + + Binary and text strings are not considered iterable and are emitted + as-is: + + >>> list(value_chain('12', '34', ['56', '78'])) + ['12', '34', '56', '78'] + + + Multiple levels of nesting are not flattened. + + """ + for value in args: + if isinstance(value, (str, bytes)): + yield value + continue + try: + yield from value + except TypeError: + yield value + + +def product_index(element, *args): + """Equivalent to ``list(product(*args)).index(element)`` + + The products of *args* can be ordered lexicographically. + :func:`product_index` computes the first index of *element* without + computing the previous products. + + >>> product_index([8, 2], range(10), range(5)) + 42 + + ``ValueError`` will be raised if the given *element* isn't in the product + of *args*. + """ + index = 0 + + for x, pool in zip_longest(element, args, fillvalue=_marker): + if x is _marker or pool is _marker: + raise ValueError('element is not a product of args') + + pool = tuple(pool) + index = index * len(pool) + pool.index(x) + + return index + + +def combination_index(element, iterable): + """Equivalent to ``list(combinations(iterable, r)).index(element)`` + + The subsequences of *iterable* that are of length *r* can be ordered + lexicographically. :func:`combination_index` computes the index of the + first *element*, without computing the previous combinations. + + >>> combination_index('adf', 'abcdefg') + 10 + + ``ValueError`` will be raised if the given *element* isn't one of the + combinations of *iterable*. + """ + element = enumerate(element) + k, y = next(element, (None, None)) + if k is None: + return 0 + + indexes = [] + pool = enumerate(iterable) + for n, x in pool: + if x == y: + indexes.append(n) + tmp, y = next(element, (None, None)) + if tmp is None: + break + else: + k = tmp + else: + raise ValueError('element is not a combination of iterable') + + n, _ = last(pool, default=(n, None)) + + # Python versiosn below 3.8 don't have math.comb + index = 1 + for i, j in enumerate(reversed(indexes), start=1): + j = n - j + if i <= j: + index += factorial(j) // (factorial(i) * factorial(j - i)) + + return factorial(n + 1) // (factorial(k + 1) * factorial(n - k)) - index + + +def permutation_index(element, iterable): + """Equivalent to ``list(permutations(iterable, r)).index(element)``` + + The subsequences of *iterable* that are of length *r* where order is + important can be ordered lexicographically. :func:`permutation_index` + computes the index of the first *element* directly, without computing + the previous permutations. + + >>> permutation_index([1, 3, 2], range(5)) + 19 + + ``ValueError`` will be raised if the given *element* isn't one of the + permutations of *iterable*. + """ + index = 0 + pool = list(iterable) + for i, x in zip(range(len(pool), -1, -1), element): + r = pool.index(x) + index = index * i + r + del pool[r] + + return index + + +class countable: + """Wrap *iterable* and keep a count of how many items have been consumed. + + The ``items_seen`` attribute starts at ``0`` and increments as the iterable + is consumed: + + >>> iterable = map(str, range(10)) + >>> it = countable(iterable) + >>> it.items_seen + 0 + >>> next(it), next(it) + ('0', '1') + >>> list(it) + ['2', '3', '4', '5', '6', '7', '8', '9'] + >>> it.items_seen + 10 + """ + + def __init__(self, iterable): + self._it = iter(iterable) + self.items_seen = 0 + + def __iter__(self): + return self + + def __next__(self): + item = next(self._it) + self.items_seen += 1 + + return item diff --git a/setuptools/_vendor/more_itertools/more.pyi b/setuptools/_vendor/more_itertools/more.pyi new file mode 100644 index 00000000..2fba9cb3 --- /dev/null +++ b/setuptools/_vendor/more_itertools/more.pyi @@ -0,0 +1,480 @@ +"""Stubs for more_itertools.more""" + +from typing import ( + Any, + Callable, + Container, + Dict, + Generic, + Hashable, + Iterable, + Iterator, + List, + Optional, + Reversible, + Sequence, + Sized, + Tuple, + Union, + TypeVar, + type_check_only, +) +from types import TracebackType +from typing_extensions import ContextManager, Protocol, Type, overload + +# Type and type variable definitions +_T = TypeVar('_T') +_U = TypeVar('_U') +_V = TypeVar('_V') +_W = TypeVar('_W') +_T_co = TypeVar('_T_co', covariant=True) +_GenFn = TypeVar('_GenFn', bound=Callable[..., Iterator[object]]) +_Raisable = Union[BaseException, 'Type[BaseException]'] + +@type_check_only +class _SizedIterable(Protocol[_T_co], Sized, Iterable[_T_co]): ... + +@type_check_only +class _SizedReversible(Protocol[_T_co], Sized, Reversible[_T_co]): ... + +def chunked( + iterable: Iterable[_T], n: int, strict: bool = ... +) -> Iterator[List[_T]]: ... +@overload +def first(iterable: Iterable[_T]) -> _T: ... +@overload +def first(iterable: Iterable[_T], default: _U) -> Union[_T, _U]: ... +@overload +def last(iterable: Iterable[_T]) -> _T: ... +@overload +def last(iterable: Iterable[_T], default: _U) -> Union[_T, _U]: ... +@overload +def nth_or_last(iterable: Iterable[_T], n: int) -> _T: ... +@overload +def nth_or_last( + iterable: Iterable[_T], n: int, default: _U +) -> Union[_T, _U]: ... + +class peekable(Generic[_T], Iterator[_T]): + def __init__(self, iterable: Iterable[_T]) -> None: ... + def __iter__(self) -> peekable[_T]: ... + def __bool__(self) -> bool: ... + @overload + def peek(self) -> _T: ... + @overload + def peek(self, default: _U) -> Union[_T, _U]: ... + def prepend(self, *items: _T) -> None: ... + def __next__(self) -> _T: ... + @overload + def __getitem__(self, index: int) -> _T: ... + @overload + def __getitem__(self, index: slice) -> List[_T]: ... + +def collate(*iterables: Iterable[_T], **kwargs: Any) -> Iterable[_T]: ... +def consumer(func: _GenFn) -> _GenFn: ... +def ilen(iterable: Iterable[object]) -> int: ... +def iterate(func: Callable[[_T], _T], start: _T) -> Iterator[_T]: ... +def with_iter( + context_manager: ContextManager[Iterable[_T]], +) -> Iterator[_T]: ... +def one( + iterable: Iterable[_T], + too_short: Optional[_Raisable] = ..., + too_long: Optional[_Raisable] = ..., +) -> _T: ... +def distinct_permutations( + iterable: Iterable[_T], r: Optional[int] = ... +) -> Iterator[Tuple[_T, ...]]: ... +def intersperse( + e: _U, iterable: Iterable[_T], n: int = ... +) -> Iterator[Union[_T, _U]]: ... +def unique_to_each(*iterables: Iterable[_T]) -> List[List[_T]]: ... +@overload +def windowed( + seq: Iterable[_T], n: int, *, step: int = ... +) -> Iterator[Tuple[Optional[_T], ...]]: ... +@overload +def windowed( + seq: Iterable[_T], n: int, fillvalue: _U, step: int = ... +) -> Iterator[Tuple[Union[_T, _U], ...]]: ... +def substrings(iterable: Iterable[_T]) -> Iterator[Tuple[_T, ...]]: ... +def substrings_indexes( + seq: Sequence[_T], reverse: bool = ... +) -> Iterator[Tuple[Sequence[_T], int, int]]: ... + +class bucket(Generic[_T, _U], Container[_U]): + def __init__( + self, + iterable: Iterable[_T], + key: Callable[[_T], _U], + validator: Optional[Callable[[object], object]] = ..., + ) -> None: ... + def __contains__(self, value: object) -> bool: ... + def __iter__(self) -> Iterator[_U]: ... + def __getitem__(self, value: object) -> Iterator[_T]: ... + +def spy( + iterable: Iterable[_T], n: int = ... +) -> Tuple[List[_T], Iterator[_T]]: ... +def interleave(*iterables: Iterable[_T]) -> Iterator[_T]: ... +def interleave_longest(*iterables: Iterable[_T]) -> Iterator[_T]: ... +def collapse( + iterable: Iterable[Any], + base_type: Optional[type] = ..., + levels: Optional[int] = ..., +) -> Iterator[Any]: ... +@overload +def side_effect( + func: Callable[[_T], object], + iterable: Iterable[_T], + chunk_size: None = ..., + before: Optional[Callable[[], object]] = ..., + after: Optional[Callable[[], object]] = ..., +) -> Iterator[_T]: ... +@overload +def side_effect( + func: Callable[[List[_T]], object], + iterable: Iterable[_T], + chunk_size: int, + before: Optional[Callable[[], object]] = ..., + after: Optional[Callable[[], object]] = ..., +) -> Iterator[_T]: ... +def sliced( + seq: Sequence[_T], n: int, strict: bool = ... +) -> Iterator[Sequence[_T]]: ... +def split_at( + iterable: Iterable[_T], + pred: Callable[[_T], object], + maxsplit: int = ..., + keep_separator: bool = ..., +) -> Iterator[List[_T]]: ... +def split_before( + iterable: Iterable[_T], pred: Callable[[_T], object], maxsplit: int = ... +) -> Iterator[List[_T]]: ... +def split_after( + iterable: Iterable[_T], pred: Callable[[_T], object], maxsplit: int = ... +) -> Iterator[List[_T]]: ... +def split_when( + iterable: Iterable[_T], + pred: Callable[[_T, _T], object], + maxsplit: int = ..., +) -> Iterator[List[_T]]: ... +def split_into( + iterable: Iterable[_T], sizes: Iterable[Optional[int]] +) -> Iterator[List[_T]]: ... +@overload +def padded( + iterable: Iterable[_T], + *, + n: Optional[int] = ..., + next_multiple: bool = ... +) -> Iterator[Optional[_T]]: ... +@overload +def padded( + iterable: Iterable[_T], + fillvalue: _U, + n: Optional[int] = ..., + next_multiple: bool = ..., +) -> Iterator[Union[_T, _U]]: ... +@overload +def repeat_last(iterable: Iterable[_T]) -> Iterator[_T]: ... +@overload +def repeat_last( + iterable: Iterable[_T], default: _U +) -> Iterator[Union[_T, _U]]: ... +def distribute(n: int, iterable: Iterable[_T]) -> List[Iterator[_T]]: ... +@overload +def stagger( + iterable: Iterable[_T], + offsets: _SizedIterable[int] = ..., + longest: bool = ..., +) -> Iterator[Tuple[Optional[_T], ...]]: ... +@overload +def stagger( + iterable: Iterable[_T], + offsets: _SizedIterable[int] = ..., + longest: bool = ..., + fillvalue: _U = ..., +) -> Iterator[Tuple[Union[_T, _U], ...]]: ... + +class UnequalIterablesError(ValueError): + def __init__( + self, details: Optional[Tuple[int, int, int]] = ... + ) -> None: ... + +def zip_equal(*iterables: Iterable[_T]) -> Iterator[Tuple[_T, ...]]: ... +@overload +def zip_offset( + *iterables: Iterable[_T], offsets: _SizedIterable[int], longest: bool = ... +) -> Iterator[Tuple[Optional[_T], ...]]: ... +@overload +def zip_offset( + *iterables: Iterable[_T], + offsets: _SizedIterable[int], + longest: bool = ..., + fillvalue: _U +) -> Iterator[Tuple[Union[_T, _U], ...]]: ... +def sort_together( + iterables: Iterable[Iterable[_T]], + key_list: Iterable[int] = ..., + key: Optional[Callable[..., Any]] = ..., + reverse: bool = ..., +) -> List[Tuple[_T, ...]]: ... +def unzip(iterable: Iterable[Sequence[_T]]) -> Tuple[Iterator[_T], ...]: ... +def divide(n: int, iterable: Iterable[_T]) -> List[Iterator[_T]]: ... +def always_iterable( + obj: object, + base_type: Union[ + type, Tuple[Union[type, Tuple[Any, ...]], ...], None + ] = ..., +) -> Iterator[Any]: ... +def adjacent( + predicate: Callable[[_T], bool], + iterable: Iterable[_T], + distance: int = ..., +) -> Iterator[Tuple[bool, _T]]: ... +def groupby_transform( + iterable: Iterable[_T], + keyfunc: Optional[Callable[[_T], _U]] = ..., + valuefunc: Optional[Callable[[_T], _V]] = ..., + reducefunc: Optional[Callable[..., _W]] = ..., +) -> Iterator[Tuple[_T, _W]]: ... + +class numeric_range(Generic[_T, _U], Sequence[_T], Hashable, Reversible[_T]): + @overload + def __init__(self, __stop: _T) -> None: ... + @overload + def __init__(self, __start: _T, __stop: _T) -> None: ... + @overload + def __init__(self, __start: _T, __stop: _T, __step: _U) -> None: ... + def __bool__(self) -> bool: ... + def __contains__(self, elem: object) -> bool: ... + def __eq__(self, other: object) -> bool: ... + @overload + def __getitem__(self, key: int) -> _T: ... + @overload + def __getitem__(self, key: slice) -> numeric_range[_T, _U]: ... + def __hash__(self) -> int: ... + def __iter__(self) -> Iterator[_T]: ... + def __len__(self) -> int: ... + def __reduce__( + self, + ) -> Tuple[Type[numeric_range[_T, _U]], Tuple[_T, _T, _U]]: ... + def __repr__(self) -> str: ... + def __reversed__(self) -> Iterator[_T]: ... + def count(self, value: _T) -> int: ... + def index(self, value: _T) -> int: ... # type: ignore + +def count_cycle( + iterable: Iterable[_T], n: Optional[int] = ... +) -> Iterable[Tuple[int, _T]]: ... +def mark_ends( + iterable: Iterable[_T], +) -> Iterable[Tuple[bool, bool, _T]]: ... +def locate( + iterable: Iterable[object], + pred: Callable[..., Any] = ..., + window_size: Optional[int] = ..., +) -> Iterator[int]: ... +def lstrip( + iterable: Iterable[_T], pred: Callable[[_T], object] +) -> Iterator[_T]: ... +def rstrip( + iterable: Iterable[_T], pred: Callable[[_T], object] +) -> Iterator[_T]: ... +def strip( + iterable: Iterable[_T], pred: Callable[[_T], object] +) -> Iterator[_T]: ... + +class islice_extended(Generic[_T], Iterator[_T]): + def __init__( + self, iterable: Iterable[_T], *args: Optional[int] + ) -> None: ... + def __iter__(self) -> islice_extended[_T]: ... + def __next__(self) -> _T: ... + def __getitem__(self, index: slice) -> islice_extended[_T]: ... + +def always_reversible(iterable: Iterable[_T]) -> Iterator[_T]: ... +def consecutive_groups( + iterable: Iterable[_T], ordering: Callable[[_T], int] = ... +) -> Iterator[Iterator[_T]]: ... +@overload +def difference( + iterable: Iterable[_T], + func: Callable[[_T, _T], _U] = ..., + *, + initial: None = ... +) -> Iterator[Union[_T, _U]]: ... +@overload +def difference( + iterable: Iterable[_T], func: Callable[[_T, _T], _U] = ..., *, initial: _U +) -> Iterator[_U]: ... + +class SequenceView(Generic[_T], Sequence[_T]): + def __init__(self, target: Sequence[_T]) -> None: ... + @overload + def __getitem__(self, index: int) -> _T: ... + @overload + def __getitem__(self, index: slice) -> Sequence[_T]: ... + def __len__(self) -> int: ... + +class seekable(Generic[_T], Iterator[_T]): + def __init__( + self, iterable: Iterable[_T], maxlen: Optional[int] = ... + ) -> None: ... + def __iter__(self) -> seekable[_T]: ... + def __next__(self) -> _T: ... + def __bool__(self) -> bool: ... + @overload + def peek(self) -> _T: ... + @overload + def peek(self, default: _U) -> Union[_T, _U]: ... + def elements(self) -> SequenceView[_T]: ... + def seek(self, index: int) -> None: ... + +class run_length: + @staticmethod + def encode(iterable: Iterable[_T]) -> Iterator[Tuple[_T, int]]: ... + @staticmethod + def decode(iterable: Iterable[Tuple[_T, int]]) -> Iterator[_T]: ... + +def exactly_n( + iterable: Iterable[_T], n: int, predicate: Callable[[_T], object] = ... +) -> bool: ... +def circular_shifts(iterable: Iterable[_T]) -> List[Tuple[_T, ...]]: ... +def make_decorator( + wrapping_func: Callable[..., _U], result_index: int = ... +) -> Callable[..., Callable[[Callable[..., Any]], Callable[..., _U]]]: ... +@overload +def map_reduce( + iterable: Iterable[_T], + keyfunc: Callable[[_T], _U], + valuefunc: None = ..., + reducefunc: None = ..., +) -> Dict[_U, List[_T]]: ... +@overload +def map_reduce( + iterable: Iterable[_T], + keyfunc: Callable[[_T], _U], + valuefunc: Callable[[_T], _V], + reducefunc: None = ..., +) -> Dict[_U, List[_V]]: ... +@overload +def map_reduce( + iterable: Iterable[_T], + keyfunc: Callable[[_T], _U], + valuefunc: None = ..., + reducefunc: Callable[[List[_T]], _W] = ..., +) -> Dict[_U, _W]: ... +@overload +def map_reduce( + iterable: Iterable[_T], + keyfunc: Callable[[_T], _U], + valuefunc: Callable[[_T], _V], + reducefunc: Callable[[List[_V]], _W], +) -> Dict[_U, _W]: ... +def rlocate( + iterable: Iterable[_T], + pred: Callable[..., object] = ..., + window_size: Optional[int] = ..., +) -> Iterator[int]: ... +def replace( + iterable: Iterable[_T], + pred: Callable[..., object], + substitutes: Iterable[_U], + count: Optional[int] = ..., + window_size: int = ..., +) -> Iterator[Union[_T, _U]]: ... +def partitions(iterable: Iterable[_T]) -> Iterator[List[List[_T]]]: ... +def set_partitions( + iterable: Iterable[_T], k: Optional[int] = ... +) -> Iterator[List[List[_T]]]: ... + +class time_limited(Generic[_T], Iterator[_T]): + def __init__( + self, limit_seconds: float, iterable: Iterable[_T] + ) -> None: ... + def __iter__(self) -> islice_extended[_T]: ... + def __next__(self) -> _T: ... + +@overload +def only( + iterable: Iterable[_T], *, too_long: Optional[_Raisable] = ... +) -> Optional[_T]: ... +@overload +def only( + iterable: Iterable[_T], default: _U, too_long: Optional[_Raisable] = ... +) -> Union[_T, _U]: ... +def ichunked(iterable: Iterable[_T], n: int) -> Iterator[Iterator[_T]]: ... +def distinct_combinations( + iterable: Iterable[_T], r: int +) -> Iterator[Tuple[_T, ...]]: ... +def filter_except( + validator: Callable[[Any], object], + iterable: Iterable[_T], + *exceptions: Type[BaseException] +) -> Iterator[_T]: ... +def map_except( + function: Callable[[Any], _U], + iterable: Iterable[_T], + *exceptions: Type[BaseException] +) -> Iterator[_U]: ... +def sample( + iterable: Iterable[_T], + k: int, + weights: Optional[Iterable[float]] = ..., +) -> List[_T]: ... +def is_sorted( + iterable: Iterable[_T], + key: Optional[Callable[[_T], _U]] = ..., + reverse: bool = False, +) -> bool: ... + +class AbortThread(BaseException): + pass + +class callback_iter(Generic[_T], Iterator[_T]): + def __init__( + self, + func: Callable[..., Any], + callback_kwd: str = ..., + wait_seconds: float = ..., + ) -> None: ... + def __enter__(self) -> callback_iter[_T]: ... + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> Optional[bool]: ... + def __iter__(self) -> callback_iter[_T]: ... + def __next__(self) -> _T: ... + def _reader(self) -> Iterator[_T]: ... + @property + def done(self) -> bool: ... + @property + def result(self) -> Any: ... + +def windowed_complete( + iterable: Iterable[_T], n: int +) -> Iterator[Tuple[_T, ...]]: ... +def all_unique( + iterable: Iterable[_T], key: Optional[Callable[[_T], _U]] = ... +) -> bool: ... +def nth_product(index: int, *args: Iterable[_T]) -> Tuple[_T, ...]: ... +def nth_permutation( + iterable: Iterable[_T], r: int, index: int +) -> Tuple[_T, ...]: ... +def value_chain(*args: Union[_T, Iterable[_T]]) -> Iterable[_T]: ... +def product_index(element: Iterable[_T], *args: Iterable[_T]) -> int: ... +def combination_index( + element: Iterable[_T], iterable: Iterable[_T] +) -> int: ... +def permutation_index( + element: Iterable[_T], iterable: Iterable[_T] +) -> int: ... + +class countable(Generic[_T], Iterator[_T]): + def __init__(self, iterable: Iterable[_T]) -> None: ... + def __iter__(self) -> countable[_T]: ... + def __next__(self) -> _T: ... diff --git a/tools/vendored.py b/tools/vendored.py index 9428e752..ee978e5c 100644 --- a/tools/vendored.py +++ b/tools/vendored.py @@ -66,13 +66,17 @@ def rewrite_importlib_resources(pkg_files, new_root): def rewrite_more_itertools(pkg_files: Path): """ - Rewrite more_itertools to remove unused more_itertools.more + Defer import of concurrent.futures. Workaround for #3090. """ - for more_file in pkg_files.glob("more.py*"): - more_file.remove() - for init_file in pkg_files.glob("__init__.py*"): - text = "".join(ln for ln in init_file.lines() if "from .more " not in ln) - init_file.write_text(text) + more_file = pkg_files.joinpath('more.py') + text = more_file.read_text() + text = re.sub(r'^.*concurrent.futures.*?\n', '', text, flags=re.MULTILINE) + text = re.sub( + 'ThreadPoolExecutor', + '__import__("concurrent.futures").futures.ThreadPoolExecutor', + text, + ) + more_file.write_text(text) def clean(vendor): -- cgit v1.2.1 From fe7975d091483e8d6d4c9befea38ccfeb29e8e49 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 8 Feb 2022 17:53:41 -0500 Subject: =?UTF-8?q?Bump=20version:=2060.8.1=20=E2=86=92=2060.8.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 12 ++++++++++++ changelog.d/3091.misc.rst | 4 ---- setup.cfg | 2 +- 4 files changed, 14 insertions(+), 6 deletions(-) delete mode 100644 changelog.d/3091.misc.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 77754fc0..7f466d4f 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 60.8.1 +current_version = 60.8.2 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index 0183241b..cd46ace6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,15 @@ +v60.8.2 +------- + + +Misc +^^^^ +* #3091: Make ``concurrent.futures`` import lazy in vendored ``more_itertools`` + package to a avoid importing threading as a side effect (which caused + `gevent/gevent#1865 `__). + -- by :user:`maciejp-ro` + + v60.8.1 ------- diff --git a/changelog.d/3091.misc.rst b/changelog.d/3091.misc.rst deleted file mode 100644 index d6664125..00000000 --- a/changelog.d/3091.misc.rst +++ /dev/null @@ -1,4 +0,0 @@ -Make ``concurrent.futures`` import lazy in vendored ``more_itertools`` -package to a avoid importing threading as a side effect (which caused -`gevent/gevent#1865 `__). --- by :user:`maciejp-ro` diff --git a/setup.cfg b/setup.cfg index d6f08caa..24eda5a3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 60.8.1 +version = 60.8.2 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From 03ca0a3b34cf4edf23cb689301f9cf657b3faac8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 10 Feb 2022 15:55:49 -0500 Subject: Removed bootstrap.py script, no longer needed. --- bootstrap.py | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 bootstrap.py diff --git a/bootstrap.py b/bootstrap.py deleted file mode 100644 index 229b9965..00000000 --- a/bootstrap.py +++ /dev/null @@ -1,7 +0,0 @@ -import warnings - - -msg = "bootstrap.py is no longer needed. Use a PEP-517-compatible builder instead." - - -__name__ == '__main__' and warnings.warn(msg) -- cgit v1.2.1 From 96ea56305df99a3c13334d42ea45f779cab2c505 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 10 Feb 2022 20:36:16 -0500 Subject: Bump pytest-mypy and remove workaround for dbader/pytest-mypy#131. --- pytest.ini | 3 --- setup.cfg | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/pytest.ini b/pytest.ini index cbbe3b15..b6880c88 100644 --- a/pytest.ini +++ b/pytest.ini @@ -13,6 +13,3 @@ filterwarnings= # 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 - - # dbader/pytest-mypy#131 - ignore:The \(fspath. py.path.local\) argument to MypyFile is deprecated.:pytest.PytestDeprecationWarning diff --git a/setup.cfg b/setup.cfg index bd1da7a2..1b048af5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,7 +35,7 @@ testing = # workaround for jaraco/skeleton#22 python_implementation != "PyPy" pytest-cov - pytest-mypy; \ + pytest-mypy >= 0.9.1; \ # workaround for jaraco/skeleton#22 python_implementation != "PyPy" pytest-enabler >= 1.0.1 -- cgit v1.2.1 From a9ea801a43fc62a569cf60e1c28e477ba510d8a0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 10 Feb 2022 21:58:57 -0500 Subject: Require jaraco.packaging 9 adding compatibility for projects with no setup.py file. --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 1b048af5..3b7ac309 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,7 +45,7 @@ testing = docs = # upstream sphinx - jaraco.packaging >= 8.2 + jaraco.packaging >= 9 rst.linker >= 1.9 # local -- cgit v1.2.1 From f38d13906be5f3cfe3dadc0333c5ef01badaf777 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 10 Feb 2022 22:21:47 -0500 Subject: Disable running of sage-ci tests except on tagged commits. Ref #3093. --- .github/workflows/ci-sage.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/ci-sage.yml b/.github/workflows/ci-sage.yml index 573b269b..b802a58c 100644 --- a/.github/workflows/ci-sage.yml +++ b/.github/workflows/ci-sage.yml @@ -7,7 +7,7 @@ name: Run Sage CI for Linux ## - continuous integration, by building and testing other software ## that depends on this project. ## -## It runs on every pull request and push of a tag to the GitHub repository. +## It runs on every push of a tag to the GitHub repository. ## ## The testing can be monitored in the "Actions" tab of the GitHub repository. ## @@ -33,11 +33,7 @@ name: Run Sage CI for Linux ## Many copies of the second step are run in parallel for each of the tested ## systems/configurations. -#on: [push, pull_request] - on: - pull_request: - types: [opened, synchronize] push: tags: - '*' -- cgit v1.2.1 From 76bfec83a18a2913529151be9220e5452dda2475 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 11 Feb 2022 11:56:22 -0500 Subject: Remove workaround for pypa/get-pip#137. --- _distutils_hack/__init__.py | 35 ----------------------------------- changelog.d/2993.change.rst | 1 + 2 files changed, 1 insertion(+), 35 deletions(-) create mode 100644 changelog.d/2993.change.rst diff --git a/_distutils_hack/__init__.py b/_distutils_hack/__init__.py index 1f8daf49..c6f7de60 100644 --- a/_distutils_hack/__init__.py +++ b/_distutils_hack/__init__.py @@ -139,33 +139,6 @@ class DistutilsMetaFinder: clear_distutils() self.spec_for_distutils = lambda: None - def spec_for_setuptools(self): - """ - get-pip imports setuptools solely for the purpose of - determining if it's installed. In this case, provide - a stubbed spec to represent setuptools being present - without invoking any behavior. - - Workaround for pypa/get-pip#137. Ref #2993. - """ - if not self.is_script('get-pip'): - return - - import importlib - - class StubbedLoader(importlib.abc.Loader): - - def create_module(self, spec): - import types - return types.ModuleType('setuptools') - - def exec_module(self, module): - pass - - return importlib.util.spec_from_loader( - 'setuptools', StubbedLoader(), - ) - @classmethod def pip_imported_during_build(cls): """ @@ -177,14 +150,6 @@ class DistutilsMetaFinder: for frame, line in traceback.walk_stack(None) ) - @staticmethod - def is_script(name): - try: - import __main__ - return os.path.basename(__main__.__file__) == f'{name}.py' - except AttributeError: - pass - @staticmethod def frame_file_is_setup(frame): """ diff --git a/changelog.d/2993.change.rst b/changelog.d/2993.change.rst new file mode 100644 index 00000000..5cb9d6aa --- /dev/null +++ b/changelog.d/2993.change.rst @@ -0,0 +1 @@ +Removed workaround in distutils hack for get-pip now that pypa/get-pip#137 is closed. -- cgit v1.2.1 From aa525e4b61c8cea24c244b85235726af0d3b8811 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 11 Feb 2022 15:28:28 -0600 Subject: Update main.yml --- .github/workflows/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 821cf883..1b015551 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,6 +2,10 @@ name: tests on: [push, pull_request, workflow_dispatch] +concurrency: + group: tests-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + jobs: test: strategy: -- cgit v1.2.1 From 6d59c76ee856cc7bb9ae0d3892a2f92fa3204d2e Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 11 Feb 2022 16:53:53 -0600 Subject: Update .github/workflows/main.yml Co-authored-by: Dustin Ingram --- .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 1b015551..62373734 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,8 +2,8 @@ name: tests on: [push, pull_request, workflow_dispatch] -concurrency: - group: tests-${{ github.head_ref || github.run_id }} +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true jobs: -- cgit v1.2.1 From f22eb5b60adbe158e458614ea0380a9071c39347 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 12 Feb 2022 09:50:31 +0000 Subject: Ignore flake8/black warnings with pytest 7.0.1 (jaraco/skeleton#58) --- pytest.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pytest.ini b/pytest.ini index b6880c88..80e98cc9 100644 --- a/pytest.ini +++ b/pytest.ini @@ -9,7 +9,9 @@ filterwarnings= # 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 -- cgit v1.2.1 From 30abf4ed16fc0b60858ea1bc67361237b4cc07d5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 12 Feb 2022 05:07:54 -0500 Subject: Bump packaging to 21.3 --- changelog.d/3098.change.rst | 1 + .../_vendor/packaging-21.2.dist-info/INSTALLER | 1 - .../_vendor/packaging-21.2.dist-info/LICENSE | 3 - .../packaging-21.2.dist-info/LICENSE.APACHE | 177 -------- .../_vendor/packaging-21.2.dist-info/LICENSE.BSD | 23 -- .../_vendor/packaging-21.2.dist-info/METADATA | 446 -------------------- .../_vendor/packaging-21.2.dist-info/RECORD | 32 -- .../_vendor/packaging-21.2.dist-info/REQUESTED | 0 .../_vendor/packaging-21.2.dist-info/WHEEL | 5 - .../_vendor/packaging-21.2.dist-info/top_level.txt | 1 - .../_vendor/packaging-21.3.dist-info/INSTALLER | 1 + .../_vendor/packaging-21.3.dist-info/LICENSE | 3 + .../packaging-21.3.dist-info/LICENSE.APACHE | 177 ++++++++ .../_vendor/packaging-21.3.dist-info/LICENSE.BSD | 23 ++ .../_vendor/packaging-21.3.dist-info/METADATA | 453 +++++++++++++++++++++ .../_vendor/packaging-21.3.dist-info/RECORD | 32 ++ .../_vendor/packaging-21.3.dist-info/REQUESTED | 0 .../_vendor/packaging-21.3.dist-info/WHEEL | 5 + .../_vendor/packaging-21.3.dist-info/top_level.txt | 1 + pkg_resources/_vendor/packaging/__about__.py | 2 +- pkg_resources/_vendor/packaging/_musllinux.py | 2 +- pkg_resources/_vendor/packaging/_structures.py | 6 - pkg_resources/_vendor/packaging/specifiers.py | 30 +- pkg_resources/_vendor/packaging/tags.py | 15 +- pkg_resources/_vendor/vendored.txt | 2 +- .../_vendor/packaging-21.2.dist-info/INSTALLER | 1 - .../_vendor/packaging-21.2.dist-info/LICENSE | 3 - .../packaging-21.2.dist-info/LICENSE.APACHE | 177 -------- .../_vendor/packaging-21.2.dist-info/LICENSE.BSD | 23 -- .../_vendor/packaging-21.2.dist-info/METADATA | 446 -------------------- setuptools/_vendor/packaging-21.2.dist-info/RECORD | 32 -- .../_vendor/packaging-21.2.dist-info/REQUESTED | 0 setuptools/_vendor/packaging-21.2.dist-info/WHEEL | 5 - .../_vendor/packaging-21.2.dist-info/top_level.txt | 1 - .../_vendor/packaging-21.3.dist-info/INSTALLER | 1 + .../_vendor/packaging-21.3.dist-info/LICENSE | 3 + .../packaging-21.3.dist-info/LICENSE.APACHE | 177 ++++++++ .../_vendor/packaging-21.3.dist-info/LICENSE.BSD | 23 ++ .../_vendor/packaging-21.3.dist-info/METADATA | 453 +++++++++++++++++++++ setuptools/_vendor/packaging-21.3.dist-info/RECORD | 32 ++ .../_vendor/packaging-21.3.dist-info/REQUESTED | 0 setuptools/_vendor/packaging-21.3.dist-info/WHEEL | 5 + .../_vendor/packaging-21.3.dist-info/top_level.txt | 1 + setuptools/_vendor/packaging/__about__.py | 2 +- setuptools/_vendor/packaging/_musllinux.py | 2 +- setuptools/_vendor/packaging/_structures.py | 6 - setuptools/_vendor/packaging/specifiers.py | 30 +- setuptools/_vendor/packaging/tags.py | 15 +- setuptools/_vendor/vendored.txt | 2 +- 49 files changed, 1419 insertions(+), 1462 deletions(-) create mode 100644 changelog.d/3098.change.rst delete mode 100644 pkg_resources/_vendor/packaging-21.2.dist-info/INSTALLER delete mode 100644 pkg_resources/_vendor/packaging-21.2.dist-info/LICENSE delete mode 100644 pkg_resources/_vendor/packaging-21.2.dist-info/LICENSE.APACHE delete mode 100644 pkg_resources/_vendor/packaging-21.2.dist-info/LICENSE.BSD delete mode 100644 pkg_resources/_vendor/packaging-21.2.dist-info/METADATA delete mode 100644 pkg_resources/_vendor/packaging-21.2.dist-info/RECORD delete mode 100644 pkg_resources/_vendor/packaging-21.2.dist-info/REQUESTED delete mode 100644 pkg_resources/_vendor/packaging-21.2.dist-info/WHEEL delete mode 100644 pkg_resources/_vendor/packaging-21.2.dist-info/top_level.txt create mode 100644 pkg_resources/_vendor/packaging-21.3.dist-info/INSTALLER create mode 100644 pkg_resources/_vendor/packaging-21.3.dist-info/LICENSE create mode 100644 pkg_resources/_vendor/packaging-21.3.dist-info/LICENSE.APACHE create mode 100644 pkg_resources/_vendor/packaging-21.3.dist-info/LICENSE.BSD create mode 100644 pkg_resources/_vendor/packaging-21.3.dist-info/METADATA create mode 100644 pkg_resources/_vendor/packaging-21.3.dist-info/RECORD create mode 100644 pkg_resources/_vendor/packaging-21.3.dist-info/REQUESTED create mode 100644 pkg_resources/_vendor/packaging-21.3.dist-info/WHEEL create mode 100644 pkg_resources/_vendor/packaging-21.3.dist-info/top_level.txt delete mode 100644 setuptools/_vendor/packaging-21.2.dist-info/INSTALLER delete mode 100644 setuptools/_vendor/packaging-21.2.dist-info/LICENSE delete mode 100644 setuptools/_vendor/packaging-21.2.dist-info/LICENSE.APACHE delete mode 100644 setuptools/_vendor/packaging-21.2.dist-info/LICENSE.BSD delete mode 100644 setuptools/_vendor/packaging-21.2.dist-info/METADATA delete mode 100644 setuptools/_vendor/packaging-21.2.dist-info/RECORD delete mode 100644 setuptools/_vendor/packaging-21.2.dist-info/REQUESTED delete mode 100644 setuptools/_vendor/packaging-21.2.dist-info/WHEEL delete mode 100644 setuptools/_vendor/packaging-21.2.dist-info/top_level.txt create mode 100644 setuptools/_vendor/packaging-21.3.dist-info/INSTALLER create mode 100644 setuptools/_vendor/packaging-21.3.dist-info/LICENSE create mode 100644 setuptools/_vendor/packaging-21.3.dist-info/LICENSE.APACHE create mode 100644 setuptools/_vendor/packaging-21.3.dist-info/LICENSE.BSD create mode 100644 setuptools/_vendor/packaging-21.3.dist-info/METADATA create mode 100644 setuptools/_vendor/packaging-21.3.dist-info/RECORD create mode 100644 setuptools/_vendor/packaging-21.3.dist-info/REQUESTED create mode 100644 setuptools/_vendor/packaging-21.3.dist-info/WHEEL create mode 100644 setuptools/_vendor/packaging-21.3.dist-info/top_level.txt diff --git a/changelog.d/3098.change.rst b/changelog.d/3098.change.rst new file mode 100644 index 00000000..10b0f53a --- /dev/null +++ b/changelog.d/3098.change.rst @@ -0,0 +1 @@ +Bump vendored packaging to 21.3. diff --git a/pkg_resources/_vendor/packaging-21.2.dist-info/INSTALLER b/pkg_resources/_vendor/packaging-21.2.dist-info/INSTALLER deleted file mode 100644 index a1b589e3..00000000 --- a/pkg_resources/_vendor/packaging-21.2.dist-info/INSTALLER +++ /dev/null @@ -1 +0,0 @@ -pip diff --git a/pkg_resources/_vendor/packaging-21.2.dist-info/LICENSE b/pkg_resources/_vendor/packaging-21.2.dist-info/LICENSE deleted file mode 100644 index 6f62d44e..00000000 --- a/pkg_resources/_vendor/packaging-21.2.dist-info/LICENSE +++ /dev/null @@ -1,3 +0,0 @@ -This software is made available under the terms of *either* of the licenses -found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made -under the terms of *both* these licenses. diff --git a/pkg_resources/_vendor/packaging-21.2.dist-info/LICENSE.APACHE b/pkg_resources/_vendor/packaging-21.2.dist-info/LICENSE.APACHE deleted file mode 100644 index f433b1a5..00000000 --- a/pkg_resources/_vendor/packaging-21.2.dist-info/LICENSE.APACHE +++ /dev/null @@ -1,177 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS diff --git a/pkg_resources/_vendor/packaging-21.2.dist-info/LICENSE.BSD b/pkg_resources/_vendor/packaging-21.2.dist-info/LICENSE.BSD deleted file mode 100644 index 42ce7b75..00000000 --- a/pkg_resources/_vendor/packaging-21.2.dist-info/LICENSE.BSD +++ /dev/null @@ -1,23 +0,0 @@ -Copyright (c) Donald Stufft and individual contributors. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pkg_resources/_vendor/packaging-21.2.dist-info/METADATA b/pkg_resources/_vendor/packaging-21.2.dist-info/METADATA deleted file mode 100644 index e8ff54d7..00000000 --- a/pkg_resources/_vendor/packaging-21.2.dist-info/METADATA +++ /dev/null @@ -1,446 +0,0 @@ -Metadata-Version: 2.1 -Name: packaging -Version: 21.2 -Summary: Core utilities for Python packages -Home-page: https://github.com/pypa/packaging -Author: Donald Stufft and individual contributors -Author-email: donald@stufft.io -License: BSD-2-Clause or Apache-2.0 -Platform: UNKNOWN -Classifier: Development Status :: 5 - Production/Stable -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: Apache Software License -Classifier: License :: OSI Approved :: BSD License -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3 :: Only -Classifier: Programming Language :: Python :: 3.6 -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 -Classifier: Programming Language :: Python :: 3.9 -Classifier: Programming Language :: Python :: 3.10 -Classifier: Programming Language :: Python :: Implementation :: CPython -Classifier: Programming Language :: Python :: Implementation :: PyPy -Requires-Python: >=3.6 -Description-Content-Type: text/x-rst -License-File: LICENSE -License-File: LICENSE.APACHE -License-File: LICENSE.BSD -Requires-Dist: pyparsing (<3,>=2.0.2) - -packaging -========= - -.. start-intro - -Reusable core utilities for various Python Packaging -`interoperability specifications `_. - -This library provides utilities that implement the interoperability -specifications which have clearly one correct behaviour (eg: :pep:`440`) -or benefit greatly from having a single shared implementation (eg: :pep:`425`). - -.. end-intro - -The ``packaging`` project includes the following: version handling, specifiers, -markers, requirements, tags, utilities. - -Documentation -------------- - -The `documentation`_ provides information and the API for the following: - -- Version Handling -- Specifiers -- Markers -- Requirements -- Tags -- Utilities - -Installation ------------- - -Use ``pip`` to install these utilities:: - - pip install packaging - -Discussion ----------- - -If you run into bugs, you can file them in our `issue tracker`_. - -You can also join ``#pypa`` on Freenode to ask questions or get involved. - - -.. _`documentation`: https://packaging.pypa.io/ -.. _`issue tracker`: https://github.com/pypa/packaging/issues - - -Code of Conduct ---------------- - -Everyone interacting in the packaging project's codebases, issue trackers, chat -rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_. - -.. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md - -Contributing ------------- - -The ``CONTRIBUTING.rst`` file outlines how to contribute to this project as -well as how to report a potential security issue. The documentation for this -project also covers information about `project development`_ and `security`_. - -.. _`project development`: https://packaging.pypa.io/en/latest/development/ -.. _`security`: https://packaging.pypa.io/en/latest/security/ - -Project History ---------------- - -Please review the ``CHANGELOG.rst`` file or the `Changelog documentation`_ for -recent changes and project history. - -.. _`Changelog documentation`: https://packaging.pypa.io/en/latest/changelog/ - -Changelog ---------- - -21.2 - 2021-10-29 -~~~~~~~~~~~~~~~~~ - -* Update documentation entry for 21.1. - -21.1 - 2021-10-29 -~~~~~~~~~~~~~~~~~ - -* Update pin to pyparsing to exclude 3.0.0. - -21.0 - 2021-07-03 -~~~~~~~~~~~~~~~~~ - -* PEP 656: musllinux support (`#411 `__) -* Drop support for Python 2.7, Python 3.4 and Python 3.5. -* Replace distutils usage with sysconfig (`#396 `__) -* Add support for zip files in ``parse_sdist_filename`` (`#429 `__) -* Use cached ``_hash`` attribute to short-circuit tag equality comparisons (`#417 `__) -* Specify the default value for the ``specifier`` argument to ``SpecifierSet`` (`#437 `__) -* Proper keyword-only "warn" argument in packaging.tags (`#403 `__) -* Correctly remove prerelease suffixes from ~= check (`#366 `__) -* Fix type hints for ``Version.post`` and ``Version.dev`` (`#393 `__) -* Use typing alias ``UnparsedVersion`` (`#398 `__) -* Improve type inference for ``packaging.specifiers.filter()`` (`#430 `__) -* Tighten the return type of ``canonicalize_version()`` (`#402 `__) - -20.9 - 2021-01-29 -~~~~~~~~~~~~~~~~~ - -* Run `isort `_ over the code base (`#377 `__) -* Add support for the ``macosx_10_*_universal2`` platform tags (`#379 `__) -* Introduce ``packaging.utils.parse_wheel_filename()`` and ``parse_sdist_filename()`` - (`#387 `__ and `#389 `__) - -20.8 - 2020-12-11 -~~~~~~~~~~~~~~~~~ - -* Revert back to setuptools for compatibility purposes for some Linux distros (`#363 `__) -* Do not insert an underscore in wheel tags when the interpreter version number - is more than 2 digits (`#372 `__) - -20.7 - 2020-11-28 -~~~~~~~~~~~~~~~~~ - -No unreleased changes. - -20.6 - 2020-11-28 -~~~~~~~~~~~~~~~~~ - -.. note:: This release was subsequently yanked, and these changes were included in 20.7. - -* Fix flit configuration, to include LICENSE files (`#357 `__) -* Make `intel` a recognized CPU architecture for the `universal` macOS platform tag (`#361 `__) -* Add some missing type hints to `packaging.requirements` (issue:`350`) - -20.5 - 2020-11-27 -~~~~~~~~~~~~~~~~~ - -* Officially support Python 3.9 (`#343 `__) -* Deprecate the ``LegacyVersion`` and ``LegacySpecifier`` classes (`#321 `__) -* Handle ``OSError`` on non-dynamic executables when attempting to resolve - the glibc version string. - -20.4 - 2020-05-19 -~~~~~~~~~~~~~~~~~ - -* Canonicalize version before comparing specifiers. (`#282 `__) -* Change type hint for ``canonicalize_name`` to return - ``packaging.utils.NormalizedName``. - This enables the use of static typing tools (like mypy) to detect mixing of - normalized and un-normalized names. - -20.3 - 2020-03-05 -~~~~~~~~~~~~~~~~~ - -* Fix changelog for 20.2. - -20.2 - 2020-03-05 -~~~~~~~~~~~~~~~~~ - -* Fix a bug that caused a 32-bit OS that runs on a 64-bit ARM CPU (e.g. ARM-v8, - aarch64), to report the wrong bitness. - -20.1 - 2020-01-24 -~~~~~~~~~~~~~~~~~~~ - -* Fix a bug caused by reuse of an exhausted iterator. (`#257 `__) - -20.0 - 2020-01-06 -~~~~~~~~~~~~~~~~~ - -* Add type hints (`#191 `__) - -* Add proper trove classifiers for PyPy support (`#198 `__) - -* Scale back depending on ``ctypes`` for manylinux support detection (`#171 `__) - -* Use ``sys.implementation.name`` where appropriate for ``packaging.tags`` (`#193 `__) - -* Expand upon the API provided by ``packaging.tags``: ``interpreter_name()``, ``mac_platforms()``, ``compatible_tags()``, ``cpython_tags()``, ``generic_tags()`` (`#187 `__) - -* Officially support Python 3.8 (`#232 `__) - -* Add ``major``, ``minor``, and ``micro`` aliases to ``packaging.version.Version`` (`#226 `__) - -* Properly mark ``packaging`` has being fully typed by adding a `py.typed` file (`#226 `__) - -19.2 - 2019-09-18 -~~~~~~~~~~~~~~~~~ - -* Remove dependency on ``attrs`` (`#178 `__, `#179 `__) - -* Use appropriate fallbacks for CPython ABI tag (`#181 `__, `#185 `__) - -* Add manylinux2014 support (`#186 `__) - -* Improve ABI detection (`#181 `__) - -* Properly handle debug wheels for Python 3.8 (`#172 `__) - -* Improve detection of debug builds on Windows (`#194 `__) - -19.1 - 2019-07-30 -~~~~~~~~~~~~~~~~~ - -* Add the ``packaging.tags`` module. (`#156 `__) - -* Correctly handle two-digit versions in ``python_version`` (`#119 `__) - - -19.0 - 2019-01-20 -~~~~~~~~~~~~~~~~~ - -* Fix string representation of PEP 508 direct URL requirements with markers. - -* Better handling of file URLs - - This allows for using ``file:///absolute/path``, which was previously - prevented due to the missing ``netloc``. - - This allows for all file URLs that ``urlunparse`` turns back into the - original URL to be valid. - - -18.0 - 2018-09-26 -~~~~~~~~~~~~~~~~~ - -* Improve error messages when invalid requirements are given. (`#129 `__) - - -17.1 - 2017-02-28 -~~~~~~~~~~~~~~~~~ - -* Fix ``utils.canonicalize_version`` when supplying non PEP 440 versions. - - -17.0 - 2017-02-28 -~~~~~~~~~~~~~~~~~ - -* Drop support for python 2.6, 3.2, and 3.3. - -* Define minimal pyparsing version to 2.0.2 (`#91 `__). - -* Add ``epoch``, ``release``, ``pre``, ``dev``, and ``post`` attributes to - ``Version`` and ``LegacyVersion`` (`#34 `__). - -* Add ``Version().is_devrelease`` and ``LegacyVersion().is_devrelease`` to - make it easy to determine if a release is a development release. - -* Add ``utils.canonicalize_version`` to canonicalize version strings or - ``Version`` instances (`#121 `__). - - -16.8 - 2016-10-29 -~~~~~~~~~~~~~~~~~ - -* Fix markers that utilize ``in`` so that they render correctly. - -* Fix an erroneous test on Python RC releases. - - -16.7 - 2016-04-23 -~~~~~~~~~~~~~~~~~ - -* Add support for the deprecated ``python_implementation`` marker which was - an undocumented setuptools marker in addition to the newer markers. - - -16.6 - 2016-03-29 -~~~~~~~~~~~~~~~~~ - -* Add support for the deprecated, PEP 345 environment markers in addition to - the newer markers. - - -16.5 - 2016-02-26 -~~~~~~~~~~~~~~~~~ - -* Fix a regression in parsing requirements with whitespaces between the comma - separators. - - -16.4 - 2016-02-22 -~~~~~~~~~~~~~~~~~ - -* Fix a regression in parsing requirements like ``foo (==4)``. - - -16.3 - 2016-02-21 -~~~~~~~~~~~~~~~~~ - -* Fix a bug where ``packaging.requirements:Requirement`` was overly strict when - matching legacy requirements. - - -16.2 - 2016-02-09 -~~~~~~~~~~~~~~~~~ - -* Add a function that implements the name canonicalization from PEP 503. - - -16.1 - 2016-02-07 -~~~~~~~~~~~~~~~~~ - -* Implement requirement specifiers from PEP 508. - - -16.0 - 2016-01-19 -~~~~~~~~~~~~~~~~~ - -* Relicense so that packaging is available under *either* the Apache License, - Version 2.0 or a 2 Clause BSD license. - -* Support installation of packaging when only distutils is available. - -* Fix ``==`` comparison when there is a prefix and a local version in play. - (`#41 `__). - -* Implement environment markers from PEP 508. - - -15.3 - 2015-08-01 -~~~~~~~~~~~~~~~~~ - -* Normalize post-release spellings for rev/r prefixes. `#35 `__ - - -15.2 - 2015-05-13 -~~~~~~~~~~~~~~~~~ - -* Fix an error where the arbitrary specifier (``===``) was not correctly - allowing pre-releases when it was being used. - -* Expose the specifier and version parts through properties on the - ``Specifier`` classes. - -* Allow iterating over the ``SpecifierSet`` to get access to all of the - ``Specifier`` instances. - -* Allow testing if a version is contained within a specifier via the ``in`` - operator. - - -15.1 - 2015-04-13 -~~~~~~~~~~~~~~~~~ - -* Fix a logic error that was causing inconsistent answers about whether or not - a pre-release was contained within a ``SpecifierSet`` or not. - - -15.0 - 2015-01-02 -~~~~~~~~~~~~~~~~~ - -* Add ``Version().is_postrelease`` and ``LegacyVersion().is_postrelease`` to - make it easy to determine if a release is a post release. - -* Add ``Version().base_version`` and ``LegacyVersion().base_version`` to make - it easy to get the public version without any pre or post release markers. - -* Support the update to PEP 440 which removed the implied ``!=V.*`` when using - either ``>V`` or ``V`` or ````) operator. - - -14.3 - 2014-11-19 -~~~~~~~~~~~~~~~~~ - -* **BACKWARDS INCOMPATIBLE** Refactor specifier support so that it can sanely - handle legacy specifiers as well as PEP 440 specifiers. - -* **BACKWARDS INCOMPATIBLE** Move the specifier support out of - ``packaging.version`` into ``packaging.specifiers``. - - -14.2 - 2014-09-10 -~~~~~~~~~~~~~~~~~ - -* Add prerelease support to ``Specifier``. -* Remove the ability to do ``item in Specifier()`` and replace it with - ``Specifier().contains(item)`` in order to allow flags that signal if a - prerelease should be accepted or not. -* Add a method ``Specifier().filter()`` which will take an iterable and returns - an iterable with items that do not match the specifier filtered out. - - -14.1 - 2014-09-08 -~~~~~~~~~~~~~~~~~ - -* Allow ``LegacyVersion`` and ``Version`` to be sorted together. -* Add ``packaging.version.parse()`` to enable easily parsing a version string - as either a ``Version`` or a ``LegacyVersion`` depending on it's PEP 440 - validity. - - -14.0 - 2014-09-05 -~~~~~~~~~~~~~~~~~ - -* Initial release. - - -.. _`master`: https://github.com/pypa/packaging/ - - diff --git a/pkg_resources/_vendor/packaging-21.2.dist-info/RECORD b/pkg_resources/_vendor/packaging-21.2.dist-info/RECORD deleted file mode 100644 index ed2291ac..00000000 --- a/pkg_resources/_vendor/packaging-21.2.dist-info/RECORD +++ /dev/null @@ -1,32 +0,0 @@ -packaging-21.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -packaging-21.2.dist-info/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197 -packaging-21.2.dist-info/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174 -packaging-21.2.dist-info/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344 -packaging-21.2.dist-info/METADATA,sha256=N4A8uSYrQwV9byem7YuI9OtVkbqiNzFlDhcDVT-suAo,14754 -packaging-21.2.dist-info/RECORD,, -packaging-21.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -packaging-21.2.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92 -packaging-21.2.dist-info/top_level.txt,sha256=zFdHrhWnPslzsiP455HutQsqPB6v0KCtNUMtUtrefDw,10 -packaging/__about__.py,sha256=IIRHpOsJlJSgkjq1UoeBoMTqhvNp3gN9FyMb5Kf8El4,661 -packaging/__init__.py,sha256=b9Kk5MF7KxhhLgcDmiUWukN-LatWFxPdNug0joPhHSk,497 -packaging/__pycache__/__about__.cpython-310.pyc,, -packaging/__pycache__/__init__.cpython-310.pyc,, -packaging/__pycache__/_manylinux.cpython-310.pyc,, -packaging/__pycache__/_musllinux.cpython-310.pyc,, -packaging/__pycache__/_structures.cpython-310.pyc,, -packaging/__pycache__/markers.cpython-310.pyc,, -packaging/__pycache__/requirements.cpython-310.pyc,, -packaging/__pycache__/specifiers.cpython-310.pyc,, -packaging/__pycache__/tags.cpython-310.pyc,, -packaging/__pycache__/utils.cpython-310.pyc,, -packaging/__pycache__/version.cpython-310.pyc,, -packaging/_manylinux.py,sha256=XcbiXB-qcjv3bcohp6N98TMpOP4_j3m-iOA8ptK2GWY,11488 -packaging/_musllinux.py,sha256=z5yeG1ygOPx4uUyLdqj-p8Dk5UBb5H_b0NIjW9yo8oA,4378 -packaging/_structures.py,sha256=TMiAgFbdUOPmIfDIfiHc3KFhSJ8kMjof2QS5I-2NyQ8,1629 -packaging/markers.py,sha256=Fygi3_eZnjQ-3VJizW5AhI5wvo0Hb6RMk4DidsKpOC0,8475 -packaging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -packaging/requirements.py,sha256=rjaGRCMepZS1mlYMjJ5Qh6rfq3gtsCRQUQmftGZ_bu8,4664 -packaging/specifiers.py,sha256=MZ-fYcNL3u7pNrt-6g2EQO7AbRXkjc-SPEYwXMQbLmc,30964 -packaging/tags.py,sha256=vGybAUQYlPKMcukzX_2e65fmafnFFuMbD25naYTEwtc,15710 -packaging/utils.py,sha256=dJjeat3BS-TYn1RrUFVwufUMasbtzLfYRoy_HXENeFQ,4200 -packaging/version.py,sha256=_fLRNrFrxYcHVfyo8vk9j8s6JM8N_xsSxVFr6RJyco8,14665 diff --git a/pkg_resources/_vendor/packaging-21.2.dist-info/REQUESTED b/pkg_resources/_vendor/packaging-21.2.dist-info/REQUESTED deleted file mode 100644 index e69de29b..00000000 diff --git a/pkg_resources/_vendor/packaging-21.2.dist-info/WHEEL b/pkg_resources/_vendor/packaging-21.2.dist-info/WHEEL deleted file mode 100644 index 5bad85fd..00000000 --- a/pkg_resources/_vendor/packaging-21.2.dist-info/WHEEL +++ /dev/null @@ -1,5 +0,0 @@ -Wheel-Version: 1.0 -Generator: bdist_wheel (0.37.0) -Root-Is-Purelib: true -Tag: py3-none-any - diff --git a/pkg_resources/_vendor/packaging-21.2.dist-info/top_level.txt b/pkg_resources/_vendor/packaging-21.2.dist-info/top_level.txt deleted file mode 100644 index 748809f7..00000000 --- a/pkg_resources/_vendor/packaging-21.2.dist-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -packaging diff --git a/pkg_resources/_vendor/packaging-21.3.dist-info/INSTALLER b/pkg_resources/_vendor/packaging-21.3.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/pkg_resources/_vendor/packaging-21.3.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/pkg_resources/_vendor/packaging-21.3.dist-info/LICENSE b/pkg_resources/_vendor/packaging-21.3.dist-info/LICENSE new file mode 100644 index 00000000..6f62d44e --- /dev/null +++ b/pkg_resources/_vendor/packaging-21.3.dist-info/LICENSE @@ -0,0 +1,3 @@ +This software is made available under the terms of *either* of the licenses +found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made +under the terms of *both* these licenses. diff --git a/pkg_resources/_vendor/packaging-21.3.dist-info/LICENSE.APACHE b/pkg_resources/_vendor/packaging-21.3.dist-info/LICENSE.APACHE new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/pkg_resources/_vendor/packaging-21.3.dist-info/LICENSE.APACHE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/pkg_resources/_vendor/packaging-21.3.dist-info/LICENSE.BSD b/pkg_resources/_vendor/packaging-21.3.dist-info/LICENSE.BSD new file mode 100644 index 00000000..42ce7b75 --- /dev/null +++ b/pkg_resources/_vendor/packaging-21.3.dist-info/LICENSE.BSD @@ -0,0 +1,23 @@ +Copyright (c) Donald Stufft and individual contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pkg_resources/_vendor/packaging-21.3.dist-info/METADATA b/pkg_resources/_vendor/packaging-21.3.dist-info/METADATA new file mode 100644 index 00000000..358ace53 --- /dev/null +++ b/pkg_resources/_vendor/packaging-21.3.dist-info/METADATA @@ -0,0 +1,453 @@ +Metadata-Version: 2.1 +Name: packaging +Version: 21.3 +Summary: Core utilities for Python packages +Home-page: https://github.com/pypa/packaging +Author: Donald Stufft and individual contributors +Author-email: donald@stufft.io +License: BSD-2-Clause or Apache-2.0 +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Apache Software License +Classifier: License :: OSI Approved :: BSD License +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Requires-Python: >=3.6 +Description-Content-Type: text/x-rst +License-File: LICENSE +License-File: LICENSE.APACHE +License-File: LICENSE.BSD +Requires-Dist: pyparsing (!=3.0.5,>=2.0.2) + +packaging +========= + +.. start-intro + +Reusable core utilities for various Python Packaging +`interoperability specifications `_. + +This library provides utilities that implement the interoperability +specifications which have clearly one correct behaviour (eg: :pep:`440`) +or benefit greatly from having a single shared implementation (eg: :pep:`425`). + +.. end-intro + +The ``packaging`` project includes the following: version handling, specifiers, +markers, requirements, tags, utilities. + +Documentation +------------- + +The `documentation`_ provides information and the API for the following: + +- Version Handling +- Specifiers +- Markers +- Requirements +- Tags +- Utilities + +Installation +------------ + +Use ``pip`` to install these utilities:: + + pip install packaging + +Discussion +---------- + +If you run into bugs, you can file them in our `issue tracker`_. + +You can also join ``#pypa`` on Freenode to ask questions or get involved. + + +.. _`documentation`: https://packaging.pypa.io/ +.. _`issue tracker`: https://github.com/pypa/packaging/issues + + +Code of Conduct +--------------- + +Everyone interacting in the packaging project's codebases, issue trackers, chat +rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_. + +.. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md + +Contributing +------------ + +The ``CONTRIBUTING.rst`` file outlines how to contribute to this project as +well as how to report a potential security issue. The documentation for this +project also covers information about `project development`_ and `security`_. + +.. _`project development`: https://packaging.pypa.io/en/latest/development/ +.. _`security`: https://packaging.pypa.io/en/latest/security/ + +Project History +--------------- + +Please review the ``CHANGELOG.rst`` file or the `Changelog documentation`_ for +recent changes and project history. + +.. _`Changelog documentation`: https://packaging.pypa.io/en/latest/changelog/ + +Changelog +--------- + +21.3 - 2021-11-17 +~~~~~~~~~~~~~~~~~ + +* Add a ``pp3-none-any`` tag (`#311 `__) +* Replace the blank pyparsing 3 exclusion with a 3.0.5 exclusion (`#481 `__, `#486 `__) +* Fix a spelling mistake (`#479 `__) + +21.2 - 2021-10-29 +~~~~~~~~~~~~~~~~~ + +* Update documentation entry for 21.1. + +21.1 - 2021-10-29 +~~~~~~~~~~~~~~~~~ + +* Update pin to pyparsing to exclude 3.0.0. + +21.0 - 2021-07-03 +~~~~~~~~~~~~~~~~~ + +* PEP 656: musllinux support (`#411 `__) +* Drop support for Python 2.7, Python 3.4 and Python 3.5. +* Replace distutils usage with sysconfig (`#396 `__) +* Add support for zip files in ``parse_sdist_filename`` (`#429 `__) +* Use cached ``_hash`` attribute to short-circuit tag equality comparisons (`#417 `__) +* Specify the default value for the ``specifier`` argument to ``SpecifierSet`` (`#437 `__) +* Proper keyword-only "warn" argument in packaging.tags (`#403 `__) +* Correctly remove prerelease suffixes from ~= check (`#366 `__) +* Fix type hints for ``Version.post`` and ``Version.dev`` (`#393 `__) +* Use typing alias ``UnparsedVersion`` (`#398 `__) +* Improve type inference for ``packaging.specifiers.filter()`` (`#430 `__) +* Tighten the return type of ``canonicalize_version()`` (`#402 `__) + +20.9 - 2021-01-29 +~~~~~~~~~~~~~~~~~ + +* Run `isort `_ over the code base (`#377 `__) +* Add support for the ``macosx_10_*_universal2`` platform tags (`#379 `__) +* Introduce ``packaging.utils.parse_wheel_filename()`` and ``parse_sdist_filename()`` + (`#387 `__ and `#389 `__) + +20.8 - 2020-12-11 +~~~~~~~~~~~~~~~~~ + +* Revert back to setuptools for compatibility purposes for some Linux distros (`#363 `__) +* Do not insert an underscore in wheel tags when the interpreter version number + is more than 2 digits (`#372 `__) + +20.7 - 2020-11-28 +~~~~~~~~~~~~~~~~~ + +No unreleased changes. + +20.6 - 2020-11-28 +~~~~~~~~~~~~~~~~~ + +.. note:: This release was subsequently yanked, and these changes were included in 20.7. + +* Fix flit configuration, to include LICENSE files (`#357 `__) +* Make `intel` a recognized CPU architecture for the `universal` macOS platform tag (`#361 `__) +* Add some missing type hints to `packaging.requirements` (issue:`350`) + +20.5 - 2020-11-27 +~~~~~~~~~~~~~~~~~ + +* Officially support Python 3.9 (`#343 `__) +* Deprecate the ``LegacyVersion`` and ``LegacySpecifier`` classes (`#321 `__) +* Handle ``OSError`` on non-dynamic executables when attempting to resolve + the glibc version string. + +20.4 - 2020-05-19 +~~~~~~~~~~~~~~~~~ + +* Canonicalize version before comparing specifiers. (`#282 `__) +* Change type hint for ``canonicalize_name`` to return + ``packaging.utils.NormalizedName``. + This enables the use of static typing tools (like mypy) to detect mixing of + normalized and un-normalized names. + +20.3 - 2020-03-05 +~~~~~~~~~~~~~~~~~ + +* Fix changelog for 20.2. + +20.2 - 2020-03-05 +~~~~~~~~~~~~~~~~~ + +* Fix a bug that caused a 32-bit OS that runs on a 64-bit ARM CPU (e.g. ARM-v8, + aarch64), to report the wrong bitness. + +20.1 - 2020-01-24 +~~~~~~~~~~~~~~~~~~~ + +* Fix a bug caused by reuse of an exhausted iterator. (`#257 `__) + +20.0 - 2020-01-06 +~~~~~~~~~~~~~~~~~ + +* Add type hints (`#191 `__) + +* Add proper trove classifiers for PyPy support (`#198 `__) + +* Scale back depending on ``ctypes`` for manylinux support detection (`#171 `__) + +* Use ``sys.implementation.name`` where appropriate for ``packaging.tags`` (`#193 `__) + +* Expand upon the API provided by ``packaging.tags``: ``interpreter_name()``, ``mac_platforms()``, ``compatible_tags()``, ``cpython_tags()``, ``generic_tags()`` (`#187 `__) + +* Officially support Python 3.8 (`#232 `__) + +* Add ``major``, ``minor``, and ``micro`` aliases to ``packaging.version.Version`` (`#226 `__) + +* Properly mark ``packaging`` has being fully typed by adding a `py.typed` file (`#226 `__) + +19.2 - 2019-09-18 +~~~~~~~~~~~~~~~~~ + +* Remove dependency on ``attrs`` (`#178 `__, `#179 `__) + +* Use appropriate fallbacks for CPython ABI tag (`#181 `__, `#185 `__) + +* Add manylinux2014 support (`#186 `__) + +* Improve ABI detection (`#181 `__) + +* Properly handle debug wheels for Python 3.8 (`#172 `__) + +* Improve detection of debug builds on Windows (`#194 `__) + +19.1 - 2019-07-30 +~~~~~~~~~~~~~~~~~ + +* Add the ``packaging.tags`` module. (`#156 `__) + +* Correctly handle two-digit versions in ``python_version`` (`#119 `__) + + +19.0 - 2019-01-20 +~~~~~~~~~~~~~~~~~ + +* Fix string representation of PEP 508 direct URL requirements with markers. + +* Better handling of file URLs + + This allows for using ``file:///absolute/path``, which was previously + prevented due to the missing ``netloc``. + + This allows for all file URLs that ``urlunparse`` turns back into the + original URL to be valid. + + +18.0 - 2018-09-26 +~~~~~~~~~~~~~~~~~ + +* Improve error messages when invalid requirements are given. (`#129 `__) + + +17.1 - 2017-02-28 +~~~~~~~~~~~~~~~~~ + +* Fix ``utils.canonicalize_version`` when supplying non PEP 440 versions. + + +17.0 - 2017-02-28 +~~~~~~~~~~~~~~~~~ + +* Drop support for python 2.6, 3.2, and 3.3. + +* Define minimal pyparsing version to 2.0.2 (`#91 `__). + +* Add ``epoch``, ``release``, ``pre``, ``dev``, and ``post`` attributes to + ``Version`` and ``LegacyVersion`` (`#34 `__). + +* Add ``Version().is_devrelease`` and ``LegacyVersion().is_devrelease`` to + make it easy to determine if a release is a development release. + +* Add ``utils.canonicalize_version`` to canonicalize version strings or + ``Version`` instances (`#121 `__). + + +16.8 - 2016-10-29 +~~~~~~~~~~~~~~~~~ + +* Fix markers that utilize ``in`` so that they render correctly. + +* Fix an erroneous test on Python RC releases. + + +16.7 - 2016-04-23 +~~~~~~~~~~~~~~~~~ + +* Add support for the deprecated ``python_implementation`` marker which was + an undocumented setuptools marker in addition to the newer markers. + + +16.6 - 2016-03-29 +~~~~~~~~~~~~~~~~~ + +* Add support for the deprecated, PEP 345 environment markers in addition to + the newer markers. + + +16.5 - 2016-02-26 +~~~~~~~~~~~~~~~~~ + +* Fix a regression in parsing requirements with whitespaces between the comma + separators. + + +16.4 - 2016-02-22 +~~~~~~~~~~~~~~~~~ + +* Fix a regression in parsing requirements like ``foo (==4)``. + + +16.3 - 2016-02-21 +~~~~~~~~~~~~~~~~~ + +* Fix a bug where ``packaging.requirements:Requirement`` was overly strict when + matching legacy requirements. + + +16.2 - 2016-02-09 +~~~~~~~~~~~~~~~~~ + +* Add a function that implements the name canonicalization from PEP 503. + + +16.1 - 2016-02-07 +~~~~~~~~~~~~~~~~~ + +* Implement requirement specifiers from PEP 508. + + +16.0 - 2016-01-19 +~~~~~~~~~~~~~~~~~ + +* Relicense so that packaging is available under *either* the Apache License, + Version 2.0 or a 2 Clause BSD license. + +* Support installation of packaging when only distutils is available. + +* Fix ``==`` comparison when there is a prefix and a local version in play. + (`#41 `__). + +* Implement environment markers from PEP 508. + + +15.3 - 2015-08-01 +~~~~~~~~~~~~~~~~~ + +* Normalize post-release spellings for rev/r prefixes. `#35 `__ + + +15.2 - 2015-05-13 +~~~~~~~~~~~~~~~~~ + +* Fix an error where the arbitrary specifier (``===``) was not correctly + allowing pre-releases when it was being used. + +* Expose the specifier and version parts through properties on the + ``Specifier`` classes. + +* Allow iterating over the ``SpecifierSet`` to get access to all of the + ``Specifier`` instances. + +* Allow testing if a version is contained within a specifier via the ``in`` + operator. + + +15.1 - 2015-04-13 +~~~~~~~~~~~~~~~~~ + +* Fix a logic error that was causing inconsistent answers about whether or not + a pre-release was contained within a ``SpecifierSet`` or not. + + +15.0 - 2015-01-02 +~~~~~~~~~~~~~~~~~ + +* Add ``Version().is_postrelease`` and ``LegacyVersion().is_postrelease`` to + make it easy to determine if a release is a post release. + +* Add ``Version().base_version`` and ``LegacyVersion().base_version`` to make + it easy to get the public version without any pre or post release markers. + +* Support the update to PEP 440 which removed the implied ``!=V.*`` when using + either ``>V`` or ``V`` or ````) operator. + + +14.3 - 2014-11-19 +~~~~~~~~~~~~~~~~~ + +* **BACKWARDS INCOMPATIBLE** Refactor specifier support so that it can sanely + handle legacy specifiers as well as PEP 440 specifiers. + +* **BACKWARDS INCOMPATIBLE** Move the specifier support out of + ``packaging.version`` into ``packaging.specifiers``. + + +14.2 - 2014-09-10 +~~~~~~~~~~~~~~~~~ + +* Add prerelease support to ``Specifier``. +* Remove the ability to do ``item in Specifier()`` and replace it with + ``Specifier().contains(item)`` in order to allow flags that signal if a + prerelease should be accepted or not. +* Add a method ``Specifier().filter()`` which will take an iterable and returns + an iterable with items that do not match the specifier filtered out. + + +14.1 - 2014-09-08 +~~~~~~~~~~~~~~~~~ + +* Allow ``LegacyVersion`` and ``Version`` to be sorted together. +* Add ``packaging.version.parse()`` to enable easily parsing a version string + as either a ``Version`` or a ``LegacyVersion`` depending on it's PEP 440 + validity. + + +14.0 - 2014-09-05 +~~~~~~~~~~~~~~~~~ + +* Initial release. + + +.. _`master`: https://github.com/pypa/packaging/ + + diff --git a/pkg_resources/_vendor/packaging-21.3.dist-info/RECORD b/pkg_resources/_vendor/packaging-21.3.dist-info/RECORD new file mode 100644 index 00000000..97cace10 --- /dev/null +++ b/pkg_resources/_vendor/packaging-21.3.dist-info/RECORD @@ -0,0 +1,32 @@ +packaging-21.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +packaging-21.3.dist-info/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197 +packaging-21.3.dist-info/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174 +packaging-21.3.dist-info/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344 +packaging-21.3.dist-info/METADATA,sha256=KuKIy6qDLP3svIt6ejCbxBDhvq11ebkgUN55MeyKFyc,15147 +packaging-21.3.dist-info/RECORD,, +packaging-21.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +packaging-21.3.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92 +packaging-21.3.dist-info/top_level.txt,sha256=zFdHrhWnPslzsiP455HutQsqPB6v0KCtNUMtUtrefDw,10 +packaging/__about__.py,sha256=ugASIO2w1oUyH8_COqQ2X_s0rDhjbhQC3yJocD03h2c,661 +packaging/__init__.py,sha256=b9Kk5MF7KxhhLgcDmiUWukN-LatWFxPdNug0joPhHSk,497 +packaging/__pycache__/__about__.cpython-310.pyc,, +packaging/__pycache__/__init__.cpython-310.pyc,, +packaging/__pycache__/_manylinux.cpython-310.pyc,, +packaging/__pycache__/_musllinux.cpython-310.pyc,, +packaging/__pycache__/_structures.cpython-310.pyc,, +packaging/__pycache__/markers.cpython-310.pyc,, +packaging/__pycache__/requirements.cpython-310.pyc,, +packaging/__pycache__/specifiers.cpython-310.pyc,, +packaging/__pycache__/tags.cpython-310.pyc,, +packaging/__pycache__/utils.cpython-310.pyc,, +packaging/__pycache__/version.cpython-310.pyc,, +packaging/_manylinux.py,sha256=XcbiXB-qcjv3bcohp6N98TMpOP4_j3m-iOA8ptK2GWY,11488 +packaging/_musllinux.py,sha256=_KGgY_qc7vhMGpoqss25n2hiLCNKRtvz9mCrS7gkqyc,4378 +packaging/_structures.py,sha256=q3eVNmbWJGG_S0Dit_S3Ao8qQqz_5PYTXFAKBZe5yr4,1431 +packaging/markers.py,sha256=Fygi3_eZnjQ-3VJizW5AhI5wvo0Hb6RMk4DidsKpOC0,8475 +packaging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +packaging/requirements.py,sha256=rjaGRCMepZS1mlYMjJ5Qh6rfq3gtsCRQUQmftGZ_bu8,4664 +packaging/specifiers.py,sha256=LRQ0kFsHrl5qfcFNEEJrIFYsnIHQUJXY9fIsakTrrqE,30110 +packaging/tags.py,sha256=lmsnGNiJ8C4D_Pf9PbM0qgbZvD9kmB9lpZBQUZa3R_Y,15699 +packaging/utils.py,sha256=dJjeat3BS-TYn1RrUFVwufUMasbtzLfYRoy_HXENeFQ,4200 +packaging/version.py,sha256=_fLRNrFrxYcHVfyo8vk9j8s6JM8N_xsSxVFr6RJyco8,14665 diff --git a/pkg_resources/_vendor/packaging-21.3.dist-info/REQUESTED b/pkg_resources/_vendor/packaging-21.3.dist-info/REQUESTED new file mode 100644 index 00000000..e69de29b diff --git a/pkg_resources/_vendor/packaging-21.3.dist-info/WHEEL b/pkg_resources/_vendor/packaging-21.3.dist-info/WHEEL new file mode 100644 index 00000000..5bad85fd --- /dev/null +++ b/pkg_resources/_vendor/packaging-21.3.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/pkg_resources/_vendor/packaging-21.3.dist-info/top_level.txt b/pkg_resources/_vendor/packaging-21.3.dist-info/top_level.txt new file mode 100644 index 00000000..748809f7 --- /dev/null +++ b/pkg_resources/_vendor/packaging-21.3.dist-info/top_level.txt @@ -0,0 +1 @@ +packaging diff --git a/pkg_resources/_vendor/packaging/__about__.py b/pkg_resources/_vendor/packaging/__about__.py index c359122f..3551bc2d 100644 --- a/pkg_resources/_vendor/packaging/__about__.py +++ b/pkg_resources/_vendor/packaging/__about__.py @@ -17,7 +17,7 @@ __title__ = "packaging" __summary__ = "Core utilities for Python packages" __uri__ = "https://github.com/pypa/packaging" -__version__ = "21.2" +__version__ = "21.3" __author__ = "Donald Stufft and individual contributors" __email__ = "donald@stufft.io" diff --git a/pkg_resources/_vendor/packaging/_musllinux.py b/pkg_resources/_vendor/packaging/_musllinux.py index 85450faf..8ac3059b 100644 --- a/pkg_resources/_vendor/packaging/_musllinux.py +++ b/pkg_resources/_vendor/packaging/_musllinux.py @@ -98,7 +98,7 @@ def _get_musl_version(executable: str) -> Optional[_MuslVersion]: with contextlib.ExitStack() as stack: try: f = stack.enter_context(open(executable, "rb")) - except IOError: + except OSError: return None ld = _parse_ld_musl_from_elf(f) if not ld: diff --git a/pkg_resources/_vendor/packaging/_structures.py b/pkg_resources/_vendor/packaging/_structures.py index 95154975..90a6465f 100644 --- a/pkg_resources/_vendor/packaging/_structures.py +++ b/pkg_resources/_vendor/packaging/_structures.py @@ -19,9 +19,6 @@ class InfinityType: def __eq__(self, other: object) -> bool: return isinstance(other, self.__class__) - def __ne__(self, other: object) -> bool: - return not isinstance(other, self.__class__) - def __gt__(self, other: object) -> bool: return True @@ -51,9 +48,6 @@ class NegativeInfinityType: def __eq__(self, other: object) -> bool: return isinstance(other, self.__class__) - def __ne__(self, other: object) -> bool: - return not isinstance(other, self.__class__) - def __gt__(self, other: object) -> bool: return False diff --git a/pkg_resources/_vendor/packaging/specifiers.py b/pkg_resources/_vendor/packaging/specifiers.py index ce66bd4a..0e218a6f 100644 --- a/pkg_resources/_vendor/packaging/specifiers.py +++ b/pkg_resources/_vendor/packaging/specifiers.py @@ -57,13 +57,6 @@ class BaseSpecifier(metaclass=abc.ABCMeta): objects are equal. """ - @abc.abstractmethod - def __ne__(self, other: object) -> bool: - """ - Returns a boolean representing whether or not the two Specifier like - objects are not equal. - """ - @abc.abstractproperty def prereleases(self) -> Optional[bool]: """ @@ -119,7 +112,7 @@ class _IndividualSpecifier(BaseSpecifier): else "" ) - return "<{}({!r}{})>".format(self.__class__.__name__, str(self), pre) + return f"<{self.__class__.__name__}({str(self)!r}{pre})>" def __str__(self) -> str: return "{}{}".format(*self._spec) @@ -142,17 +135,6 @@ class _IndividualSpecifier(BaseSpecifier): return self._canonical_spec == other._canonical_spec - def __ne__(self, other: object) -> bool: - if isinstance(other, str): - try: - other = self.__class__(str(other)) - except InvalidSpecifier: - return NotImplemented - elif not isinstance(other, self.__class__): - return NotImplemented - - return self._spec != other._spec - def _get_operator(self, op: str) -> CallableOperator: operator_callable: CallableOperator = getattr( self, f"_compare_{self._operators[op]}" @@ -667,7 +649,7 @@ class SpecifierSet(BaseSpecifier): else "" ) - return "".format(str(self), pre) + return f"" def __str__(self) -> str: return ",".join(sorted(str(s) for s in self._specs)) @@ -706,14 +688,6 @@ class SpecifierSet(BaseSpecifier): return self._specs == other._specs - def __ne__(self, other: object) -> bool: - if isinstance(other, (str, _IndividualSpecifier)): - other = SpecifierSet(str(other)) - elif not isinstance(other, SpecifierSet): - return NotImplemented - - return self._specs != other._specs - def __len__(self) -> int: return len(self._specs) diff --git a/pkg_resources/_vendor/packaging/tags.py b/pkg_resources/_vendor/packaging/tags.py index e65890a9..9a3d25a7 100644 --- a/pkg_resources/_vendor/packaging/tags.py +++ b/pkg_resources/_vendor/packaging/tags.py @@ -90,7 +90,7 @@ class Tag: return f"{self._interpreter}-{self._abi}-{self._platform}" def __repr__(self) -> str: - return "<{self} @ {self_id}>".format(self=self, self_id=id(self)) + return f"<{self} @ {id(self)}>" def parse_tag(tag: str) -> FrozenSet[Tag]: @@ -192,7 +192,7 @@ def cpython_tags( if not python_version: python_version = sys.version_info[:2] - interpreter = "cp{}".format(_version_nodot(python_version[:2])) + interpreter = f"cp{_version_nodot(python_version[:2])}" if abis is None: if len(python_version) > 1: @@ -268,11 +268,11 @@ def _py_interpreter_range(py_version: PythonVersion) -> Iterator[str]: all previous versions of that major version. """ if len(py_version) > 1: - yield "py{version}".format(version=_version_nodot(py_version[:2])) - yield "py{major}".format(major=py_version[0]) + yield f"py{_version_nodot(py_version[:2])}" + yield f"py{py_version[0]}" if len(py_version) > 1: for minor in range(py_version[1] - 1, -1, -1): - yield "py{version}".format(version=_version_nodot((py_version[0], minor))) + yield f"py{_version_nodot((py_version[0], minor))}" def compatible_tags( @@ -481,4 +481,7 @@ def sys_tags(*, warn: bool = False) -> Iterator[Tag]: else: yield from generic_tags() - yield from compatible_tags() + if interp_name == "pp": + yield from compatible_tags(interpreter="pp3") + else: + yield from compatible_tags() diff --git a/pkg_resources/_vendor/vendored.txt b/pkg_resources/_vendor/vendored.txt index 0128eb17..d5dbe736 100644 --- a/pkg_resources/_vendor/vendored.txt +++ b/pkg_resources/_vendor/vendored.txt @@ -1,4 +1,4 @@ -packaging==21.2 +packaging==21.3 pyparsing==2.2.1 appdirs==1.4.3 jaraco.text==3.7.0 diff --git a/setuptools/_vendor/packaging-21.2.dist-info/INSTALLER b/setuptools/_vendor/packaging-21.2.dist-info/INSTALLER deleted file mode 100644 index a1b589e3..00000000 --- a/setuptools/_vendor/packaging-21.2.dist-info/INSTALLER +++ /dev/null @@ -1 +0,0 @@ -pip diff --git a/setuptools/_vendor/packaging-21.2.dist-info/LICENSE b/setuptools/_vendor/packaging-21.2.dist-info/LICENSE deleted file mode 100644 index 6f62d44e..00000000 --- a/setuptools/_vendor/packaging-21.2.dist-info/LICENSE +++ /dev/null @@ -1,3 +0,0 @@ -This software is made available under the terms of *either* of the licenses -found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made -under the terms of *both* these licenses. diff --git a/setuptools/_vendor/packaging-21.2.dist-info/LICENSE.APACHE b/setuptools/_vendor/packaging-21.2.dist-info/LICENSE.APACHE deleted file mode 100644 index f433b1a5..00000000 --- a/setuptools/_vendor/packaging-21.2.dist-info/LICENSE.APACHE +++ /dev/null @@ -1,177 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS diff --git a/setuptools/_vendor/packaging-21.2.dist-info/LICENSE.BSD b/setuptools/_vendor/packaging-21.2.dist-info/LICENSE.BSD deleted file mode 100644 index 42ce7b75..00000000 --- a/setuptools/_vendor/packaging-21.2.dist-info/LICENSE.BSD +++ /dev/null @@ -1,23 +0,0 @@ -Copyright (c) Donald Stufft and individual contributors. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/setuptools/_vendor/packaging-21.2.dist-info/METADATA b/setuptools/_vendor/packaging-21.2.dist-info/METADATA deleted file mode 100644 index e8ff54d7..00000000 --- a/setuptools/_vendor/packaging-21.2.dist-info/METADATA +++ /dev/null @@ -1,446 +0,0 @@ -Metadata-Version: 2.1 -Name: packaging -Version: 21.2 -Summary: Core utilities for Python packages -Home-page: https://github.com/pypa/packaging -Author: Donald Stufft and individual contributors -Author-email: donald@stufft.io -License: BSD-2-Clause or Apache-2.0 -Platform: UNKNOWN -Classifier: Development Status :: 5 - Production/Stable -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: Apache Software License -Classifier: License :: OSI Approved :: BSD License -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3 :: Only -Classifier: Programming Language :: Python :: 3.6 -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 -Classifier: Programming Language :: Python :: 3.9 -Classifier: Programming Language :: Python :: 3.10 -Classifier: Programming Language :: Python :: Implementation :: CPython -Classifier: Programming Language :: Python :: Implementation :: PyPy -Requires-Python: >=3.6 -Description-Content-Type: text/x-rst -License-File: LICENSE -License-File: LICENSE.APACHE -License-File: LICENSE.BSD -Requires-Dist: pyparsing (<3,>=2.0.2) - -packaging -========= - -.. start-intro - -Reusable core utilities for various Python Packaging -`interoperability specifications `_. - -This library provides utilities that implement the interoperability -specifications which have clearly one correct behaviour (eg: :pep:`440`) -or benefit greatly from having a single shared implementation (eg: :pep:`425`). - -.. end-intro - -The ``packaging`` project includes the following: version handling, specifiers, -markers, requirements, tags, utilities. - -Documentation -------------- - -The `documentation`_ provides information and the API for the following: - -- Version Handling -- Specifiers -- Markers -- Requirements -- Tags -- Utilities - -Installation ------------- - -Use ``pip`` to install these utilities:: - - pip install packaging - -Discussion ----------- - -If you run into bugs, you can file them in our `issue tracker`_. - -You can also join ``#pypa`` on Freenode to ask questions or get involved. - - -.. _`documentation`: https://packaging.pypa.io/ -.. _`issue tracker`: https://github.com/pypa/packaging/issues - - -Code of Conduct ---------------- - -Everyone interacting in the packaging project's codebases, issue trackers, chat -rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_. - -.. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md - -Contributing ------------- - -The ``CONTRIBUTING.rst`` file outlines how to contribute to this project as -well as how to report a potential security issue. The documentation for this -project also covers information about `project development`_ and `security`_. - -.. _`project development`: https://packaging.pypa.io/en/latest/development/ -.. _`security`: https://packaging.pypa.io/en/latest/security/ - -Project History ---------------- - -Please review the ``CHANGELOG.rst`` file or the `Changelog documentation`_ for -recent changes and project history. - -.. _`Changelog documentation`: https://packaging.pypa.io/en/latest/changelog/ - -Changelog ---------- - -21.2 - 2021-10-29 -~~~~~~~~~~~~~~~~~ - -* Update documentation entry for 21.1. - -21.1 - 2021-10-29 -~~~~~~~~~~~~~~~~~ - -* Update pin to pyparsing to exclude 3.0.0. - -21.0 - 2021-07-03 -~~~~~~~~~~~~~~~~~ - -* PEP 656: musllinux support (`#411 `__) -* Drop support for Python 2.7, Python 3.4 and Python 3.5. -* Replace distutils usage with sysconfig (`#396 `__) -* Add support for zip files in ``parse_sdist_filename`` (`#429 `__) -* Use cached ``_hash`` attribute to short-circuit tag equality comparisons (`#417 `__) -* Specify the default value for the ``specifier`` argument to ``SpecifierSet`` (`#437 `__) -* Proper keyword-only "warn" argument in packaging.tags (`#403 `__) -* Correctly remove prerelease suffixes from ~= check (`#366 `__) -* Fix type hints for ``Version.post`` and ``Version.dev`` (`#393 `__) -* Use typing alias ``UnparsedVersion`` (`#398 `__) -* Improve type inference for ``packaging.specifiers.filter()`` (`#430 `__) -* Tighten the return type of ``canonicalize_version()`` (`#402 `__) - -20.9 - 2021-01-29 -~~~~~~~~~~~~~~~~~ - -* Run `isort `_ over the code base (`#377 `__) -* Add support for the ``macosx_10_*_universal2`` platform tags (`#379 `__) -* Introduce ``packaging.utils.parse_wheel_filename()`` and ``parse_sdist_filename()`` - (`#387 `__ and `#389 `__) - -20.8 - 2020-12-11 -~~~~~~~~~~~~~~~~~ - -* Revert back to setuptools for compatibility purposes for some Linux distros (`#363 `__) -* Do not insert an underscore in wheel tags when the interpreter version number - is more than 2 digits (`#372 `__) - -20.7 - 2020-11-28 -~~~~~~~~~~~~~~~~~ - -No unreleased changes. - -20.6 - 2020-11-28 -~~~~~~~~~~~~~~~~~ - -.. note:: This release was subsequently yanked, and these changes were included in 20.7. - -* Fix flit configuration, to include LICENSE files (`#357 `__) -* Make `intel` a recognized CPU architecture for the `universal` macOS platform tag (`#361 `__) -* Add some missing type hints to `packaging.requirements` (issue:`350`) - -20.5 - 2020-11-27 -~~~~~~~~~~~~~~~~~ - -* Officially support Python 3.9 (`#343 `__) -* Deprecate the ``LegacyVersion`` and ``LegacySpecifier`` classes (`#321 `__) -* Handle ``OSError`` on non-dynamic executables when attempting to resolve - the glibc version string. - -20.4 - 2020-05-19 -~~~~~~~~~~~~~~~~~ - -* Canonicalize version before comparing specifiers. (`#282 `__) -* Change type hint for ``canonicalize_name`` to return - ``packaging.utils.NormalizedName``. - This enables the use of static typing tools (like mypy) to detect mixing of - normalized and un-normalized names. - -20.3 - 2020-03-05 -~~~~~~~~~~~~~~~~~ - -* Fix changelog for 20.2. - -20.2 - 2020-03-05 -~~~~~~~~~~~~~~~~~ - -* Fix a bug that caused a 32-bit OS that runs on a 64-bit ARM CPU (e.g. ARM-v8, - aarch64), to report the wrong bitness. - -20.1 - 2020-01-24 -~~~~~~~~~~~~~~~~~~~ - -* Fix a bug caused by reuse of an exhausted iterator. (`#257 `__) - -20.0 - 2020-01-06 -~~~~~~~~~~~~~~~~~ - -* Add type hints (`#191 `__) - -* Add proper trove classifiers for PyPy support (`#198 `__) - -* Scale back depending on ``ctypes`` for manylinux support detection (`#171 `__) - -* Use ``sys.implementation.name`` where appropriate for ``packaging.tags`` (`#193 `__) - -* Expand upon the API provided by ``packaging.tags``: ``interpreter_name()``, ``mac_platforms()``, ``compatible_tags()``, ``cpython_tags()``, ``generic_tags()`` (`#187 `__) - -* Officially support Python 3.8 (`#232 `__) - -* Add ``major``, ``minor``, and ``micro`` aliases to ``packaging.version.Version`` (`#226 `__) - -* Properly mark ``packaging`` has being fully typed by adding a `py.typed` file (`#226 `__) - -19.2 - 2019-09-18 -~~~~~~~~~~~~~~~~~ - -* Remove dependency on ``attrs`` (`#178 `__, `#179 `__) - -* Use appropriate fallbacks for CPython ABI tag (`#181 `__, `#185 `__) - -* Add manylinux2014 support (`#186 `__) - -* Improve ABI detection (`#181 `__) - -* Properly handle debug wheels for Python 3.8 (`#172 `__) - -* Improve detection of debug builds on Windows (`#194 `__) - -19.1 - 2019-07-30 -~~~~~~~~~~~~~~~~~ - -* Add the ``packaging.tags`` module. (`#156 `__) - -* Correctly handle two-digit versions in ``python_version`` (`#119 `__) - - -19.0 - 2019-01-20 -~~~~~~~~~~~~~~~~~ - -* Fix string representation of PEP 508 direct URL requirements with markers. - -* Better handling of file URLs - - This allows for using ``file:///absolute/path``, which was previously - prevented due to the missing ``netloc``. - - This allows for all file URLs that ``urlunparse`` turns back into the - original URL to be valid. - - -18.0 - 2018-09-26 -~~~~~~~~~~~~~~~~~ - -* Improve error messages when invalid requirements are given. (`#129 `__) - - -17.1 - 2017-02-28 -~~~~~~~~~~~~~~~~~ - -* Fix ``utils.canonicalize_version`` when supplying non PEP 440 versions. - - -17.0 - 2017-02-28 -~~~~~~~~~~~~~~~~~ - -* Drop support for python 2.6, 3.2, and 3.3. - -* Define minimal pyparsing version to 2.0.2 (`#91 `__). - -* Add ``epoch``, ``release``, ``pre``, ``dev``, and ``post`` attributes to - ``Version`` and ``LegacyVersion`` (`#34 `__). - -* Add ``Version().is_devrelease`` and ``LegacyVersion().is_devrelease`` to - make it easy to determine if a release is a development release. - -* Add ``utils.canonicalize_version`` to canonicalize version strings or - ``Version`` instances (`#121 `__). - - -16.8 - 2016-10-29 -~~~~~~~~~~~~~~~~~ - -* Fix markers that utilize ``in`` so that they render correctly. - -* Fix an erroneous test on Python RC releases. - - -16.7 - 2016-04-23 -~~~~~~~~~~~~~~~~~ - -* Add support for the deprecated ``python_implementation`` marker which was - an undocumented setuptools marker in addition to the newer markers. - - -16.6 - 2016-03-29 -~~~~~~~~~~~~~~~~~ - -* Add support for the deprecated, PEP 345 environment markers in addition to - the newer markers. - - -16.5 - 2016-02-26 -~~~~~~~~~~~~~~~~~ - -* Fix a regression in parsing requirements with whitespaces between the comma - separators. - - -16.4 - 2016-02-22 -~~~~~~~~~~~~~~~~~ - -* Fix a regression in parsing requirements like ``foo (==4)``. - - -16.3 - 2016-02-21 -~~~~~~~~~~~~~~~~~ - -* Fix a bug where ``packaging.requirements:Requirement`` was overly strict when - matching legacy requirements. - - -16.2 - 2016-02-09 -~~~~~~~~~~~~~~~~~ - -* Add a function that implements the name canonicalization from PEP 503. - - -16.1 - 2016-02-07 -~~~~~~~~~~~~~~~~~ - -* Implement requirement specifiers from PEP 508. - - -16.0 - 2016-01-19 -~~~~~~~~~~~~~~~~~ - -* Relicense so that packaging is available under *either* the Apache License, - Version 2.0 or a 2 Clause BSD license. - -* Support installation of packaging when only distutils is available. - -* Fix ``==`` comparison when there is a prefix and a local version in play. - (`#41 `__). - -* Implement environment markers from PEP 508. - - -15.3 - 2015-08-01 -~~~~~~~~~~~~~~~~~ - -* Normalize post-release spellings for rev/r prefixes. `#35 `__ - - -15.2 - 2015-05-13 -~~~~~~~~~~~~~~~~~ - -* Fix an error where the arbitrary specifier (``===``) was not correctly - allowing pre-releases when it was being used. - -* Expose the specifier and version parts through properties on the - ``Specifier`` classes. - -* Allow iterating over the ``SpecifierSet`` to get access to all of the - ``Specifier`` instances. - -* Allow testing if a version is contained within a specifier via the ``in`` - operator. - - -15.1 - 2015-04-13 -~~~~~~~~~~~~~~~~~ - -* Fix a logic error that was causing inconsistent answers about whether or not - a pre-release was contained within a ``SpecifierSet`` or not. - - -15.0 - 2015-01-02 -~~~~~~~~~~~~~~~~~ - -* Add ``Version().is_postrelease`` and ``LegacyVersion().is_postrelease`` to - make it easy to determine if a release is a post release. - -* Add ``Version().base_version`` and ``LegacyVersion().base_version`` to make - it easy to get the public version without any pre or post release markers. - -* Support the update to PEP 440 which removed the implied ``!=V.*`` when using - either ``>V`` or ``V`` or ````) operator. - - -14.3 - 2014-11-19 -~~~~~~~~~~~~~~~~~ - -* **BACKWARDS INCOMPATIBLE** Refactor specifier support so that it can sanely - handle legacy specifiers as well as PEP 440 specifiers. - -* **BACKWARDS INCOMPATIBLE** Move the specifier support out of - ``packaging.version`` into ``packaging.specifiers``. - - -14.2 - 2014-09-10 -~~~~~~~~~~~~~~~~~ - -* Add prerelease support to ``Specifier``. -* Remove the ability to do ``item in Specifier()`` and replace it with - ``Specifier().contains(item)`` in order to allow flags that signal if a - prerelease should be accepted or not. -* Add a method ``Specifier().filter()`` which will take an iterable and returns - an iterable with items that do not match the specifier filtered out. - - -14.1 - 2014-09-08 -~~~~~~~~~~~~~~~~~ - -* Allow ``LegacyVersion`` and ``Version`` to be sorted together. -* Add ``packaging.version.parse()`` to enable easily parsing a version string - as either a ``Version`` or a ``LegacyVersion`` depending on it's PEP 440 - validity. - - -14.0 - 2014-09-05 -~~~~~~~~~~~~~~~~~ - -* Initial release. - - -.. _`master`: https://github.com/pypa/packaging/ - - diff --git a/setuptools/_vendor/packaging-21.2.dist-info/RECORD b/setuptools/_vendor/packaging-21.2.dist-info/RECORD deleted file mode 100644 index ed2291ac..00000000 --- a/setuptools/_vendor/packaging-21.2.dist-info/RECORD +++ /dev/null @@ -1,32 +0,0 @@ -packaging-21.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -packaging-21.2.dist-info/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197 -packaging-21.2.dist-info/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174 -packaging-21.2.dist-info/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344 -packaging-21.2.dist-info/METADATA,sha256=N4A8uSYrQwV9byem7YuI9OtVkbqiNzFlDhcDVT-suAo,14754 -packaging-21.2.dist-info/RECORD,, -packaging-21.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -packaging-21.2.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92 -packaging-21.2.dist-info/top_level.txt,sha256=zFdHrhWnPslzsiP455HutQsqPB6v0KCtNUMtUtrefDw,10 -packaging/__about__.py,sha256=IIRHpOsJlJSgkjq1UoeBoMTqhvNp3gN9FyMb5Kf8El4,661 -packaging/__init__.py,sha256=b9Kk5MF7KxhhLgcDmiUWukN-LatWFxPdNug0joPhHSk,497 -packaging/__pycache__/__about__.cpython-310.pyc,, -packaging/__pycache__/__init__.cpython-310.pyc,, -packaging/__pycache__/_manylinux.cpython-310.pyc,, -packaging/__pycache__/_musllinux.cpython-310.pyc,, -packaging/__pycache__/_structures.cpython-310.pyc,, -packaging/__pycache__/markers.cpython-310.pyc,, -packaging/__pycache__/requirements.cpython-310.pyc,, -packaging/__pycache__/specifiers.cpython-310.pyc,, -packaging/__pycache__/tags.cpython-310.pyc,, -packaging/__pycache__/utils.cpython-310.pyc,, -packaging/__pycache__/version.cpython-310.pyc,, -packaging/_manylinux.py,sha256=XcbiXB-qcjv3bcohp6N98TMpOP4_j3m-iOA8ptK2GWY,11488 -packaging/_musllinux.py,sha256=z5yeG1ygOPx4uUyLdqj-p8Dk5UBb5H_b0NIjW9yo8oA,4378 -packaging/_structures.py,sha256=TMiAgFbdUOPmIfDIfiHc3KFhSJ8kMjof2QS5I-2NyQ8,1629 -packaging/markers.py,sha256=Fygi3_eZnjQ-3VJizW5AhI5wvo0Hb6RMk4DidsKpOC0,8475 -packaging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -packaging/requirements.py,sha256=rjaGRCMepZS1mlYMjJ5Qh6rfq3gtsCRQUQmftGZ_bu8,4664 -packaging/specifiers.py,sha256=MZ-fYcNL3u7pNrt-6g2EQO7AbRXkjc-SPEYwXMQbLmc,30964 -packaging/tags.py,sha256=vGybAUQYlPKMcukzX_2e65fmafnFFuMbD25naYTEwtc,15710 -packaging/utils.py,sha256=dJjeat3BS-TYn1RrUFVwufUMasbtzLfYRoy_HXENeFQ,4200 -packaging/version.py,sha256=_fLRNrFrxYcHVfyo8vk9j8s6JM8N_xsSxVFr6RJyco8,14665 diff --git a/setuptools/_vendor/packaging-21.2.dist-info/REQUESTED b/setuptools/_vendor/packaging-21.2.dist-info/REQUESTED deleted file mode 100644 index e69de29b..00000000 diff --git a/setuptools/_vendor/packaging-21.2.dist-info/WHEEL b/setuptools/_vendor/packaging-21.2.dist-info/WHEEL deleted file mode 100644 index 5bad85fd..00000000 --- a/setuptools/_vendor/packaging-21.2.dist-info/WHEEL +++ /dev/null @@ -1,5 +0,0 @@ -Wheel-Version: 1.0 -Generator: bdist_wheel (0.37.0) -Root-Is-Purelib: true -Tag: py3-none-any - diff --git a/setuptools/_vendor/packaging-21.2.dist-info/top_level.txt b/setuptools/_vendor/packaging-21.2.dist-info/top_level.txt deleted file mode 100644 index 748809f7..00000000 --- a/setuptools/_vendor/packaging-21.2.dist-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -packaging diff --git a/setuptools/_vendor/packaging-21.3.dist-info/INSTALLER b/setuptools/_vendor/packaging-21.3.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/setuptools/_vendor/packaging-21.3.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/setuptools/_vendor/packaging-21.3.dist-info/LICENSE b/setuptools/_vendor/packaging-21.3.dist-info/LICENSE new file mode 100644 index 00000000..6f62d44e --- /dev/null +++ b/setuptools/_vendor/packaging-21.3.dist-info/LICENSE @@ -0,0 +1,3 @@ +This software is made available under the terms of *either* of the licenses +found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made +under the terms of *both* these licenses. diff --git a/setuptools/_vendor/packaging-21.3.dist-info/LICENSE.APACHE b/setuptools/_vendor/packaging-21.3.dist-info/LICENSE.APACHE new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/setuptools/_vendor/packaging-21.3.dist-info/LICENSE.APACHE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/setuptools/_vendor/packaging-21.3.dist-info/LICENSE.BSD b/setuptools/_vendor/packaging-21.3.dist-info/LICENSE.BSD new file mode 100644 index 00000000..42ce7b75 --- /dev/null +++ b/setuptools/_vendor/packaging-21.3.dist-info/LICENSE.BSD @@ -0,0 +1,23 @@ +Copyright (c) Donald Stufft and individual contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/setuptools/_vendor/packaging-21.3.dist-info/METADATA b/setuptools/_vendor/packaging-21.3.dist-info/METADATA new file mode 100644 index 00000000..358ace53 --- /dev/null +++ b/setuptools/_vendor/packaging-21.3.dist-info/METADATA @@ -0,0 +1,453 @@ +Metadata-Version: 2.1 +Name: packaging +Version: 21.3 +Summary: Core utilities for Python packages +Home-page: https://github.com/pypa/packaging +Author: Donald Stufft and individual contributors +Author-email: donald@stufft.io +License: BSD-2-Clause or Apache-2.0 +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Apache Software License +Classifier: License :: OSI Approved :: BSD License +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Requires-Python: >=3.6 +Description-Content-Type: text/x-rst +License-File: LICENSE +License-File: LICENSE.APACHE +License-File: LICENSE.BSD +Requires-Dist: pyparsing (!=3.0.5,>=2.0.2) + +packaging +========= + +.. start-intro + +Reusable core utilities for various Python Packaging +`interoperability specifications `_. + +This library provides utilities that implement the interoperability +specifications which have clearly one correct behaviour (eg: :pep:`440`) +or benefit greatly from having a single shared implementation (eg: :pep:`425`). + +.. end-intro + +The ``packaging`` project includes the following: version handling, specifiers, +markers, requirements, tags, utilities. + +Documentation +------------- + +The `documentation`_ provides information and the API for the following: + +- Version Handling +- Specifiers +- Markers +- Requirements +- Tags +- Utilities + +Installation +------------ + +Use ``pip`` to install these utilities:: + + pip install packaging + +Discussion +---------- + +If you run into bugs, you can file them in our `issue tracker`_. + +You can also join ``#pypa`` on Freenode to ask questions or get involved. + + +.. _`documentation`: https://packaging.pypa.io/ +.. _`issue tracker`: https://github.com/pypa/packaging/issues + + +Code of Conduct +--------------- + +Everyone interacting in the packaging project's codebases, issue trackers, chat +rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_. + +.. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md + +Contributing +------------ + +The ``CONTRIBUTING.rst`` file outlines how to contribute to this project as +well as how to report a potential security issue. The documentation for this +project also covers information about `project development`_ and `security`_. + +.. _`project development`: https://packaging.pypa.io/en/latest/development/ +.. _`security`: https://packaging.pypa.io/en/latest/security/ + +Project History +--------------- + +Please review the ``CHANGELOG.rst`` file or the `Changelog documentation`_ for +recent changes and project history. + +.. _`Changelog documentation`: https://packaging.pypa.io/en/latest/changelog/ + +Changelog +--------- + +21.3 - 2021-11-17 +~~~~~~~~~~~~~~~~~ + +* Add a ``pp3-none-any`` tag (`#311 `__) +* Replace the blank pyparsing 3 exclusion with a 3.0.5 exclusion (`#481 `__, `#486 `__) +* Fix a spelling mistake (`#479 `__) + +21.2 - 2021-10-29 +~~~~~~~~~~~~~~~~~ + +* Update documentation entry for 21.1. + +21.1 - 2021-10-29 +~~~~~~~~~~~~~~~~~ + +* Update pin to pyparsing to exclude 3.0.0. + +21.0 - 2021-07-03 +~~~~~~~~~~~~~~~~~ + +* PEP 656: musllinux support (`#411 `__) +* Drop support for Python 2.7, Python 3.4 and Python 3.5. +* Replace distutils usage with sysconfig (`#396 `__) +* Add support for zip files in ``parse_sdist_filename`` (`#429 `__) +* Use cached ``_hash`` attribute to short-circuit tag equality comparisons (`#417 `__) +* Specify the default value for the ``specifier`` argument to ``SpecifierSet`` (`#437 `__) +* Proper keyword-only "warn" argument in packaging.tags (`#403 `__) +* Correctly remove prerelease suffixes from ~= check (`#366 `__) +* Fix type hints for ``Version.post`` and ``Version.dev`` (`#393 `__) +* Use typing alias ``UnparsedVersion`` (`#398 `__) +* Improve type inference for ``packaging.specifiers.filter()`` (`#430 `__) +* Tighten the return type of ``canonicalize_version()`` (`#402 `__) + +20.9 - 2021-01-29 +~~~~~~~~~~~~~~~~~ + +* Run `isort `_ over the code base (`#377 `__) +* Add support for the ``macosx_10_*_universal2`` platform tags (`#379 `__) +* Introduce ``packaging.utils.parse_wheel_filename()`` and ``parse_sdist_filename()`` + (`#387 `__ and `#389 `__) + +20.8 - 2020-12-11 +~~~~~~~~~~~~~~~~~ + +* Revert back to setuptools for compatibility purposes for some Linux distros (`#363 `__) +* Do not insert an underscore in wheel tags when the interpreter version number + is more than 2 digits (`#372 `__) + +20.7 - 2020-11-28 +~~~~~~~~~~~~~~~~~ + +No unreleased changes. + +20.6 - 2020-11-28 +~~~~~~~~~~~~~~~~~ + +.. note:: This release was subsequently yanked, and these changes were included in 20.7. + +* Fix flit configuration, to include LICENSE files (`#357 `__) +* Make `intel` a recognized CPU architecture for the `universal` macOS platform tag (`#361 `__) +* Add some missing type hints to `packaging.requirements` (issue:`350`) + +20.5 - 2020-11-27 +~~~~~~~~~~~~~~~~~ + +* Officially support Python 3.9 (`#343 `__) +* Deprecate the ``LegacyVersion`` and ``LegacySpecifier`` classes (`#321 `__) +* Handle ``OSError`` on non-dynamic executables when attempting to resolve + the glibc version string. + +20.4 - 2020-05-19 +~~~~~~~~~~~~~~~~~ + +* Canonicalize version before comparing specifiers. (`#282 `__) +* Change type hint for ``canonicalize_name`` to return + ``packaging.utils.NormalizedName``. + This enables the use of static typing tools (like mypy) to detect mixing of + normalized and un-normalized names. + +20.3 - 2020-03-05 +~~~~~~~~~~~~~~~~~ + +* Fix changelog for 20.2. + +20.2 - 2020-03-05 +~~~~~~~~~~~~~~~~~ + +* Fix a bug that caused a 32-bit OS that runs on a 64-bit ARM CPU (e.g. ARM-v8, + aarch64), to report the wrong bitness. + +20.1 - 2020-01-24 +~~~~~~~~~~~~~~~~~~~ + +* Fix a bug caused by reuse of an exhausted iterator. (`#257 `__) + +20.0 - 2020-01-06 +~~~~~~~~~~~~~~~~~ + +* Add type hints (`#191 `__) + +* Add proper trove classifiers for PyPy support (`#198 `__) + +* Scale back depending on ``ctypes`` for manylinux support detection (`#171 `__) + +* Use ``sys.implementation.name`` where appropriate for ``packaging.tags`` (`#193 `__) + +* Expand upon the API provided by ``packaging.tags``: ``interpreter_name()``, ``mac_platforms()``, ``compatible_tags()``, ``cpython_tags()``, ``generic_tags()`` (`#187 `__) + +* Officially support Python 3.8 (`#232 `__) + +* Add ``major``, ``minor``, and ``micro`` aliases to ``packaging.version.Version`` (`#226 `__) + +* Properly mark ``packaging`` has being fully typed by adding a `py.typed` file (`#226 `__) + +19.2 - 2019-09-18 +~~~~~~~~~~~~~~~~~ + +* Remove dependency on ``attrs`` (`#178 `__, `#179 `__) + +* Use appropriate fallbacks for CPython ABI tag (`#181 `__, `#185 `__) + +* Add manylinux2014 support (`#186 `__) + +* Improve ABI detection (`#181 `__) + +* Properly handle debug wheels for Python 3.8 (`#172 `__) + +* Improve detection of debug builds on Windows (`#194 `__) + +19.1 - 2019-07-30 +~~~~~~~~~~~~~~~~~ + +* Add the ``packaging.tags`` module. (`#156 `__) + +* Correctly handle two-digit versions in ``python_version`` (`#119 `__) + + +19.0 - 2019-01-20 +~~~~~~~~~~~~~~~~~ + +* Fix string representation of PEP 508 direct URL requirements with markers. + +* Better handling of file URLs + + This allows for using ``file:///absolute/path``, which was previously + prevented due to the missing ``netloc``. + + This allows for all file URLs that ``urlunparse`` turns back into the + original URL to be valid. + + +18.0 - 2018-09-26 +~~~~~~~~~~~~~~~~~ + +* Improve error messages when invalid requirements are given. (`#129 `__) + + +17.1 - 2017-02-28 +~~~~~~~~~~~~~~~~~ + +* Fix ``utils.canonicalize_version`` when supplying non PEP 440 versions. + + +17.0 - 2017-02-28 +~~~~~~~~~~~~~~~~~ + +* Drop support for python 2.6, 3.2, and 3.3. + +* Define minimal pyparsing version to 2.0.2 (`#91 `__). + +* Add ``epoch``, ``release``, ``pre``, ``dev``, and ``post`` attributes to + ``Version`` and ``LegacyVersion`` (`#34 `__). + +* Add ``Version().is_devrelease`` and ``LegacyVersion().is_devrelease`` to + make it easy to determine if a release is a development release. + +* Add ``utils.canonicalize_version`` to canonicalize version strings or + ``Version`` instances (`#121 `__). + + +16.8 - 2016-10-29 +~~~~~~~~~~~~~~~~~ + +* Fix markers that utilize ``in`` so that they render correctly. + +* Fix an erroneous test on Python RC releases. + + +16.7 - 2016-04-23 +~~~~~~~~~~~~~~~~~ + +* Add support for the deprecated ``python_implementation`` marker which was + an undocumented setuptools marker in addition to the newer markers. + + +16.6 - 2016-03-29 +~~~~~~~~~~~~~~~~~ + +* Add support for the deprecated, PEP 345 environment markers in addition to + the newer markers. + + +16.5 - 2016-02-26 +~~~~~~~~~~~~~~~~~ + +* Fix a regression in parsing requirements with whitespaces between the comma + separators. + + +16.4 - 2016-02-22 +~~~~~~~~~~~~~~~~~ + +* Fix a regression in parsing requirements like ``foo (==4)``. + + +16.3 - 2016-02-21 +~~~~~~~~~~~~~~~~~ + +* Fix a bug where ``packaging.requirements:Requirement`` was overly strict when + matching legacy requirements. + + +16.2 - 2016-02-09 +~~~~~~~~~~~~~~~~~ + +* Add a function that implements the name canonicalization from PEP 503. + + +16.1 - 2016-02-07 +~~~~~~~~~~~~~~~~~ + +* Implement requirement specifiers from PEP 508. + + +16.0 - 2016-01-19 +~~~~~~~~~~~~~~~~~ + +* Relicense so that packaging is available under *either* the Apache License, + Version 2.0 or a 2 Clause BSD license. + +* Support installation of packaging when only distutils is available. + +* Fix ``==`` comparison when there is a prefix and a local version in play. + (`#41 `__). + +* Implement environment markers from PEP 508. + + +15.3 - 2015-08-01 +~~~~~~~~~~~~~~~~~ + +* Normalize post-release spellings for rev/r prefixes. `#35 `__ + + +15.2 - 2015-05-13 +~~~~~~~~~~~~~~~~~ + +* Fix an error where the arbitrary specifier (``===``) was not correctly + allowing pre-releases when it was being used. + +* Expose the specifier and version parts through properties on the + ``Specifier`` classes. + +* Allow iterating over the ``SpecifierSet`` to get access to all of the + ``Specifier`` instances. + +* Allow testing if a version is contained within a specifier via the ``in`` + operator. + + +15.1 - 2015-04-13 +~~~~~~~~~~~~~~~~~ + +* Fix a logic error that was causing inconsistent answers about whether or not + a pre-release was contained within a ``SpecifierSet`` or not. + + +15.0 - 2015-01-02 +~~~~~~~~~~~~~~~~~ + +* Add ``Version().is_postrelease`` and ``LegacyVersion().is_postrelease`` to + make it easy to determine if a release is a post release. + +* Add ``Version().base_version`` and ``LegacyVersion().base_version`` to make + it easy to get the public version without any pre or post release markers. + +* Support the update to PEP 440 which removed the implied ``!=V.*`` when using + either ``>V`` or ``V`` or ````) operator. + + +14.3 - 2014-11-19 +~~~~~~~~~~~~~~~~~ + +* **BACKWARDS INCOMPATIBLE** Refactor specifier support so that it can sanely + handle legacy specifiers as well as PEP 440 specifiers. + +* **BACKWARDS INCOMPATIBLE** Move the specifier support out of + ``packaging.version`` into ``packaging.specifiers``. + + +14.2 - 2014-09-10 +~~~~~~~~~~~~~~~~~ + +* Add prerelease support to ``Specifier``. +* Remove the ability to do ``item in Specifier()`` and replace it with + ``Specifier().contains(item)`` in order to allow flags that signal if a + prerelease should be accepted or not. +* Add a method ``Specifier().filter()`` which will take an iterable and returns + an iterable with items that do not match the specifier filtered out. + + +14.1 - 2014-09-08 +~~~~~~~~~~~~~~~~~ + +* Allow ``LegacyVersion`` and ``Version`` to be sorted together. +* Add ``packaging.version.parse()`` to enable easily parsing a version string + as either a ``Version`` or a ``LegacyVersion`` depending on it's PEP 440 + validity. + + +14.0 - 2014-09-05 +~~~~~~~~~~~~~~~~~ + +* Initial release. + + +.. _`master`: https://github.com/pypa/packaging/ + + diff --git a/setuptools/_vendor/packaging-21.3.dist-info/RECORD b/setuptools/_vendor/packaging-21.3.dist-info/RECORD new file mode 100644 index 00000000..97cace10 --- /dev/null +++ b/setuptools/_vendor/packaging-21.3.dist-info/RECORD @@ -0,0 +1,32 @@ +packaging-21.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +packaging-21.3.dist-info/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197 +packaging-21.3.dist-info/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174 +packaging-21.3.dist-info/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344 +packaging-21.3.dist-info/METADATA,sha256=KuKIy6qDLP3svIt6ejCbxBDhvq11ebkgUN55MeyKFyc,15147 +packaging-21.3.dist-info/RECORD,, +packaging-21.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +packaging-21.3.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92 +packaging-21.3.dist-info/top_level.txt,sha256=zFdHrhWnPslzsiP455HutQsqPB6v0KCtNUMtUtrefDw,10 +packaging/__about__.py,sha256=ugASIO2w1oUyH8_COqQ2X_s0rDhjbhQC3yJocD03h2c,661 +packaging/__init__.py,sha256=b9Kk5MF7KxhhLgcDmiUWukN-LatWFxPdNug0joPhHSk,497 +packaging/__pycache__/__about__.cpython-310.pyc,, +packaging/__pycache__/__init__.cpython-310.pyc,, +packaging/__pycache__/_manylinux.cpython-310.pyc,, +packaging/__pycache__/_musllinux.cpython-310.pyc,, +packaging/__pycache__/_structures.cpython-310.pyc,, +packaging/__pycache__/markers.cpython-310.pyc,, +packaging/__pycache__/requirements.cpython-310.pyc,, +packaging/__pycache__/specifiers.cpython-310.pyc,, +packaging/__pycache__/tags.cpython-310.pyc,, +packaging/__pycache__/utils.cpython-310.pyc,, +packaging/__pycache__/version.cpython-310.pyc,, +packaging/_manylinux.py,sha256=XcbiXB-qcjv3bcohp6N98TMpOP4_j3m-iOA8ptK2GWY,11488 +packaging/_musllinux.py,sha256=_KGgY_qc7vhMGpoqss25n2hiLCNKRtvz9mCrS7gkqyc,4378 +packaging/_structures.py,sha256=q3eVNmbWJGG_S0Dit_S3Ao8qQqz_5PYTXFAKBZe5yr4,1431 +packaging/markers.py,sha256=Fygi3_eZnjQ-3VJizW5AhI5wvo0Hb6RMk4DidsKpOC0,8475 +packaging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +packaging/requirements.py,sha256=rjaGRCMepZS1mlYMjJ5Qh6rfq3gtsCRQUQmftGZ_bu8,4664 +packaging/specifiers.py,sha256=LRQ0kFsHrl5qfcFNEEJrIFYsnIHQUJXY9fIsakTrrqE,30110 +packaging/tags.py,sha256=lmsnGNiJ8C4D_Pf9PbM0qgbZvD9kmB9lpZBQUZa3R_Y,15699 +packaging/utils.py,sha256=dJjeat3BS-TYn1RrUFVwufUMasbtzLfYRoy_HXENeFQ,4200 +packaging/version.py,sha256=_fLRNrFrxYcHVfyo8vk9j8s6JM8N_xsSxVFr6RJyco8,14665 diff --git a/setuptools/_vendor/packaging-21.3.dist-info/REQUESTED b/setuptools/_vendor/packaging-21.3.dist-info/REQUESTED new file mode 100644 index 00000000..e69de29b diff --git a/setuptools/_vendor/packaging-21.3.dist-info/WHEEL b/setuptools/_vendor/packaging-21.3.dist-info/WHEEL new file mode 100644 index 00000000..5bad85fd --- /dev/null +++ b/setuptools/_vendor/packaging-21.3.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/setuptools/_vendor/packaging-21.3.dist-info/top_level.txt b/setuptools/_vendor/packaging-21.3.dist-info/top_level.txt new file mode 100644 index 00000000..748809f7 --- /dev/null +++ b/setuptools/_vendor/packaging-21.3.dist-info/top_level.txt @@ -0,0 +1 @@ +packaging diff --git a/setuptools/_vendor/packaging/__about__.py b/setuptools/_vendor/packaging/__about__.py index c359122f..3551bc2d 100644 --- a/setuptools/_vendor/packaging/__about__.py +++ b/setuptools/_vendor/packaging/__about__.py @@ -17,7 +17,7 @@ __title__ = "packaging" __summary__ = "Core utilities for Python packages" __uri__ = "https://github.com/pypa/packaging" -__version__ = "21.2" +__version__ = "21.3" __author__ = "Donald Stufft and individual contributors" __email__ = "donald@stufft.io" diff --git a/setuptools/_vendor/packaging/_musllinux.py b/setuptools/_vendor/packaging/_musllinux.py index 85450faf..8ac3059b 100644 --- a/setuptools/_vendor/packaging/_musllinux.py +++ b/setuptools/_vendor/packaging/_musllinux.py @@ -98,7 +98,7 @@ def _get_musl_version(executable: str) -> Optional[_MuslVersion]: with contextlib.ExitStack() as stack: try: f = stack.enter_context(open(executable, "rb")) - except IOError: + except OSError: return None ld = _parse_ld_musl_from_elf(f) if not ld: diff --git a/setuptools/_vendor/packaging/_structures.py b/setuptools/_vendor/packaging/_structures.py index 95154975..90a6465f 100644 --- a/setuptools/_vendor/packaging/_structures.py +++ b/setuptools/_vendor/packaging/_structures.py @@ -19,9 +19,6 @@ class InfinityType: def __eq__(self, other: object) -> bool: return isinstance(other, self.__class__) - def __ne__(self, other: object) -> bool: - return not isinstance(other, self.__class__) - def __gt__(self, other: object) -> bool: return True @@ -51,9 +48,6 @@ class NegativeInfinityType: def __eq__(self, other: object) -> bool: return isinstance(other, self.__class__) - def __ne__(self, other: object) -> bool: - return not isinstance(other, self.__class__) - def __gt__(self, other: object) -> bool: return False diff --git a/setuptools/_vendor/packaging/specifiers.py b/setuptools/_vendor/packaging/specifiers.py index ce66bd4a..0e218a6f 100644 --- a/setuptools/_vendor/packaging/specifiers.py +++ b/setuptools/_vendor/packaging/specifiers.py @@ -57,13 +57,6 @@ class BaseSpecifier(metaclass=abc.ABCMeta): objects are equal. """ - @abc.abstractmethod - def __ne__(self, other: object) -> bool: - """ - Returns a boolean representing whether or not the two Specifier like - objects are not equal. - """ - @abc.abstractproperty def prereleases(self) -> Optional[bool]: """ @@ -119,7 +112,7 @@ class _IndividualSpecifier(BaseSpecifier): else "" ) - return "<{}({!r}{})>".format(self.__class__.__name__, str(self), pre) + return f"<{self.__class__.__name__}({str(self)!r}{pre})>" def __str__(self) -> str: return "{}{}".format(*self._spec) @@ -142,17 +135,6 @@ class _IndividualSpecifier(BaseSpecifier): return self._canonical_spec == other._canonical_spec - def __ne__(self, other: object) -> bool: - if isinstance(other, str): - try: - other = self.__class__(str(other)) - except InvalidSpecifier: - return NotImplemented - elif not isinstance(other, self.__class__): - return NotImplemented - - return self._spec != other._spec - def _get_operator(self, op: str) -> CallableOperator: operator_callable: CallableOperator = getattr( self, f"_compare_{self._operators[op]}" @@ -667,7 +649,7 @@ class SpecifierSet(BaseSpecifier): else "" ) - return "".format(str(self), pre) + return f"" def __str__(self) -> str: return ",".join(sorted(str(s) for s in self._specs)) @@ -706,14 +688,6 @@ class SpecifierSet(BaseSpecifier): return self._specs == other._specs - def __ne__(self, other: object) -> bool: - if isinstance(other, (str, _IndividualSpecifier)): - other = SpecifierSet(str(other)) - elif not isinstance(other, SpecifierSet): - return NotImplemented - - return self._specs != other._specs - def __len__(self) -> int: return len(self._specs) diff --git a/setuptools/_vendor/packaging/tags.py b/setuptools/_vendor/packaging/tags.py index e65890a9..9a3d25a7 100644 --- a/setuptools/_vendor/packaging/tags.py +++ b/setuptools/_vendor/packaging/tags.py @@ -90,7 +90,7 @@ class Tag: return f"{self._interpreter}-{self._abi}-{self._platform}" def __repr__(self) -> str: - return "<{self} @ {self_id}>".format(self=self, self_id=id(self)) + return f"<{self} @ {id(self)}>" def parse_tag(tag: str) -> FrozenSet[Tag]: @@ -192,7 +192,7 @@ def cpython_tags( if not python_version: python_version = sys.version_info[:2] - interpreter = "cp{}".format(_version_nodot(python_version[:2])) + interpreter = f"cp{_version_nodot(python_version[:2])}" if abis is None: if len(python_version) > 1: @@ -268,11 +268,11 @@ def _py_interpreter_range(py_version: PythonVersion) -> Iterator[str]: all previous versions of that major version. """ if len(py_version) > 1: - yield "py{version}".format(version=_version_nodot(py_version[:2])) - yield "py{major}".format(major=py_version[0]) + yield f"py{_version_nodot(py_version[:2])}" + yield f"py{py_version[0]}" if len(py_version) > 1: for minor in range(py_version[1] - 1, -1, -1): - yield "py{version}".format(version=_version_nodot((py_version[0], minor))) + yield f"py{_version_nodot((py_version[0], minor))}" def compatible_tags( @@ -481,4 +481,7 @@ def sys_tags(*, warn: bool = False) -> Iterator[Tag]: else: yield from generic_tags() - yield from compatible_tags() + if interp_name == "pp": + yield from compatible_tags(interpreter="pp3") + else: + yield from compatible_tags() diff --git a/setuptools/_vendor/vendored.txt b/setuptools/_vendor/vendored.txt index 0639990b..d9aa2487 100644 --- a/setuptools/_vendor/vendored.txt +++ b/setuptools/_vendor/vendored.txt @@ -1,4 +1,4 @@ -packaging==21.2 +packaging==21.3 pyparsing==2.2.1 ordered-set==3.1.1 more_itertools==8.8.0 -- cgit v1.2.1 From 1c966147212a3a00988a12c6098694155c984e95 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 12 Feb 2022 05:24:36 -0500 Subject: Use always_iterable to fix --global-option in one expression. --- setuptools/build_meta.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index 0b95ab2d..ba4a068a 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -38,6 +38,8 @@ import warnings import setuptools import distutils from ._reqs import parse_strings +from .extern.more_itertools import always_iterable + __all__ = ['get_requires_for_build_sdist', 'get_requires_for_build_wheel', @@ -129,9 +131,8 @@ class _BuildMetaBackend(object): def _fix_config(self, config_settings): config_settings = config_settings or {} - config_settings.setdefault('--global-option', []) - if isinstance(config_settings["--global-option"], str): - config_settings["--global-option"] = [config_settings["--global-option"]] + config_settings['--global-option'] = list(always_iterable( + config_settings.get('--global-option'))) return config_settings def _get_build_requires(self, config_settings, requirements): -- cgit v1.2.1 From 8a1fe874396f6bbb62c501ae4603c0fa2c9e0379 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 12 Feb 2022 05:28:39 -0500 Subject: Remove duplicate invocations of _BuildMetaBackend._fix_config. --- setuptools/build_meta.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index 1daa77c9..4204a565 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -157,12 +157,10 @@ class _BuildMetaBackend(object): exec(compile(code, __file__, 'exec'), locals()) def get_requires_for_build_wheel(self, config_settings=None): - config_settings = self._fix_config(config_settings) return self._get_build_requires( config_settings, requirements=['wheel']) def get_requires_for_build_sdist(self, config_settings=None): - config_settings = self._fix_config(config_settings) return self._get_build_requires(config_settings, requirements=[]) def prepare_metadata_for_build_wheel(self, metadata_directory, -- cgit v1.2.1 From b653408e78891c6ce827ea237d3cd9f6e6a9a2f3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 12 Feb 2022 05:29:44 -0500 Subject: Remove new style class declaration, now the default. --- setuptools/build_meta.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index 4204a565..2a5b529a 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -125,7 +125,7 @@ def suppress_known_deprecation(): yield -class _BuildMetaBackend(object): +class _BuildMetaBackend: def _fix_config(self, config_settings): config_settings = config_settings or {} -- cgit v1.2.1 From 4b9db29bbed982ace75fbc802f71330d50320820 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 12 Feb 2022 05:34:52 -0500 Subject: Make _BuildMetaBackend._fix_config static and add tests. --- setuptools/build_meta.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index ba4a068a..09b4a873 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -129,7 +129,21 @@ def suppress_known_deprecation(): class _BuildMetaBackend(object): - def _fix_config(self, config_settings): + @staticmethod + def _fix_config(config_settings): + """ + Ensure config settings meet certain expectations. + + >>> fc = _BuildMetaBackend._fix_config + >>> fc(None) + {'--global-option': []} + >>> fc({}) + {'--global-option': []} + >>> fc({'--global-option': 'foo'}) + {'--global-option': ['foo']} + >>> fc({'--global-option': ['foo']}) + {'--global-option': ['foo']} + """ config_settings = config_settings or {} config_settings['--global-option'] = list(always_iterable( config_settings.get('--global-option'))) -- cgit v1.2.1 From 6b0b410572b11e0be9a62ff478616abfddc8633a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 12 Feb 2022 05:36:32 -0500 Subject: Update changelog --- changelog.d/2876.change.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/2876.change.rst diff --git a/changelog.d/2876.change.rst b/changelog.d/2876.change.rst new file mode 100644 index 00000000..e220d213 --- /dev/null +++ b/changelog.d/2876.change.rst @@ -0,0 +1 @@ +In the build backend, allow single config settings to be supplied. -- cgit v1.2.1 From 3d5f9d10418f0b4f1a7440d56cd19c30de6e00ae Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 12 Feb 2022 05:41:12 -0500 Subject: Update changelog. Co-authored-by: Sviatoslav Sydorenko --- changelog.d/3085.change.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/3085.change.rst b/changelog.d/3085.change.rst index 3dd3045c..f900a40e 100644 --- a/changelog.d/3085.change.rst +++ b/changelog.d/3085.change.rst @@ -1 +1 @@ -Setuptools no longer relies on pkg_resources for entry point handling. +Setuptools no longer relies on ``pkg_resources`` for entry point handling. -- cgit v1.2.1 From f4b0e3ac5b20328fb71034e4605761ec90362afe Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 12 Feb 2022 05:52:15 -0500 Subject: Re-use ensure_valid in validate. --- setuptools/_entry_points.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/setuptools/_entry_points.py b/setuptools/_entry_points.py index 816e61b6..0cf2691b 100644 --- a/setuptools/_entry_points.py +++ b/setuptools/_entry_points.py @@ -6,6 +6,7 @@ from .extern.jaraco.text import yield_lines from .extern.jaraco.functools import pass_none from ._importlib import metadata from ._itertools import ensure_unique +from .extern.more_itertools import consume def ensure_valid(ep): @@ -14,7 +15,6 @@ def ensure_valid(ep): the pattern match. """ ep.extras - return ep def load_group(value, group): @@ -34,11 +34,9 @@ def by_group_and_name(ep): def validate(eps: metadata.EntryPoints): """ - Ensure entry points are unique by group and name and validate the pattern. + Ensure entry points are unique by group and name and validate each. """ - for ep in ensure_unique(eps, key=by_group_and_name): - # exercise one of the dynamic properties to trigger validation - ep.extras + consume(map(ensure_valid, ensure_unique(eps, key=by_group_and_name))) return eps -- cgit v1.2.1 From b466350c7afcfae7634711f1da40a558ee52ec2e Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 12 Feb 2022 17:34:47 +0000 Subject: Add timeout to test_build_meta As discussed in #3087, `test_build_meta` seems to be flaky specially for the combination of Windows + PyPy. This is a proposed workaround for such scenarios. --- setuptools/tests/test_build_meta.py | 9 ++++++++- tox.ini | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index 0f4a1a73..ea82f82c 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -11,6 +11,9 @@ from jaraco import path from .textwrap import DALS +TIMEOUT = os.getenv("TIMEOUT_BACKEND_TEST", 3 * 60) + + class BuildBackendBase: def __init__(self, cwd='.', env={}, backend_name='setuptools.build_meta'): self.cwd = cwd @@ -31,7 +34,11 @@ class BuildBackend(BuildBackendBase): def method(*args, **kw): root = os.path.abspath(self.cwd) caller = BuildBackendCaller(root, self.env, self.backend_name) - return self.pool.submit(caller, name, *args, **kw).result() + task = self.pool.submit(caller, name, *args, **kw) + try: + return task.result(TIMEOUT) + except futures.TimeoutError: + pytest.xfail(f"Backend did not respond before timeout ({TIMEOUT} s)") return method diff --git a/tox.ini b/tox.ini index 26aefada..70eb743b 100644 --- a/tox.ini +++ b/tox.ini @@ -14,6 +14,7 @@ usedevelop = True extras = testing passenv = SETUPTOOLS_USE_DISTUTILS + TIMEOUT_BACKEND_TEST # timeout for test_build_meta windir # required for test_pkg_resources # honor git config in pytest-perf HOME -- cgit v1.2.1 From f7834144b49f28011e99c48845dbe18a91420bba Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 12 Feb 2022 13:37:18 -0500 Subject: =?UTF-8?q?Bump=20version:=2060.8.2=20=E2=86=92=2060.9.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 12 ++++++++++++ changelog.d/2876.change.rst | 1 - changelog.d/2993.change.rst | 1 - changelog.d/3085.change.rst | 1 - changelog.d/3098.change.rst | 1 - setup.cfg | 2 +- 7 files changed, 14 insertions(+), 6 deletions(-) delete mode 100644 changelog.d/2876.change.rst delete mode 100644 changelog.d/2993.change.rst delete mode 100644 changelog.d/3085.change.rst delete mode 100644 changelog.d/3098.change.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 7f466d4f..93624e4c 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 60.8.2 +current_version = 60.9.0 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index cd46ace6..f4cddb2b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,15 @@ +v60.9.0 +------- + + +Changes +^^^^^^^ +* #2876: In the build backend, allow single config settings to be supplied. +* #2993: Removed workaround in distutils hack for get-pip now that pypa/get-pip#137 is closed. +* #3085: Setuptools no longer relies on ``pkg_resources`` for entry point handling. +* #3098: Bump vendored packaging to 21.3. + + v60.8.2 ------- diff --git a/changelog.d/2876.change.rst b/changelog.d/2876.change.rst deleted file mode 100644 index e220d213..00000000 --- a/changelog.d/2876.change.rst +++ /dev/null @@ -1 +0,0 @@ -In the build backend, allow single config settings to be supplied. diff --git a/changelog.d/2993.change.rst b/changelog.d/2993.change.rst deleted file mode 100644 index 5cb9d6aa..00000000 --- a/changelog.d/2993.change.rst +++ /dev/null @@ -1 +0,0 @@ -Removed workaround in distutils hack for get-pip now that pypa/get-pip#137 is closed. diff --git a/changelog.d/3085.change.rst b/changelog.d/3085.change.rst deleted file mode 100644 index f900a40e..00000000 --- a/changelog.d/3085.change.rst +++ /dev/null @@ -1 +0,0 @@ -Setuptools no longer relies on ``pkg_resources`` for entry point handling. diff --git a/changelog.d/3098.change.rst b/changelog.d/3098.change.rst deleted file mode 100644 index 10b0f53a..00000000 --- a/changelog.d/3098.change.rst +++ /dev/null @@ -1 +0,0 @@ -Bump vendored packaging to 21.3. diff --git a/setup.cfg b/setup.cfg index 32d97498..59999cc7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 60.8.2 +version = 60.9.0 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From e2425d2e88364797f77bab414f58b524194289e4 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 12 Feb 2022 18:52:23 +0000 Subject: Prevent type error from env var --- setuptools/tests/test_build_meta.py | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index ea82f82c..af98da68 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -11,7 +11,7 @@ from jaraco import path from .textwrap import DALS -TIMEOUT = os.getenv("TIMEOUT_BACKEND_TEST", 3 * 60) +TIMEOUT = int(os.getenv("TIMEOUT_BACKEND_TEST", "180")) # in seconds class BuildBackendBase: diff --git a/tox.ini b/tox.ini index 70eb743b..61f3b4d1 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,7 @@ usedevelop = True extras = testing passenv = SETUPTOOLS_USE_DISTUTILS - TIMEOUT_BACKEND_TEST # timeout for test_build_meta + TIMEOUT_BACKEND_TEST # timeout (in seconds) for test_build_meta windir # required for test_pkg_resources # honor git config in pytest-perf HOME -- cgit v1.2.1 From 11e9022ea9a61e18baf017254ff9312efe85a1ab Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 12 Feb 2022 12:56:18 -0600 Subject: Add concurrency limit to CI --- .github/workflows/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 35685723..6fca2f69 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,6 +2,10 @@ name: tests on: [push, pull_request] +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + env: # pypa/distutils#99 VIRTUALENV_NO_SETUPTOOLS: 1 -- cgit v1.2.1 From 357292252c188c7b9288e81dc68038cd43ca52e6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 12 Feb 2022 15:51:36 -0500 Subject: Remove invocation of bootstrap script, no longer needed. Fixes #3100. --- .github/workflows/ci-sage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-sage.yml b/.github/workflows/ci-sage.yml index b802a58c..425681d7 100644 --- a/.github/workflows/ci-sage.yml +++ b/.github/workflows/ci-sage.yml @@ -75,7 +75,7 @@ jobs: python3 -m pip install build - name: Run make dist, prepare upstream artifact run: | - (cd build/pkgs/${{ env.SPKG }}/src && python3 -m bootstrap && python3 -m build --sdist) \ + (cd build/pkgs/${{ env.SPKG }}/src && python3 -m build --sdist) \ && mkdir -p upstream && cp build/pkgs/${{ env.SPKG }}/src/dist/*.tar.gz upstream/${{ env.SPKG }}-git.tar.gz \ && echo "sage-package create ${{ env.SPKG }} --version git --tarball ${{ env.SPKG }}-git.tar.gz --type=standard" > upstream/update-pkgs.sh \ && if [ -n "${{ env.REMOVE_PATCHES }}" ]; then echo "(cd ../build/pkgs/${{ env.SPKG }}/patches && rm -f ${{ env.REMOVE_PATCHES }}; :)" >> upstream/update-pkgs.sh; fi \ -- cgit v1.2.1 From 9e875b197a6785ad3f1791f1bc78b965e31a3e73 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 12 Feb 2022 18:21:56 -0500 Subject: Limit tests for stdlib distutils to one job. Ref #3093. --- .github/workflows/main.yml | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 62373734..c985f851 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,7 +11,6 @@ jobs: strategy: matrix: distutils: - - stdlib - local python: - pypy-3.7 @@ -23,6 +22,10 @@ jobs: - ubuntu-latest - macos-latest - windows-latest + include: + - platform: ubuntu-latest + python: "3.10" + distutils: stdlib runs-on: ${{ matrix.platform }} env: SETUPTOOLS_USE_DISTUTILS: ${{ matrix.distutils }} @@ -46,14 +49,7 @@ jobs: ${{ matrix.python }} test_cygwin: - strategy: - matrix: - distutils: - - stdlib - - local runs-on: windows-latest - env: - SETUPTOOLS_USE_DISTUTILS: ${{ matrix.distutils }} steps: - uses: actions/checkout@v2 - name: Install Cygwin with Python @@ -76,11 +72,6 @@ jobs: tox -- --cov-report xml integration-test: - strategy: - matrix: - distutils: - - stdlib - - local needs: test if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && contains(github.ref, 'refs/tags/')) # To avoid long times and high resource usage, we assume that: @@ -91,8 +82,6 @@ jobs: # "integration") # With that in mind, the integration tests can run for a single setup runs-on: ubuntu-latest - env: - SETUPTOOLS_USE_DISTUTILS: ${{ matrix.distutils }} steps: - uses: actions/checkout@v2 - name: Install OS-level dependencies -- cgit v1.2.1 From 0504b5cdba5667153a4f08074a756fb29450357b Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sun, 13 Feb 2022 10:12:06 +0000 Subject: Kill process pool after timeout in test_build_meta This is an attempt to avoid tasks lingering for hours as observed in the CI for the combination Windows+PyPy. --- setuptools/tests/test_build_meta.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index af98da68..11e1297e 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -38,6 +38,7 @@ class BuildBackend(BuildBackendBase): try: return task.result(TIMEOUT) except futures.TimeoutError: + self.pool.shutdown(wait=False) pytest.xfail(f"Backend did not respond before timeout ({TIMEOUT} s)") return method -- cgit v1.2.1 From fb5fe6cd23c7cfd0656aee77a6f32a18e6283313 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 13 Feb 2022 16:41:02 -0500 Subject: Remove invocation of bootstrap script in release process. --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 26aefada..4152c1b3 100644 --- a/tox.ini +++ b/tox.ini @@ -66,7 +66,6 @@ passenv = setenv = TWINE_USERNAME = {env:TWINE_USERNAME:__token__} commands = - python -m bootstrap python -c "import shutil; shutil.rmtree('dist', ignore_errors=True)" # unset tag_build and tag_date pypa/setuptools#2500 python setup.py egg_info -Db "" saveopts -- cgit v1.2.1 From abbaacc62aa26c611d68b935363765af702c0861 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 13 Feb 2022 16:43:33 -0500 Subject: Include mention of bootstrap script removal. --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index f4cddb2b..dc1fac34 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,7 @@ Changes * #2993: Removed workaround in distutils hack for get-pip now that pypa/get-pip#137 is closed. * #3085: Setuptools no longer relies on ``pkg_resources`` for entry point handling. * #3098: Bump vendored packaging to 21.3. +* Removed bootstrap script. v60.8.2 -- cgit v1.2.1 From 3c17f761a40913e796e8e89ed6a9b5acbb81c7ff Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 14 Feb 2022 14:43:51 +0000 Subject: Kill individual worker process after timeout in test_build_meta According to the Python docs, shutting down the process pool does not terminate the tasks that are already running, so it is necessary to manually kill the individual processes in the pool. --- setuptools/tests/test_build_meta.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index 11e1297e..b8a70a85 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -1,7 +1,9 @@ import os import shutil +import signal import tarfile import importlib +import contextlib from concurrent import futures import re @@ -34,15 +36,23 @@ class BuildBackend(BuildBackendBase): def method(*args, **kw): root = os.path.abspath(self.cwd) caller = BuildBackendCaller(root, self.env, self.backend_name) - task = self.pool.submit(caller, name, *args, **kw) + pid = None try: - return task.result(TIMEOUT) + pid = self.pool.submit(os.getpid).result(TIMEOUT) + return self.pool.submit(caller, name, *args, **kw).result(TIMEOUT) except futures.TimeoutError: - self.pool.shutdown(wait=False) + self.pool.shutdown(wait=False) # doesn't stop already running processes + self._kill(pid) pytest.xfail(f"Backend did not respond before timeout ({TIMEOUT} s)") return method + def _kill(self, pid): + if pid is None: + return + with contextlib.suppress(ProcessLookupError): + os.kill(pid, signal.SIGKILL) + class BuildBackendCaller(BuildBackendBase): def __init__(self, *args, **kwargs): -- cgit v1.2.1 From 8a3cbd086f6e2b9e3db1cfc7e909f4150f049b25 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 14 Feb 2022 18:53:26 +0000 Subject: Ensure process killing does not fail on Windows --- setuptools/tests/test_build_meta.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index b8a70a85..9270aa7c 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -50,8 +50,8 @@ class BuildBackend(BuildBackendBase): def _kill(self, pid): if pid is None: return - with contextlib.suppress(ProcessLookupError): - os.kill(pid, signal.SIGKILL) + with contextlib.suppress(ProcessLookupError, OSError): + os.kill(pid, signal.SIGTERM if os.name == "nt" else signal.SIGKILL) class BuildBackendCaller(BuildBackendBase): -- cgit v1.2.1 From d7dfa90dc95ff02301f08e9fff41edeea6f10700 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 14 Feb 2022 17:47:19 -0500 Subject: Add test for loading entry points from a string. Ref #3103. --- setuptools/_entry_points.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/setuptools/_entry_points.py b/setuptools/_entry_points.py index 0cf2691b..2641eaa4 100644 --- a/setuptools/_entry_points.py +++ b/setuptools/_entry_points.py @@ -53,6 +53,15 @@ def load(eps): @load.register(str) def _(eps): + r""" + >>> ep, = load('[console_scripts]\nfoo=bar') + >>> ep.group + 'console_scripts' + >>> ep.name + 'foo' + >>> ep.value + 'bar' + """ return validate(metadata.EntryPoints._from_text(eps)) -- cgit v1.2.1 From 7844b38bba1ea9357af9eb1f2a8e925cd9001547 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 14 Feb 2022 17:50:14 -0500 Subject: Fix issue where string-based entry points would be omitted. Fixes #3103. --- changelog.d/3103.misc.rst | 1 + setuptools/_entry_points.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/3103.misc.rst diff --git a/changelog.d/3103.misc.rst b/changelog.d/3103.misc.rst new file mode 100644 index 00000000..4e9f2b47 --- /dev/null +++ b/changelog.d/3103.misc.rst @@ -0,0 +1 @@ +Fixed issue where string-based entry points would be omitted. diff --git a/setuptools/_entry_points.py b/setuptools/_entry_points.py index 2641eaa4..f087681b 100644 --- a/setuptools/_entry_points.py +++ b/setuptools/_entry_points.py @@ -62,7 +62,7 @@ def _(eps): >>> ep.value 'bar' """ - return validate(metadata.EntryPoints._from_text(eps)) + return validate(metadata.EntryPoints(metadata.EntryPoints._from_text(eps))) load.register(type(None), lambda x: x) -- cgit v1.2.1 From 48196907ea789225269c52a898b3edcb76feb28f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 14 Feb 2022 19:03:50 -0500 Subject: Prevent vendored importlib_metadata from loading distributions from older importlib_metadata. Fixes #3102. --- changelog.d/3102.misc.rst | 1 + setuptools/_importlib.py | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 changelog.d/3102.misc.rst diff --git a/changelog.d/3102.misc.rst b/changelog.d/3102.misc.rst new file mode 100644 index 00000000..2aa4d73a --- /dev/null +++ b/changelog.d/3102.misc.rst @@ -0,0 +1 @@ +Prevent vendored importlib_metadata from loading distributions from older importlib_metadata. diff --git a/setuptools/_importlib.py b/setuptools/_importlib.py index c529ccd3..c1ac137e 100644 --- a/setuptools/_importlib.py +++ b/setuptools/_importlib.py @@ -1,8 +1,31 @@ import sys +def disable_importlib_metadata_finder(metadata): + """ + Ensure importlib_metadata doesn't provide older, incompatible + Distributions. + + Workaround for #3102. + """ + try: + import importlib_metadata + except ImportError: + return + if importlib_metadata is metadata: + return + to_remove = [ + ob + for ob in sys.meta_path + if isinstance(ob, importlib_metadata.MetadataPathFinder) + ] + for item in to_remove: + sys.meta_path.remove(item) + + if sys.version_info < (3, 10): from setuptools.extern import importlib_metadata as metadata + disable_importlib_metadata_finder(metadata) else: import importlib.metadata as metadata # noqa: F401 -- cgit v1.2.1 From e688cb5124e774d6b89e2d5745574640bdf134e2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 14 Feb 2022 21:48:48 -0500 Subject: Bump importlib_metadata to 4.11.1. Fixes #3107. --- changelog.d/3107.misc.rst | 1 + .../importlib_metadata-4.10.1.dist-info/INSTALLER | 1 - .../importlib_metadata-4.10.1.dist-info/LICENSE | 13 --- .../importlib_metadata-4.10.1.dist-info/METADATA | 118 --------------------- .../importlib_metadata-4.10.1.dist-info/RECORD | 24 ----- .../importlib_metadata-4.10.1.dist-info/REQUESTED | 0 .../importlib_metadata-4.10.1.dist-info/WHEEL | 5 - .../top_level.txt | 1 - .../importlib_metadata-4.11.1.dist-info/INSTALLER | 1 + .../importlib_metadata-4.11.1.dist-info/LICENSE | 13 +++ .../importlib_metadata-4.11.1.dist-info/METADATA | 118 +++++++++++++++++++++ .../importlib_metadata-4.11.1.dist-info/RECORD | 24 +++++ .../importlib_metadata-4.11.1.dist-info/REQUESTED | 0 .../importlib_metadata-4.11.1.dist-info/WHEEL | 5 + .../top_level.txt | 1 + setuptools/_vendor/importlib_metadata/__init__.py | 28 +++-- setuptools/_vendor/vendored.txt | 2 +- 17 files changed, 176 insertions(+), 179 deletions(-) create mode 100644 changelog.d/3107.misc.rst delete mode 100644 setuptools/_vendor/importlib_metadata-4.10.1.dist-info/INSTALLER delete mode 100644 setuptools/_vendor/importlib_metadata-4.10.1.dist-info/LICENSE delete mode 100644 setuptools/_vendor/importlib_metadata-4.10.1.dist-info/METADATA delete mode 100644 setuptools/_vendor/importlib_metadata-4.10.1.dist-info/RECORD delete mode 100644 setuptools/_vendor/importlib_metadata-4.10.1.dist-info/REQUESTED delete mode 100644 setuptools/_vendor/importlib_metadata-4.10.1.dist-info/WHEEL delete mode 100644 setuptools/_vendor/importlib_metadata-4.10.1.dist-info/top_level.txt create mode 100644 setuptools/_vendor/importlib_metadata-4.11.1.dist-info/INSTALLER create mode 100644 setuptools/_vendor/importlib_metadata-4.11.1.dist-info/LICENSE create mode 100644 setuptools/_vendor/importlib_metadata-4.11.1.dist-info/METADATA create mode 100644 setuptools/_vendor/importlib_metadata-4.11.1.dist-info/RECORD create mode 100644 setuptools/_vendor/importlib_metadata-4.11.1.dist-info/REQUESTED create mode 100644 setuptools/_vendor/importlib_metadata-4.11.1.dist-info/WHEEL create mode 100644 setuptools/_vendor/importlib_metadata-4.11.1.dist-info/top_level.txt diff --git a/changelog.d/3107.misc.rst b/changelog.d/3107.misc.rst new file mode 100644 index 00000000..6a7f776b --- /dev/null +++ b/changelog.d/3107.misc.rst @@ -0,0 +1 @@ +Bump importlib_metadata to 4.11.1 addressing issue with parsing requirements in egg-info as found in PyPy. diff --git a/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/INSTALLER b/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/INSTALLER deleted file mode 100644 index a1b589e3..00000000 --- a/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/INSTALLER +++ /dev/null @@ -1 +0,0 @@ -pip diff --git a/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/LICENSE b/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/LICENSE deleted file mode 100644 index be7e092b..00000000 --- a/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/LICENSE +++ /dev/null @@ -1,13 +0,0 @@ -Copyright 2017-2019 Jason R. Coombs, Barry Warsaw - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/METADATA b/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/METADATA deleted file mode 100644 index 7327b888..00000000 --- a/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/METADATA +++ /dev/null @@ -1,118 +0,0 @@ -Metadata-Version: 2.1 -Name: importlib-metadata -Version: 4.10.1 -Summary: Read metadata from Python packages -Home-page: https://github.com/python/importlib_metadata -Author: Jason R. Coombs -Author-email: jaraco@jaraco.com -License: UNKNOWN -Platform: UNKNOWN -Classifier: Development Status :: 5 - Production/Stable -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: Apache Software License -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3 :: Only -Requires-Python: >=3.7 -License-File: LICENSE -Requires-Dist: zipp (>=0.5) -Requires-Dist: typing-extensions (>=3.6.4) ; python_version < "3.8" -Provides-Extra: docs -Requires-Dist: sphinx ; extra == 'docs' -Requires-Dist: jaraco.packaging (>=8.2) ; extra == 'docs' -Requires-Dist: rst.linker (>=1.9) ; extra == 'docs' -Provides-Extra: perf -Requires-Dist: ipython ; extra == 'perf' -Provides-Extra: testing -Requires-Dist: pytest (>=6) ; extra == 'testing' -Requires-Dist: pytest-checkdocs (>=2.4) ; extra == 'testing' -Requires-Dist: pytest-flake8 ; extra == 'testing' -Requires-Dist: pytest-cov ; extra == 'testing' -Requires-Dist: pytest-enabler (>=1.0.1) ; extra == 'testing' -Requires-Dist: packaging ; extra == 'testing' -Requires-Dist: pyfakefs ; extra == 'testing' -Requires-Dist: flufl.flake8 ; extra == 'testing' -Requires-Dist: pytest-perf (>=0.9.2) ; extra == 'testing' -Requires-Dist: pytest-black (>=0.3.7) ; (platform_python_implementation != "PyPy") and extra == 'testing' -Requires-Dist: pytest-mypy ; (platform_python_implementation != "PyPy") and extra == 'testing' -Requires-Dist: importlib-resources (>=1.3) ; (python_version < "3.9") and extra == 'testing' - -.. image:: https://img.shields.io/pypi/v/importlib_metadata.svg - :target: `PyPI link`_ - -.. image:: https://img.shields.io/pypi/pyversions/importlib_metadata.svg - :target: `PyPI link`_ - -.. _PyPI link: https://pypi.org/project/importlib_metadata - -.. image:: https://github.com/python/importlib_metadata/workflows/tests/badge.svg - :target: https://github.com/python/importlib_metadata/actions?query=workflow%3A%22tests%22 - :alt: tests - -.. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/psf/black - :alt: Code style: Black - -.. image:: https://readthedocs.org/projects/importlib-metadata/badge/?version=latest - :target: https://importlib-metadata.readthedocs.io/en/latest/?badge=latest - -.. image:: https://img.shields.io/badge/skeleton-2021-informational - :target: https://blog.jaraco.com/skeleton - - -Library to access the metadata for a Python package. - -This package supplies third-party access to the functionality of -`importlib.metadata `_ -including improvements added to subsequent Python versions. - - -Compatibility -============= - -New features are introduced in this third-party library and later merged -into CPython. The following table indicates which versions of this library -were contributed to different versions in the standard library: - -.. list-table:: - :header-rows: 1 - - * - importlib_metadata - - stdlib - * - 4.8 - - 3.11 - * - 4.4 - - 3.10 - * - 1.4 - - 3.8 - - -Usage -===== - -See the `online documentation `_ -for usage details. - -`Finder authors -`_ can -also add support for custom package installers. See the above documentation -for details. - - -Caveats -======= - -This project primarily supports third-party packages installed by PyPA -tools (or other conforming packages). It does not support: - -- Packages in the stdlib. -- Packages installed without metadata. - -Project details -=============== - - * Project home: https://github.com/python/importlib_metadata - * Report bugs at: https://github.com/python/importlib_metadata/issues - * Code hosting: https://github.com/python/importlib_metadata - * Documentation: https://importlib_metadata.readthedocs.io/ - - diff --git a/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/RECORD b/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/RECORD deleted file mode 100644 index ebedf904..00000000 --- a/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/RECORD +++ /dev/null @@ -1,24 +0,0 @@ -importlib_metadata-4.10.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 -importlib_metadata-4.10.1.dist-info/LICENSE,sha256=wNe6dAchmJ1VvVB8D9oTc-gHHadCuaSBAev36sYEM6U,571 -importlib_metadata-4.10.1.dist-info/METADATA,sha256=-HDYj3iK6bcjwN5MAoO58Op6WQIYQfbhl6ZaPqL0IZI,3989 -importlib_metadata-4.10.1.dist-info/RECORD,, -importlib_metadata-4.10.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 -importlib_metadata-4.10.1.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92 -importlib_metadata-4.10.1.dist-info/top_level.txt,sha256=CO3fD9yylANiXkrMo4qHLV_mqXL2sC5JFKgt1yWAT-A,19 -importlib_metadata/__init__.py,sha256=7WxDdbPPu4Wy3VeMTApd-JlPQoENgVDyDH6aqyE7acE,30175 -importlib_metadata/__pycache__/__init__.cpython-310.pyc,, -importlib_metadata/__pycache__/_adapters.cpython-310.pyc,, -importlib_metadata/__pycache__/_collections.cpython-310.pyc,, -importlib_metadata/__pycache__/_compat.cpython-310.pyc,, -importlib_metadata/__pycache__/_functools.cpython-310.pyc,, -importlib_metadata/__pycache__/_itertools.cpython-310.pyc,, -importlib_metadata/__pycache__/_meta.cpython-310.pyc,, -importlib_metadata/__pycache__/_text.cpython-310.pyc,, -importlib_metadata/_adapters.py,sha256=B6fCi5-8mLVDFUZj3krI5nAo-mKp1dH_qIavyIyFrJs,1862 -importlib_metadata/_collections.py,sha256=CJ0OTCHIjWA0ZIVS4voORAsn2R4R2cQBEtPsZEJpASY,743 -importlib_metadata/_compat.py,sha256=EU2XCFBPFByuI0Of6XkAuBYbzqSyjwwwwqmsK4ccna0,1826 -importlib_metadata/_functools.py,sha256=PsY2-4rrKX4RVeRC1oGp1lB1pmC9eKN88_f-bD9uOoA,2895 -importlib_metadata/_itertools.py,sha256=cvr_2v8BRbxcIl5x5ldfqdHjhI8Yi8s8yk50G_nm6jQ,2068 -importlib_metadata/_meta.py,sha256=_F48Hu_jFxkfKWz5wcYS8vO23qEygbVdF9r-6qh-hjE,1154 -importlib_metadata/_text.py,sha256=HCsFksZpJLeTP3NEk_ngrAeXVRRtTrtyh9eOABoRP4A,2166 -importlib_metadata/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/REQUESTED b/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/REQUESTED deleted file mode 100644 index e69de29b..00000000 diff --git a/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/WHEEL b/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/WHEEL deleted file mode 100644 index becc9a66..00000000 --- a/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/WHEEL +++ /dev/null @@ -1,5 +0,0 @@ -Wheel-Version: 1.0 -Generator: bdist_wheel (0.37.1) -Root-Is-Purelib: true -Tag: py3-none-any - diff --git a/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/top_level.txt b/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/top_level.txt deleted file mode 100644 index bbb07547..00000000 --- a/setuptools/_vendor/importlib_metadata-4.10.1.dist-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -importlib_metadata diff --git a/setuptools/_vendor/importlib_metadata-4.11.1.dist-info/INSTALLER b/setuptools/_vendor/importlib_metadata-4.11.1.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/setuptools/_vendor/importlib_metadata-4.11.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/setuptools/_vendor/importlib_metadata-4.11.1.dist-info/LICENSE b/setuptools/_vendor/importlib_metadata-4.11.1.dist-info/LICENSE new file mode 100644 index 00000000..be7e092b --- /dev/null +++ b/setuptools/_vendor/importlib_metadata-4.11.1.dist-info/LICENSE @@ -0,0 +1,13 @@ +Copyright 2017-2019 Jason R. Coombs, Barry Warsaw + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/setuptools/_vendor/importlib_metadata-4.11.1.dist-info/METADATA b/setuptools/_vendor/importlib_metadata-4.11.1.dist-info/METADATA new file mode 100644 index 00000000..fda4bc75 --- /dev/null +++ b/setuptools/_vendor/importlib_metadata-4.11.1.dist-info/METADATA @@ -0,0 +1,118 @@ +Metadata-Version: 2.1 +Name: importlib-metadata +Version: 4.11.1 +Summary: Read metadata from Python packages +Home-page: https://github.com/python/importlib_metadata +Author: Jason R. Coombs +Author-email: jaraco@jaraco.com +License: UNKNOWN +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Requires-Python: >=3.7 +License-File: LICENSE +Requires-Dist: zipp (>=0.5) +Requires-Dist: typing-extensions (>=3.6.4) ; python_version < "3.8" +Provides-Extra: docs +Requires-Dist: sphinx ; extra == 'docs' +Requires-Dist: jaraco.packaging (>=8.2) ; extra == 'docs' +Requires-Dist: rst.linker (>=1.9) ; extra == 'docs' +Provides-Extra: perf +Requires-Dist: ipython ; extra == 'perf' +Provides-Extra: testing +Requires-Dist: pytest (>=6) ; extra == 'testing' +Requires-Dist: pytest-checkdocs (>=2.4) ; extra == 'testing' +Requires-Dist: pytest-flake8 ; extra == 'testing' +Requires-Dist: pytest-cov ; extra == 'testing' +Requires-Dist: pytest-enabler (>=1.0.1) ; extra == 'testing' +Requires-Dist: packaging ; extra == 'testing' +Requires-Dist: pyfakefs ; extra == 'testing' +Requires-Dist: flufl.flake8 ; extra == 'testing' +Requires-Dist: pytest-perf (>=0.9.2) ; extra == 'testing' +Requires-Dist: pytest-black (>=0.3.7) ; (platform_python_implementation != "PyPy") and extra == 'testing' +Requires-Dist: pytest-mypy (>=0.9.1) ; (platform_python_implementation != "PyPy") and extra == 'testing' +Requires-Dist: importlib-resources (>=1.3) ; (python_version < "3.9") and extra == 'testing' + +.. image:: https://img.shields.io/pypi/v/importlib_metadata.svg + :target: `PyPI link`_ + +.. image:: https://img.shields.io/pypi/pyversions/importlib_metadata.svg + :target: `PyPI link`_ + +.. _PyPI link: https://pypi.org/project/importlib_metadata + +.. image:: https://github.com/python/importlib_metadata/workflows/tests/badge.svg + :target: https://github.com/python/importlib_metadata/actions?query=workflow%3A%22tests%22 + :alt: tests + +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + :alt: Code style: Black + +.. image:: https://readthedocs.org/projects/importlib-metadata/badge/?version=latest + :target: https://importlib-metadata.readthedocs.io/en/latest/?badge=latest + +.. image:: https://img.shields.io/badge/skeleton-2022-informational + :target: https://blog.jaraco.com/skeleton + + +Library to access the metadata for a Python package. + +This package supplies third-party access to the functionality of +`importlib.metadata `_ +including improvements added to subsequent Python versions. + + +Compatibility +============= + +New features are introduced in this third-party library and later merged +into CPython. The following table indicates which versions of this library +were contributed to different versions in the standard library: + +.. list-table:: + :header-rows: 1 + + * - importlib_metadata + - stdlib + * - 4.8 + - 3.11 + * - 4.4 + - 3.10 + * - 1.4 + - 3.8 + + +Usage +===== + +See the `online documentation `_ +for usage details. + +`Finder authors +`_ can +also add support for custom package installers. See the above documentation +for details. + + +Caveats +======= + +This project primarily supports third-party packages installed by PyPA +tools (or other conforming packages). It does not support: + +- Packages in the stdlib. +- Packages installed without metadata. + +Project details +=============== + + * Project home: https://github.com/python/importlib_metadata + * Report bugs at: https://github.com/python/importlib_metadata/issues + * Code hosting: https://github.com/python/importlib_metadata + * Documentation: https://importlib_metadata.readthedocs.io/ + + diff --git a/setuptools/_vendor/importlib_metadata-4.11.1.dist-info/RECORD b/setuptools/_vendor/importlib_metadata-4.11.1.dist-info/RECORD new file mode 100644 index 00000000..d8c2dff6 --- /dev/null +++ b/setuptools/_vendor/importlib_metadata-4.11.1.dist-info/RECORD @@ -0,0 +1,24 @@ +importlib_metadata-4.11.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +importlib_metadata-4.11.1.dist-info/LICENSE,sha256=wNe6dAchmJ1VvVB8D9oTc-gHHadCuaSBAev36sYEM6U,571 +importlib_metadata-4.11.1.dist-info/METADATA,sha256=XNgM09x6V8tbt6ugvKjiUxH9yB7pBdILWuWE5YNWHRw,3999 +importlib_metadata-4.11.1.dist-info/RECORD,, +importlib_metadata-4.11.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +importlib_metadata-4.11.1.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92 +importlib_metadata-4.11.1.dist-info/top_level.txt,sha256=CO3fD9yylANiXkrMo4qHLV_mqXL2sC5JFKgt1yWAT-A,19 +importlib_metadata/__init__.py,sha256=Wkh_tb0u0Ds_615ByV9VLLjqgoOWirwMY8EW40oO3nM,30122 +importlib_metadata/__pycache__/__init__.cpython-310.pyc,, +importlib_metadata/__pycache__/_adapters.cpython-310.pyc,, +importlib_metadata/__pycache__/_collections.cpython-310.pyc,, +importlib_metadata/__pycache__/_compat.cpython-310.pyc,, +importlib_metadata/__pycache__/_functools.cpython-310.pyc,, +importlib_metadata/__pycache__/_itertools.cpython-310.pyc,, +importlib_metadata/__pycache__/_meta.cpython-310.pyc,, +importlib_metadata/__pycache__/_text.cpython-310.pyc,, +importlib_metadata/_adapters.py,sha256=B6fCi5-8mLVDFUZj3krI5nAo-mKp1dH_qIavyIyFrJs,1862 +importlib_metadata/_collections.py,sha256=CJ0OTCHIjWA0ZIVS4voORAsn2R4R2cQBEtPsZEJpASY,743 +importlib_metadata/_compat.py,sha256=EU2XCFBPFByuI0Of6XkAuBYbzqSyjwwwwqmsK4ccna0,1826 +importlib_metadata/_functools.py,sha256=PsY2-4rrKX4RVeRC1oGp1lB1pmC9eKN88_f-bD9uOoA,2895 +importlib_metadata/_itertools.py,sha256=cvr_2v8BRbxcIl5x5ldfqdHjhI8Yi8s8yk50G_nm6jQ,2068 +importlib_metadata/_meta.py,sha256=_F48Hu_jFxkfKWz5wcYS8vO23qEygbVdF9r-6qh-hjE,1154 +importlib_metadata/_text.py,sha256=HCsFksZpJLeTP3NEk_ngrAeXVRRtTrtyh9eOABoRP4A,2166 +importlib_metadata/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/setuptools/_vendor/importlib_metadata-4.11.1.dist-info/REQUESTED b/setuptools/_vendor/importlib_metadata-4.11.1.dist-info/REQUESTED new file mode 100644 index 00000000..e69de29b diff --git a/setuptools/_vendor/importlib_metadata-4.11.1.dist-info/WHEEL b/setuptools/_vendor/importlib_metadata-4.11.1.dist-info/WHEEL new file mode 100644 index 00000000..becc9a66 --- /dev/null +++ b/setuptools/_vendor/importlib_metadata-4.11.1.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.1) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/setuptools/_vendor/importlib_metadata-4.11.1.dist-info/top_level.txt b/setuptools/_vendor/importlib_metadata-4.11.1.dist-info/top_level.txt new file mode 100644 index 00000000..bbb07547 --- /dev/null +++ b/setuptools/_vendor/importlib_metadata-4.11.1.dist-info/top_level.txt @@ -0,0 +1 @@ +importlib_metadata diff --git a/setuptools/_vendor/importlib_metadata/__init__.py b/setuptools/_vendor/importlib_metadata/__init__.py index 45541179..292e0c6d 100644 --- a/setuptools/_vendor/importlib_metadata/__init__.py +++ b/setuptools/_vendor/importlib_metadata/__init__.py @@ -283,6 +283,8 @@ class DeprecatedList(list): 1 """ + __slots__ = () + _warn = functools.partial( warnings.warn, "EntryPoints list interface is deprecated. Cast to list if needed.", @@ -295,21 +297,15 @@ class DeprecatedList(list): self._warn() return getattr(super(), method_name)(*args, **kwargs) - return wrapped - - for method_name in [ - '__setitem__', - '__delitem__', - 'append', - 'reverse', - 'extend', - 'pop', - 'remove', - '__iadd__', - 'insert', - 'sort', - ]: - locals()[method_name] = _wrap_deprecated_method(method_name) + return method_name, wrapped + + locals().update( + map( + _wrap_deprecated_method, + '__setitem__ __delitem__ append reverse extend pop remove ' + '__iadd__ insert sort'.split(), + ) + ) def __add__(self, other): if not isinstance(other, tuple): @@ -663,7 +659,7 @@ class Distribution: def _read_egg_info_reqs(self): source = self.read_text('requires.txt') - return source and self._deps_from_requires_text(source) + return pass_none(self._deps_from_requires_text)(source) @classmethod def _deps_from_requires_text(cls, source): diff --git a/setuptools/_vendor/vendored.txt b/setuptools/_vendor/vendored.txt index 1dd32ef2..db24b402 100644 --- a/setuptools/_vendor/vendored.txt +++ b/setuptools/_vendor/vendored.txt @@ -4,7 +4,7 @@ ordered-set==3.1.1 more_itertools==8.8.0 jaraco.text==3.7.0 importlib_resources==5.4.0 -importlib_metadata==4.10.1 +importlib_metadata==4.11.1 # required for importlib_metadata on older Pythons typing_extensions==4.0.1 # required for importlib_resources and _metadata on older Pythons -- cgit v1.2.1 From 5b2b20d32b59f57d703723aadacfc308126034d7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 14 Feb 2022 21:56:51 -0500 Subject: =?UTF-8?q?Bump=20version:=2060.9.0=20=E2=86=92=2060.9.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 11 +++++++++++ changelog.d/3102.misc.rst | 1 - changelog.d/3103.misc.rst | 1 - changelog.d/3107.misc.rst | 1 - setup.cfg | 2 +- 6 files changed, 13 insertions(+), 5 deletions(-) delete mode 100644 changelog.d/3102.misc.rst delete mode 100644 changelog.d/3103.misc.rst delete mode 100644 changelog.d/3107.misc.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 93624e4c..7f5864f8 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 60.9.0 +current_version = 60.9.1 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index dc1fac34..efe34d3c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,14 @@ +v60.9.1 +------- + + +Misc +^^^^ +* #3102: Prevent vendored importlib_metadata from loading distributions from older importlib_metadata. +* #3103: Fixed issue where string-based entry points would be omitted. +* #3107: Bump importlib_metadata to 4.11.1 addressing issue with parsing requirements in egg-info as found in PyPy. + + v60.9.0 ------- diff --git a/changelog.d/3102.misc.rst b/changelog.d/3102.misc.rst deleted file mode 100644 index 2aa4d73a..00000000 --- a/changelog.d/3102.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Prevent vendored importlib_metadata from loading distributions from older importlib_metadata. diff --git a/changelog.d/3103.misc.rst b/changelog.d/3103.misc.rst deleted file mode 100644 index 4e9f2b47..00000000 --- a/changelog.d/3103.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed issue where string-based entry points would be omitted. diff --git a/changelog.d/3107.misc.rst b/changelog.d/3107.misc.rst deleted file mode 100644 index 6a7f776b..00000000 --- a/changelog.d/3107.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Bump importlib_metadata to 4.11.1 addressing issue with parsing requirements in egg-info as found in PyPy. diff --git a/setup.cfg b/setup.cfg index 59999cc7..555038c4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 60.9.0 +version = 60.9.1 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From ae6f313591bc66918708899ae2f1ce31df83a05d Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 15 Feb 2022 22:24:40 +0000 Subject: Reduce GitHub Actions timeout from 6h to 75min From times to times, tests (specially on PyPy) seem to get stuck and be cancelled by GitHub after the default timeout (6h). Before these tests are cancelled, however they count as an active job and limit the amount of concurrent jobs/workflows PyPA's organization may have running. Currently, the slowest job running for the `main` workflow seem to be the tests for PyPy+Windows, which take around 55 min. Therefore, chances are that, if a test is taking much more than 1h to run, it got stuck. We can be pragmatic and reduce the timeout for the `main` workflow and free the resources quickly. --- .github/workflows/main.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c985f851..5a8d510d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -29,6 +29,7 @@ jobs: runs-on: ${{ matrix.platform }} env: SETUPTOOLS_USE_DISTUTILS: ${{ matrix.distutils }} + timeout-minutes: 75 steps: - uses: actions/checkout@v2 - name: Setup Python @@ -50,6 +51,7 @@ jobs: test_cygwin: runs-on: windows-latest + timeout-minutes: 75 steps: - uses: actions/checkout@v2 - name: Install Cygwin with Python @@ -82,6 +84,7 @@ jobs: # "integration") # With that in mind, the integration tests can run for a single setup runs-on: ubuntu-latest + timeout-minutes: 75 steps: - uses: actions/checkout@v2 - name: Install OS-level dependencies @@ -103,7 +106,7 @@ jobs: needs: [test, test_cygwin, integration-test] if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') runs-on: ubuntu-latest - + timeout-minutes: 75 steps: - uses: actions/checkout@v2 - name: Setup Python -- cgit v1.2.1 From ad29c0a3ae83088ce7c246b0d0985e3bcd0a103f Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 15 Feb 2022 23:20:45 +0000 Subject: Disable coverage on PyPy Currently, CI jobs running on PyPy are (painfully) slow. This seems to be a well know side effect of enabling coverage. The change proposed here is to disable coverage on PyPy for both Windows and MacOS (which seem to be very slow). Coverage for PyPy on Ubuntu is preserved because, while being slower than CPython, it is still OK-ish. Hopefully tests running on PyPy/Ubuntu will include most of PyPy's specific corner cases. --- .github/workflows/main.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c985f851..6fa600e9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -38,6 +38,11 @@ jobs: - name: Install tox run: | python -m pip install tox + - name: Disable coverage on PyPy + # Coverage seems to slow things down on PyPy (ubuntu is still OK-ish) + if: contains(matrix.python, 'pypy') && matrix.platform != 'ubuntu-latest' + shell: bash + run: echo 'PYTEST_ADDOPTS=-p no:cov' >> $GITHUB_ENV - name: Run tests run: tox -- --cov-report xml - name: Publish coverage -- cgit v1.2.1 From 2bd13ae65e6cb502c48a1e0bed86fcb3e2d2c5b8 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 15 Feb 2022 23:47:16 +0000 Subject: Skip pytest-cov in setup.cfg for PyPy Instead of disabling coverage in the GitHub workflow it might be easier to just not install it in the test environment. --- .github/workflows/main.yml | 5 ----- setup.cfg | 4 +++- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6fa600e9..c985f851 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -38,11 +38,6 @@ jobs: - name: Install tox run: | python -m pip install tox - - name: Disable coverage on PyPy - # Coverage seems to slow things down on PyPy (ubuntu is still OK-ish) - if: contains(matrix.python, 'pypy') && matrix.platform != 'ubuntu-latest' - shell: bash - run: echo 'PYTEST_ADDOPTS=-p no:cov' >> $GITHUB_ENV - name: Run tests run: tox -- --cov-report xml - name: Publish coverage diff --git a/setup.cfg b/setup.cfg index 555038c4..78ee4725 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,7 +46,9 @@ testing = pytest-black >= 0.3.7; \ # workaround for jaraco/skeleton#22 python_implementation != "PyPy" - pytest-cov + pytest-cov; \ + # coverage seems to make PyPy extremely slow + python_implementation != "PyPy" pytest-mypy >= 0.9.1; \ # workaround for jaraco/skeleton#22 python_implementation != "PyPy" -- cgit v1.2.1 From 2b40489be6a33053754b08cad785fe8be580994e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 15 Feb 2022 19:20:52 -0500 Subject: When loading distutils from the vendored copy, rewrite __name__ to ensure consistent importing from inside and out. Fixes #3035. --- _distutils_hack/__init__.py | 2 ++ setuptools/tests/test_distutils_adoption.py | 16 ++-------------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/_distutils_hack/__init__.py b/_distutils_hack/__init__.py index c6f7de60..605a6edc 100644 --- a/_distutils_hack/__init__.py +++ b/_distutils_hack/__init__.py @@ -57,6 +57,7 @@ def ensure_local_distutils(): # check that submodules load as expected core = importlib.import_module('distutils.core') assert '_distutils' in core.__file__, core.__file__ + assert 'setuptools._distutils.log' not in sys.modules def do_override(): @@ -112,6 +113,7 @@ class DistutilsMetaFinder: class DistutilsLoader(importlib.abc.Loader): def create_module(self, spec): + mod.__name__ = 'distutils' return mod def exec_module(self, module): diff --git a/setuptools/tests/test_distutils_adoption.py b/setuptools/tests/test_distutils_adoption.py index 9aca16dc..df8f3541 100644 --- a/setuptools/tests/test_distutils_adoption.py +++ b/setuptools/tests/test_distutils_adoption.py @@ -123,10 +123,7 @@ print("success") ("stdlib", "file_util"), ("stdlib", "archive_util"), ("local", "dir_util"), - pytest.param( - "local", "file_util", - marks=pytest.mark.xfail(reason="duplicated distutils.file_util, #3042") - ), + ("local", "file_util"), ("local", "archive_util"), ] ) @@ -153,16 +150,7 @@ print("success") """ -@pytest.mark.parametrize( - "distutils_version", - [ - pytest.param( - "local", - marks=pytest.mark.xfail(reason="duplicated distutils.log, #3038 #3042") - ), - "stdlib" - ] -) +@pytest.mark.parametrize("distutils_version", "local stdlib".split()) def test_log_module_is_not_duplicated_on_import(distutils_version, tmpdir_cwd, venv): env = dict(SETUPTOOLS_USE_DISTUTILS=distutils_version) cmd = ['python', '-c', ENSURE_LOG_IMPORT_IS_NOT_DUPLICATED] -- cgit v1.2.1 From 9c8957a13dbe7fe5c0dac148f846a08e640f969c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 15 Feb 2022 19:22:05 -0500 Subject: Update changelog --- changelog.d/3035.misc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/3035.misc.rst diff --git a/changelog.d/3035.misc.rst b/changelog.d/3035.misc.rst new file mode 100644 index 00000000..25cf8781 --- /dev/null +++ b/changelog.d/3035.misc.rst @@ -0,0 +1 @@ +When loading distutils from the vendored copy, rewrite ``__name__`` to ensure consistent importing from inside and out. -- cgit v1.2.1 From 270904f630e9f2142f47d0f701bd225ad149cc00 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 15 Feb 2022 21:16:11 -0500 Subject: =?UTF-8?q?Bump=20version:=2060.9.1=20=E2=86=92=2060.9.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 9 +++++++++ changelog.d/3035.misc.rst | 1 - setup.cfg | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/3035.misc.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 7f5864f8..e5562928 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 60.9.1 +current_version = 60.9.2 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index efe34d3c..38999331 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,12 @@ +v60.9.2 +------- + + +Misc +^^^^ +* #3035: When loading distutils from the vendored copy, rewrite ``__name__`` to ensure consistent importing from inside and out. + + v60.9.1 ------- diff --git a/changelog.d/3035.misc.rst b/changelog.d/3035.misc.rst deleted file mode 100644 index 25cf8781..00000000 --- a/changelog.d/3035.misc.rst +++ /dev/null @@ -1 +0,0 @@ -When loading distutils from the vendored copy, rewrite ``__name__`` to ensure consistent importing from inside and out. diff --git a/setup.cfg b/setup.cfg index 555038c4..dc921fbf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 60.9.1 +version = 60.9.2 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From e3943aa52a9eb0de2fe9cfe62358fbf8b974922b Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 16 Feb 2022 15:44:31 +0000 Subject: Prevent CI from trying to use '--cov' when pytest-cov is not installed --- .github/workflows/main.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c985f851..da2b57b7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -39,9 +39,12 @@ jobs: run: | python -m pip install tox - name: Run tests - run: tox -- --cov-report xml + run: tox + - name: Create coverage report + if: hashFiles('.coverage') != '' # Rudimentary `file.exists()` + run: pipx run coverage xml --ignore-errors - name: Publish coverage - if: false # disabled for #2727 + if: hashFiles('coverage.xml') != '' # Rudimentary `file.exists()` uses: codecov/codecov-action@v1 with: flags: >- # Mark which lines are covered by which envs -- cgit v1.2.1 From eb77ddb7fadfd478d130279a95b8bd996e258a49 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 16 Feb 2022 16:15:42 +0000 Subject: Treat codecov action as informational only --- .codecov.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.codecov.yml b/.codecov.yml index 51b248ba..8505b9e3 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -3,4 +3,8 @@ coverage: status: project: default: + informational: true # Treat coverage info as informational only threshold: 0.5% + patch: + default: + informational: true # Treat coverage info as informational only -- cgit v1.2.1 From 71088995134755112b6a0f342048e10b2a35e458 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 16 Feb 2022 16:38:49 +0000 Subject: Prevent codecov from polluting the diff view in PRs --- .codecov.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.codecov.yml b/.codecov.yml index 8505b9e3..bb829c41 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -8,3 +8,5 @@ coverage: patch: default: informational: true # Treat coverage info as informational only +github_checks: + annotations: false # Codecov may pollute the "files" diff view -- cgit v1.2.1 From 57747b71e6ad2e5ab0e5e86fe4157cd5ad9fd98b Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 16 Feb 2022 23:29:53 +0000 Subject: Tweak concurrency group for CI so tags are not cancelled by pushes --- .github/workflows/main.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5a8d510d..2960ed3b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,7 +3,10 @@ name: tests on: [push, pull_request, workflow_dispatch] concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + group: >- + ${{ github.workflow }}- + ${{ github.ref_type }}- + ${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true jobs: -- cgit v1.2.1 From fd6a5084ae38f8bbc3776849a9e573ad8435f7da Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 17 Feb 2022 08:19:19 -0500 Subject: Add changelog --- changelog.d/3093.misc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/3093.misc.rst diff --git a/changelog.d/3093.misc.rst b/changelog.d/3093.misc.rst new file mode 100644 index 00000000..8bf784ba --- /dev/null +++ b/changelog.d/3093.misc.rst @@ -0,0 +1 @@ +Repaired automated release process. -- cgit v1.2.1 From 62745be68e088b3752f902e055da57a5be358b68 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 17 Feb 2022 19:16:39 -0500 Subject: =?UTF-8?q?Bump=20version:=2060.9.2=20=E2=86=92=2060.9.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- CHANGES.rst | 9 +++++++++ changelog.d/3093.misc.rst | 1 - setup.cfg | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 changelog.d/3093.misc.rst diff --git a/.bumpversion.cfg b/.bumpversion.cfg index e5562928..79260da6 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 60.9.2 +current_version = 60.9.3 commit = True tag = True diff --git a/CHANGES.rst b/CHANGES.rst index 38999331..339e81f3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,12 @@ +v60.9.3 +------- + + +Misc +^^^^ +* #3093: Repaired automated release process. + + v60.9.2 ------- diff --git a/changelog.d/3093.misc.rst b/changelog.d/3093.misc.rst deleted file mode 100644 index 8bf784ba..00000000 --- a/changelog.d/3093.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Repaired automated release process. diff --git a/setup.cfg b/setup.cfg index 841aed7d..4099e27b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = setuptools -version = 60.9.2 +version = 60.9.3 author = Python Packaging Authority author_email = distutils-sig@python.org description = Easily download, build, install, upgrade, and uninstall Python packages -- cgit v1.2.1 From a5e663d83bee3ec96890a5f9b5d818c1fdd2d6bc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 17 Feb 2022 19:36:43 -0500 Subject: Deprecated upload_docs command. Ref #2971 --- changelog.d/2971.change.rst | 1 + setuptools/command/upload_docs.py | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 changelog.d/2971.change.rst diff --git a/changelog.d/2971.change.rst b/changelog.d/2971.change.rst new file mode 100644 index 00000000..b9a093b4 --- /dev/null +++ b/changelog.d/2971.change.rst @@ -0,0 +1 @@ +Deprecated upload_docs command, to be removed in the future. diff --git a/setuptools/command/upload_docs.py b/setuptools/command/upload_docs.py index f429f568..a5480005 100644 --- a/setuptools/command/upload_docs.py +++ b/setuptools/command/upload_docs.py @@ -17,8 +17,10 @@ import itertools import functools import http.client import urllib.parse +import warnings from .._importlib import metadata +from .. import SetuptoolsDeprecationWarning from .upload import upload @@ -89,6 +91,12 @@ class upload_docs(upload): zip_file.close() def run(self): + warnings.warn( + "upload_docs is deprecated and will be removed in a future " + "version. Use tools like httpie or curl instead.", + SetuptoolsDeprecationWarning, + ) + # Run sub commands for cmd_name in self.get_sub_commands(): self.run_command(cmd_name) -- cgit v1.2.1 From 19bfd4d13755011a027013a63a637f02bda0c0cd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 17 Feb 2022 19:48:48 -0500 Subject: Remove tests for upload_docs. Removes test dependency on sphinx. Ref #2971. --- setup.cfg | 1 - setuptools/tests/test_sphinx_upload_docs.py | 37 ----------------- setuptools/tests/test_upload_docs.py | 64 ----------------------------- 3 files changed, 102 deletions(-) delete mode 100644 setuptools/tests/test_sphinx_upload_docs.py delete mode 100644 setuptools/tests/test_upload_docs.py diff --git a/setup.cfg b/setup.cfg index 4099e27b..6171f624 100644 --- a/setup.cfg +++ b/setup.cfg @@ -63,7 +63,6 @@ testing = pip>=19.1 # For proper file:// URLs support. jaraco.envs>=2.2 pytest-xdist - sphinx>=4.3.2 jaraco.path>=3.2.0 build[virtualenv] filelock>=3.4.0 diff --git a/setuptools/tests/test_sphinx_upload_docs.py b/setuptools/tests/test_sphinx_upload_docs.py deleted file mode 100644 index f24077fd..00000000 --- a/setuptools/tests/test_sphinx_upload_docs.py +++ /dev/null @@ -1,37 +0,0 @@ -import pytest - -from jaraco import path - -from setuptools.command.upload_docs import upload_docs -from setuptools.dist import Distribution - - -@pytest.fixture -def sphinx_doc_sample_project(tmpdir_cwd): - path.build({ - 'setup.py': 'from setuptools import setup; setup()', - 'build': { - 'docs': { - 'conf.py': 'project="test"', - 'index.rst': ".. toctree::\ - :maxdepth: 2\ - :caption: Contents:", - }, - }, - }) - - -@pytest.mark.usefixtures('sphinx_doc_sample_project') -class TestSphinxUploadDocs: - def test_sphinx_doc(self): - params = dict( - packages=['test'], - ) - dist = Distribution(params) - - cmd = upload_docs(dist) - - cmd.initialize_options() - assert cmd.upload_dir is None - assert cmd.has_sphinx() is True - cmd.finalize_options() diff --git a/setuptools/tests/test_upload_docs.py b/setuptools/tests/test_upload_docs.py deleted file mode 100644 index 68977a5d..00000000 --- a/setuptools/tests/test_upload_docs.py +++ /dev/null @@ -1,64 +0,0 @@ -import os -import zipfile -import contextlib - -import pytest -from jaraco import path - -from setuptools.command.upload_docs import upload_docs -from setuptools.dist import Distribution - -from .textwrap import DALS -from . import contexts - - -@pytest.fixture -def sample_project(tmpdir_cwd): - path.build({ - 'setup.py': DALS(""" - from setuptools import setup - - setup() - """), - 'build': { - 'index.html': 'Hello world.', - 'empty': {}, - } - }) - - -@pytest.mark.usefixtures('sample_project') -@pytest.mark.usefixtures('user_override') -class TestUploadDocsTest: - def test_create_zipfile(self): - """ - Ensure zipfile creation handles common cases, including a folder - containing an empty folder. - """ - - dist = Distribution() - - cmd = upload_docs(dist) - cmd.target_dir = cmd.upload_dir = 'build' - with contexts.tempdir() as tmp_dir: - tmp_file = os.path.join(tmp_dir, 'foo.zip') - zip_file = cmd.create_zipfile(tmp_file) - - assert zipfile.is_zipfile(tmp_file) - - with contextlib.closing(zipfile.ZipFile(tmp_file)) as zip_file: - assert zip_file.namelist() == ['index.html'] - - def test_build_multipart(self): - data = dict( - a="foo", - b="bar", - file=('file.txt', b'content'), - ) - body, content_type = upload_docs._build_multipart(data) - assert 'form-data' in content_type - assert "b'" not in content_type - assert 'b"' not in content_type - assert isinstance(body, bytes) - assert b'foo' in body - assert b'content' in body -- cgit v1.2.1 From 951e57e0c103f0ce6b2c8be1626bc32cec905711 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 18 Feb 2022 16:49:19 +0000 Subject: Attempt to workaround yet another problem with PyPy tests --- setuptools/tests/test_build_meta.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index 9270aa7c..76f560e7 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -1,4 +1,5 @@ import os +import sys import shutil import signal import tarfile @@ -14,6 +15,7 @@ from .textwrap import DALS TIMEOUT = int(os.getenv("TIMEOUT_BACKEND_TEST", "180")) # in seconds +IS_PYPY = '__pypy__' in sys.builtin_module_names class BuildBackendBase: @@ -44,6 +46,10 @@ class BuildBackend(BuildBackendBase): self.pool.shutdown(wait=False) # doesn't stop already running processes self._kill(pid) pytest.xfail(f"Backend did not respond before timeout ({TIMEOUT} s)") + except (futures.process.BrokenProcessPool, MemoryError): + if IS_PYPY: + pytest.xfail("PyPy frequently fails tests with ProcessPoolExector") + raise return method -- cgit v1.2.1 From 456e9708e975a6b709c36036d71b126a24a86830 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 19 Feb 2022 11:57:02 +0000 Subject: Add news fragment --- changelog.d/3120.misc.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelog.d/3120.misc.rst diff --git a/changelog.d/3120.misc.rst b/changelog.d/3120.misc.rst new file mode 100644 index 00000000..3531a0ab --- /dev/null +++ b/changelog.d/3120.misc.rst @@ -0,0 +1,4 @@ +Added workaround for intermittent failures of backend tests on PyPy. +These tests now are marked with `XFAIL +`_, instead of erroring +out directly. -- cgit v1.2.1 From 07a657b1b3d0cf87560f0bd2c644fd0495ad0758 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 19 Feb 2022 12:15:25 +0000 Subject: Avoid accidental URL replacement in CHANGELOG Use negative look-behind regex on docs/conf.py to improve `issues` URL matching. --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index bfd45a69..ae9397f7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,7 +10,7 @@ link_files = { ), replace=[ dict( - pattern=r'(Issue )?#(?P\d+)', + pattern=r'(?\d+)', url='{package_url}/issues/{issue}', ), dict( -- cgit v1.2.1 From 351942ce8731784469b861ae4304e34be3f6f740 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 19 Feb 2022 12:27:28 +0000 Subject: Extend matching of issue/commit links to all PyPA repositories Currently the link for the pypa/get-pip issue was rendering wrongly. This change extends the matching for issues and commits (currently only handling `distutils`) to any PyPA repository. --- docs/conf.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index ae9397f7..97586ede 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -62,12 +62,12 @@ link_files = { url='{GH}/jaraco/setuptools_svn/issues/{setuptools_svn}', ), dict( - pattern=r'pypa/distutils#(?P\d+)', - url='{GH}/pypa/distutils/issues/{distutils}', + pattern=r'pypa/(?P[\-\.\w]+)#(?P\d+)', + url='{GH}/pypa/{issue_repo}/issues/{issue_number}', ), dict( - pattern=r'pypa/distutils@(?P[\da-f]+)', - url='{GH}/pypa/distutils/commit/{distutils_commit}', + pattern=r'pypa/(?P[\-\.\w]+)@(?P[\da-f]+)', + url='{GH}/pypa/{commit_repo}/commit/{commit_number}', ), dict( pattern=r'^(?m)((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n', -- cgit v1.2.1 From bfcb45df40fd3973681fb756164aafc6f87d3492 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 19 Feb 2022 12:52:45 +0000 Subject: Fix some URLs that are being incorrectly displayed --- CHANGES.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 339e81f3..a24cd2ad 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -543,7 +543,7 @@ Changes Documentation changes ^^^^^^^^^^^^^^^^^^^^^ -* #2792: Document how the legacy and non-legacy versions are compared, and reference to the `PEP 440 `_ scheme. +* #2792: Document how the legacy and non-legacy versions are compared, and reference to the PEP 440 scheme. v58.1.0 @@ -4758,8 +4758,7 @@ how it parses version numbers. Jython. * Work around Jython #1980 and Jython #1981. * Distribute #334: Provide workaround for packages that reference ``sys.__stdout__`` - such as numpy does. This change should address - `virtualenv #359 `_ as long + such as numpy does. This change should address pypa/virtualenv#359 as long as the system encoding is UTF-8 or the IO encoding is specified in the environment, i.e.:: @@ -4785,7 +4784,7 @@ how it parses version numbers. * BB Pull Request #14: Honor file permissions in zip files. * Distribute #327: Merged pull request #24 to fix a dependency problem with pip. -* Merged pull request #23 to fix https://github.com/pypa/virtualenv/issues/301. +* Merged pull request #23 to fix pypa/virtualenv#301. * If Sphinx is installed, the ``upload_docs`` command now runs ``build_sphinx`` to produce uploadable documentation. * Distribute #326: ``upload_docs`` provided mangled auth credentials under Python 3. -- cgit v1.2.1 From 0fca628c255cdd29df969cf94b0bc867f9b8a087 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 19 Feb 2022 13:03:46 +0000 Subject: Add news fragment --- changelog.d/3124.misc.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelog.d/3124.misc.rst diff --git a/changelog.d/3124.misc.rst b/changelog.d/3124.misc.rst new file mode 100644 index 00000000..aba19b80 --- /dev/null +++ b/changelog.d/3124.misc.rst @@ -0,0 +1,2 @@ +Improved configuration for :pypi:`rst-linker` (extension used to build the +changelog). -- cgit v1.2.1 From e0007989edd2b8ed15a5f8da21b9c13434588d34 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 19 Feb 2022 13:20:52 +0000 Subject: Improve regex for autolinking PEPs on changelog --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 97586ede..e57131b1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -54,7 +54,7 @@ link_files = { url='{GH}/pypa/packaging/blob/{packaging_ver}/CHANGELOG.rst', ), dict( - pattern=r'PEP[- ](?P\d+)', + pattern=r'(?\d+)', url='https://www.python.org/dev/peps/pep-{pep_number:0>4}/', ), dict( -- cgit v1.2.1 From 2933688f5183b73ce376e0f63d108c26f1e46171 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sun, 20 Feb 2022 15:31:58 +0000 Subject: XFAIL on OSError in test_build_meta for PyPY --- setuptools/tests/test_build_meta.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index 76f560e7..eb43fe9b 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -46,7 +46,7 @@ class BuildBackend(BuildBackendBase): self.pool.shutdown(wait=False) # doesn't stop already running processes self._kill(pid) pytest.xfail(f"Backend did not respond before timeout ({TIMEOUT} s)") - except (futures.process.BrokenProcessPool, MemoryError): + except (futures.process.BrokenProcessPool, MemoryError, OSError): if IS_PYPY: pytest.xfail("PyPy frequently fails tests with ProcessPoolExector") raise -- cgit v1.2.1 From 05c961b808bfd8d2e87e569e5694694cfd35702b Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 23 Feb 2022 11:14:26 +0000 Subject: Don't warn on false positive for author/maintainer's email While I was working to support pyproject.toml metadata in setuptools, I received as a feedback from the community[^1] that setuptools warns the following message when `author_email` and `maintainer_email` are given in the form of `Person Name `: > warning: check: missing meta-data: either (author and author_email) > or (maintainer and maintainer_email) should be supplied This can be seen as a false positive, because indeed both author's name and email are provided. This warning seems to happen because distutils define the `check` command as a subcommand for `sdist`. This change aims to remove this false positive result from the checks. [^1]: https://discuss.python.org/t/help-testing-experimental-features-in-setuptools/13821/18 --- distutils/command/check.py | 40 +++++++++++++++++++++++++++++++--------- distutils/tests/test_check.py | 22 ++++++++++++++++++++++ 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/distutils/command/check.py b/distutils/command/check.py index 525540b6..af311ca9 100644 --- a/distutils/command/check.py +++ b/distutils/command/check.py @@ -2,6 +2,8 @@ Implements the Distutils 'check' command. """ +from email.utils import getaddresses + from distutils.core import Command from distutils.errors import DistutilsSetupError @@ -96,19 +98,39 @@ class check(Command): if missing: self.warn("missing required meta-data: %s" % ', '.join(missing)) - if metadata.author: - if not metadata.author_email: - self.warn("missing meta-data: if 'author' supplied, " + - "'author_email' should be supplied too") - elif metadata.maintainer: - if not metadata.maintainer_email: - self.warn("missing meta-data: if 'maintainer' supplied, " + - "'maintainer_email' should be supplied too") - else: + 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 + def check_restructuredtext(self): """Checks if the long string fields are reST-compliant.""" data = self.distribution.get_long_description() diff --git a/distutils/tests/test_check.py b/distutils/tests/test_check.py index 91bcdceb..b41dba3d 100644 --- a/distutils/tests/test_check.py +++ b/distutils/tests/test_check.py @@ -71,6 +71,28 @@ class CheckTestCase(support.LoggingSilencer, cmd = self._run(metadata) self.assertEqual(cmd._warnings, 0) + def test_check_author_maintainer(self): + for kind in ("author", "maintainer"): + # ensure no warning when author_email or maintainer_email is given + # (the spec allows these fields to take the form "Name ") + metadata = {'url': 'xxx', + kind + '_email': 'Name ', + 'name': 'xxx', 'version': 'xxx'} + 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 + metadata[kind + '_email'] = 'name@email.com' + cmd = self._run(metadata) + self.assertEqual(cmd._warnings, 1) + + # the check should warn if only the name is given + metadata[kind] = "Name" + del metadata[kind + '_email'] + cmd = self._run(metadata) + self.assertEqual(cmd._warnings, 1) + @unittest.skipUnless(HAS_DOCUTILS, "won't test without docutils") def test_check_document(self): pkg_info, dist = self.create_dist() -- cgit v1.2.1 From b198417b4a80450d1aeaaa7997ea8d00df38cf9f Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sat, 26 Feb 2022 11:42:39 -0600 Subject: When building C++ extensions, replace all of linker command instead of just one word of the linker command. This is to support use case of CXX=g++ and CC=ccache gcc --- distutils/unixccompiler.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/distutils/unixccompiler.py b/distutils/unixccompiler.py index a07e5988..74877794 100644 --- a/distutils/unixccompiler.py +++ b/distutils/unixccompiler.py @@ -196,7 +196,12 @@ class UnixCCompiler(CCompiler): else: offset = 0 - linker[i+offset] = self.compiler_cxx[i] + if len(linker) >= len(self.linker_exe) and \ + linker[:len(self.linker_exe)] == self.linker_exe: + linker = linker[:(i + offset)] + self.compiler_cxx + \ + linker[len(self.linker_exe):] + else: + linker[i+offset] = self.compiler_cxx[i] if sys.platform == 'darwin': linker = _osx_support.compiler_fixup(linker, ld_args) -- cgit v1.2.1 From c44e416b44e5e7126f435a7c0b9adc9b88b85cbd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 26 Feb 2022 12:44:11 -0500 Subject: Prefer range().__contains__ for bounds check. --- setuptools/command/easy_install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index 5b73e6e9..07b45e59 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -358,7 +358,7 @@ class easy_install(Command): if not isinstance(self.optimize, int): try: self.optimize = int(self.optimize) - if not (0 <= self.optimize <= 2): + if self.optimize not in range(3): raise ValueError except ValueError as e: raise DistutilsOptionError( -- cgit v1.2.1 From 66dcd5e54fd8fb1f9413b4fac04e073984ed0713 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 26 Feb 2022 12:29:32 -0500 Subject: Use samefile from stdlib, supported on Windows since Python 3.2. --- changelog.d/3137.change.rst | 1 + setuptools/command/easy_install.py | 20 ++------------------ setuptools/package_index.py | 3 +-- 3 files changed, 4 insertions(+), 20 deletions(-) create mode 100644 changelog.d/3137.change.rst diff --git a/changelog.d/3137.change.rst b/changelog.d/3137.change.rst new file mode 100644 index 00000000..e4186054 --- /dev/null +++ b/changelog.d/3137.change.rst @@ -0,0 +1 @@ +Use samefile from stdlib, supported on Windows since Python 3.2. diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index 07b45e59..63403d19 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -70,7 +70,7 @@ from ..extern.jaraco.text import yield_lines warnings.filterwarnings("default", category=pkg_resources.PEP440Warning) __all__ = [ - 'samefile', 'easy_install', 'PthDistributions', 'extract_wininst_cfg', + 'easy_install', 'PthDistributions', 'extract_wininst_cfg', 'get_exe_prefixes', ] @@ -79,22 +79,6 @@ def is_64bit(): return struct.calcsize("P") == 8 -def samefile(p1, p2): - """ - Determine if two paths reference the same file. - - Augments os.path.samefile to work on Windows and - suppresses errors if the path doesn't exist. - """ - both_exist = os.path.exists(p1) and os.path.exists(p2) - use_samefile = hasattr(os.path, 'samefile') and both_exist - if use_samefile: - return os.path.samefile(p1, p2) - norm_p1 = os.path.normpath(os.path.normcase(p1)) - norm_p2 = os.path.normpath(os.path.normcase(p2)) - return norm_p1 == norm_p2 - - def _to_bytes(s): return s.encode('utf8') @@ -928,7 +912,7 @@ class easy_install(Command): ensure_directory(destination) dist = self.egg_distribution(egg_path) - if not samefile(egg_path, destination): + if not os.path.samefile(egg_path, destination): if os.path.isdir(destination) and not os.path.islink(destination): dir_util.remove_tree(destination, dry_run=self.dry_run) elif os.path.exists(destination): diff --git a/setuptools/package_index.py b/setuptools/package_index.py index 051e523a..4b127f8c 100644 --- a/setuptools/package_index.py +++ b/setuptools/package_index.py @@ -680,8 +680,7 @@ class PackageIndex(Environment): # Make sure the file has been downloaded to the temp dir. if os.path.dirname(filename) != tmpdir: dst = os.path.join(tmpdir, basename) - from setuptools.command.easy_install import samefile - if not samefile(filename, dst): + if not os.path.samefile(filename, dst): shutil.copy2(filename, dst) filename = dst -- cgit v1.2.1 From bbe8b50eccb5700c44bf793346dd09540bff97ee Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 26 Feb 2022 12:48:11 -0500 Subject: Extract method to validate optimize parameter. --- setuptools/command/easy_install.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index 07b45e59..abf25eb9 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -355,15 +355,7 @@ class easy_install(Command): if not self.no_find_links: self.package_index.add_find_links(self.find_links) self.set_undefined_options('install_lib', ('optimize', 'optimize')) - if not isinstance(self.optimize, int): - try: - self.optimize = int(self.optimize) - if self.optimize not in range(3): - raise ValueError - except ValueError as e: - raise DistutilsOptionError( - "--optimize must be 0, 1, or 2" - ) from e + self.optimize = self._validate_optimize(self.optimize) if self.editable and not self.build_directory: raise DistutilsArgError( @@ -375,6 +367,22 @@ class easy_install(Command): self.outputs = [] + @staticmethod + def _validate_optimize(value): + if isinstance(value, int): + return value + + try: + value = int(value) + if value not in range(3): + raise ValueError + except ValueError as e: + raise DistutilsOptionError( + "--optimize must be 0, 1, or 2" + ) from e + + return value + def _fix_install_dir_for_user_site(self): """ Fix the install_dir if "--user" was used. -- cgit v1.2.1 From 99f5ac503ab030c4622cbd8b5129e0880103a68f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 26 Feb 2022 12:49:54 -0500 Subject: Remove 'isinstance(int)' check and just validate unconditionally. --- setuptools/command/easy_install.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index abf25eb9..e2a6543e 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -369,9 +369,6 @@ class easy_install(Command): @staticmethod def _validate_optimize(value): - if isinstance(value, int): - return value - try: value = int(value) if value not in range(3): -- cgit v1.2.1 From d387ae78b3c6384cee30a441045e5b33f2a226b4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 26 Feb 2022 12:51:43 -0500 Subject: Move normpath into if block. --- setuptools/command/easy_install.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index e2a6543e..a526d705 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -309,11 +309,9 @@ class easy_install(Command): self.script_dir = self.install_scripts # default --record from the install command self.set_undefined_options('install', ('record', 'record')) - # Should this be moved to the if statement below? It's not used - # elsewhere - normpath = map(normalize_path, sys.path) self.all_site_dirs = get_site_dirs() if self.site_dirs is not None: + normpath = map(normalize_path, sys.path) site_dirs = [ os.path.expanduser(s.strip()) for s in self.site_dirs.split(',') -- cgit v1.2.1 From 339c29920abdabdd9e6b5983ae711efb61b15d76 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 26 Feb 2022 12:57:38 -0500 Subject: Extract method for processing site dirs --- setuptools/command/easy_install.py | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index a526d705..905bc627 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -310,21 +310,8 @@ class easy_install(Command): # default --record from the install command self.set_undefined_options('install', ('record', 'record')) self.all_site_dirs = get_site_dirs() - if self.site_dirs is not None: - normpath = map(normalize_path, sys.path) - site_dirs = [ - os.path.expanduser(s.strip()) for s in - self.site_dirs.split(',') - ] - for d in site_dirs: - if not os.path.isdir(d): - log.warn("%s (in --site-dirs) does not exist", d) - elif normalize_path(d) not in normpath: - raise DistutilsOptionError( - d + " (in --site-dirs) is not on sys.path" - ) - else: - self.all_site_dirs.append(normalize_path(d)) + self.all_site_dirs.extend(self._process_site_dirs(self.site_dirs)) + if not self.editable: self.check_site_dir() self.index_url = self.index_url or "https://pypi.org/simple/" @@ -365,6 +352,26 @@ class easy_install(Command): self.outputs = [] + @staticmethod + def _process_site_dirs(site_dirs): + if site_dirs is None: + return + + normpath = map(normalize_path, sys.path) + site_dirs = [ + os.path.expanduser(s.strip()) for s in + site_dirs.split(',') + ] + for d in site_dirs: + if not os.path.isdir(d): + log.warn("%s (in --site-dirs) does not exist", d) + elif normalize_path(d) not in normpath: + raise DistutilsOptionError( + d + " (in --site-dirs) is not on sys.path" + ) + else: + yield normalize_path(d) + @staticmethod def _validate_optimize(value): try: -- cgit v1.2.1 From 5ae9aa41369b8b0c8e1710475988ac0e9e3cf431 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 26 Feb 2022 14:16:16 -0500 Subject: Disable tests on Windows while build issues exist. Ref pypa/distutils#118. --- .github/workflows/main.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c680fb36..d2979efd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,7 +24,8 @@ jobs: platform: - ubuntu-latest - macos-latest - - windows-latest + # disable tests on Windows due to pypa/distutils#118 + # - windows-latest include: - platform: ubuntu-latest python: "3.10" -- cgit v1.2.1 From 93c8f674d6d559f08784744d9c467c7c3479c430 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 26 Feb 2022 14:16:16 -0500 Subject: Disable tests on Windows while build issues exist. Ref pypa/distutils#118. --- .github/workflows/main.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 35685723..1589069f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,7 +18,8 @@ jobs: platform: - ubuntu-latest - macos-latest - - windows-latest + # disable tests on Windows due to pypa/distutils#118 + # - windows-latest runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v2 -- cgit v1.2.1 From 45e32fe940fedc01aa3961ae2aff2a91b7a47f25 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sat, 26 Feb 2022 19:21:57 -0600 Subject: add a test for CC with two words --- distutils/tests/test_unixccompiler.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/distutils/tests/test_unixccompiler.py b/distutils/tests/test_unixccompiler.py index 4574f77f..cd282fbe 100644 --- a/distutils/tests/test_unixccompiler.py +++ b/distutils/tests/test_unixccompiler.py @@ -3,6 +3,7 @@ import os import sys import unittest from test.support import run_unittest +from unittest.mock import patch from .py38compat import EnvironmentVarGuard @@ -214,6 +215,38 @@ class UnixCCompilerTestCase(support.TempdirManager, unittest.TestCase): sysconfig.customize_compiler(self.cc) self.assertEqual(self.cc.linker_so[0], 'my_cc') + @unittest.skipIf(sys.platform == 'win32', "can't test on Windows") + def test_cc_overrides_ldshared_for_cxx_correctly(self): + # Issue #18080: + # ensure that setting CC env variable also changes default linker + def gcv(v): + if v == 'LDSHARED': + return 'gcc-4.2 -bundle -undefined dynamic_lookup ' + elif v == 'CXX': + return 'g++-4.2' + return 'gcc-4.2' + + def gcvs(*args, _orig=sysconfig.get_config_vars): + if args: + return list(map(sysconfig.get_config_var, args)) + return _orig() + + sysconfig.get_config_var = gcv + sysconfig.get_config_vars = gcvs + with patch.object(self.cc, 'spawn', return_value=None) as mock_spawn, \ + patch.object(self.cc, '_need_link', return_value=True) as mock_need, \ + patch.object(self.cc, 'mkpath', return_value=None) as mock_mkpath, \ + EnvironmentVarGuard() as env: + env['CC'] = 'ccache my_cc' + env['CXX'] = 'my_cxx' + del env['LDSHARED'] + sysconfig.customize_compiler(self.cc) + self.assertEqual(self.cc.linker_so[0:2], ['ccache','my_cc']) + self.cc.link(None, [], 'a.out', target_lang='c++') + call_args = mock_spawn.call_args[0][0] + if len(call_args) >= 2: + assert(call_args[:2] != ['my_cxx', 'my_cc']) + @unittest.skipIf(sys.platform == 'win32', "can't test on Windows") def test_explicit_ldshared(self): # Issue #18080: -- cgit v1.2.1 From fb7b30d64eb1475a0f5692e015ac123834ff6c40 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sun, 27 Feb 2022 18:43:22 +0000 Subject: Check for file existence before using samefile --- setuptools/command/easy_install.py | 3 ++- setuptools/package_index.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index 3aed8caa..80ff6347 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -922,7 +922,8 @@ class easy_install(Command): ensure_directory(destination) dist = self.egg_distribution(egg_path) - if not os.path.samefile(egg_path, destination): + both_exist = os.path.exists(egg_path) and os.path.exists(destination) + if not (both_exist and os.path.samefile(egg_path, destination)): if os.path.isdir(destination) and not os.path.islink(destination): dir_util.remove_tree(destination, dry_run=self.dry_run) elif os.path.exists(destination): diff --git a/setuptools/package_index.py b/setuptools/package_index.py index 4b127f8c..2c85ff2a 100644 --- a/setuptools/package_index.py +++ b/setuptools/package_index.py @@ -680,7 +680,8 @@ class PackageIndex(Environment): # Make sure the file has been downloaded to the temp dir. if os.path.dirname(filename) != tmpdir: dst = os.path.join(tmpdir, basename) - if not os.path.samefile(filename, dst): + both_exist = os.path.exists(filename) and os.path.exists(dst) + if not (both_exist and os.path.samefile(filename, dst)): shutil.copy2(filename, dst) filename = dst -- cgit v1.2.1 From 597ff8774e505803a565d9bebde2f8a48519b033 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sun, 27 Feb 2022 18:57:50 +0000 Subject: Just check for if destination file exists --- setuptools/command/easy_install.py | 5 +++-- setuptools/package_index.py | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index 80ff6347..6da39e73 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -922,8 +922,9 @@ class easy_install(Command): ensure_directory(destination) dist = self.egg_distribution(egg_path) - both_exist = os.path.exists(egg_path) and os.path.exists(destination) - if not (both_exist and os.path.samefile(egg_path, destination)): + if not ( + os.path.exists(destination) and os.path.samefile(egg_path, destination) + ): if os.path.isdir(destination) and not os.path.islink(destination): dir_util.remove_tree(destination, dry_run=self.dry_run) elif os.path.exists(destination): diff --git a/setuptools/package_index.py b/setuptools/package_index.py index 2c85ff2a..14881d29 100644 --- a/setuptools/package_index.py +++ b/setuptools/package_index.py @@ -680,8 +680,7 @@ class PackageIndex(Environment): # Make sure the file has been downloaded to the temp dir. if os.path.dirname(filename) != tmpdir: dst = os.path.join(tmpdir, basename) - both_exist = os.path.exists(filename) and os.path.exists(dst) - if not (both_exist and os.path.samefile(filename, dst)): + if not (os.path.exists(dst) and os.path.samefile(filename, dst)): shutil.copy2(filename, dst) filename = dst -- cgit v1.2.1 From 342f19f9decc902b7cdbb97350d426cf4cdf9dc0 Mon Sep 17 00:00:00 2001 From: Karolina Surma Date: Thu, 24 Feb 2022 11:20:50 +0100 Subject: Prevent leaking PYTHONPATH to spawned processes in tests This enhances environment isolation, as in special cases, like downstream distro packaging, PYTHONPATH can be set to point to a specific setuptools codebase. When it leaks, it shadows the virtual environment's paths and produces wrong test results. --- setuptools/tests/environment.py | 13 +++++++++++++ setuptools/tests/fixtures.py | 13 ++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/setuptools/tests/environment.py b/setuptools/tests/environment.py index a0c0ec6e..79407d9f 100644 --- a/setuptools/tests/environment.py +++ b/setuptools/tests/environment.py @@ -18,6 +18,19 @@ class VirtualEnv(jaraco.envs.VirtualEnv): def run(self, cmd, *args, **kwargs): cmd = [self.exe(cmd[0])] + cmd[1:] kwargs = {"cwd": self.root, **kwargs} # Allow overriding + # In some environments (eg. downstream distro packaging), where: + # - tox isn't used to run tests and + # - PYTHONPATH is set to point to a specific setuptools codebase and + # - no custom env is explicitly set by a test + # that PYTHONPATH leaks to the spawned processes. + # In that case tests look for module in the wrong place (on PYTHONPATH). + # Unless the test sets its own special env, pass a copy of the existing + # environment with removed PYTHONPATH to the subprocesses. + if "env" not in kwargs: + env = dict(os.environ) + if "PYTHONPATH" in env: + del env["PYTHONPATH"] + kwargs["env"] = env return subprocess.check_output(cmd, *args, **kwargs) diff --git a/setuptools/tests/fixtures.py b/setuptools/tests/fixtures.py index 7599e655..837e6490 100644 --- a/setuptools/tests/fixtures.py +++ b/setuptools/tests/fixtures.py @@ -98,7 +98,18 @@ def venv(tmp_path, setuptools_wheel): env = environment.VirtualEnv() env.root = path.Path(tmp_path / 'venv') env.req = str(setuptools_wheel) - return env.create() + # In some environments (eg. downstream distro packaging), + # where tox isn't used to run tests and PYTHONPATH is set to point to + # a specific setuptools codebase, that PYTHONPATH leaks to the spawned + # processes. + # env.create() should install the just created setuptools + # wheel, but it doesn't if it finds another existing matching setuptools + # installation present on PYTHONPATH: + # `setuptools is already installed with the same version as the provided + # wheel. Use --force-reinstall to force an installation of the wheel.` + # This prevents leaking PYTHONPATH to the created environment. + with contexts.environment(PYTHONPATH=None): + return env.create() @pytest.fixture -- cgit v1.2.1 From 6cabd18d6fa251d9c08b4298cb0b44a29cc1ae1d Mon Sep 17 00:00:00 2001 From: Karolina Surma Date: Thu, 24 Feb 2022 13:53:44 +0100 Subject: Add news fragment --- changelog.d/3133.misc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/3133.misc.rst diff --git a/changelog.d/3133.misc.rst b/changelog.d/3133.misc.rst new file mode 100644 index 00000000..3377e061 --- /dev/null +++ b/changelog.d/3133.misc.rst @@ -0,0 +1 @@ +Enhanced isolation of tests using virtual environments - PYTHONPATH is not leaking to spawned subprocesses -- by :user:`befeleme` -- cgit v1.2.1 From 634dd7e1779663d98cc2fa0382656e8f578b669e Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 25 Feb 2022 15:14:03 +0000 Subject: Apply suggestions from code review --- setuptools/tests/environment.py | 2 +- setuptools/tests/fixtures.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setuptools/tests/environment.py b/setuptools/tests/environment.py index 79407d9f..bcf29601 100644 --- a/setuptools/tests/environment.py +++ b/setuptools/tests/environment.py @@ -22,7 +22,7 @@ class VirtualEnv(jaraco.envs.VirtualEnv): # - tox isn't used to run tests and # - PYTHONPATH is set to point to a specific setuptools codebase and # - no custom env is explicitly set by a test - # that PYTHONPATH leaks to the spawned processes. + # PYTHONPATH will leak into the spawned processes. # In that case tests look for module in the wrong place (on PYTHONPATH). # Unless the test sets its own special env, pass a copy of the existing # environment with removed PYTHONPATH to the subprocesses. diff --git a/setuptools/tests/fixtures.py b/setuptools/tests/fixtures.py index 837e6490..e912399d 100644 --- a/setuptools/tests/fixtures.py +++ b/setuptools/tests/fixtures.py @@ -100,7 +100,7 @@ def venv(tmp_path, setuptools_wheel): env.req = str(setuptools_wheel) # In some environments (eg. downstream distro packaging), # where tox isn't used to run tests and PYTHONPATH is set to point to - # a specific setuptools codebase, that PYTHONPATH leaks to the spawned + # a specific setuptools codebase, PYTHONPATH will leak into the spawned # processes. # env.create() should install the just created setuptools # wheel, but it doesn't if it finds another existing matching setuptools -- cgit v1.2.1 From cb229fa27a86fc48bd40340eacbec60fe5aa609b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Feb 2022 20:38:16 -0500 Subject: Use super throughout. --- setuptools/command/easy_install.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index 6da39e73..107850a9 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -1655,14 +1655,14 @@ class PthDistributions(Environment): if new_path: self.paths.append(dist.location) self.dirty = True - Environment.add(self, dist) + super().add(dist) def remove(self, dist): """Remove `dist` from the distribution map""" while dist.location in self.paths: self.paths.remove(dist.location) self.dirty = True - Environment.remove(self, dist) + super().remove(dist) def make_relative(self, path): npath, last = os.path.split(normalize_path(path)) -- cgit v1.2.1 From e2ea5d62b12daddd924b3da883bd8e32e585749e Mon Sep 17 00:00:00 2001 From: Xing Han Lu Date: Wed, 2 Mar 2022 12:29:51 -0500 Subject: Update entry_point.rst --- docs/userguide/entry_point.rst | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/docs/userguide/entry_point.rst b/docs/userguide/entry_point.rst index 21edc697..ea73bb5e 100644 --- a/docs/userguide/entry_point.rst +++ b/docs/userguide/entry_point.rst @@ -54,11 +54,32 @@ above example, to create a command ``hello-world`` that invokes ``timmins.hello_world``, add a console script entry point to ``setup.cfg``: -.. code-block:: ini +.. tab:: setup.cfg + + .. code-block:: ini + + [options.entry_points] + console_scripts = + hello-world = timmins:hello_world + +.. tab:: setup.py + + .. code-block:: python + + from setuptools import setup + + setup( + name='timmins', + version='0.0.1', + packages=['timmins'], + # ... + entry_points={ + 'console_scripts': [ + 'hello-world=timmins:hello_world', + ] + } + ) - [options.entry_points] - console_scripts = - hello-world = timmins:hello_world After installing the package, a user may invoke that function by simply calling ``hello-world`` on the command line. -- cgit v1.2.1 From bad82c5dcc73657a97c410ad8c16470a68c7142f Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 3 Mar 2022 16:08:31 +0000 Subject: Add links to MANIFEST.in docs and clarify data files inclusion --- docs/setuptools.rst | 28 ++++++++++++++++++++++++++-- docs/userguide/datafiles.rst | 41 +++++++++++++++++++++++++++-------------- 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/docs/setuptools.rst b/docs/setuptools.rst index d0fb9a9c..53cf54b2 100644 --- a/docs/setuptools.rst +++ b/docs/setuptools.rst @@ -21,8 +21,9 @@ Feature Highlights: individually in setup.py * Automatically include all relevant files in your source distributions, - without needing to create a ``MANIFEST.in`` file, and without having to force - regeneration of the ``MANIFEST`` file when your source tree changes. + without needing to create a |MANIFEST.in|_ file, and without having to force + regeneration of the ``MANIFEST`` file when your source tree changes + [#manifest]_. * Automatically generate wrapper scripts or Windows (console and GUI) .exe files for any number of "main" functions in your project. (Note: this is not @@ -211,3 +212,26 @@ set of steps to reproduce. .. _GitHub Discussions: https://github.com/pypa/setuptools/discussions .. _setuptools bug tracker: https://github.com/pypa/setuptools/ + + +---- + + +.. [#manifest] For the most common use cases, ``setuptools`` will automatically + find out which files are necessary for distributing the package. + This includes all pure Python modules in the ``py_modules`` or ``packages`` + configuration and all C sources listed as part of extensions + (it doesn't catch C headers, though). + + More complex packages (e.g. packages that include non-Python files, or that + need to use custom C headers), might still need to specify |MANIFEST.in|_ or + use a plugin like :pypi:`setuptools-scm` or :pypi:`setuptools-svn` + to automatically include files tracked by your Revision Control System. + + Please note that only files **inside the package directory** are included in + the final wheel distribution, by default. See :doc:`userguide/datafiles` for + more information. + + +.. |MANIFEST.in| replace:: ``MANIFEST.in`` +.. _MANIFEST.in: https://packaging.python.org/en/latest/guides/using-manifest-in/ diff --git a/docs/userguide/datafiles.rst b/docs/userguide/datafiles.rst index 69cf36e6..28faa84f 100644 --- a/docs/userguide/datafiles.rst +++ b/docs/userguide/datafiles.rst @@ -5,11 +5,11 @@ Data Files Support The distutils have traditionally allowed installation of "data files", which are placed in a platform-specific location. However, the most common use case for data files distributed with a package is for use *by* the package, usually -by including the data files in the package directory. +by including the data files **inside the package directory**. -Setuptools offers three ways to specify data files to be included in your -packages. First, you can simply use the ``include_package_data`` keyword, -e.g.:: +Setuptools offers three ways to specify this most common type of data files to +be included in your packages [#datafiles]_. +First, you can simply use the ``include_package_data`` keyword, e.g.:: from setuptools import setup, find_packages setup( @@ -18,9 +18,10 @@ e.g.:: ) This tells setuptools to install any data files it finds in your packages. -The data files must be specified via the distutils' ``MANIFEST.in`` file. +The data files must be specified via the distutils' |MANIFEST.in|_ file. (They can also be tracked by a revision control system, using an appropriate -plugin. See the section below on :ref:`Adding Support for Revision +plugin such as :pypi:`setuptools-scm` or :pypi:`setuptools-svn`. +See the section below on :ref:`Adding Support for Revision Control Systems` for information on how to write such plugins.) If you want finer-grained control over what files are included (for example, @@ -87,12 +88,11 @@ When building an ``sdist``, the datafiles are also drawn from the ``package_name.egg-info/SOURCES.txt`` file, so make sure that this is removed if the ``setup.py`` ``package_data`` list is updated before calling ``setup.py``. -(Note: although the ``package_data`` argument was previously only available in -``setuptools``, it was also added to the Python ``distutils`` package as of -Python 2.4; there is `some documentation for the feature`__ available on the -python.org website. If using the setuptools-specific ``include_package_data`` -argument, files specified by ``package_data`` will *not* be automatically -added to the manifest unless they are listed in the MANIFEST.in file.) +.. note:: + If using the ``include_package_data`` argument, files specified by + ``package_data`` will *not* be automatically added to the manifest unless + they are listed in the |MANIFEST.in|_ file or by a plugin like + :pypi:`setuptools-scm` or :pypi:`setuptools-svn`. __ https://docs.python.org/3/distutils/setupscript.html#installing-package-data @@ -125,11 +125,13 @@ included as a result of using ``include_package_data``. In summary, the three options allow you to: ``include_package_data`` - Accept all data files and directories matched by ``MANIFEST.in``. + Accept all data files and directories matched by |MANIFEST.in|_ or added by + a :ref:` Adding Support for Revision Control Systems`. ``package_data`` Specify additional patterns to match files that may or may - not be matched by ``MANIFEST.in`` or found in source control. + not be matched by ``MANIFEST.in`` or added by + a :ref:` Adding Support for Revision Control Systems`. ``exclude_package_data`` Specify patterns for data files and directories that should *not* be @@ -175,3 +177,14 @@ no supported facility to reliably retrieve these resources. Instead, the PyPA recommends that any data files you wish to be accessible at run time be included in the package. + + +---- + +.. [#datafiles] ``setuptools`` consider a *package data file* any non-Python + file **inside the package directory** (i.e., that co-exists in the same + location as the regular ``.py`` files being distributed). + + +.. |MANIFEST.in| replace:: ``MANIFEST.in`` +.. _MANIFEST.in: https://packaging.python.org/en/latest/guides/using-manifest-in/ -- cgit v1.2.1 From 2f1dffb7b53be6943b98ab73f548c1837f460e68 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 3 Mar 2022 16:27:01 +0000 Subject: Add a note about data files being read-only --- docs/userguide/datafiles.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/userguide/datafiles.rst b/docs/userguide/datafiles.rst index 28faa84f..bfec2afb 100644 --- a/docs/userguide/datafiles.rst +++ b/docs/userguide/datafiles.rst @@ -166,6 +166,19 @@ a quick example of converting code that uses ``__file__`` to use .. _Importlib Resources: https://docs.python.org/3/library/importlib.html#module-importlib.resources +.. tip:: Files inside the package directory should be *read-only* to avoid a + series of common problems (e.g. when multiple users share a common Python + installation, when the package is loaded from a zip file, or when multiple + instances of a Python application run in parallel). + + If your Python package needs to write to a file for shared data or configuration, + you can use standard platform/OS-specific system directories, such as + ``~/.local/config/$appname`` or ``/usr/share/$appname/$version`` (Linux specific) [#system-dirs]_. + A common approach is to add a read-only template file to the package + directory that is then copied to the correct system directory if no + pre-existing file is found. + + Non-Package Data Files ---------------------- @@ -185,6 +198,9 @@ run time be included in the package. file **inside the package directory** (i.e., that co-exists in the same location as the regular ``.py`` files being distributed). +.. [#system-dirs] These locations can be discovered with the help of + third-party libraries such as :pypi:`platformdirs`. + .. |MANIFEST.in| replace:: ``MANIFEST.in`` .. _MANIFEST.in: https://packaging.python.org/en/latest/guides/using-manifest-in/ -- cgit v1.2.1 From 523e7c7d008bd043ae6ad707a21e004f53aa9531 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 3 Mar 2022 17:02:26 +0000 Subject: Modify datafiles docs to emphasize importlib.resources over pkg_resorueces --- docs/conf.py | 3 +++ docs/userguide/datafiles.rst | 24 ++++++++++++------------ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index e57131b1..0443799d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -199,3 +199,6 @@ favicons = [ ] intersphinx_mapping['pip'] = 'https://pip.pypa.io/en/latest', None +intersphinx_mapping['importlib-resources'] = ( + 'https://importlib-resources.readthedocs.io/en/latest', None +) diff --git a/docs/userguide/datafiles.rst b/docs/userguide/datafiles.rst index bfec2afb..ce62f3ab 100644 --- a/docs/userguide/datafiles.rst +++ b/docs/userguide/datafiles.rst @@ -94,7 +94,7 @@ the ``setup.py`` ``package_data`` list is updated before calling ``setup.py``. they are listed in the |MANIFEST.in|_ file or by a plugin like :pypi:`setuptools-scm` or :pypi:`setuptools-svn`. -__ https://docs.python.org/3/distutils/setupscript.html#installing-package-data +.. https://docs.python.org/3/distutils/setupscript.html#installing-package-data Sometimes, the ``include_package_data`` or ``package_data`` options alone aren't sufficient to precisely define what files you want included. For @@ -126,12 +126,12 @@ In summary, the three options allow you to: ``include_package_data`` Accept all data files and directories matched by |MANIFEST.in|_ or added by - a :ref:` Adding Support for Revision Control Systems`. + a :ref:`plugin `. ``package_data`` Specify additional patterns to match files that may or may not be matched by ``MANIFEST.in`` or added by - a :ref:` Adding Support for Revision Control Systems`. + a :ref:`plugin `. ``exclude_package_data`` Specify patterns for data files and directories that should *not* be @@ -156,15 +156,10 @@ Typically, existing programs manipulate a package's ``__file__`` attribute in order to find the location of data files. However, this manipulation isn't compatible with PEP 302-based import hooks, including importing from zip files and Python Eggs. It is strongly recommended that, if you are using data files, -you should use the :ref:`ResourceManager API` of ``pkg_resources`` to access -them. The ``pkg_resources`` module is distributed as part of setuptools, so if -you're using setuptools to distribute your package, there is no reason not to -use its resource management API. See also `Importlib Resources`_ for -a quick example of converting code that uses ``__file__`` to use -``pkg_resources`` instead. - -.. _Importlib Resources: https://docs.python.org/3/library/importlib.html#module-importlib.resources - +you should use :mod:`importlib.resources` to access them. +:mod:`importlib.resources` is available since Python 3.7 and the latest version of +the library is also available via the :pypi:`importlib-resources` backport. +See :doc:`importlib-resources:using` for detailed instructions [#importlib]_. .. tip:: Files inside the package directory should be *read-only* to avoid a series of common problems (e.g. when multiple users share a common Python @@ -201,6 +196,11 @@ run time be included in the package. .. [#system-dirs] These locations can be discovered with the help of third-party libraries such as :pypi:`platformdirs`. +.. [#importlib] Recent versions of :mod:`importlib.resources` available in + Pythons' standard library should be API compatible with + :pypi:`importlib-metadata`. However this might vary depending on which version + of Python is installed. + .. |MANIFEST.in| replace:: ``MANIFEST.in`` .. _MANIFEST.in: https://packaging.python.org/en/latest/guides/using-manifest-in/ -- cgit v1.2.1 From 7d6eac45f32d4908413bf035ad623538f321f0e4 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 3 Mar 2022 17:37:55 +0000 Subject: Add section about distributed files to miscellaneous --- docs/setuptools.rst | 19 +++++-------------- docs/userguide/miscellaneous.rst | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/docs/setuptools.rst b/docs/setuptools.rst index 53cf54b2..aa638300 100644 --- a/docs/setuptools.rst +++ b/docs/setuptools.rst @@ -217,20 +217,11 @@ set of steps to reproduce. ---- -.. [#manifest] For the most common use cases, ``setuptools`` will automatically - find out which files are necessary for distributing the package. - This includes all pure Python modules in the ``py_modules`` or ``packages`` - configuration and all C sources listed as part of extensions - (it doesn't catch C headers, though). - - More complex packages (e.g. packages that include non-Python files, or that - need to use custom C headers), might still need to specify |MANIFEST.in|_ or - use a plugin like :pypi:`setuptools-scm` or :pypi:`setuptools-svn` - to automatically include files tracked by your Revision Control System. - - Please note that only files **inside the package directory** are included in - the final wheel distribution, by default. See :doc:`userguide/datafiles` for - more information. +.. [#manifest] The default behaviour for ``setuptools`` will work well for pure + Python packages, or packages with simple C extensions (that don't require + any special C header). See :ref:`Controlling files in the distribution` and + :doc:`userguide/datafiles` for more information about complex scenarios, if + you want to include other types of files. .. |MANIFEST.in| replace:: ``MANIFEST.in`` diff --git a/docs/userguide/miscellaneous.rst b/docs/userguide/miscellaneous.rst index 3df327d7..ad565ed4 100644 --- a/docs/userguide/miscellaneous.rst +++ b/docs/userguide/miscellaneous.rst @@ -94,3 +94,38 @@ correctly when installed as a zipfile, correct any problems if you can, and then make an explicit declaration of ``True`` or ``False`` for the ``zip_safe`` flag, so that it will not be necessary for ``bdist_egg`` to try to guess whether your project can work as a zipfile. + + +.. _Controlling files in the distribution: + +Controlling files in the distribution +------------------------------------- + +For the most common use cases, ``setuptools`` will automatically +find out which files are necessary for distributing the package. +This includes all pure Python modules in the ``py_modules`` or ``packages`` +configuration and all C sources listed as part of extensions +(it doesn't catch C headers, though). + +However, when building more complex packages (e.g. packages that include +non-Python files, or that need to use custom C headers), you might find that +not all files present in your project folder are included in package +distribution archive. In these situations you can use a ``setuptools`` +:ref:`plugin `, such as +:pypi:`setuptools-scm` or :pypi:`setuptools-svn` to automatically include all +files tracked by your Revision Control System to the source distribution +archive (``sdist``). + +.. _Using MANIFEST.in: + +In the case you need fine control over the included files you can also specify +a ``MANIFEST.in`` file at the root of your project with precise +instructions. A comprehensive guide to ``MANIFEST.in`` syntax is available at +the `PyPA's packaging user guide`_. + +Please note that, by default, only files **inside the package directory** are +included in the final ``wheel`` distribution. See :doc:`/userguide/datafiles` for +more information. + + +.. _PyPa's packaging user guide: https://packaging.python.org/en/latest/guides/using-manifest-in/ -- cgit v1.2.1 From e8b418d0b3ccd9b0c70401ef1fe38481980c5578 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 3 Mar 2022 17:41:13 +0000 Subject: Add link to MANIFEST.in in quickstart --- docs/userguide/quickstart.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst index 203d6204..61ab7f97 100644 --- a/docs/userguide/quickstart.rst +++ b/docs/userguide/quickstart.rst @@ -180,7 +180,9 @@ can simply use the ``include_package_data`` keyword: include_package_data = True This tells setuptools to install any data files it finds in your packages. -The data files must be specified via the distutils' ``MANIFEST.in`` file. +The data files must be specified via the distutils' |MANIFEST.in| file +or automatically added by a :ref:`Revision Control System plugin +`. For more details, see :doc:`datafiles` @@ -228,3 +230,7 @@ Resources on Python packaging Packaging in Python can be hard and is constantly evolving. `Python Packaging User Guide `_ has tutorials and up-to-date references that can help you when it is time to distribute your work. + + +.. |MANIFEST.in| replace:: ``MANIFEST.in`` +.. _MANIFEST.in: https://packaging.python.org/en/latest/guides/using-manifest-in/ -- cgit v1.2.1 From c0bdfb66a08e3f5c68e2bc91f33ca6d1a7757511 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 3 Mar 2022 18:16:24 +0000 Subject: Add news fragment --- changelog.d/3148.doc.1.rst | 3 +++ changelog.d/3148.doc.2.rst | 4 ++++ 2 files changed, 7 insertions(+) create mode 100644 changelog.d/3148.doc.1.rst create mode 100644 changelog.d/3148.doc.2.rst diff --git a/changelog.d/3148.doc.1.rst b/changelog.d/3148.doc.1.rst new file mode 100644 index 00000000..af89bde2 --- /dev/null +++ b/changelog.d/3148.doc.1.rst @@ -0,0 +1,3 @@ +Added clarifications about ``MANIFEST.in``, that include links to PyPUG docs +and more prominent mentions to using a revision control system plugin as an +alternative. diff --git a/changelog.d/3148.doc.2.rst b/changelog.d/3148.doc.2.rst new file mode 100644 index 00000000..f46fb248 --- /dev/null +++ b/changelog.d/3148.doc.2.rst @@ -0,0 +1,4 @@ +Removed mention to ``pkg_resources`` as the recommended way of accessing data +files, in favour of :doc:`importlib.resources`. +Additionally more emphasis was put on the fact that *package data files* reside +**inside** the *package directory* (and therefore should be *read-only*). -- cgit v1.2.1 From 580801296f57c829a57567006284d312853b1f2d Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 3 Mar 2022 18:24:10 +0000 Subject: Add missing link to PyPUG MANIFEST.in docs --- docs/userguide/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst index 61ab7f97..f3183624 100644 --- a/docs/userguide/quickstart.rst +++ b/docs/userguide/quickstart.rst @@ -180,7 +180,7 @@ can simply use the ``include_package_data`` keyword: include_package_data = True This tells setuptools to install any data files it finds in your packages. -The data files must be specified via the distutils' |MANIFEST.in| file +The data files must be specified via the distutils' |MANIFEST.in|_ file or automatically added by a :ref:`Revision Control System plugin `. For more details, see :doc:`datafiles` -- cgit v1.2.1 From f529729ebd858f6d37ba9c6abd319a7cf8e6dc68 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 3 Mar 2022 18:26:37 +0000 Subject: Add another missing link to PyPUG --- docs/userguide/datafiles.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/userguide/datafiles.rst b/docs/userguide/datafiles.rst index ce62f3ab..32f91aff 100644 --- a/docs/userguide/datafiles.rst +++ b/docs/userguide/datafiles.rst @@ -130,7 +130,7 @@ In summary, the three options allow you to: ``package_data`` Specify additional patterns to match files that may or may - not be matched by ``MANIFEST.in`` or added by + not be matched by |MANIFEST.in|_ or added by a :ref:`plugin `. ``exclude_package_data`` -- cgit v1.2.1 From 7c2b42292fcdb0076630b00ec09778c598a2a1eb Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 3 Mar 2022 18:28:32 +0000 Subject: Avoid using a same set of words repeatedly too close --- docs/userguide/datafiles.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/userguide/datafiles.rst b/docs/userguide/datafiles.rst index 32f91aff..d974a301 100644 --- a/docs/userguide/datafiles.rst +++ b/docs/userguide/datafiles.rst @@ -157,7 +157,7 @@ order to find the location of data files. However, this manipulation isn't compatible with PEP 302-based import hooks, including importing from zip files and Python Eggs. It is strongly recommended that, if you are using data files, you should use :mod:`importlib.resources` to access them. -:mod:`importlib.resources` is available since Python 3.7 and the latest version of +:mod:`importlib.resources` was added to Python 3.7 and the latest version of the library is also available via the :pypi:`importlib-resources` backport. See :doc:`importlib-resources:using` for detailed instructions [#importlib]_. -- cgit v1.2.1 From d0b9e825fe6c61d4af7b86512dab6ca5ddaafd3c Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 3 Mar 2022 18:29:33 +0000 Subject: Emphasize data files should be included inside the package --- docs/userguide/datafiles.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/userguide/datafiles.rst b/docs/userguide/datafiles.rst index d974a301..e4e94f98 100644 --- a/docs/userguide/datafiles.rst +++ b/docs/userguide/datafiles.rst @@ -184,7 +184,7 @@ fall back to the platform-specific location for installing data files, there is no supported facility to reliably retrieve these resources. Instead, the PyPA recommends that any data files you wish to be accessible at -run time be included in the package. +run time be included **inside the package**. ---- -- cgit v1.2.1 From 5553c276e8071533d45f99db5c00108e852839ba Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 4 Mar 2022 13:08:36 +0000 Subject: Apply suggestions from code review Co-authored-by: Steven Silvester --- docs/userguide/datafiles.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/userguide/datafiles.rst b/docs/userguide/datafiles.rst index e4e94f98..9817e639 100644 --- a/docs/userguide/datafiles.rst +++ b/docs/userguide/datafiles.rst @@ -8,7 +8,7 @@ for data files distributed with a package is for use *by* the package, usually by including the data files **inside the package directory**. Setuptools offers three ways to specify this most common type of data files to -be included in your packages [#datafiles]_. +be included in your package's [#datafiles]_. First, you can simply use the ``include_package_data`` keyword, e.g.:: from setuptools import setup, find_packages @@ -18,7 +18,7 @@ First, you can simply use the ``include_package_data`` keyword, e.g.:: ) This tells setuptools to install any data files it finds in your packages. -The data files must be specified via the distutils' |MANIFEST.in|_ file. +The data files must be specified via the |MANIFEST.in|_ file. (They can also be tracked by a revision control system, using an appropriate plugin such as :pypi:`setuptools-scm` or :pypi:`setuptools-svn`. See the section below on :ref:`Adding Support for Revision -- cgit v1.2.1 From 82529755d433de93fcce1c48385dce3cf4003253 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 4 Mar 2022 15:21:14 +0000 Subject: Clarify the relationship between wheel <> sdist --- docs/conf.py | 1 + docs/userguide/miscellaneous.rst | 70 ++++++++++++++++++++++++++++------------ 2 files changed, 50 insertions(+), 21 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 0443799d..da4d9f33 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -199,6 +199,7 @@ favicons = [ ] intersphinx_mapping['pip'] = 'https://pip.pypa.io/en/latest', None +intersphinx_mapping['PyPUG'] = ('https://packaging.python.org/en/latest/', None) intersphinx_mapping['importlib-resources'] = ( 'https://importlib-resources.readthedocs.io/en/latest', None ) diff --git a/docs/userguide/miscellaneous.rst b/docs/userguide/miscellaneous.rst index ad565ed4..8d494d16 100644 --- a/docs/userguide/miscellaneous.rst +++ b/docs/userguide/miscellaneous.rst @@ -101,31 +101,59 @@ whether your project can work as a zipfile. Controlling files in the distribution ------------------------------------- -For the most common use cases, ``setuptools`` will automatically -find out which files are necessary for distributing the package. -This includes all pure Python modules in the ``py_modules`` or ``packages`` -configuration and all C sources listed as part of extensions -(it doesn't catch C headers, though). +For the most common use cases, ``setuptools`` will automatically find out which +files are necessary for distributing the package. +This includes all :term:`pure Python modules ` in the +``py_modules`` or ``packages`` configuration, and the C sources (but not C +headers) listed as part of extensions when creating a :term:`Source +Distribution (or "sdist")`. However, when building more complex packages (e.g. packages that include non-Python files, or that need to use custom C headers), you might find that not all files present in your project folder are included in package -distribution archive. In these situations you can use a ``setuptools`` -:ref:`plugin `, such as -:pypi:`setuptools-scm` or :pypi:`setuptools-svn` to automatically include all -files tracked by your Revision Control System to the source distribution -archive (``sdist``). +:term:`distribution archive `. -.. _Using MANIFEST.in: - -In the case you need fine control over the included files you can also specify -a ``MANIFEST.in`` file at the root of your project with precise -instructions. A comprehensive guide to ``MANIFEST.in`` syntax is available at -the `PyPA's packaging user guide`_. - -Please note that, by default, only files **inside the package directory** are -included in the final ``wheel`` distribution. See :doc:`/userguide/datafiles` for -more information. +In these situations you can use a ``setuptools`` +:ref:`plugin `, +such as :pypi:`setuptools-scm` or :pypi:`setuptools-svn` to automatically +include all files tracked by your Revision Control System into the ``sdist``. +.. _Using MANIFEST.in: -.. _PyPa's packaging user guide: https://packaging.python.org/en/latest/guides/using-manifest-in/ +Alternatively, if you need finer control, you can add a ``MANIFEST.in`` file at +the root of your project. +This file contains instructions that tell ``setuptools`` which files exactly +should be part of the ``sdist`` (or not). +A comprehensive guide to ``MANIFEST.in`` syntax is available at the +:doc:`PyPA's Packaging User Guide `. + +Once the correct files are present in the ``sdist``, they can then be used by +binary extensions during the build process, or included in the final +:term:`wheel ` [#build-process]_ if you configure ``setuptools`` with +``include_package_data=True``. + +.. important:: + Please note that, when using ``include_package_data=True``, only files **inside + the package directory** are included in the final ``wheel``, by default. + + So for example, if you create a :term:`Python project ` that uses + :pypi:`setuptools-scm` and have a ``tests`` directory outside of the package + folder, the ``tests`` directory will be present in the ``sdist`` but not in the + ``wheel`` [#wheel-vs-sdist]_. + + See :doc:`/userguide/datafiles` for more information. + +---- + +.. [#build-process] + You can think about the build process as two stages: first the ``sdist`` + will be created and then the ``whell`` will be produced from that ``sdist``. + +.. [#wheel-vs-sdist] + This happens because the ``sdist`` can contain files that are useful during + development or the build process itself, but not in runtime (e.g. tests, + docs, examples, etc...). + The ``wheel``, on the other hand, is a file format that has been optimized + and ready to be unpacked into a running installation of Python or + :term:`Virtual Environment`. + Therefore it only contains items that are required during runtime. -- cgit v1.2.1 From bb7c45e348fef933da97f514c79d693af8a96283 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 4 Mar 2022 16:25:22 +0000 Subject: Apply suggestions from code review Co-authored-by: Steven Silvester --- docs/userguide/miscellaneous.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/userguide/miscellaneous.rst b/docs/userguide/miscellaneous.rst index 8d494d16..e545adae 100644 --- a/docs/userguide/miscellaneous.rst +++ b/docs/userguide/miscellaneous.rst @@ -147,7 +147,7 @@ binary extensions during the build process, or included in the final .. [#build-process] You can think about the build process as two stages: first the ``sdist`` - will be created and then the ``whell`` will be produced from that ``sdist``. + will be created and then the ``wheel`` will be produced from that ``sdist``. .. [#wheel-vs-sdist] This happens because the ``sdist`` can contain files that are useful during -- cgit v1.2.1 From 1cbe68dce532d4b14f7685bd1332ba4dc838bfca Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 4 Mar 2022 16:34:57 +0000 Subject: Update docs/userguide/miscellaneous.rst --- docs/userguide/miscellaneous.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/userguide/miscellaneous.rst b/docs/userguide/miscellaneous.rst index e545adae..5fd2f0a8 100644 --- a/docs/userguide/miscellaneous.rst +++ b/docs/userguide/miscellaneous.rst @@ -154,6 +154,6 @@ binary extensions during the build process, or included in the final development or the build process itself, but not in runtime (e.g. tests, docs, examples, etc...). The ``wheel``, on the other hand, is a file format that has been optimized - and ready to be unpacked into a running installation of Python or + and is ready to be unpacked into a running installation of Python or :term:`Virtual Environment`. Therefore it only contains items that are required during runtime. -- cgit v1.2.1 From 1ee962510ba66578f6069e6a675b3715ad12ac0b Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 17 Nov 2021 10:53:40 +0000 Subject: Move *PackageFinder to the new 'discovery' module Following up the discussion in #2887 and #2329, it seems that setuptools is moving towards more automatic discovery features. PackageFinder and PEP420PackageFinder are fundamental pieces of this puzzle and grouping together them togheter with the code implementing these new discovery features make a lot of sense. --- setuptools/__init__.py | 82 +-------------------------------------------- setuptools/discovery.py | 89 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 81 deletions(-) create mode 100644 setuptools/discovery.py diff --git a/setuptools/__init__.py b/setuptools/__init__.py index 06991b65..15b1786e 100644 --- a/setuptools/__init__.py +++ b/setuptools/__init__.py @@ -1,6 +1,5 @@ """Extensions to the 'distutils' for large or complex distributions""" -from fnmatch import fnmatchcase import functools import os import re @@ -9,7 +8,6 @@ import _distutils_hack.override # noqa: F401 import distutils.core from distutils.errors import DistutilsOptionError -from distutils.util import convert_path from ._deprecation_warning import SetuptoolsDeprecationWarning @@ -17,6 +15,7 @@ import setuptools.version from setuptools.extension import Extension from setuptools.dist import Distribution from setuptools.depends import Require +from setuptools.discovery import PackageFinder, PEP420PackageFinder from . import monkey from . import logging @@ -37,85 +36,6 @@ __version__ = setuptools.version.__version__ bootstrap_install_from = None -class PackageFinder: - """ - Generate a list of all Python packages found within a directory - """ - - @classmethod - def find(cls, where='.', exclude=(), include=('*',)): - """Return a list all Python packages found within directory 'where' - - 'where' is the root directory which will be searched for packages. It - should be supplied as a "cross-platform" (i.e. URL-style) path; it will - be converted to the appropriate local path syntax. - - 'exclude' is a sequence of package names to exclude; '*' can be used - as a wildcard in the names, such that 'foo.*' will exclude all - subpackages of 'foo' (but not 'foo' itself). - - 'include' is a sequence of package names to include. If it's - specified, only the named packages will be included. If it's not - specified, all found packages will be included. 'include' can contain - shell style wildcard patterns just like 'exclude'. - """ - - return list( - cls._find_packages_iter( - convert_path(where), - cls._build_filter('ez_setup', '*__pycache__', *exclude), - cls._build_filter(*include), - ) - ) - - @classmethod - def _find_packages_iter(cls, where, exclude, include): - """ - All the packages found in 'where' that pass the 'include' filter, but - not the 'exclude' filter. - """ - for root, dirs, files in os.walk(where, followlinks=True): - # Copy dirs to iterate over it, then empty dirs. - all_dirs = dirs[:] - dirs[:] = [] - - for dir in all_dirs: - full_path = os.path.join(root, dir) - rel_path = os.path.relpath(full_path, where) - package = rel_path.replace(os.path.sep, '.') - - # Skip directory trees that are not valid packages - if '.' in dir or not cls._looks_like_package(full_path): - continue - - # Should this package be included? - if include(package) and not exclude(package): - yield package - - # Keep searching subdirectories, as there may be more packages - # down there, even if the parent was excluded. - dirs.append(dir) - - @staticmethod - def _looks_like_package(path): - """Does a directory look like a package?""" - return os.path.isfile(os.path.join(path, '__init__.py')) - - @staticmethod - def _build_filter(*patterns): - """ - Given a list of patterns, return a callable that will be true only if - the input matches at least one of the patterns. - """ - return lambda name: any(fnmatchcase(name, pat=pat) for pat in patterns) - - -class PEP420PackageFinder(PackageFinder): - @staticmethod - def _looks_like_package(path): - return True - - find_packages = PackageFinder.find find_namespace_packages = PEP420PackageFinder.find diff --git a/setuptools/discovery.py b/setuptools/discovery.py new file mode 100644 index 00000000..eef0461c --- /dev/null +++ b/setuptools/discovery.py @@ -0,0 +1,89 @@ +"""Automatic discovery for Python modules and packages for inclusion in the +distribution. +""" + +import os +from fnmatch import fnmatchcase + +import _distutils_hack.override # noqa: F401 + +from distutils.util import convert_path + + +class PackageFinder: + """ + Generate a list of all Python packages found within a directory + """ + + @classmethod + def find(cls, where='.', exclude=(), include=('*',)): + """Return a list all Python packages found within directory 'where' + + 'where' is the root directory which will be searched for packages. It + should be supplied as a "cross-platform" (i.e. URL-style) path; it will + be converted to the appropriate local path syntax. + + 'exclude' is a sequence of package names to exclude; '*' can be used + as a wildcard in the names, such that 'foo.*' will exclude all + subpackages of 'foo' (but not 'foo' itself). + + 'include' is a sequence of package names to include. If it's + specified, only the named packages will be included. If it's not + specified, all found packages will be included. 'include' can contain + shell style wildcard patterns just like 'exclude'. + """ + + return list( + cls._find_packages_iter( + convert_path(where), + cls._build_filter('ez_setup', '*__pycache__', *exclude), + cls._build_filter(*include), + ) + ) + + @classmethod + def _find_packages_iter(cls, where, exclude, include): + """ + All the packages found in 'where' that pass the 'include' filter, but + not the 'exclude' filter. + """ + for root, dirs, files in os.walk(where, followlinks=True): + # Copy dirs to iterate over it, then empty dirs. + all_dirs = dirs[:] + dirs[:] = [] + + for dir in all_dirs: + full_path = os.path.join(root, dir) + rel_path = os.path.relpath(full_path, where) + package = rel_path.replace(os.path.sep, '.') + + # Skip directory trees that are not valid packages + if '.' in dir or not cls._looks_like_package(full_path): + continue + + # Should this package be included? + if include(package) and not exclude(package): + yield package + + # Keep searching subdirectories, as there may be more packages + # down there, even if the parent was excluded. + dirs.append(dir) + + @staticmethod + def _looks_like_package(path): + """Does a directory look like a package?""" + return os.path.isfile(os.path.join(path, '__init__.py')) + + @staticmethod + def _build_filter(*patterns): + """ + Given a list of patterns, return a callable that will be true only if + the input matches at least one of the patterns. + """ + return lambda name: any(fnmatchcase(name, pat=pat) for pat in patterns) + + +class PEP420PackageFinder(PackageFinder): + @staticmethod + def _looks_like_package(path): + return True -- cgit v1.2.1 From 097887618e33761501442b28d9d69d26f74c7c9c Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 17 Nov 2021 12:09:55 +0000 Subject: Add a more careful package finder for flat-layout use case --- setuptools/discovery.py | 67 ++++++++++++++++++++++++++++++++++ setuptools/tests/test_find_packages.py | 61 +++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/setuptools/discovery.py b/setuptools/discovery.py index eef0461c..f6955200 100644 --- a/setuptools/discovery.py +++ b/setuptools/discovery.py @@ -1,5 +1,39 @@ """Automatic discovery for Python modules and packages for inclusion in the distribution. + +For the purposes of this module, the following nomenclature is used: + +- "src-layout": a directory representing a Python project that contains a "src" + folder. Everything under the "src" folder is meant to be included in the + distribution when packaging the project. Example:: + + . + ├── tox.ini + ├── pyproject.toml + └── src/ + └── mypkg/ + ├── __init__.py + ├── mymodule.py + └── my_data_file.txt + +- "flat-layout": a Python project that does not use "src-layout" but instead + have a folder direct under the project root for each package:: + + . + ├── tox.ini + ├── pyproject.toml + └── mypkg/ + ├── __init__.py + ├── mymodule.py + └── my_data_file.txt + +- "single-module": a project that contains a single Python script:: + + . + ├── tox.ini + ├── pyproject.toml + └── mymodule.py + """ import os @@ -87,3 +121,36 @@ class PEP420PackageFinder(PackageFinder): @staticmethod def _looks_like_package(path): return True + + +class FlatLayoutPackageFinder(PEP420PackageFinder): + """When trying to find packages right under the root directory of a + repository/project, we have to be extra careful to not include things that + are not meant for inclusion (such as tool configuration files) + """ + + EXCLUDE = ( + "doc", + "docs", + "test", + "tests", + "example", + "examples", + # ---- Task runners / Build tools ---- + "tasks", # invoke + "fabfile", # fabric + "site_scons", # SCons + # ---- Hidden directories/Private packages ---- + ".*", + "_*" + ) + + @classmethod + def find(cls, where='.', exclude=(), include=('*',)): + exclude = [*exclude, *cls.EXCLUDE] + [f"{e}.*" for e in cls.EXCLUDE] + return super().find(where, exclude, include) + + @staticmethod + def _looks_like_package(path): + # Ignore invalid names that cannot be imported directly + return os.path.basename(path).isidentifier() diff --git a/setuptools/tests/test_find_packages.py b/setuptools/tests/test_find_packages.py index 906713f6..6fe71a93 100644 --- a/setuptools/tests/test_find_packages.py +++ b/setuptools/tests/test_find_packages.py @@ -9,6 +9,7 @@ import pytest from setuptools import find_packages from setuptools import find_namespace_packages +from setuptools.discovery import FlatLayoutPackageFinder # modeled after CPython's test.support.can_symlink @@ -178,3 +179,63 @@ class TestFindPackages: shutil.rmtree(os.path.join(self.dist_dir, 'pkg/subpkg/assets')) packages = find_namespace_packages(self.dist_dir) self._assert_packages(packages, ['pkg', 'pkg.nspkg', 'pkg.subpkg']) + + +class TestFlatLayoutPackageFinder: + EXAMPLES = { + "hidden-folders": ( + [".pkg/__init__.py", "pkg/__init__.py", "pkg/nested/file.txt"], + ["pkg", "pkg.nested"] + ), + "private-packages": ( + ["_pkg/__init__.py", "pkg/_private/__init__.py"], + ["pkg", "pkg._private"] + ), + "invalid-name": ( + ["invalid-pkg/__init__.py", "other.pkg/__init__.py", "yet,another/file.py"], + [] + ), + "docs": ( + ["pkg/__init__.py", "docs/conf.py", "docs/readme.rst"], + ["pkg"] + ), + "tests": ( + ["pkg/__init__.py", "tests/test_pkg.py", "tests/__init__.py"], + ["pkg"] + ), + "examples": ( + [ + "pkg/__init__.py", + "examples/__init__.py", + "examples/file.py" + "example/other_file.py", + # Sub-packages should always be fine + "pkg/example/__init__.py", + "pkg/examples/__init__.py", + ], + ["pkg", "pkg.examples", "pkg.example"] + ), + "tool-specific": ( + [ + "pkg/__init__.py", + "tasks/__init__.py", + "fabfile/__init__.py", + # Sub-packages should always be fine + "pkg/tasks/__init__.py", + "pkg/fabfile/__init__.py", + ], + ["pkg", "pkg.tasks", "pkg.fabfile"] + ) + } + + @pytest.mark.parametrize("example", EXAMPLES.keys()) + def test_unwanted_directories_not_included(self, tmp_path, example): + package_files, packages = self.EXAMPLES[example] + + for file in package_files: + path = tmp_path / file + path.parent.mkdir(parents=True, exist_ok=True) + path.touch() + + found_packages = FlatLayoutPackageFinder.find(tmp_path) + assert set(found_packages) == set(packages) -- cgit v1.2.1 From cc8060e834925e59df9ec9a8c856070ea888d40b Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 17 Nov 2021 13:14:00 +0000 Subject: Add module finder --- setuptools/discovery.py | 96 +++++++++++++++++++++++++++----- setuptools/tests/test_find_packages.py | 18 +++--- setuptools/tests/test_find_py_modules.py | 85 ++++++++++++++++++++++++++++ 3 files changed, 176 insertions(+), 23 deletions(-) create mode 100644 setuptools/tests/test_find_py_modules.py diff --git a/setuptools/discovery.py b/setuptools/discovery.py index f6955200..9d0a1c2f 100644 --- a/setuptools/discovery.py +++ b/setuptools/discovery.py @@ -37,6 +37,7 @@ For the purposes of this module, the following nomenclature is used: """ import os +from glob import glob from fnmatch import fnmatchcase import _distutils_hack.override # noqa: F401 @@ -44,7 +45,22 @@ import _distutils_hack.override # noqa: F401 from distutils.util import convert_path -class PackageFinder: +def _valid_name(path): + # Ignore invalid names that cannot be imported directly + return os.path.basename(path).isidentifier() + + +class Finder: + @staticmethod + def _build_filter(*patterns): + """ + Given a list of patterns, return a callable that will be true only if + the input matches at least one of the patterns. + """ + return lambda name: any(fnmatchcase(name, pat) for pat in patterns) + + +class PackageFinder(Finder): """ Generate a list of all Python packages found within a directory """ @@ -108,14 +124,6 @@ class PackageFinder: """Does a directory look like a package?""" return os.path.isfile(os.path.join(path, '__init__.py')) - @staticmethod - def _build_filter(*patterns): - """ - Given a list of patterns, return a callable that will be true only if - the input matches at least one of the patterns. - """ - return lambda name: any(fnmatchcase(name, pat=pat) for pat in patterns) - class PEP420PackageFinder(PackageFinder): @staticmethod @@ -141,8 +149,7 @@ class FlatLayoutPackageFinder(PEP420PackageFinder): "fabfile", # fabric "site_scons", # SCons # ---- Hidden directories/Private packages ---- - ".*", - "_*" + "[._]*", ) @classmethod @@ -150,7 +157,66 @@ class FlatLayoutPackageFinder(PEP420PackageFinder): exclude = [*exclude, *cls.EXCLUDE] + [f"{e}.*" for e in cls.EXCLUDE] return super().find(where, exclude, include) - @staticmethod - def _looks_like_package(path): - # Ignore invalid names that cannot be imported directly - return os.path.basename(path).isidentifier() + _looks_like_package = staticmethod(_valid_name) + + +class ModuleFinder(Finder): + INCLUDE = () + EXCLUDE = () + + @classmethod + def find(cls, where='.', exclude=(), include=('*',)): + """Find isolated Python modules. + + The arguments ``where``, ``exclude`` and ``include`` have basically the + same meaning as in PackageFinder. This function will **not** recurse + subdirectories. + """ + return list( + cls._find_modules_iter( + convert_path(where), + cls._build_filter(*cls.EXCLUDE, *exclude), + cls._build_filter(*cls.INCLUDE, *include), + ) + ) + + @classmethod + def _find_modules_iter(cls, where, exclude, include): + for file in glob(os.path.join(where, "*.py")): + module, _ext = os.path.splitext(os.path.basename(file)) + + if not cls._looks_like_module(module): + continue + + if include(module) and not exclude(module): + yield module + + _looks_like_module = staticmethod(_valid_name) + + +class FlatLayoutModuleFinder(ModuleFinder): + """We have to be very careful in the case of flat layout and + single-modules + """ + + EXCLUDE = ( + "setup", + "conftest", + "test", + "tests", + "example", + "examples", + # ---- Task runners ---- + "pavement", + "tasks", + "noxfile", + "dodo", + "fabfile", + # ---- Other tools ---- + "[Ss][Cc]onstruct", # SCons + "conanfile", # Connan: C/C++ build tool + "manage", # Django + # ---- Hidden files/Private modules ---- + "[._]*", + ) + _looks_like_module = staticmethod(_valid_name) diff --git a/setuptools/tests/test_find_packages.py b/setuptools/tests/test_find_packages.py index 6fe71a93..f7930e7f 100644 --- a/setuptools/tests/test_find_packages.py +++ b/setuptools/tests/test_find_packages.py @@ -1,4 +1,4 @@ -"""Tests for setuptools.find_packages().""" +"""Tests for automatic package discovery""" import os import sys import shutil @@ -230,12 +230,14 @@ class TestFlatLayoutPackageFinder: @pytest.mark.parametrize("example", EXAMPLES.keys()) def test_unwanted_directories_not_included(self, tmp_path, example): - package_files, packages = self.EXAMPLES[example] + files, expected_packages = self.EXAMPLES[example] + ensure_files(tmp_path, files) + found_packages = FlatLayoutPackageFinder.find(str(tmp_path)) + assert set(found_packages) == set(expected_packages) - for file in package_files: - path = tmp_path / file - path.parent.mkdir(parents=True, exist_ok=True) - path.touch() - found_packages = FlatLayoutPackageFinder.find(tmp_path) - assert set(found_packages) == set(packages) +def ensure_files(root_path, files): + for file in files: + path = root_path / file + path.parent.mkdir(parents=True, exist_ok=True) + path.touch() diff --git a/setuptools/tests/test_find_py_modules.py b/setuptools/tests/test_find_py_modules.py new file mode 100644 index 00000000..306e1693 --- /dev/null +++ b/setuptools/tests/test_find_py_modules.py @@ -0,0 +1,85 @@ +"""Tests for automatic discovery of modules""" +import os +import sys +import shutil +import tempfile +import platform + +import pytest + +from setuptools.discovery import ModuleFinder, FlatLayoutModuleFinder + +from .test_find_packages import has_symlink, ensure_files + + +class TestModuleFinder: + def find(self, path, *args, **kwargs): + return set(ModuleFinder.find(str(path), *args, **kwargs)) + + EXAMPLES = { + # circumstance: (files, kwargs, expected_modules) + "simple_folder": ( + ["file.py", "other.py"], + {}, # kwargs + ["file", "other"], + ), + "exclude": ( + ["file.py", "other.py"], + {"exclude": ["f*"]}, + ["other"], + ), + "include": ( + ["file.py", "fole.py", "other.py"], + {"include": ["f*"], "exclude": ["fo*"]}, + ["file"], + ), + "invalid-name": ( + ["my-file.py", "other.file.py"], + {}, + [] + ) + } + + @pytest.mark.parametrize("example", EXAMPLES.keys()) + def test_finder(self, tmp_path, example): + files, kwargs, expected_modules = self.EXAMPLES[example] + ensure_files(tmp_path, files) + assert self.find(tmp_path, **kwargs) == set(expected_modules) + + @pytest.mark.skipif(not has_symlink(), reason='Symlink support required') + def test_symlinked_packages_are_included(self, tmp_path): + src = "_myfiles/file.py" + ensure_files(tmp_path, [src]) + os.symlink(tmp_path / src, tmp_path / "link.py") + assert self.find(tmp_path) == {"link"} + + +class TestFlatLayoutModuleFinder: + def find(self, path, *args, **kwargs): + return set(FlatLayoutModuleFinder.find(str(path))) + + EXAMPLES = { + # circumstance: (files, expected_modules) + "hidden-files": ( + [".module.py"], + [] + ), + "private-modules": ( + ["_module.py"], + [] + ), + "common-names": ( + ["setup.py", "conftest.py", "test.py", "tests.py", "example.py", "mod.py"], + ["mod"] + ), + "tool-specific": ( + ["tasks.py", "fabfile.py", "noxfile.py", "dodo.py", "manage.py", "mod.py"], + ["mod"] + ) + } + + @pytest.mark.parametrize("example", EXAMPLES.keys()) + def test_unwanted_files_not_included(self, tmp_path, example): + files, expected_modules = self.EXAMPLES[example] + ensure_files(tmp_path, files) + assert self.find(tmp_path) == set(expected_modules) -- cgit v1.2.1 From 068782e646fad940382c65dce144a41592d20583 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 17 Nov 2021 13:40:05 +0000 Subject: Refactor finders to share code via common base --- setuptools/discovery.py | 149 ++++++++++++++++++++++-------------------------- 1 file changed, 69 insertions(+), 80 deletions(-) diff --git a/setuptools/discovery.py b/setuptools/discovery.py index 9d0a1c2f..aa7a4947 100644 --- a/setuptools/discovery.py +++ b/setuptools/discovery.py @@ -50,7 +50,46 @@ def _valid_name(path): return os.path.basename(path).isidentifier() -class Finder: +class _Finder: + """Base class that exposes functionality for module/package finders""" + + ALWAYS_EXCLUDE = () + DEFAULT_EXCLUDE = () + + @classmethod + def find(cls, where='.', exclude=(), include=('*',)): + """Return a list of all Python items (packages or modules, depending on + the finder implementation) found within directory 'where'. + + 'where' is the root directory which will be searched. + It should be supplied as a "cross-platform" (i.e. URL-style) path; + it will be converted to the appropriate local path syntax. + + 'exclude' is a sequence of names to exclude; '*' can be used + as a wildcard in the names. + When finding packages, 'foo.*' will exclude all subpackages of 'foo' + (but not 'foo' itself). + + 'include' is a sequence of names to include. + If it's specified, only the named items will be included. + If it's not specified, all found items will be included. + 'include' can contain shell style wildcard patterns just like + 'exclude'. + """ + + exclude = exclude or cls.DEFAULT_EXCLUDE + return list( + cls._find_iter( + convert_path(where), + cls._build_filter(*cls.ALWAYS_EXCLUDE, *exclude), + cls._build_filter(*include), + ) + ) + + @classmethod + def _find_iter(cls, where, exclude, include): + raise NotImplementedError + @staticmethod def _build_filter(*patterns): """ @@ -60,39 +99,15 @@ class Finder: return lambda name: any(fnmatchcase(name, pat) for pat in patterns) -class PackageFinder(Finder): +class PackageFinder(_Finder): """ Generate a list of all Python packages found within a directory """ - @classmethod - def find(cls, where='.', exclude=(), include=('*',)): - """Return a list all Python packages found within directory 'where' - - 'where' is the root directory which will be searched for packages. It - should be supplied as a "cross-platform" (i.e. URL-style) path; it will - be converted to the appropriate local path syntax. - - 'exclude' is a sequence of package names to exclude; '*' can be used - as a wildcard in the names, such that 'foo.*' will exclude all - subpackages of 'foo' (but not 'foo' itself). - - 'include' is a sequence of package names to include. If it's - specified, only the named packages will be included. If it's not - specified, all found packages will be included. 'include' can contain - shell style wildcard patterns just like 'exclude'. - """ - - return list( - cls._find_packages_iter( - convert_path(where), - cls._build_filter('ez_setup', '*__pycache__', *exclude), - cls._build_filter(*include), - ) - ) + ALWAYS_EXCLUDE = ("ez_setup", "*__pycache__") @classmethod - def _find_packages_iter(cls, where, exclude, include): + def _find_iter(cls, where, exclude, include): """ All the packages found in 'where' that pass the 'include' filter, but not the 'exclude' filter. @@ -131,13 +146,31 @@ class PEP420PackageFinder(PackageFinder): return True -class FlatLayoutPackageFinder(PEP420PackageFinder): - """When trying to find packages right under the root directory of a - repository/project, we have to be extra careful to not include things that - are not meant for inclusion (such as tool configuration files) +class ModuleFinder(_Finder): + """Find isolated Python modules. + This function will **not** recurse subdirectories. """ - EXCLUDE = ( + @classmethod + def _find_iter(cls, where, exclude, include): + for file in glob(os.path.join(where, "*.py")): + module, _ext = os.path.splitext(os.path.basename(file)) + + if not cls._looks_like_module(module): + continue + + if include(module) and not exclude(module): + yield module + + _looks_like_module = staticmethod(_valid_name) + + +# We have to be extra careful in the case of flat layout to not include files +# and directories not meant for distribution (e.g. tool-related) + + +class FlatLayoutPackageFinder(PEP420PackageFinder): + DEFAULT_EXCLUDE = ( "doc", "docs", "test", @@ -152,54 +185,11 @@ class FlatLayoutPackageFinder(PEP420PackageFinder): "[._]*", ) - @classmethod - def find(cls, where='.', exclude=(), include=('*',)): - exclude = [*exclude, *cls.EXCLUDE] + [f"{e}.*" for e in cls.EXCLUDE] - return super().find(where, exclude, include) - _looks_like_package = staticmethod(_valid_name) -class ModuleFinder(Finder): - INCLUDE = () - EXCLUDE = () - - @classmethod - def find(cls, where='.', exclude=(), include=('*',)): - """Find isolated Python modules. - - The arguments ``where``, ``exclude`` and ``include`` have basically the - same meaning as in PackageFinder. This function will **not** recurse - subdirectories. - """ - return list( - cls._find_modules_iter( - convert_path(where), - cls._build_filter(*cls.EXCLUDE, *exclude), - cls._build_filter(*cls.INCLUDE, *include), - ) - ) - - @classmethod - def _find_modules_iter(cls, where, exclude, include): - for file in glob(os.path.join(where, "*.py")): - module, _ext = os.path.splitext(os.path.basename(file)) - - if not cls._looks_like_module(module): - continue - - if include(module) and not exclude(module): - yield module - - _looks_like_module = staticmethod(_valid_name) - - class FlatLayoutModuleFinder(ModuleFinder): - """We have to be very careful in the case of flat layout and - single-modules - """ - - EXCLUDE = ( + DEFAULT_EXCLUDE = ( "setup", "conftest", "test", @@ -207,10 +197,10 @@ class FlatLayoutModuleFinder(ModuleFinder): "example", "examples", # ---- Task runners ---- - "pavement", - "tasks", "noxfile", + "pavement", "dodo", + "tasks", "fabfile", # ---- Other tools ---- "[Ss][Cc]onstruct", # SCons @@ -219,4 +209,3 @@ class FlatLayoutModuleFinder(ModuleFinder): # ---- Hidden files/Private modules ---- "[._]*", ) - _looks_like_module = staticmethod(_valid_name) -- cgit v1.2.1 From ebf984b3249ae9adc990d54a06b77df455f91bd1 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 17 Nov 2021 18:04:39 +0000 Subject: Cleanup test imports --- setuptools/tests/test_find_py_modules.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/setuptools/tests/test_find_py_modules.py b/setuptools/tests/test_find_py_modules.py index 306e1693..4ef68801 100644 --- a/setuptools/tests/test_find_py_modules.py +++ b/setuptools/tests/test_find_py_modules.py @@ -1,15 +1,11 @@ """Tests for automatic discovery of modules""" import os -import sys -import shutil -import tempfile -import platform import pytest -from setuptools.discovery import ModuleFinder, FlatLayoutModuleFinder +from setuptools.discovery import FlatLayoutModuleFinder, ModuleFinder -from .test_find_packages import has_symlink, ensure_files +from .test_find_packages import ensure_files, has_symlink class TestModuleFinder: -- cgit v1.2.1 From d87f1a68bcee00d3360c833fb71ace223447a565 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 17 Nov 2021 18:05:53 +0000 Subject: Add tests to specify automatic option discovery --- setuptools/tests/test_config_discovery.py | 177 ++++++++++++++++++++++++++++++ setuptools/tests/test_dist.py | 146 ++++++++++++++++++++++-- 2 files changed, 314 insertions(+), 9 deletions(-) create mode 100644 setuptools/tests/test_config_discovery.py diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py new file mode 100644 index 00000000..0f86a98f --- /dev/null +++ b/setuptools/tests/test_config_discovery.py @@ -0,0 +1,177 @@ +import os +import subprocess +import sys +import tarfile +from configparser import ConfigParser +from pathlib import Path +from subprocess import CalledProcessError +from zipfile import ZipFile + +import pytest + +from setuptools.command.sdist import sdist +from setuptools.dist import Distribution + +from .contexts import quiet +from .test_find_packages import ensure_files + + +class TestDiscoverPackagesAndPyModules: + """Make sure discovered values for ``packages`` and ``py_modules`` work + similarly to explicit configuration for the simple scenarios. + """ + OPTIONS = { + # Different options according to the circumstance being tested + "explicit-src": { + "package_dir": {"": "src"}, + "packages": ["pkg"] + }, + "explicit-flat": { + "packages": ["pkg"] + }, + "explicit-single_module": { + "py_modules": ["pkg"] + }, + "explicit-namespace": { + "packages": ["ns", "ns.pkg"] + }, + "automatic-src": {}, + "automatic-flat": {}, + "automatic-single_module": {}, + "automatic-namespace": {} + } + FILES = { + "src": ["src/pkg/__init__.py", "src/pkg/main.py"], + "flat": ["pkg/__init__.py", "pkg/main.py"], + "single_module": ["pkg.py"], + "namespace": ["ns/pkg/__init__.py"] + } + + def _get_info(self, circumstance): + _, _, layout = circumstance.partition("-") + files = self.FILES[layout] + options = self.OPTIONS[circumstance] + return files, options + + @pytest.mark.parametrize("circumstance", OPTIONS.keys()) + def test_sdist_filelist(self, tmp_path, circumstance): + files, options = self._get_info(circumstance) + _populate_project_dir(tmp_path, files, options) + + here = os.getcwd() + dist = Distribution({**options, "src_root": tmp_path}) + dist.script_name = 'setup.py' + dist.set_defaults() + cmd = sdist(dist) + cmd.ensure_finalized() + assert cmd.distribution.packages or cmd.distribution.py_modules + + with quiet(): + try: + os.chdir(tmp_path) + cmd.run() + finally: + os.chdir(here) + + manifest = [f.replace(os.sep, "/") for f in cmd.filelist.files] + for file in files: + assert any(f.endswith(file) for f in manifest) + + @pytest.mark.parametrize("circumstance", OPTIONS.keys()) + def test_project(self, tmp_path, circumstance): + files, options = self._get_info(circumstance) + _populate_project_dir(tmp_path, files, options) + + _run_build(tmp_path) + + sdist_files = _get_sdist_members(next(tmp_path.glob("dist/*.tar.gz"))) + print("~~~~~ sdist_members ~~~~~") + print('\n'.join(sdist_files)) + assert sdist_files >= set(files) + + wheel_files = _get_wheel_members(next(tmp_path.glob("dist/*.whl"))) + print("~~~~~ wheel_members ~~~~~") + print('\n'.join(wheel_files)) + assert wheel_files >= {f.replace("src/", "") for f in files} + + +class TestNoConfig: + DEFAULT_VERSION = "0.0.0" # Default version given by setuptools + + EXAMPLES = { + "pkg1": ["src/pkg1.py"], + "pkg2": ["src/pkg2/__init__.py"], + "ns.nested.pkg3": ["src/ns/nested/pkg3/__init__.py"] + } + + @pytest.mark.parametrize("example", EXAMPLES.keys()) + def test_discover_name(self, tmp_path, example): + _populate_project_dir(tmp_path, self.EXAMPLES[example], {}) + _run_build(tmp_path, "--sdist") + # Expected distribution file + dist_file = tmp_path / f"dist/{example}-{self.DEFAULT_VERSION}.tar.gz" + assert dist_file.is_file() + + +def _populate_project_dir(root, files, options): + # NOTE: Currently pypa/build will refuse to build the project if no + # `pyproject.toml` or `setup.py` is found. So it is impossible to do + # completely "config-less" projects. + (root / "setup.py").write_text("import setuptools\nsetuptools.setup()") + (root / "README.md").write_text("# Example Package") + (root / "LICENSE").write_text("Copyright (c) 2018") + _write_setupcfg(root, options) + ensure_files(root, files) + + +def _write_setupcfg(root, options): + if not options: + print("~~~~~ **NO** setup.cfg ~~~~~") + return + setupcfg = ConfigParser() + setupcfg.add_section("options") + for key, value in options.items(): + if isinstance(value, list): + setupcfg["options"][key] = ", ".join(value) + elif isinstance(value, dict): + str_value = "\n".join(f"\t{k} = {v}" for k, v in value.items()) + setupcfg["options"][key] = "\n" + str_value + else: + setupcfg["options"][key] = str(value) + with open(root / "setup.cfg", "w") as f: + setupcfg.write(f) + print("~~~~~ setup.cfg ~~~~~") + print((root / "setup.cfg").read_text()) + + +def _get_sdist_members(sdist_path): + with tarfile.open(sdist_path, "r:gz") as tar: + files = [Path(f) for f in tar.getnames()] + relative_files = ("/".join(f.parts[1:]) for f in files) + # remove root folder + return {f for f in relative_files if f} + + +def _get_wheel_members(wheel_path): + with ZipFile(wheel_path) as zipfile: + return set(zipfile.namelist()) + + +def _run_build(path, *flags): + cmd = [sys.executable, "-m", "build", "--no-isolation", *flags, str(path)] + r = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + env={**os.environ, 'DISTUTILS_DEBUG': '1'} + ) + out = r.stdout + "\n" + r.stderr + print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") + print("Command", repr(cmd), "returncode", r.returncode) + print(out) + map(print, path.glob("*")) + + if r.returncode != 0: + raise CalledProcessError(r.returncode, cmd, r.stdout, r.stderr) + return out diff --git a/setuptools/tests/test_dist.py b/setuptools/tests/test_dist.py index 4980f2c3..39dba4f4 100644 --- a/setuptools/tests/test_dist.py +++ b/setuptools/tests/test_dist.py @@ -18,6 +18,7 @@ from setuptools import Distribution from .textwrap import DALS from .test_easy_install import make_nspkg_sdist +from .test_find_packages import ensure_files import pytest @@ -69,16 +70,19 @@ def test_dist__get_unpatched_deprecated(): pytest.warns(DistDeprecationWarning, _get_unpatched, [""]) +EXAMPLE_BASE_INFO = dict( + name="package", + version="0.0.1", + author="Foo Bar", + author_email="foo@bar.net", + long_description="Long\ndescription", + description="Short description", + keywords=["one", "two"], +) + + def __read_test_cases(): - base = dict( - name="package", - version="0.0.1", - author="Foo Bar", - author_email="foo@bar.net", - long_description="Long\ndescription", - description="Short description", - keywords=["one", "two"], - ) + base = EXAMPLE_BASE_INFO params = functools.partial(dict, base) @@ -379,3 +383,127 @@ def test_rfc822_unescape(content, result): def test_metadata_name(): with pytest.raises(DistutilsSetupError, match='missing.*name'): Distribution()._validate_metadata() + + +@pytest.mark.parametrize( + "dist_name, py_module", + [ + ("my.pkg", "my_pkg"), + ("my-pkg", "my_pkg"), + ("my_pkg", "my_pkg"), + ("pkg", "pkg"), + ] +) +def test_dist_default_py_modules(tmp_path, dist_name, py_module): + (tmp_path / f"{py_module}.py").touch() + + (tmp_path / "setup.py").touch() + (tmp_path / "noxfile.py").touch() + # ^-- make sure common tool files are ignored + + attrs = { + **EXAMPLE_BASE_INFO, + "name": dist_name, + "src_root": str(tmp_path) + } + # Find `py_modules` corresponding to dist_name if not given + dist = Distribution(attrs) + dist.set_defaults() + assert dist.py_modules == [py_module] + # When `py_modules` is given, don't do anything + dist = Distribution({**attrs, "py_modules": ["explicity_py_module"]}) + dist.set_defaults() + assert dist.py_modules == ["explicity_py_module"] + # When `packages` is given, don't do anything + dist = Distribution({**attrs, "packages": ["explicity_package"]}) + dist.set_defaults() + assert not dist.py_modules + + +@pytest.mark.parametrize( + "dist_name, package_dir, package_files, packages", + [ + ("my.pkg", None, ["my_pkg/__init__.py", "my_pkg/mod.py"], ["my_pkg"]), + ("my-pkg", None, ["my_pkg/__init__.py", "my_pkg/mod.py"], ["my_pkg"]), + ("my_pkg", None, ["my_pkg/__init__.py", "my_pkg/mod.py"], ["my_pkg"]), + ("my.pkg", None, ["my/pkg/__init__.py"], ["my", "my.pkg"]), + ( + "my_pkg", + None, + ["src/my_pkg/__init__.py", "src/my_pkg2/__init__.py"], + ["my_pkg", "my_pkg2"] + ), + ( + "my_pkg", + {"pkg": "lib", "pkg2": "lib2"}, + ["lib/__init__.py", "lib/nested/__init__.pyt", "lib2/__init__.py"], + ["pkg", "pkg.nested", "pkg2"] + ), + ] +) +def test_dist_default_packages( + tmp_path, dist_name, package_dir, package_files, packages +): + ensure_files(tmp_path, package_files) + + (tmp_path / "setup.py").touch() + (tmp_path / "noxfile.py").touch() + # ^-- should not be included by default + + attrs = { + **EXAMPLE_BASE_INFO, + "name": dist_name, + "src_root": str(tmp_path), + "package_dir": package_dir + } + # Find `packages` either corresponding to dist_name or inside src + dist = Distribution(attrs) + dist.set_defaults() + assert not dist.py_modules + assert not dist.py_modules + assert set(dist.packages) == set(packages) + # When `py_modules` is given, don't do anything + dist = Distribution({**attrs, "py_modules": ["explicit_py_module"]}) + dist.set_defaults() + assert not dist.packages + assert set(dist.py_modules) == {"explicit_py_module"} + # When `packages` is given, don't do anything + dist = Distribution({**attrs, "packages": ["explicit_package"]}) + dist.set_defaults() + assert not dist.py_modules + assert set(dist.packages) == {"explicit_package"} + + +@pytest.mark.parametrize( + "dist_name, package_dir, package_files", + [ + ("my.pkg.nested", None, ["my/pkg/nested/__init__.py"]), + ("my.pkg", None, ["my/pkg/__init__.py", "my/pkg/file.py"]), + ("my_pkg", None, ["my_pkg.py"]), + ("my_pkg", None, ["my_pkg/__init__.py", "my_pkg/nested/__init__.py"]), + ("my_pkg", None, ["src/my_pkg/__init__.py", "src/my_pkg/nested/__init__.py"]), + ( + "my_pkg", + {"my_pkg": "lib", "my_pkg.lib2": "lib2"}, + ["lib/__init__.py", "lib/nested/__init__.pyt", "lib2/__init__.py"], + ), + # Should not try to guess a name from multiple py_modules/packages + ("UNKNOWN", None, ["mod1.py", "mod2.py"]), + ("UNKNOWN", None, ["pkg1/__ini__.py", "pkg2/__init__.py"]), + ("UNKNOWN", None, ["src/pkg1/__ini__.py", "src/pkg2/__init__.py"]), + ] +) +def test_dist_default_name(tmp_path, dist_name, package_dir, package_files): + """Make sure dist.name is discovered from packages/py_modules""" + ensure_files(tmp_path, package_files) + attrs = { + **EXAMPLE_BASE_INFO, + "src_root": str(tmp_path), + "package_dir": package_dir + } + del attrs["name"] + + dist = Distribution(attrs) + dist.set_defaults() + assert dist.py_modules or dist.packages + assert dist.get_name() == dist_name -- cgit v1.2.1 From 1203ee23c979175b0f9c7e4eb3854e19df95e3b2 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 17 Nov 2021 18:07:28 +0000 Subject: Add implementation for automatic config discovery --- setuptools/discovery.py | 182 ++++++++++++++++++++++++++++++++++++++++++++++-- setuptools/dist.py | 11 +++ 2 files changed, 188 insertions(+), 5 deletions(-) diff --git a/setuptools/discovery.py b/setuptools/discovery.py index aa7a4947..c1d3b0b0 100644 --- a/setuptools/discovery.py +++ b/setuptools/discovery.py @@ -1,5 +1,5 @@ -"""Automatic discovery for Python modules and packages for inclusion in the -distribution. +"""Automatic discovery of Python modules and packages (for inclusion in the +distribution) and other config values. For the purposes of this module, the following nomenclature is used: @@ -17,7 +17,7 @@ For the purposes of this module, the following nomenclature is used: └── my_data_file.txt - "flat-layout": a Python project that does not use "src-layout" but instead - have a folder direct under the project root for each package:: + have a directory under the project root for each package:: . ├── tox.ini @@ -27,7 +27,8 @@ For the purposes of this module, the following nomenclature is used: ├── mymodule.py └── my_data_file.txt -- "single-module": a project that contains a single Python script:: +- "single-module": a project that contains a single Python script direct under + the project root (no directory used):: . ├── tox.ini @@ -36,12 +37,14 @@ For the purposes of this module, the following nomenclature is used: """ +import itertools import os -from glob import glob from fnmatch import fnmatchcase +from glob import glob import _distutils_hack.override # noqa: F401 +from distutils import log from distutils.util import convert_path @@ -209,3 +212,172 @@ class FlatLayoutModuleFinder(ModuleFinder): # ---- Hidden files/Private modules ---- "[._]*", ) + + +def _find_packages_within(root_pkg, pkg_dir): + nested = PEP420PackageFinder.find(pkg_dir) + return [root_pkg] + [".".join((root_pkg, n)) for n in nested] + + +class ConfigDiscovery: + """Fill-in metadata and options that can be automatically derived + (from other metadata/options, the file system or conventions) + """ + + def __init__(self, distribution): + self.dist = distribution + self._called = False + self._root_dir = distribution.src_root or os.getcwd() + + def __call__(self, force=False): + """Automatically discover missing configuration fields + and modifies the given ``distribution`` object in-place. + + Note that by default this will only have an effect the first time the + ``ConfigDiscovery`` object is called. + + To repeatedly invoke automatic discovery (e.g. when the project + directory changes), please use ``force=True`` (or create a new + ``ConfigDiscovery`` instance). + """ + if force is False and self._called: + # Avoid overhead of multiple calls + return + + self._analyse_package_layout() + self._analyse_name() # depends on ``packages`` and ``py_modules`` + + self._called = True + + def _analyse_package_layout(self): + if self.dist.packages or self.dist.py_modules: + # For backward compatibility, just try to find modules/packages + # when nothing is given + return None + + log.debug( + "No `packages` or `py_modules` configuration, performing " + "automatic discovery." + ) + + return ( + self._analyse_explicit_layout() + or self._analyse_src_layout() + # flat-layout is the trickiest for discovery so it should be last + or self._analyse_flat_layout() + ) + + def _analyse_explicit_layout(self): + """The user can explicitly give a package layout via ``package_dir``""" + package_dir = (self.dist.package_dir or {}).copy() + package_dir.pop("", None) # This falls under the "src-layout" umbrella + root_dir = self._root_dir + + if not package_dir: + return False + + pkgs = itertools.chain.from_iterable( + _find_packages_within(pkg, os.path.join(root_dir, parent_dir)) + for pkg, parent_dir in package_dir.items() + ) + self.dist.packages = list(pkgs) + log.debug(f"`explicit-layout` detected -- analysing {package_dir}") + return True + + def _analyse_src_layout(self): + """Try to find all packages or modules under the ``src`` directory + (or anything pointed by ``package_dir[""]``). + + The "src-layout" is relatively safe for automatic discovery. + We assume that everything within is meant to be included in the + distribution. + + If ``package_dir[""]`` is not given, but the ``src`` directory exists, + this function will set ``package_dir[""] = "src"``. + """ + package_dir = self.dist.package_dir = self.dist.package_dir or {} + src_dir = os.path.join(self._root_dir, package_dir.get("", "src")) + if not os.path.isdir(src_dir): + return False + + package_dir.setdefault("", os.path.basename(src_dir)) + self.dist.packages = PEP420PackageFinder.find(src_dir) + self.dist.py_modules = ModuleFinder.find(src_dir) + log.debug(f"`src-layout` detected -- analysing {src_dir}") + return True + + def _analyse_flat_layout(self): + """Try to find all packages and modules under the project root""" + self.dist.packages = FlatLayoutPackageFinder.find(self._root_dir) + self.dist.py_modules = FlatLayoutModuleFinder.find(self._root_dir) + log.debug(f"`flat-layout` detected -- analysing {self._root_dir}") + return True + + def _analyse_name(self): + """The packages/modules are the essential contribution of the author. + Therefore the name of the distribution can be derived from them. + """ + if self.dist.metadata.name or self.dist.name: + # get_name() is not reliable (can return "UNKNOWN") + return None + + log.debug("No `name` configuration, performing automatic discovery") + + name = ( + self._find_name_single_package_or_module() + or self._find_name_from_packages() + ) + if name: + self.dist.metadata.name = name + self.dist.name = name + + def _find_name_single_package_or_module(self): + """Exactly one module or package""" + for field in ('packages', 'py_modules'): + items = getattr(self.dist, field, None) or [] + if items and len(items) == 1: + log.debug(f"Single module/package detected, name: {items[0]}") + return items[0] + + return None + + def _find_name_from_packages(self): + """Try to find the root package that is not a PEP 420 namespace""" + if not self.dist.packages: + return None + + packages = sorted(self.dist.packages, key=len) + common_ancestors = [] + for i, name in enumerate(packages): + if not all(n.startswith(name) for n in packages[i+1:]): + # Since packages are sorted by length, this condition is able + # to find a list of all common ancestors. + # When there is divergence (e.g. multiple root packages) + # the list will be empty + break + common_ancestors.append(name) + + for name in common_ancestors: + init = os.path.join(self._find_package_path(name), "__init__.py") + if os.path.isfile(init): + log.debug(f"Common parent package detected, name: {name}") + return name + + log.warn("No parent package detected, impossible to derive `name`") + return None + + def _find_package_path(self, name): + """Given a package name, return the path where it should be found on + disk, considering the ``package_dir`` option. + """ + package_dir = self.dist.package_dir or {} + parts = name.split(".") + for i in range(len(parts), 0, -1): + # Look backwards, the most specific package_dir first + partial_name = ".".join(parts[:i]) + if partial_name in package_dir: + parent = package_dir[partial_name] + return os.path.join(self._root_dir, parent, *parts[i:]) + + parent = (package_dir.get("") or "").split("/") + return os.path.join(self._root_dir, *parent, *parts) diff --git a/setuptools/dist.py b/setuptools/dist.py index e825785e..79be2cdf 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -39,6 +39,8 @@ import setuptools.command from setuptools import windows_support from setuptools.monkey import get_unpatched from setuptools.config import parse_configuration +from setuptools.discovery import ConfigDiscovery + import pkg_resources from setuptools.extern.packaging import version, requirements from . import _reqs @@ -464,6 +466,8 @@ class Distribution(_Distribution): }, ) + self.set_defaults = ConfigDiscovery(self) + self._set_metadata_defaults(attrs) self.metadata.version = self._normalize_version( @@ -1186,6 +1190,13 @@ class Distribution(_Distribution): sys.stdout.detach(), encoding, errors, newline, line_buffering ) + def run_command(self, command): + self.set_defaults() + # Postpone defaults until all explicit configuration is considered + # (setup() args, config files, command line and plugins) + + super().run_command(command) + class DistDeprecationWarning(SetuptoolsDeprecationWarning): """Class for warning about deprecations in dist in -- cgit v1.2.1 From 52c2a332d5c5501b11dd135a23f977cec8e53aaa Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 17 Nov 2021 18:42:57 +0000 Subject: Add news fragment --- changelog.d/2887.change.1.rst | 19 +++++++++++++++++++ changelog.d/2887.change.2.rst | 9 +++++++++ 2 files changed, 28 insertions(+) create mode 100644 changelog.d/2887.change.1.rst create mode 100644 changelog.d/2887.change.2.rst diff --git a/changelog.d/2887.change.1.rst b/changelog.d/2887.change.1.rst new file mode 100644 index 00000000..b23be5ab --- /dev/null +++ b/changelog.d/2887.change.1.rst @@ -0,0 +1,19 @@ +Added automatic discovery for ``py_modules`` and ``packages`` +-- by :user:`abravalheri`. + +Setuptools will try to find these values assuming that the package uses either +the *src-layout* (a ``src`` directory containing all the packages or modules), +the *flat-layout* (package directories directly under the project root), +or the *single-module* approach (isolated Python files, directly under +the project root). + +The automatic discovery will also respect layouts that are explicit configured +using the ``package_dir`` option. + +For backward-compatibility, this behavior will be observed **only if both +``py_modules`` and ``packages`` are not explicitly set**. + +If setuptools detects modules or packages that are not supposed to be in the +distribution, please explicitly set ``py_modules`` and ``packages``. +If you are using a *flat-layout*, you can also consider switching to +*src-layout*. diff --git a/changelog.d/2887.change.2.rst b/changelog.d/2887.change.2.rst new file mode 100644 index 00000000..75870e50 --- /dev/null +++ b/changelog.d/2887.change.2.rst @@ -0,0 +1,9 @@ +Added automatic configuration for the ``name`` metadata +-- by :user:`abravalheri`. + +Setuptools will adopt the name of the top-level package (or module in the case +of single-module distributions), **only when ``name`` is not explicitly +provided**. + +Please note that it is not possible to automatically derive a single name when +the distribution consists of multiple top-level packages or modules. -- cgit v1.2.1 From 5754afd7d3ecc19b97f8fe058f61ec505721812b Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 17 Nov 2021 19:03:08 +0000 Subject: Add build as a test dependency --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.cfg b/setup.cfg index 6171f624..871fbff9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -81,6 +81,8 @@ testing-integration = filelock>=3.4.0 + build + docs = # upstream sphinx -- cgit v1.2.1 From c130315b7a2ba59281aa30c01b416f4f1cfb149e Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 17 Nov 2021 19:08:17 +0000 Subject: Exclude subpackages in FlatLayoutPackageFinder --- setuptools/discovery.py | 8 ++++++-- setuptools/tests/test_find_packages.py | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/setuptools/discovery.py b/setuptools/discovery.py index c1d3b0b0..0df69ddb 100644 --- a/setuptools/discovery.py +++ b/setuptools/discovery.py @@ -47,6 +47,8 @@ import _distutils_hack.override # noqa: F401 from distutils import log from distutils.util import convert_path +chain_iter = itertools.chain.from_iterable + def _valid_name(path): # Ignore invalid names that cannot be imported directly @@ -173,7 +175,7 @@ class ModuleFinder(_Finder): class FlatLayoutPackageFinder(PEP420PackageFinder): - DEFAULT_EXCLUDE = ( + _EXCLUDE = ( "doc", "docs", "test", @@ -188,6 +190,8 @@ class FlatLayoutPackageFinder(PEP420PackageFinder): "[._]*", ) + DEFAULT_EXCLUDE = tuple(chain_iter((p, f"{p}.*") for p in _EXCLUDE)) + _looks_like_package = staticmethod(_valid_name) @@ -276,7 +280,7 @@ class ConfigDiscovery: if not package_dir: return False - pkgs = itertools.chain.from_iterable( + pkgs = chain_iter( _find_packages_within(pkg, os.path.join(root_dir, parent_dir)) for pkg, parent_dir in package_dir.items() ) diff --git a/setuptools/tests/test_find_packages.py b/setuptools/tests/test_find_packages.py index f7930e7f..efcce924 100644 --- a/setuptools/tests/test_find_packages.py +++ b/setuptools/tests/test_find_packages.py @@ -219,7 +219,9 @@ class TestFlatLayoutPackageFinder: [ "pkg/__init__.py", "tasks/__init__.py", + "tasks/subpackage/__init__.py", "fabfile/__init__.py", + "fabfile/subpackage/__init__.py", # Sub-packages should always be fine "pkg/tasks/__init__.py", "pkg/fabfile/__init__.py", -- cgit v1.2.1 From 5e507770443cffec4b9687b4426ed96d67239f01 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 17 Nov 2021 19:15:14 +0000 Subject: Fix rst markup on news fragments --- changelog.d/2887.change.1.rst | 4 ++-- changelog.d/2887.change.2.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/changelog.d/2887.change.1.rst b/changelog.d/2887.change.1.rst index b23be5ab..8c513a60 100644 --- a/changelog.d/2887.change.1.rst +++ b/changelog.d/2887.change.1.rst @@ -10,8 +10,8 @@ the project root). The automatic discovery will also respect layouts that are explicit configured using the ``package_dir`` option. -For backward-compatibility, this behavior will be observed **only if both -``py_modules`` and ``packages`` are not explicitly set**. +For backward-compatibility, this behavior will be observed **only if both** +``py_modules`` **and** ``packages`` **are not explicitly set**. If setuptools detects modules or packages that are not supposed to be in the distribution, please explicitly set ``py_modules`` and ``packages``. diff --git a/changelog.d/2887.change.2.rst b/changelog.d/2887.change.2.rst index 75870e50..a6aa041a 100644 --- a/changelog.d/2887.change.2.rst +++ b/changelog.d/2887.change.2.rst @@ -2,7 +2,7 @@ Added automatic configuration for the ``name`` metadata -- by :user:`abravalheri`. Setuptools will adopt the name of the top-level package (or module in the case -of single-module distributions), **only when ``name`` is not explicitly +of single-module distributions), **only when** ``name`` **is not explicitly provided**. Please note that it is not possible to automatically derive a single name when -- cgit v1.2.1 From b545b5322c7dc6d20a75bc517e649855f6ace1b4 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 17 Nov 2021 19:49:36 +0000 Subject: Fix path handling on Windows --- setuptools/discovery.py | 4 +++- setuptools/tests/test_config_discovery.py | 3 ++- setuptools/tests/test_dist.py | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/setuptools/discovery.py b/setuptools/discovery.py index 0df69ddb..0f739344 100644 --- a/setuptools/discovery.py +++ b/setuptools/discovery.py @@ -231,7 +231,7 @@ class ConfigDiscovery: def __init__(self, distribution): self.dist = distribution self._called = False - self._root_dir = distribution.src_root or os.getcwd() + self._root_dir = None # delay so `src_root` can be set in dist def __call__(self, force=False): """Automatically discover missing configuration fields @@ -248,6 +248,8 @@ class ConfigDiscovery: # Avoid overhead of multiple calls return + self._root_dir = self.dist.src_root or os.curdir + self._analyse_package_layout() self._analyse_name() # depends on ``packages`` and ``py_modules`` diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py index 0f86a98f..f13db27d 100644 --- a/setuptools/tests/test_config_discovery.py +++ b/setuptools/tests/test_config_discovery.py @@ -59,7 +59,8 @@ class TestDiscoverPackagesAndPyModules: _populate_project_dir(tmp_path, files, options) here = os.getcwd() - dist = Distribution({**options, "src_root": tmp_path}) + root = "/".join(os.path.split(tmp_path)) # POSIX-style + dist = Distribution({**options, "src_root": root}) dist.script_name = 'setup.py' dist.set_defaults() cmd = sdist(dist) diff --git a/setuptools/tests/test_dist.py b/setuptools/tests/test_dist.py index 39dba4f4..049576a7 100644 --- a/setuptools/tests/test_dist.py +++ b/setuptools/tests/test_dist.py @@ -2,6 +2,7 @@ import io import collections import re import functools +import os import urllib.request import urllib.parse from distutils.errors import DistutilsSetupError @@ -498,7 +499,7 @@ def test_dist_default_name(tmp_path, dist_name, package_dir, package_files): ensure_files(tmp_path, package_files) attrs = { **EXAMPLE_BASE_INFO, - "src_root": str(tmp_path), + "src_root": "/".join(os.path.split(tmp_path)), # POSIX-style "package_dir": package_dir } del attrs["name"] -- cgit v1.2.1 From 5ba27d8d532b9dcb5effaa6beda92d336bbccd05 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 17 Nov 2021 20:50:19 +0000 Subject: Add documentation about automatic discovery --- docs/userguide/package_discovery.rst | 80 ++++++++++++++++++++++++++++++++++-- 1 file changed, 76 insertions(+), 4 deletions(-) diff --git a/docs/userguide/package_discovery.rst b/docs/userguide/package_discovery.rst index 61da2d66..71ee539b 100644 --- a/docs/userguide/package_discovery.rst +++ b/docs/userguide/package_discovery.rst @@ -38,8 +38,80 @@ included manually in the following manner: packages=['mypkg1', 'mypkg2'] ) -This can get tiresome really quickly. To speed things up, we introduce two -functions provided by setuptools: +This can get tiresome really quickly. To speed things up, you can rely on +setuptools automatic discovery, or use the provided functions, as explained in +the following sections. + + +Automatic discovery +=================== + +By default setuptools will consider 2 popular project layouts, each one with +its own set of advantages and disadvantages [#layout1]_ [#layout2]_. + +src-layout: + The project should contain a ``src`` directory under the project root and + all modules and packages meant for distribution are placed inside this + directory. + This layout is very handy when you wish to use automatic discovery, + since you don't have to worry about other Python files or folder in your + project root being distributed by mistake. In some circumstances it can + also less error-prone for testing or when using :pep:`420`-style packages. + On the other hand you cannot rely on the implicit ``PYTHONPATH=.`` to fire + up the Python REPL and play with the your package (you will need an + `editable install`_ to be able to do that). + +flat-layout (also known as "adhoc"): + The package folder(s) are placed directly under the project root. + 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 + of folders or Python files hanging around your project root) + +There is also a variation of the *flat-layout* for utilities/libraries that can +be implemented with a single Python file: + +single-module approach (or "few top-level modules"): + This Python files are placed directly under the project root, + instead of inside a package folder. + +Setuptools will automatically scan your project directory looking for these +layouts and try to guess the correct values for the :doc:`packages +` and :doc:`py_modules `. + +To avoid confusion, file and folder names that are used by popular tools (or +that correspond to well-known conventions, such as distributing documentation +alongside the project code) are automatically filtered out of the +*flat-layout*: + +- reserved package names: + .. autodata:: setuptools.discovery.FlatLayoutPackageFinder.DEFAULT_EXCLUDE + +- reserved top-level module names: +.. autodata:: setuptools.discovery.FlatLayoutModuleFinder.DEFAULT_EXCLUDE + +Also note that you can customise your project layout by explicitly setting +:doc:`package_dir `. + +.. important:: Automatic discovery will **only** be enabled if you don't + provide any configuration for both ``packages`` and ``py_modules``. + If at least one of them is explicitly set, automatic discovery will not take + place. + + +.. [#layout1] https://blog.ionelmc.ro/2014/05/25/python-packaging/#the-structure +.. [#layout2] https://blog.ionelmc.ro/2017/09/25/rehashing-the-src-layout/ + +.. _editable install: https://pip.pypa.io/en/stable/cli/pip_install/#editable-installs + + +Using setuptools functions +========================== + +If the automatic discovery does not work for you +(e.g., you want to *include* in the distribution top-level packages with +reserved names such as ``tasks``, ``example`` or ``docs``, or you want to +*exclude* nested packages that would be otherwise included), you can set up +setuptools to use special functions for the package discovery: .. tab:: setup.cfg @@ -61,7 +133,7 @@ functions provided by setuptools: Using ``find:`` or ``find_packages`` -==================================== +------------------------------------ Let's start with the first tool. ``find:`` (``find_packages``) takes a source directory and two lists of package name patterns to exclude and include, and then return a list of ``str`` representing the packages it could find. To use @@ -113,7 +185,7 @@ in ``src`` that starts with the name ``pkg`` and not ``additional``: .. _Namespace Packages: Using ``find_namespace:`` or ``find_namespace_packages`` -======================================================== +-------------------------------------------------------- ``setuptools`` provides the ``find_namespace:`` (``find_namespace_packages``) which behaves similarly to ``find:`` but works with namespace package. Before diving in, it is important to have a good understanding of what namespace -- cgit v1.2.1 From 2159b25d575fc6ff1908f08dcad8fd527b34e10d Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 17 Nov 2021 21:01:42 +0000 Subject: Attempt to improve autodoc --- docs/userguide/package_discovery.rst | 37 ++++++++++++++++++++++++++++++------ setuptools/discovery.py | 2 ++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/docs/userguide/package_discovery.rst b/docs/userguide/package_discovery.rst index 71ee539b..7e9169bb 100644 --- a/docs/userguide/package_discovery.rst +++ b/docs/userguide/package_discovery.rst @@ -52,7 +52,18 @@ its own set of advantages and disadvantages [#layout1]_ [#layout2]_. src-layout: The project should contain a ``src`` directory under the project root and all modules and packages meant for distribution are placed inside this - directory. + directory:: + + project_root_directory + ├── pyproject.toml + ├── setup.cfg # or setup.py + ├── ... + └── src/ + └── mypkg/ + ├── __init__.py + ├── ... + └── mymodule.py + This layout is very handy when you wish to use automatic discovery, since you don't have to worry about other Python files or folder in your project root being distributed by mistake. In some circumstances it can @@ -62,7 +73,17 @@ src-layout: `editable install`_ to be able to do that). flat-layout (also known as "adhoc"): - The package folder(s) are placed directly under the project root. + The package folder(s) are placed directly under the project root:: + + project_root_directory + ├── pyproject.toml + ├── setup.cfg # or setup.py + ├── ... + └── mypkg/ + ├── __init__.py + ├── ... + └── 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 of folders or Python files hanging around your project root) @@ -72,7 +93,13 @@ be implemented with a single Python file: single-module approach (or "few top-level modules"): This Python files are placed directly under the project root, - instead of inside a package folder. + instead of inside a package folder:: + + project_root_directory + ├── pyproject.toml + ├── setup.cfg # or setup.py + ├── ... + └── single_file_lib.py Setuptools will automatically scan your project directory looking for these layouts and try to guess the correct values for the :doc:`packages @@ -83,10 +110,8 @@ that correspond to well-known conventions, such as distributing documentation alongside the project code) are automatically filtered out of the *flat-layout*: -- reserved package names: - .. autodata:: setuptools.discovery.FlatLayoutPackageFinder.DEFAULT_EXCLUDE +.. autodata:: setuptools.discovery.FlatLayoutPackageFinder.DEFAULT_EXCLUDE -- reserved top-level module names: .. autodata:: setuptools.discovery.FlatLayoutModuleFinder.DEFAULT_EXCLUDE Also note that you can customise your project layout by explicitly setting diff --git a/setuptools/discovery.py b/setuptools/discovery.py index 0f739344..d8aa6d24 100644 --- a/setuptools/discovery.py +++ b/setuptools/discovery.py @@ -191,6 +191,7 @@ class FlatLayoutPackageFinder(PEP420PackageFinder): ) DEFAULT_EXCLUDE = tuple(chain_iter((p, f"{p}.*") for p in _EXCLUDE)) + """Reserved package names""" _looks_like_package = staticmethod(_valid_name) @@ -216,6 +217,7 @@ class FlatLayoutModuleFinder(ModuleFinder): # ---- Hidden files/Private modules ---- "[._]*", ) + """Reserved top-level module names""" def _find_packages_within(root_pkg, pkg_dir): -- cgit v1.2.1 From cc0110e3eb1549f780bd3be3e4dfd68441d26db4 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 17 Nov 2021 21:04:34 +0000 Subject: Improve news fragment --- changelog.d/2887.change.1.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/changelog.d/2887.change.1.rst b/changelog.d/2887.change.1.rst index 8c513a60..11d7a716 100644 --- a/changelog.d/2887.change.1.rst +++ b/changelog.d/2887.change.1.rst @@ -7,13 +7,14 @@ the *flat-layout* (package directories directly under the project root), or the *single-module* approach (isolated Python files, directly under the project root). -The automatic discovery will also respect layouts that are explicit configured -using the ``package_dir`` option. +The automatic discovery will also respect layouts that are explicitly +configured using the ``package_dir`` option. For backward-compatibility, this behavior will be observed **only if both** -``py_modules`` **and** ``packages`` **are not explicitly set**. +``py_modules`` **and** ``packages`` **are not set**. If setuptools detects modules or packages that are not supposed to be in the -distribution, please explicitly set ``py_modules`` and ``packages``. +distribution, please manually set ``py_modules`` and ``packages`` in your +``setup.cfg`` or ``setup.py`` file. If you are using a *flat-layout*, you can also consider switching to *src-layout*. -- cgit v1.2.1 From 45e2c439a4b31bcf21086e1805a48281d899aa31 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 17 Nov 2021 21:06:22 +0000 Subject: Small doc improvement for package_discovery --- docs/userguide/package_discovery.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/userguide/package_discovery.rst b/docs/userguide/package_discovery.rst index 7e9169bb..5ab854b8 100644 --- a/docs/userguide/package_discovery.rst +++ b/docs/userguide/package_discovery.rst @@ -107,8 +107,8 @@ layouts and try to guess the correct values for the :doc:`packages To avoid confusion, file and folder names that are used by popular tools (or that correspond to well-known conventions, such as distributing documentation -alongside the project code) are automatically filtered out of the -*flat-layout*: +alongside the project code) are automatically filtered in the case of +*flat-layouts*: .. autodata:: setuptools.discovery.FlatLayoutPackageFinder.DEFAULT_EXCLUDE -- cgit v1.2.1 From 5d4ce7df84cc92d316934083bf50ecfc30ee6b83 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 17 Nov 2021 21:11:49 +0000 Subject: Improve autodoc for finder exclude --- docs/userguide/package_discovery.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/userguide/package_discovery.rst b/docs/userguide/package_discovery.rst index 5ab854b8..a6b5061c 100644 --- a/docs/userguide/package_discovery.rst +++ b/docs/userguide/package_discovery.rst @@ -110,9 +110,9 @@ that correspond to well-known conventions, such as distributing documentation alongside the project code) are automatically filtered in the case of *flat-layouts*: -.. autodata:: setuptools.discovery.FlatLayoutPackageFinder.DEFAULT_EXCLUDE +.. autoattribute:: setuptools.discovery.FlatLayoutPackageFinder.DEFAULT_EXCLUDE -.. autodata:: setuptools.discovery.FlatLayoutModuleFinder.DEFAULT_EXCLUDE +.. autoattribute:: setuptools.discovery.FlatLayoutModuleFinder.DEFAULT_EXCLUDE Also note that you can customise your project layout by explicitly setting :doc:`package_dir `. @@ -123,12 +123,6 @@ Also note that you can customise your project layout by explicitly setting place. -.. [#layout1] https://blog.ionelmc.ro/2014/05/25/python-packaging/#the-structure -.. [#layout2] https://blog.ionelmc.ro/2017/09/25/rehashing-the-src-layout/ - -.. _editable install: https://pip.pypa.io/en/stable/cli/pip_install/#editable-installs - - Using setuptools functions ========================== @@ -346,3 +340,9 @@ file contains the following: __path__ = __import__('pkgutil').extend_path(__path__, __name__) The project layout remains the same and ``setup.cfg`` remains the same. + + +.. [#layout1] https://blog.ionelmc.ro/2014/05/25/python-packaging/#the-structure +.. [#layout2] https://blog.ionelmc.ro/2017/09/25/rehashing-the-src-layout/ + +.. _editable install: https://pip.pypa.io/en/stable/cli/pip_install/#editable-installs -- cgit v1.2.1 From 4ac3ec5fef406b4bb5011455a4cf840e3dbf648e Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 17 Nov 2021 21:17:32 +0000 Subject: Improve text in package discovery docs --- docs/userguide/package_discovery.rst | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/userguide/package_discovery.rst b/docs/userguide/package_discovery.rst index a6b5061c..5a9a468e 100644 --- a/docs/userguide/package_discovery.rst +++ b/docs/userguide/package_discovery.rst @@ -39,7 +39,7 @@ included manually in the following manner: ) This can get tiresome really quickly. To speed things up, you can rely on -setuptools automatic discovery, or use the provided functions, as explained in +setuptools automatic discovery, or use the provided tools, as explained in the following sections. @@ -88,12 +88,12 @@ flat-layout (also known as "adhoc"): it can be 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) -There is also a variation of the *flat-layout* for utilities/libraries that can -be implemented with a single Python file: +There is also a handy variation of the *flat-layout* for utilities/libraries +that can be implemented with a single Python file: single-module approach (or "few top-level modules"): - This Python files are placed directly under the project root, - instead of inside a package folder:: + Modules are placed directly under the project root, instead of inside + a package folder:: project_root_directory ├── pyproject.toml @@ -103,7 +103,7 @@ single-module approach (or "few top-level modules"): Setuptools will automatically scan your project directory looking for these layouts and try to guess the correct values for the :doc:`packages -` and :doc:`py_modules `. +` and :doc:`py_modules ` configuration. To avoid confusion, file and folder names that are used by popular tools (or that correspond to well-known conventions, such as distributing documentation @@ -123,14 +123,14 @@ Also note that you can customise your project layout by explicitly setting place. -Using setuptools functions -========================== +Custom discovery +================ If the automatic discovery does not work for you (e.g., you want to *include* in the distribution top-level packages with reserved names such as ``tasks``, ``example`` or ``docs``, or you want to -*exclude* nested packages that would be otherwise included), you can set up -setuptools to use special functions for the package discovery: +*exclude* nested packages that would be otherwise included), you can use +the provided tools for package discovery: .. tab:: setup.cfg -- cgit v1.2.1 From 6fc5fb0c648b8c9c9badcb20a55e93c0b4373219 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 17 Nov 2021 21:41:38 +0000 Subject: Add examples for package_dir customisation --- docs/userguide/package_discovery.rst | 45 +++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/docs/userguide/package_discovery.rst b/docs/userguide/package_discovery.rst index 5a9a468e..4e130e67 100644 --- a/docs/userguide/package_discovery.rst +++ b/docs/userguide/package_discovery.rst @@ -115,7 +115,50 @@ alongside the project code) are automatically filtered in the case of .. autoattribute:: setuptools.discovery.FlatLayoutModuleFinder.DEFAULT_EXCLUDE Also note that you can customise your project layout by explicitly setting -:doc:`package_dir `. +``package_dir``: + +.. tab:: setup.cfg + + .. code-block:: ini + + [options] + # ... + package_dir = + = lib + # similar to "src-layout" but using the "lib" folder + # pkg.mod corresponds to lib/pkg/mod.py + # OR + package_dir = + pkg1 = lib1 + # pkg1.mod corresponds to lib1/mod.py + # pkg1.subpkg.mod corresponds to lib1/subpkg/mod.py + pkg2 = lib2 + # pkg2.mod corresponds to lib2/mod.py + pkg2.subpkg = lib3 + # pkg2.subpkg.mod corresponds to lib3/mod.py + +.. tab:: setup.py + + .. code-block:: python + + setup( + # ... + package_dir = {"": "pkg"} + # similar to "src-layout" but using the "pkg" folder + # mylib.mod corresponds to pkg/mylib/mod.py + ) + + # OR + + setup( + # ... + package_dir = { + "pkg1": "lib1", # pkg1.mod corresponds to lib1/mod.py + # pkg1.subpkg.mod corresponds to lib1/subpkg/mod.py + "pkg2": "lib2", # pkg2.mod corresponds to lib2/mod.py + "pkg2.subpkg": "lib3" # pkg2.subpkg.mod corresponds to lib3/mod.py + # ... + ) .. important:: Automatic discovery will **only** be enabled if you don't provide any configuration for both ``packages`` and ``py_modules``. -- cgit v1.2.1 From 48828eabfc1de7ce44b44395a80f98d9ebe50ea7 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 17 Nov 2021 22:21:30 +0000 Subject: Sync setup.cfg/.py examples for package_dir --- docs/userguide/package_discovery.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/userguide/package_discovery.rst b/docs/userguide/package_discovery.rst index 4e130e67..2c466715 100644 --- a/docs/userguide/package_discovery.rst +++ b/docs/userguide/package_discovery.rst @@ -143,9 +143,9 @@ Also note that you can customise your project layout by explicitly setting setup( # ... - package_dir = {"": "pkg"} - # similar to "src-layout" but using the "pkg" folder - # mylib.mod corresponds to pkg/mylib/mod.py + package_dir = {"": "lib"} + # similar to "src-layout" but using the "lib" folder + # pkg.mod corresponds to lib/pkg/mod.py ) # OR -- cgit v1.2.1 From e5a0f0fa9bf69f83204694f780363aa07b724683 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 17 Nov 2021 22:21:30 +0000 Subject: Small fixes for text in package_discovery --- docs/userguide/package_discovery.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/userguide/package_discovery.rst b/docs/userguide/package_discovery.rst index 2c466715..afd4f576 100644 --- a/docs/userguide/package_discovery.rst +++ b/docs/userguide/package_discovery.rst @@ -65,11 +65,11 @@ src-layout: └── mymodule.py This layout is very handy when you wish to use automatic discovery, - since you don't have to worry about other Python files or folder in your - project root being distributed by mistake. In some circumstances it can + since you don't have to worry about other Python files or folders in your + project root being distributed by mistake. In some circumstances it can be also less error-prone for testing or when using :pep:`420`-style packages. On the other hand you cannot rely on the implicit ``PYTHONPATH=.`` to fire - up the Python REPL and play with the your package (you will need an + up the Python REPL and play with your package (you will need an `editable install`_ to be able to do that). flat-layout (also known as "adhoc"): @@ -92,8 +92,8 @@ There is also a handy variation of the *flat-layout* for utilities/libraries that can be implemented with a single Python file: single-module approach (or "few top-level modules"): - Modules are placed directly under the project root, instead of inside - a package folder:: + Standalone modules are placed directly under the project root, instead of + inside a package folder:: project_root_directory ├── pyproject.toml @@ -102,8 +102,8 @@ single-module approach (or "few top-level modules"): └── single_file_lib.py Setuptools will automatically scan your project directory looking for these -layouts and try to guess the correct values for the :doc:`packages -` and :doc:`py_modules ` configuration. +layouts and try to guess the correct values for the :ref:`packages ` and :doc:`py_modules ` configuration. To avoid confusion, file and folder names that are used by popular tools (or that correspond to well-known conventions, such as distributing documentation -- cgit v1.2.1 From 513cc87b0db2830c75196b3c1d51fea87819c7ff Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 18 Nov 2021 12:54:39 +0000 Subject: Exclude 'bin' dir in discovery --- setuptools/discovery.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setuptools/discovery.py b/setuptools/discovery.py index d8aa6d24..f183a6b1 100644 --- a/setuptools/discovery.py +++ b/setuptools/discovery.py @@ -176,6 +176,7 @@ class ModuleFinder(_Finder): class FlatLayoutPackageFinder(PEP420PackageFinder): _EXCLUDE = ( + "bin", "doc", "docs", "test", -- cgit v1.2.1 From 9d62dd794936c219e45ccca6bfcc0e25ec78792b Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 19 Nov 2021 16:03:26 +0000 Subject: Add news fragment with instructions for empty distributions --- changelog.d/2894.breaking.rst | 4 ++++ docs/userguide/package_discovery.rst | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelog.d/2894.breaking.rst diff --git a/changelog.d/2894.breaking.rst b/changelog.d/2894.breaking.rst new file mode 100644 index 00000000..111f9d30 --- /dev/null +++ b/changelog.d/2894.breaking.rst @@ -0,0 +1,4 @@ +If you purposefully want to create an *"empty distribution"*, please be aware +that some Python files (or general folders) might be automatically detected and +included. You can check details about the automatic discovery behaviour (and +how to configure a different one) in :doc:`/userguide/package_discovery`. diff --git a/docs/userguide/package_discovery.rst b/docs/userguide/package_discovery.rst index afd4f576..99e45bed 100644 --- a/docs/userguide/package_discovery.rst +++ b/docs/userguide/package_discovery.rst @@ -107,7 +107,7 @@ config>` and :doc:`py_modules ` configuration. To avoid confusion, file and folder names that are used by popular tools (or that correspond to well-known conventions, such as distributing documentation -alongside the project code) are automatically filtered in the case of +alongside the project code) are automatically filtered out in the case of *flat-layouts*: .. autoattribute:: setuptools.discovery.FlatLayoutPackageFinder.DEFAULT_EXCLUDE -- cgit v1.2.1 From 4630f42f28be3a508105ca4bb887699074b30e39 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 7 Feb 2022 19:30:29 +0000 Subject: Remove repeated dependency --- setup.cfg | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 871fbff9..b27edb92 100644 --- a/setup.cfg +++ b/setup.cfg @@ -80,9 +80,6 @@ testing-integration = build[virtualenv] filelock>=3.4.0 - - build - docs = # upstream sphinx -- cgit v1.2.1 From 3b17401988033654bf71ed4a22742cb67e62f945 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 7 Feb 2022 19:38:39 +0000 Subject: Ignore some other folders and files by default --- setuptools/discovery.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setuptools/discovery.py b/setuptools/discovery.py index f183a6b1..dbed63db 100644 --- a/setuptools/discovery.py +++ b/setuptools/discovery.py @@ -179,10 +179,13 @@ class FlatLayoutPackageFinder(PEP420PackageFinder): "bin", "doc", "docs", + "documentation", "test", "tests", "example", "examples", + "scripts", + "tools", # ---- Task runners / Build tools ---- "tasks", # invoke "fabfile", # fabric @@ -205,6 +208,7 @@ class FlatLayoutModuleFinder(ModuleFinder): "tests", "example", "examples", + "build", # ---- Task runners ---- "noxfile", "pavement", -- cgit v1.2.1 From f0b1de18a998262590ca3feec0dffbc0f83c479b Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 12 Feb 2022 18:35:55 +0000 Subject: Reuse integration helper --- setuptools/tests/integration/helpers.py | 14 +++++++++++ setuptools/tests/test_config_discovery.py | 40 ++++--------------------------- 2 files changed, 18 insertions(+), 36 deletions(-) diff --git a/setuptools/tests/integration/helpers.py b/setuptools/tests/integration/helpers.py index 43f43902..24c02be0 100644 --- a/setuptools/tests/integration/helpers.py +++ b/setuptools/tests/integration/helpers.py @@ -8,6 +8,7 @@ import os import subprocess import tarfile from zipfile import ZipFile +from pathlib import Path def run(cmd, env=None): @@ -59,3 +60,16 @@ class Archive: raise ValueError(msg) return str(content.read(), "utf-8") return str(self._obj.read(zip_or_tar_info), "utf-8") + + +def get_sdist_members(sdist_path): + with tarfile.open(sdist_path, "r:gz") as tar: + files = [Path(f) for f in tar.getnames()] + # remove root folder + relative_files = ("/".join(f.parts[1:]) for f in files) + return {f for f in relative_files if f} + + +def get_wheel_members(wheel_path): + with ZipFile(wheel_path) as zipfile: + return set(zipfile.namelist()) diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py index f13db27d..363b8248 100644 --- a/setuptools/tests/test_config_discovery.py +++ b/setuptools/tests/test_config_discovery.py @@ -1,11 +1,6 @@ import os -import subprocess import sys -import tarfile from configparser import ConfigParser -from pathlib import Path -from subprocess import CalledProcessError -from zipfile import ZipFile import pytest @@ -14,6 +9,7 @@ from setuptools.dist import Distribution from .contexts import quiet from .test_find_packages import ensure_files +from .integration.helpers import get_sdist_members, get_wheel_members, run class TestDiscoverPackagesAndPyModules: @@ -85,12 +81,12 @@ class TestDiscoverPackagesAndPyModules: _run_build(tmp_path) - sdist_files = _get_sdist_members(next(tmp_path.glob("dist/*.tar.gz"))) + sdist_files = get_sdist_members(next(tmp_path.glob("dist/*.tar.gz"))) print("~~~~~ sdist_members ~~~~~") print('\n'.join(sdist_files)) assert sdist_files >= set(files) - wheel_files = _get_wheel_members(next(tmp_path.glob("dist/*.whl"))) + wheel_files = get_wheel_members(next(tmp_path.glob("dist/*.whl"))) print("~~~~~ wheel_members ~~~~~") print('\n'.join(wheel_files)) assert wheel_files >= {f.replace("src/", "") for f in files} @@ -145,34 +141,6 @@ def _write_setupcfg(root, options): print((root / "setup.cfg").read_text()) -def _get_sdist_members(sdist_path): - with tarfile.open(sdist_path, "r:gz") as tar: - files = [Path(f) for f in tar.getnames()] - relative_files = ("/".join(f.parts[1:]) for f in files) - # remove root folder - return {f for f in relative_files if f} - - -def _get_wheel_members(wheel_path): - with ZipFile(wheel_path) as zipfile: - return set(zipfile.namelist()) - - def _run_build(path, *flags): cmd = [sys.executable, "-m", "build", "--no-isolation", *flags, str(path)] - r = subprocess.run( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True, - env={**os.environ, 'DISTUTILS_DEBUG': '1'} - ) - out = r.stdout + "\n" + r.stderr - print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") - print("Command", repr(cmd), "returncode", r.returncode) - print(out) - map(print, path.glob("*")) - - if r.returncode != 0: - raise CalledProcessError(r.returncode, cmd, r.stdout, r.stderr) - return out + return run(cmd, env={'DISTUTILS_DEBUG': '1'}) -- cgit v1.2.1 From 2b8933ac58d3fb5c24d0868e1268b0d74cd57f0a Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 12 Feb 2022 18:36:22 +0000 Subject: Avoid importing a test inside other test --- setuptools/tests/test_config_discovery.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py index 363b8248..01ccad50 100644 --- a/setuptools/tests/test_config_discovery.py +++ b/setuptools/tests/test_config_discovery.py @@ -8,7 +8,6 @@ from setuptools.command.sdist import sdist from setuptools.dist import Distribution from .contexts import quiet -from .test_find_packages import ensure_files from .integration.helpers import get_sdist_members, get_wheel_members, run @@ -118,7 +117,10 @@ def _populate_project_dir(root, files, options): (root / "README.md").write_text("# Example Package") (root / "LICENSE").write_text("Copyright (c) 2018") _write_setupcfg(root, options) - ensure_files(root, files) + paths = (root / f for f in files) + for path in paths: + path.parent.mkdir(exist_ok=True, parents=True) + path.touch() def _write_setupcfg(root, options): -- cgit v1.2.1 From b58e76892130c189362218f28a054448da6ff752 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sun, 20 Feb 2022 12:42:19 +0000 Subject: Ignore build and dist folders in flat-layout --- setuptools/discovery.py | 2 ++ setuptools/tests/test_config_discovery.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/setuptools/discovery.py b/setuptools/discovery.py index dbed63db..5ad6d8f1 100644 --- a/setuptools/discovery.py +++ b/setuptools/discovery.py @@ -186,6 +186,8 @@ class FlatLayoutPackageFinder(PEP420PackageFinder): "examples", "scripts", "tools", + "build", + "dist", # ---- Task runners / Build tools ---- "tasks", # invoke "fabfile", # fabric diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py index 01ccad50..c27ee319 100644 --- a/setuptools/tests/test_config_discovery.py +++ b/setuptools/tests/test_config_discovery.py @@ -78,6 +78,16 @@ class TestDiscoverPackagesAndPyModules: files, options = self._get_info(circumstance) _populate_project_dir(tmp_path, files, options) + # Simulate a pre-existing `build` directory + (tmp_path / "build").mkdir() + (tmp_path / "build/lib").mkdir() + (tmp_path / "build/bdist.linux-x86_64").mkdir() + (tmp_path / "build/bdist.linux-x86_64/file.py").touch() + (tmp_path / "build/lib/__init__.py").touch() + (tmp_path / "build/lib/file.py").touch() + (tmp_path / "dist").mkdir() + (tmp_path / "dist/file.py").touch() + _run_build(tmp_path) sdist_files = get_sdist_members(next(tmp_path.glob("dist/*.tar.gz"))) @@ -90,6 +100,11 @@ class TestDiscoverPackagesAndPyModules: print('\n'.join(wheel_files)) assert wheel_files >= {f.replace("src/", "") for f in files} + # Make sure build files are not included by mistake + for file in wheel_files: + assert "build" not in files + assert "dist" not in files + class TestNoConfig: DEFAULT_VERSION = "0.0.0" # Default version given by setuptools -- cgit v1.2.1 From 82b4ed83cbb12aea662b0ca5be275f38ab18bc2b Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sun, 20 Feb 2022 16:02:01 +0000 Subject: Improve news fragment about breaking change --- changelog.d/2894.breaking.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/changelog.d/2894.breaking.rst b/changelog.d/2894.breaking.rst index 111f9d30..687ae511 100644 --- a/changelog.d/2894.breaking.rst +++ b/changelog.d/2894.breaking.rst @@ -1,4 +1,10 @@ If you purposefully want to create an *"empty distribution"*, please be aware that some Python files (or general folders) might be automatically detected and -included. You can check details about the automatic discovery behaviour (and +included. + +Projects that currently don't specify both ``packages`` and ``py_modules`` in their +configuration and have extra Python files and folders (not meant for distribution), +might see these files being included in the wheel archive. + +You can check details about the automatic discovery behaviour (and how to configure a different one) in :doc:`/userguide/package_discovery`. -- cgit v1.2.1 From ed3b7c3f4afb19df5ea4c5de61a5bff118dec5a6 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sun, 20 Feb 2022 17:33:39 +0000 Subject: Don't overwrite if the user specifies empty packages/py_modules --- setuptools/discovery.py | 2 +- setuptools/tests/test_config_discovery.py | 58 +++++++++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/setuptools/discovery.py b/setuptools/discovery.py index 5ad6d8f1..9073f660 100644 --- a/setuptools/discovery.py +++ b/setuptools/discovery.py @@ -265,7 +265,7 @@ class ConfigDiscovery: self._called = True def _analyse_package_layout(self): - if self.dist.packages or self.dist.py_modules: + if self.dist.packages is not None or self.dist.py_modules is not None: # For backward compatibility, just try to find modules/packages # when nothing is given return None diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py index c27ee319..e4ccc648 100644 --- a/setuptools/tests/test_config_discovery.py +++ b/setuptools/tests/test_config_discovery.py @@ -1,14 +1,16 @@ import os import sys from configparser import ConfigParser - -import pytest +from itertools import product from setuptools.command.sdist import sdist from setuptools.dist import Distribution +import pytest + from .contexts import quiet from .integration.helpers import get_sdist_members, get_wheel_members, run +from .textwrap import DALS class TestDiscoverPackagesAndPyModules: @@ -105,6 +107,58 @@ class TestDiscoverPackagesAndPyModules: assert "build" not in files assert "dist" not in files + PURPOSEFULLY_EMPY = { + "setup.cfg": DALS( + """ + [metadata] + name = myproj + version = 0.0.0 + + [options] + {param} = + """ + ), + "setup.py": DALS( + """ + __import__('setuptools').setup( + name="myproj", + version="0.0.0", + {param}=[] + ) + """ + ), + "pyproject.toml": DALS( + """ + [build-system] + requires = [] + build-backend = 'setuptools.build_meta' + """ + ) + } + + @pytest.mark.parametrize( + "config_file, param, circumstance", + product(["setup.cfg", "setup.py"], ["packages", "py_modules"], FILES.keys()) + ) + def test_purposefully_empty(self, tmp_path, config_file, param, circumstance): + files = self.FILES[circumstance] + _populate_project_dir(tmp_path, files, {}) + config = self.PURPOSEFULLY_EMPY[config_file].format(param=param) + (tmp_path / config_file).write_text(config) + + # Make sure build works with or without setup.cfg + pyproject = self.PURPOSEFULLY_EMPY["pyproject.toml"] + (tmp_path / "pyproject.toml").write_text(pyproject) + + _run_build(tmp_path) + + wheel_files = get_wheel_members(next(tmp_path.glob("dist/*.whl"))) + print("~~~~~ wheel_members ~~~~~") + print('\n'.join(wheel_files)) + for file in files: + name = file.replace("src/", "") + assert name not in wheel_files + class TestNoConfig: DEFAULT_VERSION = "0.0.0" # Default version given by setuptools -- cgit v1.2.1 From f39edae1951af486347e632dcd535ded9dcebfaf Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 23 Feb 2022 01:42:49 +0000 Subject: Test auto-discovery with explicit variation of src layout --- setuptools/tests/test_config_discovery.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py index e4ccc648..2215cddb 100644 --- a/setuptools/tests/test_config_discovery.py +++ b/setuptools/tests/test_config_discovery.py @@ -23,6 +23,9 @@ class TestDiscoverPackagesAndPyModules: "package_dir": {"": "src"}, "packages": ["pkg"] }, + "variation-lib": { + "package_dir": {"": "lib"}, # variation of the source-layout + }, "explicit-flat": { "packages": ["pkg"] }, @@ -39,6 +42,7 @@ class TestDiscoverPackagesAndPyModules: } FILES = { "src": ["src/pkg/__init__.py", "src/pkg/main.py"], + "lib": ["lib/pkg/__init__.py", "lib/pkg/main.py"], "flat": ["pkg/__init__.py", "pkg/main.py"], "single_module": ["pkg.py"], "namespace": ["ns/pkg/__init__.py"] @@ -100,7 +104,8 @@ class TestDiscoverPackagesAndPyModules: wheel_files = get_wheel_members(next(tmp_path.glob("dist/*.whl"))) print("~~~~~ wheel_members ~~~~~") print('\n'.join(wheel_files)) - assert wheel_files >= {f.replace("src/", "") for f in files} + orig_files = {f.replace("src/", "").replace("lib/", "") for f in files} + assert wheel_files >= orig_files # Make sure build files are not included by mistake for file in wheel_files: -- cgit v1.2.1 From 49b7a60050836868ecd63dc38ad0729626a356f3 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 1 Dec 2021 20:07:30 +0000 Subject: Rename `config` to `config.setupcfg` This will facilitate the implementation of other configuration formats (such as pyproject.toml as initially defined by PEP 621) --- setuptools/config.py | 751 ------------------------- setuptools/config/__init__.py | 11 + setuptools/config/setupcfg.py | 751 +++++++++++++++++++++++++ setuptools/tests/config/__init__.py | 0 setuptools/tests/config/test_setupcfg.py | 919 +++++++++++++++++++++++++++++++ setuptools/tests/test_config.py | 919 ------------------------------- 6 files changed, 1681 insertions(+), 1670 deletions(-) delete mode 100644 setuptools/config.py create mode 100644 setuptools/config/__init__.py create mode 100644 setuptools/config/setupcfg.py create mode 100644 setuptools/tests/config/__init__.py create mode 100644 setuptools/tests/config/test_setupcfg.py delete mode 100644 setuptools/tests/test_config.py diff --git a/setuptools/config.py b/setuptools/config.py deleted file mode 100644 index b4e968e5..00000000 --- a/setuptools/config.py +++ /dev/null @@ -1,751 +0,0 @@ -import ast -import io -import os -import sys - -import warnings -import functools -import importlib -from collections import defaultdict -from functools import partial -from functools import wraps -from glob import iglob -import contextlib - -from distutils.errors import DistutilsOptionError, DistutilsFileError -from setuptools.extern.packaging.version import Version, InvalidVersion -from setuptools.extern.packaging.specifiers import SpecifierSet - - -class StaticModule: - """ - Attempt to load the module by the name - """ - - def __init__(self, name): - spec = importlib.util.find_spec(name) - with open(spec.origin) as strm: - src = strm.read() - module = ast.parse(src) - vars(self).update(locals()) - del self.self - - def __getattr__(self, attr): - try: - return next( - ast.literal_eval(statement.value) - for statement in self.module.body - if isinstance(statement, ast.Assign) - for target in statement.targets - if isinstance(target, ast.Name) and target.id == attr - ) - except Exception as e: - raise AttributeError( - "{self.name} has no attribute {attr}".format(**locals()) - ) from e - - -@contextlib.contextmanager -def patch_path(path): - """ - Add path to front of sys.path for the duration of the context. - """ - try: - sys.path.insert(0, path) - yield - finally: - sys.path.remove(path) - - -def read_configuration(filepath, find_others=False, ignore_option_errors=False): - """Read given configuration file and returns options from it as a dict. - - :param str|unicode filepath: Path to configuration file - to get options from. - - :param bool find_others: Whether to search for other configuration files - which could be on in various places. - - :param bool ignore_option_errors: Whether to silently ignore - options, values of which could not be resolved (e.g. due to exceptions - in directives such as file:, attr:, etc.). - If False exceptions are propagated as expected. - - :rtype: dict - """ - from setuptools.dist import Distribution, _Distribution - - filepath = os.path.abspath(filepath) - - if not os.path.isfile(filepath): - raise DistutilsFileError('Configuration file %s does not exist.' % filepath) - - current_directory = os.getcwd() - os.chdir(os.path.dirname(filepath)) - - try: - dist = Distribution() - - filenames = dist.find_config_files() if find_others else [] - if filepath not in filenames: - filenames.append(filepath) - - _Distribution.parse_config_files(dist, filenames=filenames) - - handlers = parse_configuration( - dist, dist.command_options, ignore_option_errors=ignore_option_errors - ) - - finally: - os.chdir(current_directory) - - return configuration_to_dict(handlers) - - -def _get_option(target_obj, key): - """ - Given a target object and option key, get that option from - the target object, either through a get_{key} method or - from an attribute directly. - """ - getter_name = 'get_{key}'.format(**locals()) - by_attribute = functools.partial(getattr, target_obj, key) - getter = getattr(target_obj, getter_name, by_attribute) - return getter() - - -def configuration_to_dict(handlers): - """Returns configuration data gathered by given handlers as a dict. - - :param list[ConfigHandler] handlers: Handlers list, - usually from parse_configuration() - - :rtype: dict - """ - config_dict = defaultdict(dict) - - for handler in handlers: - for option in handler.set_options: - value = _get_option(handler.target_obj, option) - config_dict[handler.section_prefix][option] = value - - return config_dict - - -def parse_configuration(distribution, command_options, ignore_option_errors=False): - """Performs additional parsing of configuration options - for a distribution. - - Returns a list of used option handlers. - - :param Distribution distribution: - :param dict command_options: - :param bool ignore_option_errors: Whether to silently ignore - options, values of which could not be resolved (e.g. due to exceptions - in directives such as file:, attr:, etc.). - If False exceptions are propagated as expected. - :rtype: list - """ - options = ConfigOptionsHandler(distribution, command_options, ignore_option_errors) - options.parse() - - meta = ConfigMetadataHandler( - distribution.metadata, - command_options, - ignore_option_errors, - distribution.package_dir, - ) - meta.parse() - - return meta, options - - -class ConfigHandler: - """Handles metadata supplied in configuration files.""" - - section_prefix = None - """Prefix for config sections handled by this handler. - Must be provided by class heirs. - - """ - - aliases = {} - """Options aliases. - For compatibility with various packages. E.g.: d2to1 and pbr. - Note: `-` in keys is replaced with `_` by config parser. - - """ - - def __init__(self, target_obj, options, ignore_option_errors=False): - sections = {} - - section_prefix = self.section_prefix - for section_name, section_options in options.items(): - if not section_name.startswith(section_prefix): - continue - - section_name = section_name.replace(section_prefix, '').strip('.') - sections[section_name] = section_options - - self.ignore_option_errors = ignore_option_errors - self.target_obj = target_obj - self.sections = sections - self.set_options = [] - - @property - def parsers(self): - """Metadata item name to parser function mapping.""" - raise NotImplementedError( - '%s must provide .parsers property' % self.__class__.__name__ - ) - - def __setitem__(self, option_name, value): - unknown = tuple() - target_obj = self.target_obj - - # Translate alias into real name. - option_name = self.aliases.get(option_name, option_name) - - current_value = getattr(target_obj, option_name, unknown) - - if current_value is unknown: - raise KeyError(option_name) - - if current_value: - # Already inhabited. Skipping. - return - - skip_option = False - parser = self.parsers.get(option_name) - if parser: - try: - value = parser(value) - - except Exception: - skip_option = True - if not self.ignore_option_errors: - raise - - if skip_option: - return - - setter = getattr(target_obj, 'set_%s' % option_name, None) - if setter is None: - setattr(target_obj, option_name, value) - else: - setter(value) - - self.set_options.append(option_name) - - @classmethod - def _parse_list(cls, value, separator=','): - """Represents value as a list. - - Value is split either by separator (defaults to comma) or by lines. - - :param value: - :param separator: List items separator character. - :rtype: list - """ - if isinstance(value, list): # _get_parser_compound case - return value - - if '\n' in value: - value = value.splitlines() - else: - value = value.split(separator) - - return [chunk.strip() for chunk in value if chunk.strip()] - - @classmethod - def _parse_list_glob(cls, value, separator=','): - """Equivalent to _parse_list() but expands any glob patterns using glob(). - - However, unlike with glob() calls, the results remain relative paths. - - :param value: - :param separator: List items separator character. - :rtype: list - """ - glob_characters = ('*', '?', '[', ']', '{', '}') - values = cls._parse_list(value, separator=separator) - expanded_values = [] - for value in values: - - # Has globby characters? - if any(char in value for char in glob_characters): - # then expand the glob pattern while keeping paths *relative*: - expanded_values.extend(sorted( - os.path.relpath(path, os.getcwd()) - for path in iglob(os.path.abspath(value)))) - - else: - # take the value as-is: - expanded_values.append(value) - - return expanded_values - - @classmethod - def _parse_dict(cls, value): - """Represents value as a dict. - - :param value: - :rtype: dict - """ - separator = '=' - result = {} - for line in cls._parse_list(value): - key, sep, val = line.partition(separator) - if sep != separator: - raise DistutilsOptionError( - 'Unable to parse option value to dict: %s' % value - ) - result[key.strip()] = val.strip() - - return result - - @classmethod - def _parse_bool(cls, value): - """Represents value as boolean. - - :param value: - :rtype: bool - """ - value = value.lower() - return value in ('1', 'true', 'yes') - - @classmethod - def _exclude_files_parser(cls, key): - """Returns a parser function to make sure field inputs - are not files. - - Parses a value after getting the key so error messages are - more informative. - - :param key: - :rtype: callable - """ - - def parser(value): - exclude_directive = 'file:' - if value.startswith(exclude_directive): - raise ValueError( - 'Only strings are accepted for the {0} field, ' - 'files are not accepted'.format(key) - ) - return value - - return parser - - @classmethod - def _parse_file(cls, value): - """Represents value as a string, allowing including text - from nearest files using `file:` directive. - - Directive is sandboxed and won't reach anything outside - directory with setup.py. - - Examples: - file: README.rst, CHANGELOG.md, src/file.txt - - :param str value: - :rtype: str - """ - include_directive = 'file:' - - if not isinstance(value, str): - return value - - if not value.startswith(include_directive): - return value - - spec = value[len(include_directive) :] - filepaths = (os.path.abspath(path.strip()) for path in spec.split(',')) - return '\n'.join( - cls._read_file(path) - for path in filepaths - if (cls._assert_local(path) or True) and os.path.isfile(path) - ) - - @staticmethod - def _assert_local(filepath): - if not filepath.startswith(os.getcwd()): - raise DistutilsOptionError('`file:` directive can not access %s' % filepath) - - @staticmethod - def _read_file(filepath): - with io.open(filepath, encoding='utf-8') as f: - return f.read() - - @classmethod - def _parse_attr(cls, value, package_dir=None): - """Represents value as a module attribute. - - Examples: - attr: package.attr - attr: package.module.attr - - :param str value: - :rtype: str - """ - attr_directive = 'attr:' - if not value.startswith(attr_directive): - return value - - attrs_path = value.replace(attr_directive, '').strip().split('.') - attr_name = attrs_path.pop() - - module_name = '.'.join(attrs_path) - module_name = module_name or '__init__' - - parent_path = os.getcwd() - if package_dir: - if attrs_path[0] in package_dir: - # A custom path was specified for the module we want to import - custom_path = package_dir[attrs_path[0]] - parts = custom_path.rsplit('/', 1) - if len(parts) > 1: - parent_path = os.path.join(os.getcwd(), parts[0]) - module_name = parts[1] - else: - module_name = custom_path - elif '' in package_dir: - # A custom parent directory was specified for all root modules - parent_path = os.path.join(os.getcwd(), package_dir['']) - - with patch_path(parent_path): - try: - # attempt to load value statically - return getattr(StaticModule(module_name), attr_name) - except Exception: - # fallback to simple import - module = importlib.import_module(module_name) - - return getattr(module, attr_name) - - @classmethod - def _get_parser_compound(cls, *parse_methods): - """Returns parser function to represents value as a list. - - Parses a value applying given methods one after another. - - :param parse_methods: - :rtype: callable - """ - - def parse(value): - parsed = value - - for method in parse_methods: - parsed = method(parsed) - - return parsed - - return parse - - @classmethod - def _parse_section_to_dict(cls, section_options, values_parser=None): - """Parses section options into a dictionary. - - Optionally applies a given parser to values. - - :param dict section_options: - :param callable values_parser: - :rtype: dict - """ - value = {} - values_parser = values_parser or (lambda val: val) - for key, (_, val) in section_options.items(): - value[key] = values_parser(val) - return value - - def parse_section(self, section_options): - """Parses configuration file section. - - :param dict section_options: - """ - for (name, (_, value)) in section_options.items(): - try: - self[name] = value - - except KeyError: - pass # Keep silent for a new option may appear anytime. - - def parse(self): - """Parses configuration file items from one - or more related sections. - - """ - for section_name, section_options in self.sections.items(): - - method_postfix = '' - if section_name: # [section.option] variant - method_postfix = '_%s' % section_name - - section_parser_method = getattr( - self, - # Dots in section names are translated into dunderscores. - ('parse_section%s' % method_postfix).replace('.', '__'), - None, - ) - - if section_parser_method is None: - raise DistutilsOptionError( - 'Unsupported distribution option section: [%s.%s]' - % (self.section_prefix, section_name) - ) - - section_parser_method(section_options) - - def _deprecated_config_handler(self, func, msg, warning_class): - """this function will wrap around parameters that are deprecated - - :param msg: deprecation message - :param warning_class: class of warning exception to be raised - :param func: function to be wrapped around - """ - - @wraps(func) - def config_handler(*args, **kwargs): - warnings.warn(msg, warning_class) - return func(*args, **kwargs) - - return config_handler - - -class ConfigMetadataHandler(ConfigHandler): - - section_prefix = 'metadata' - - aliases = { - 'home_page': 'url', - 'summary': 'description', - 'classifier': 'classifiers', - 'platform': 'platforms', - } - - strict_mode = False - """We need to keep it loose, to be partially compatible with - `pbr` and `d2to1` packages which also uses `metadata` section. - - """ - - def __init__( - self, target_obj, options, ignore_option_errors=False, package_dir=None - ): - super(ConfigMetadataHandler, self).__init__( - target_obj, options, ignore_option_errors - ) - self.package_dir = package_dir - - @property - def parsers(self): - """Metadata item name to parser function mapping.""" - parse_list = self._parse_list - parse_file = self._parse_file - parse_dict = self._parse_dict - exclude_files_parser = self._exclude_files_parser - - return { - 'platforms': parse_list, - 'keywords': parse_list, - 'provides': parse_list, - 'requires': self._deprecated_config_handler( - parse_list, - "The requires parameter is deprecated, please use " - "install_requires for runtime dependencies.", - DeprecationWarning, - ), - 'obsoletes': parse_list, - 'classifiers': self._get_parser_compound(parse_file, parse_list), - 'license': exclude_files_parser('license'), - 'license_file': self._deprecated_config_handler( - exclude_files_parser('license_file'), - "The license_file parameter is deprecated, " - "use license_files instead.", - DeprecationWarning, - ), - 'license_files': parse_list, - 'description': parse_file, - 'long_description': parse_file, - 'version': self._parse_version, - 'project_urls': parse_dict, - } - - def _parse_version(self, value): - """Parses `version` option value. - - :param value: - :rtype: str - - """ - version = self._parse_file(value) - - if version != value: - version = version.strip() - # Be strict about versions loaded from file because it's easy to - # accidentally include newlines and other unintended content - try: - Version(version) - except InvalidVersion: - tmpl = ( - 'Version loaded from {value} does not ' - 'comply with PEP 440: {version}' - ) - raise DistutilsOptionError(tmpl.format(**locals())) - - return version - - version = self._parse_attr(value, self.package_dir) - - if callable(version): - version = version() - - if not isinstance(version, str): - if hasattr(version, '__iter__'): - version = '.'.join(map(str, version)) - else: - version = '%s' % version - - return version - - -class ConfigOptionsHandler(ConfigHandler): - - section_prefix = 'options' - - @property - def parsers(self): - """Metadata item name to parser function mapping.""" - parse_list = self._parse_list - parse_list_semicolon = partial(self._parse_list, separator=';') - parse_bool = self._parse_bool - parse_dict = self._parse_dict - parse_cmdclass = self._parse_cmdclass - - return { - 'zip_safe': parse_bool, - 'include_package_data': parse_bool, - 'package_dir': parse_dict, - 'scripts': parse_list, - 'eager_resources': parse_list, - 'dependency_links': parse_list, - 'namespace_packages': parse_list, - 'install_requires': parse_list_semicolon, - 'setup_requires': parse_list_semicolon, - 'tests_require': parse_list_semicolon, - 'packages': self._parse_packages, - 'entry_points': self._parse_file, - 'py_modules': parse_list, - 'python_requires': SpecifierSet, - 'cmdclass': parse_cmdclass, - } - - def _parse_cmdclass(self, value): - def resolve_class(qualified_class_name): - idx = qualified_class_name.rfind('.') - class_name = qualified_class_name[idx + 1 :] - pkg_name = qualified_class_name[:idx] - - module = __import__(pkg_name) - - return getattr(module, class_name) - - return {k: resolve_class(v) for k, v in self._parse_dict(value).items()} - - def _parse_packages(self, value): - """Parses `packages` option value. - - :param value: - :rtype: list - """ - find_directives = ['find:', 'find_namespace:'] - trimmed_value = value.strip() - - if trimmed_value not in find_directives: - return self._parse_list(value) - - findns = trimmed_value == find_directives[1] - - # Read function arguments from a dedicated section. - find_kwargs = self.parse_section_packages__find( - self.sections.get('packages.find', {}) - ) - - if findns: - from setuptools import find_namespace_packages as find_packages - else: - from setuptools import find_packages - - return find_packages(**find_kwargs) - - def parse_section_packages__find(self, section_options): - """Parses `packages.find` configuration file section. - - To be used in conjunction with _parse_packages(). - - :param dict section_options: - """ - section_data = self._parse_section_to_dict(section_options, self._parse_list) - - valid_keys = ['where', 'include', 'exclude'] - - find_kwargs = dict( - [(k, v) for k, v in section_data.items() if k in valid_keys and v] - ) - - where = find_kwargs.get('where') - if where is not None: - find_kwargs['where'] = where[0] # cast list to single val - - return find_kwargs - - def parse_section_entry_points(self, section_options): - """Parses `entry_points` configuration file section. - - :param dict section_options: - """ - parsed = self._parse_section_to_dict(section_options, self._parse_list) - self['entry_points'] = parsed - - def _parse_package_data(self, section_options): - parsed = self._parse_section_to_dict(section_options, self._parse_list) - - root = parsed.get('*') - if root: - parsed[''] = root - del parsed['*'] - - return parsed - - def parse_section_package_data(self, section_options): - """Parses `package_data` configuration file section. - - :param dict section_options: - """ - self['package_data'] = self._parse_package_data(section_options) - - def parse_section_exclude_package_data(self, section_options): - """Parses `exclude_package_data` configuration file section. - - :param dict section_options: - """ - self['exclude_package_data'] = self._parse_package_data(section_options) - - def parse_section_extras_require(self, section_options): - """Parses `extras_require` configuration file section. - - :param dict section_options: - """ - parse_list = partial(self._parse_list, separator=';') - self['extras_require'] = self._parse_section_to_dict( - section_options, parse_list - ) - - def parse_section_data_files(self, section_options): - """Parses `data_files` configuration file section. - - :param dict section_options: - """ - parsed = self._parse_section_to_dict(section_options, self._parse_list_glob) - self['data_files'] = [(k, v) for k, v in parsed.items()] diff --git a/setuptools/config/__init__.py b/setuptools/config/__init__.py new file mode 100644 index 00000000..0d190ecf --- /dev/null +++ b/setuptools/config/__init__.py @@ -0,0 +1,11 @@ +# For backward compatibility, the following classes/functions are exposed +# from `config.setupcfg` +from setuptools.config.setupcfg import ( + parse_configuration, + read_configuration, +) + +__all__ = [ + 'parse_configuration', + 'read_configuration' +] diff --git a/setuptools/config/setupcfg.py b/setuptools/config/setupcfg.py new file mode 100644 index 00000000..b4e968e5 --- /dev/null +++ b/setuptools/config/setupcfg.py @@ -0,0 +1,751 @@ +import ast +import io +import os +import sys + +import warnings +import functools +import importlib +from collections import defaultdict +from functools import partial +from functools import wraps +from glob import iglob +import contextlib + +from distutils.errors import DistutilsOptionError, DistutilsFileError +from setuptools.extern.packaging.version import Version, InvalidVersion +from setuptools.extern.packaging.specifiers import SpecifierSet + + +class StaticModule: + """ + Attempt to load the module by the name + """ + + def __init__(self, name): + spec = importlib.util.find_spec(name) + with open(spec.origin) as strm: + src = strm.read() + module = ast.parse(src) + vars(self).update(locals()) + del self.self + + def __getattr__(self, attr): + try: + return next( + ast.literal_eval(statement.value) + for statement in self.module.body + if isinstance(statement, ast.Assign) + for target in statement.targets + if isinstance(target, ast.Name) and target.id == attr + ) + except Exception as e: + raise AttributeError( + "{self.name} has no attribute {attr}".format(**locals()) + ) from e + + +@contextlib.contextmanager +def patch_path(path): + """ + Add path to front of sys.path for the duration of the context. + """ + try: + sys.path.insert(0, path) + yield + finally: + sys.path.remove(path) + + +def read_configuration(filepath, find_others=False, ignore_option_errors=False): + """Read given configuration file and returns options from it as a dict. + + :param str|unicode filepath: Path to configuration file + to get options from. + + :param bool find_others: Whether to search for other configuration files + which could be on in various places. + + :param bool ignore_option_errors: Whether to silently ignore + options, values of which could not be resolved (e.g. due to exceptions + in directives such as file:, attr:, etc.). + If False exceptions are propagated as expected. + + :rtype: dict + """ + from setuptools.dist import Distribution, _Distribution + + filepath = os.path.abspath(filepath) + + if not os.path.isfile(filepath): + raise DistutilsFileError('Configuration file %s does not exist.' % filepath) + + current_directory = os.getcwd() + os.chdir(os.path.dirname(filepath)) + + try: + dist = Distribution() + + filenames = dist.find_config_files() if find_others else [] + if filepath not in filenames: + filenames.append(filepath) + + _Distribution.parse_config_files(dist, filenames=filenames) + + handlers = parse_configuration( + dist, dist.command_options, ignore_option_errors=ignore_option_errors + ) + + finally: + os.chdir(current_directory) + + return configuration_to_dict(handlers) + + +def _get_option(target_obj, key): + """ + Given a target object and option key, get that option from + the target object, either through a get_{key} method or + from an attribute directly. + """ + getter_name = 'get_{key}'.format(**locals()) + by_attribute = functools.partial(getattr, target_obj, key) + getter = getattr(target_obj, getter_name, by_attribute) + return getter() + + +def configuration_to_dict(handlers): + """Returns configuration data gathered by given handlers as a dict. + + :param list[ConfigHandler] handlers: Handlers list, + usually from parse_configuration() + + :rtype: dict + """ + config_dict = defaultdict(dict) + + for handler in handlers: + for option in handler.set_options: + value = _get_option(handler.target_obj, option) + config_dict[handler.section_prefix][option] = value + + return config_dict + + +def parse_configuration(distribution, command_options, ignore_option_errors=False): + """Performs additional parsing of configuration options + for a distribution. + + Returns a list of used option handlers. + + :param Distribution distribution: + :param dict command_options: + :param bool ignore_option_errors: Whether to silently ignore + options, values of which could not be resolved (e.g. due to exceptions + in directives such as file:, attr:, etc.). + If False exceptions are propagated as expected. + :rtype: list + """ + options = ConfigOptionsHandler(distribution, command_options, ignore_option_errors) + options.parse() + + meta = ConfigMetadataHandler( + distribution.metadata, + command_options, + ignore_option_errors, + distribution.package_dir, + ) + meta.parse() + + return meta, options + + +class ConfigHandler: + """Handles metadata supplied in configuration files.""" + + section_prefix = None + """Prefix for config sections handled by this handler. + Must be provided by class heirs. + + """ + + aliases = {} + """Options aliases. + For compatibility with various packages. E.g.: d2to1 and pbr. + Note: `-` in keys is replaced with `_` by config parser. + + """ + + def __init__(self, target_obj, options, ignore_option_errors=False): + sections = {} + + section_prefix = self.section_prefix + for section_name, section_options in options.items(): + if not section_name.startswith(section_prefix): + continue + + section_name = section_name.replace(section_prefix, '').strip('.') + sections[section_name] = section_options + + self.ignore_option_errors = ignore_option_errors + self.target_obj = target_obj + self.sections = sections + self.set_options = [] + + @property + def parsers(self): + """Metadata item name to parser function mapping.""" + raise NotImplementedError( + '%s must provide .parsers property' % self.__class__.__name__ + ) + + def __setitem__(self, option_name, value): + unknown = tuple() + target_obj = self.target_obj + + # Translate alias into real name. + option_name = self.aliases.get(option_name, option_name) + + current_value = getattr(target_obj, option_name, unknown) + + if current_value is unknown: + raise KeyError(option_name) + + if current_value: + # Already inhabited. Skipping. + return + + skip_option = False + parser = self.parsers.get(option_name) + if parser: + try: + value = parser(value) + + except Exception: + skip_option = True + if not self.ignore_option_errors: + raise + + if skip_option: + return + + setter = getattr(target_obj, 'set_%s' % option_name, None) + if setter is None: + setattr(target_obj, option_name, value) + else: + setter(value) + + self.set_options.append(option_name) + + @classmethod + def _parse_list(cls, value, separator=','): + """Represents value as a list. + + Value is split either by separator (defaults to comma) or by lines. + + :param value: + :param separator: List items separator character. + :rtype: list + """ + if isinstance(value, list): # _get_parser_compound case + return value + + if '\n' in value: + value = value.splitlines() + else: + value = value.split(separator) + + return [chunk.strip() for chunk in value if chunk.strip()] + + @classmethod + def _parse_list_glob(cls, value, separator=','): + """Equivalent to _parse_list() but expands any glob patterns using glob(). + + However, unlike with glob() calls, the results remain relative paths. + + :param value: + :param separator: List items separator character. + :rtype: list + """ + glob_characters = ('*', '?', '[', ']', '{', '}') + values = cls._parse_list(value, separator=separator) + expanded_values = [] + for value in values: + + # Has globby characters? + if any(char in value for char in glob_characters): + # then expand the glob pattern while keeping paths *relative*: + expanded_values.extend(sorted( + os.path.relpath(path, os.getcwd()) + for path in iglob(os.path.abspath(value)))) + + else: + # take the value as-is: + expanded_values.append(value) + + return expanded_values + + @classmethod + def _parse_dict(cls, value): + """Represents value as a dict. + + :param value: + :rtype: dict + """ + separator = '=' + result = {} + for line in cls._parse_list(value): + key, sep, val = line.partition(separator) + if sep != separator: + raise DistutilsOptionError( + 'Unable to parse option value to dict: %s' % value + ) + result[key.strip()] = val.strip() + + return result + + @classmethod + def _parse_bool(cls, value): + """Represents value as boolean. + + :param value: + :rtype: bool + """ + value = value.lower() + return value in ('1', 'true', 'yes') + + @classmethod + def _exclude_files_parser(cls, key): + """Returns a parser function to make sure field inputs + are not files. + + Parses a value after getting the key so error messages are + more informative. + + :param key: + :rtype: callable + """ + + def parser(value): + exclude_directive = 'file:' + if value.startswith(exclude_directive): + raise ValueError( + 'Only strings are accepted for the {0} field, ' + 'files are not accepted'.format(key) + ) + return value + + return parser + + @classmethod + def _parse_file(cls, value): + """Represents value as a string, allowing including text + from nearest files using `file:` directive. + + Directive is sandboxed and won't reach anything outside + directory with setup.py. + + Examples: + file: README.rst, CHANGELOG.md, src/file.txt + + :param str value: + :rtype: str + """ + include_directive = 'file:' + + if not isinstance(value, str): + return value + + if not value.startswith(include_directive): + return value + + spec = value[len(include_directive) :] + filepaths = (os.path.abspath(path.strip()) for path in spec.split(',')) + return '\n'.join( + cls._read_file(path) + for path in filepaths + if (cls._assert_local(path) or True) and os.path.isfile(path) + ) + + @staticmethod + def _assert_local(filepath): + if not filepath.startswith(os.getcwd()): + raise DistutilsOptionError('`file:` directive can not access %s' % filepath) + + @staticmethod + def _read_file(filepath): + with io.open(filepath, encoding='utf-8') as f: + return f.read() + + @classmethod + def _parse_attr(cls, value, package_dir=None): + """Represents value as a module attribute. + + Examples: + attr: package.attr + attr: package.module.attr + + :param str value: + :rtype: str + """ + attr_directive = 'attr:' + if not value.startswith(attr_directive): + return value + + attrs_path = value.replace(attr_directive, '').strip().split('.') + attr_name = attrs_path.pop() + + module_name = '.'.join(attrs_path) + module_name = module_name or '__init__' + + parent_path = os.getcwd() + if package_dir: + if attrs_path[0] in package_dir: + # A custom path was specified for the module we want to import + custom_path = package_dir[attrs_path[0]] + parts = custom_path.rsplit('/', 1) + if len(parts) > 1: + parent_path = os.path.join(os.getcwd(), parts[0]) + module_name = parts[1] + else: + module_name = custom_path + elif '' in package_dir: + # A custom parent directory was specified for all root modules + parent_path = os.path.join(os.getcwd(), package_dir['']) + + with patch_path(parent_path): + try: + # attempt to load value statically + return getattr(StaticModule(module_name), attr_name) + except Exception: + # fallback to simple import + module = importlib.import_module(module_name) + + return getattr(module, attr_name) + + @classmethod + def _get_parser_compound(cls, *parse_methods): + """Returns parser function to represents value as a list. + + Parses a value applying given methods one after another. + + :param parse_methods: + :rtype: callable + """ + + def parse(value): + parsed = value + + for method in parse_methods: + parsed = method(parsed) + + return parsed + + return parse + + @classmethod + def _parse_section_to_dict(cls, section_options, values_parser=None): + """Parses section options into a dictionary. + + Optionally applies a given parser to values. + + :param dict section_options: + :param callable values_parser: + :rtype: dict + """ + value = {} + values_parser = values_parser or (lambda val: val) + for key, (_, val) in section_options.items(): + value[key] = values_parser(val) + return value + + def parse_section(self, section_options): + """Parses configuration file section. + + :param dict section_options: + """ + for (name, (_, value)) in section_options.items(): + try: + self[name] = value + + except KeyError: + pass # Keep silent for a new option may appear anytime. + + def parse(self): + """Parses configuration file items from one + or more related sections. + + """ + for section_name, section_options in self.sections.items(): + + method_postfix = '' + if section_name: # [section.option] variant + method_postfix = '_%s' % section_name + + section_parser_method = getattr( + self, + # Dots in section names are translated into dunderscores. + ('parse_section%s' % method_postfix).replace('.', '__'), + None, + ) + + if section_parser_method is None: + raise DistutilsOptionError( + 'Unsupported distribution option section: [%s.%s]' + % (self.section_prefix, section_name) + ) + + section_parser_method(section_options) + + def _deprecated_config_handler(self, func, msg, warning_class): + """this function will wrap around parameters that are deprecated + + :param msg: deprecation message + :param warning_class: class of warning exception to be raised + :param func: function to be wrapped around + """ + + @wraps(func) + def config_handler(*args, **kwargs): + warnings.warn(msg, warning_class) + return func(*args, **kwargs) + + return config_handler + + +class ConfigMetadataHandler(ConfigHandler): + + section_prefix = 'metadata' + + aliases = { + 'home_page': 'url', + 'summary': 'description', + 'classifier': 'classifiers', + 'platform': 'platforms', + } + + strict_mode = False + """We need to keep it loose, to be partially compatible with + `pbr` and `d2to1` packages which also uses `metadata` section. + + """ + + def __init__( + self, target_obj, options, ignore_option_errors=False, package_dir=None + ): + super(ConfigMetadataHandler, self).__init__( + target_obj, options, ignore_option_errors + ) + self.package_dir = package_dir + + @property + def parsers(self): + """Metadata item name to parser function mapping.""" + parse_list = self._parse_list + parse_file = self._parse_file + parse_dict = self._parse_dict + exclude_files_parser = self._exclude_files_parser + + return { + 'platforms': parse_list, + 'keywords': parse_list, + 'provides': parse_list, + 'requires': self._deprecated_config_handler( + parse_list, + "The requires parameter is deprecated, please use " + "install_requires for runtime dependencies.", + DeprecationWarning, + ), + 'obsoletes': parse_list, + 'classifiers': self._get_parser_compound(parse_file, parse_list), + 'license': exclude_files_parser('license'), + 'license_file': self._deprecated_config_handler( + exclude_files_parser('license_file'), + "The license_file parameter is deprecated, " + "use license_files instead.", + DeprecationWarning, + ), + 'license_files': parse_list, + 'description': parse_file, + 'long_description': parse_file, + 'version': self._parse_version, + 'project_urls': parse_dict, + } + + def _parse_version(self, value): + """Parses `version` option value. + + :param value: + :rtype: str + + """ + version = self._parse_file(value) + + if version != value: + version = version.strip() + # Be strict about versions loaded from file because it's easy to + # accidentally include newlines and other unintended content + try: + Version(version) + except InvalidVersion: + tmpl = ( + 'Version loaded from {value} does not ' + 'comply with PEP 440: {version}' + ) + raise DistutilsOptionError(tmpl.format(**locals())) + + return version + + version = self._parse_attr(value, self.package_dir) + + if callable(version): + version = version() + + if not isinstance(version, str): + if hasattr(version, '__iter__'): + version = '.'.join(map(str, version)) + else: + version = '%s' % version + + return version + + +class ConfigOptionsHandler(ConfigHandler): + + section_prefix = 'options' + + @property + def parsers(self): + """Metadata item name to parser function mapping.""" + parse_list = self._parse_list + parse_list_semicolon = partial(self._parse_list, separator=';') + parse_bool = self._parse_bool + parse_dict = self._parse_dict + parse_cmdclass = self._parse_cmdclass + + return { + 'zip_safe': parse_bool, + 'include_package_data': parse_bool, + 'package_dir': parse_dict, + 'scripts': parse_list, + 'eager_resources': parse_list, + 'dependency_links': parse_list, + 'namespace_packages': parse_list, + 'install_requires': parse_list_semicolon, + 'setup_requires': parse_list_semicolon, + 'tests_require': parse_list_semicolon, + 'packages': self._parse_packages, + 'entry_points': self._parse_file, + 'py_modules': parse_list, + 'python_requires': SpecifierSet, + 'cmdclass': parse_cmdclass, + } + + def _parse_cmdclass(self, value): + def resolve_class(qualified_class_name): + idx = qualified_class_name.rfind('.') + class_name = qualified_class_name[idx + 1 :] + pkg_name = qualified_class_name[:idx] + + module = __import__(pkg_name) + + return getattr(module, class_name) + + return {k: resolve_class(v) for k, v in self._parse_dict(value).items()} + + def _parse_packages(self, value): + """Parses `packages` option value. + + :param value: + :rtype: list + """ + find_directives = ['find:', 'find_namespace:'] + trimmed_value = value.strip() + + if trimmed_value not in find_directives: + return self._parse_list(value) + + findns = trimmed_value == find_directives[1] + + # Read function arguments from a dedicated section. + find_kwargs = self.parse_section_packages__find( + self.sections.get('packages.find', {}) + ) + + if findns: + from setuptools import find_namespace_packages as find_packages + else: + from setuptools import find_packages + + return find_packages(**find_kwargs) + + def parse_section_packages__find(self, section_options): + """Parses `packages.find` configuration file section. + + To be used in conjunction with _parse_packages(). + + :param dict section_options: + """ + section_data = self._parse_section_to_dict(section_options, self._parse_list) + + valid_keys = ['where', 'include', 'exclude'] + + find_kwargs = dict( + [(k, v) for k, v in section_data.items() if k in valid_keys and v] + ) + + where = find_kwargs.get('where') + if where is not None: + find_kwargs['where'] = where[0] # cast list to single val + + return find_kwargs + + def parse_section_entry_points(self, section_options): + """Parses `entry_points` configuration file section. + + :param dict section_options: + """ + parsed = self._parse_section_to_dict(section_options, self._parse_list) + self['entry_points'] = parsed + + def _parse_package_data(self, section_options): + parsed = self._parse_section_to_dict(section_options, self._parse_list) + + root = parsed.get('*') + if root: + parsed[''] = root + del parsed['*'] + + return parsed + + def parse_section_package_data(self, section_options): + """Parses `package_data` configuration file section. + + :param dict section_options: + """ + self['package_data'] = self._parse_package_data(section_options) + + def parse_section_exclude_package_data(self, section_options): + """Parses `exclude_package_data` configuration file section. + + :param dict section_options: + """ + self['exclude_package_data'] = self._parse_package_data(section_options) + + def parse_section_extras_require(self, section_options): + """Parses `extras_require` configuration file section. + + :param dict section_options: + """ + parse_list = partial(self._parse_list, separator=';') + self['extras_require'] = self._parse_section_to_dict( + section_options, parse_list + ) + + def parse_section_data_files(self, section_options): + """Parses `data_files` configuration file section. + + :param dict section_options: + """ + parsed = self._parse_section_to_dict(section_options, self._parse_list_glob) + self['data_files'] = [(k, v) for k, v in parsed.items()] diff --git a/setuptools/tests/config/__init__.py b/setuptools/tests/config/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/setuptools/tests/config/test_setupcfg.py b/setuptools/tests/config/test_setupcfg.py new file mode 100644 index 00000000..af4b69bc --- /dev/null +++ b/setuptools/tests/config/test_setupcfg.py @@ -0,0 +1,919 @@ +import types +import sys + +import contextlib +import configparser + +import pytest + +from distutils.errors import DistutilsOptionError, DistutilsFileError +from mock import patch +from setuptools.dist import Distribution, _Distribution +from setuptools.config.setupcfg import ConfigHandler, read_configuration +from distutils.core import Command +from ..textwrap import DALS + + +class ErrConfigHandler(ConfigHandler): + """Erroneous handler. Fails to implement required methods.""" + + +def make_package_dir(name, base_dir, ns=False): + dir_package = base_dir + for dir_name in name.split('/'): + dir_package = dir_package.mkdir(dir_name) + init_file = None + if not ns: + init_file = dir_package.join('__init__.py') + init_file.write('') + return dir_package, init_file + + +def fake_env( + tmpdir, setup_cfg, setup_py=None, encoding='ascii', package_path='fake_package' +): + + if setup_py is None: + setup_py = 'from setuptools import setup\n' 'setup()\n' + + tmpdir.join('setup.py').write(setup_py) + config = tmpdir.join('setup.cfg') + config.write(setup_cfg.encode(encoding), mode='wb') + + package_dir, init_file = make_package_dir(package_path, tmpdir) + + init_file.write( + 'VERSION = (1, 2, 3)\n' + '\n' + 'VERSION_MAJOR = 1' + '\n' + 'def get_version():\n' + ' return [3, 4, 5, "dev"]\n' + '\n' + ) + + return package_dir, config + + +@contextlib.contextmanager +def get_dist(tmpdir, kwargs_initial=None, parse=True): + kwargs_initial = kwargs_initial or {} + + with tmpdir.as_cwd(): + dist = Distribution(kwargs_initial) + dist.script_name = 'setup.py' + parse and dist.parse_config_files() + + yield dist + + +def test_parsers_implemented(): + + with pytest.raises(NotImplementedError): + handler = ErrConfigHandler(None, {}) + handler.parsers + + +class TestConfigurationReader: + def test_basic(self, tmpdir): + _, config = fake_env( + tmpdir, + '[metadata]\n' + 'version = 10.1.1\n' + 'keywords = one, two\n' + '\n' + '[options]\n' + 'scripts = bin/a.py, bin/b.py\n', + ) + config_dict = read_configuration('%s' % config) + assert config_dict['metadata']['version'] == '10.1.1' + assert config_dict['metadata']['keywords'] == ['one', 'two'] + assert config_dict['options']['scripts'] == ['bin/a.py', 'bin/b.py'] + + def test_no_config(self, tmpdir): + with pytest.raises(DistutilsFileError): + read_configuration('%s' % tmpdir.join('setup.cfg')) + + def test_ignore_errors(self, tmpdir): + _, config = fake_env( + tmpdir, + '[metadata]\n' 'version = attr: none.VERSION\n' 'keywords = one, two\n', + ) + with pytest.raises(ImportError): + read_configuration('%s' % config) + + config_dict = read_configuration('%s' % config, ignore_option_errors=True) + + assert config_dict['metadata']['keywords'] == ['one', 'two'] + assert 'version' not in config_dict['metadata'] + + config.remove() + + +class TestMetadata: + def test_basic(self, tmpdir): + + fake_env( + tmpdir, + '[metadata]\n' + 'version = 10.1.1\n' + 'description = Some description\n' + 'long_description_content_type = text/something\n' + 'long_description = file: README\n' + 'name = fake_name\n' + 'keywords = one, two\n' + 'provides = package, package.sub\n' + 'license = otherlic\n' + 'download_url = http://test.test.com/test/\n' + 'maintainer_email = test@test.com\n', + ) + + tmpdir.join('README').write('readme contents\nline2') + + meta_initial = { + # This will be used so `otherlic` won't replace it. + 'license': 'BSD 3-Clause License', + } + + with get_dist(tmpdir, meta_initial) as dist: + metadata = dist.metadata + + assert metadata.version == '10.1.1' + assert metadata.description == 'Some description' + assert metadata.long_description_content_type == 'text/something' + assert metadata.long_description == 'readme contents\nline2' + assert metadata.provides == ['package', 'package.sub'] + assert metadata.license == 'BSD 3-Clause License' + assert metadata.name == 'fake_name' + assert metadata.keywords == ['one', 'two'] + assert metadata.download_url == 'http://test.test.com/test/' + assert metadata.maintainer_email == 'test@test.com' + + def test_license_cfg(self, tmpdir): + fake_env( + tmpdir, + DALS( + """ + [metadata] + name=foo + version=0.0.1 + license=Apache 2.0 + """ + ), + ) + + with get_dist(tmpdir) as dist: + metadata = dist.metadata + + assert metadata.name == "foo" + assert metadata.version == "0.0.1" + assert metadata.license == "Apache 2.0" + + def test_file_mixed(self, tmpdir): + + fake_env( + tmpdir, + '[metadata]\n' 'long_description = file: README.rst, CHANGES.rst\n' '\n', + ) + + tmpdir.join('README.rst').write('readme contents\nline2') + tmpdir.join('CHANGES.rst').write('changelog contents\nand stuff') + + with get_dist(tmpdir) as dist: + assert dist.metadata.long_description == ( + 'readme contents\nline2\n' 'changelog contents\nand stuff' + ) + + def test_file_sandboxed(self, tmpdir): + + fake_env(tmpdir, '[metadata]\n' 'long_description = file: ../../README\n') + + with get_dist(tmpdir, parse=False) as dist: + with pytest.raises(DistutilsOptionError): + dist.parse_config_files() # file: out of sandbox + + def test_aliases(self, tmpdir): + + fake_env( + tmpdir, + '[metadata]\n' + 'author_email = test@test.com\n' + 'home_page = http://test.test.com/test/\n' + 'summary = Short summary\n' + 'platform = a, b\n' + 'classifier =\n' + ' Framework :: Django\n' + ' Programming Language :: Python :: 3.5\n', + ) + + with get_dist(tmpdir) as dist: + metadata = dist.metadata + assert metadata.author_email == 'test@test.com' + assert metadata.url == 'http://test.test.com/test/' + assert metadata.description == 'Short summary' + assert metadata.platforms == ['a', 'b'] + assert metadata.classifiers == [ + 'Framework :: Django', + 'Programming Language :: Python :: 3.5', + ] + + def test_multiline(self, tmpdir): + + fake_env( + tmpdir, + '[metadata]\n' + 'name = fake_name\n' + 'keywords =\n' + ' one\n' + ' two\n' + 'classifiers =\n' + ' Framework :: Django\n' + ' Programming Language :: Python :: 3.5\n', + ) + with get_dist(tmpdir) as dist: + metadata = dist.metadata + assert metadata.keywords == ['one', 'two'] + assert metadata.classifiers == [ + 'Framework :: Django', + 'Programming Language :: Python :: 3.5', + ] + + def test_dict(self, tmpdir): + + fake_env( + tmpdir, + '[metadata]\n' + 'project_urls =\n' + ' Link One = https://example.com/one/\n' + ' Link Two = https://example.com/two/\n', + ) + with get_dist(tmpdir) as dist: + metadata = dist.metadata + assert metadata.project_urls == { + 'Link One': 'https://example.com/one/', + 'Link Two': 'https://example.com/two/', + } + + def test_version(self, tmpdir): + + package_dir, config = fake_env( + tmpdir, '[metadata]\n' 'version = attr: fake_package.VERSION\n' + ) + + sub_a = package_dir.mkdir('subpkg_a') + sub_a.join('__init__.py').write('') + sub_a.join('mod.py').write('VERSION = (2016, 11, 26)') + + sub_b = package_dir.mkdir('subpkg_b') + sub_b.join('__init__.py').write('') + sub_b.join('mod.py').write( + 'import third_party_module\n' 'VERSION = (2016, 11, 26)' + ) + + with get_dist(tmpdir) as dist: + assert dist.metadata.version == '1.2.3' + + config.write('[metadata]\n' 'version = attr: fake_package.get_version\n') + with get_dist(tmpdir) as dist: + assert dist.metadata.version == '3.4.5.dev' + + config.write('[metadata]\n' 'version = attr: fake_package.VERSION_MAJOR\n') + with get_dist(tmpdir) as dist: + assert dist.metadata.version == '1' + + config.write( + '[metadata]\n' 'version = attr: fake_package.subpkg_a.mod.VERSION\n' + ) + with get_dist(tmpdir) as dist: + assert dist.metadata.version == '2016.11.26' + + config.write( + '[metadata]\n' 'version = attr: fake_package.subpkg_b.mod.VERSION\n' + ) + with get_dist(tmpdir) as dist: + assert dist.metadata.version == '2016.11.26' + + def test_version_file(self, tmpdir): + + _, config = fake_env( + tmpdir, '[metadata]\n' 'version = file: fake_package/version.txt\n' + ) + tmpdir.join('fake_package', 'version.txt').write('1.2.3\n') + + with get_dist(tmpdir) as dist: + assert dist.metadata.version == '1.2.3' + + tmpdir.join('fake_package', 'version.txt').write('1.2.3\n4.5.6\n') + with pytest.raises(DistutilsOptionError): + with get_dist(tmpdir) as dist: + dist.metadata.version + + def test_version_with_package_dir_simple(self, tmpdir): + + _, config = fake_env( + tmpdir, + '[metadata]\n' + 'version = attr: fake_package_simple.VERSION\n' + '[options]\n' + 'package_dir =\n' + ' = src\n', + package_path='src/fake_package_simple', + ) + + with get_dist(tmpdir) as dist: + assert dist.metadata.version == '1.2.3' + + def test_version_with_package_dir_rename(self, tmpdir): + + _, config = fake_env( + tmpdir, + '[metadata]\n' + 'version = attr: fake_package_rename.VERSION\n' + '[options]\n' + 'package_dir =\n' + ' fake_package_rename = fake_dir\n', + package_path='fake_dir', + ) + + with get_dist(tmpdir) as dist: + assert dist.metadata.version == '1.2.3' + + def test_version_with_package_dir_complex(self, tmpdir): + + _, config = fake_env( + tmpdir, + '[metadata]\n' + 'version = attr: fake_package_complex.VERSION\n' + '[options]\n' + 'package_dir =\n' + ' fake_package_complex = src/fake_dir\n', + package_path='src/fake_dir', + ) + + with get_dist(tmpdir) as dist: + assert dist.metadata.version == '1.2.3' + + def test_unknown_meta_item(self, tmpdir): + + fake_env(tmpdir, '[metadata]\n' 'name = fake_name\n' 'unknown = some\n') + with get_dist(tmpdir, parse=False) as dist: + dist.parse_config_files() # Skip unknown. + + def test_usupported_section(self, tmpdir): + + fake_env(tmpdir, '[metadata.some]\n' 'key = val\n') + with get_dist(tmpdir, parse=False) as dist: + with pytest.raises(DistutilsOptionError): + dist.parse_config_files() + + def test_classifiers(self, tmpdir): + expected = set( + [ + 'Framework :: Django', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.5', + ] + ) + + # From file. + _, config = fake_env(tmpdir, '[metadata]\n' 'classifiers = file: classifiers\n') + + tmpdir.join('classifiers').write( + 'Framework :: Django\n' + 'Programming Language :: Python :: 3\n' + 'Programming Language :: Python :: 3.5\n' + ) + + with get_dist(tmpdir) as dist: + assert set(dist.metadata.classifiers) == expected + + # From list notation + config.write( + '[metadata]\n' + 'classifiers =\n' + ' Framework :: Django\n' + ' Programming Language :: Python :: 3\n' + ' Programming Language :: Python :: 3.5\n' + ) + with get_dist(tmpdir) as dist: + assert set(dist.metadata.classifiers) == expected + + def test_deprecated_config_handlers(self, tmpdir): + fake_env( + tmpdir, + '[metadata]\n' + 'version = 10.1.1\n' + 'description = Some description\n' + 'requires = some, requirement\n', + ) + + with pytest.deprecated_call(): + with get_dist(tmpdir) as dist: + metadata = dist.metadata + + assert metadata.version == '10.1.1' + assert metadata.description == 'Some description' + assert metadata.requires == ['some', 'requirement'] + + def test_interpolation(self, tmpdir): + fake_env(tmpdir, '[metadata]\n' 'description = %(message)s\n') + with pytest.raises(configparser.InterpolationMissingOptionError): + with get_dist(tmpdir): + pass + + def test_non_ascii_1(self, tmpdir): + fake_env(tmpdir, '[metadata]\n' 'description = éàïôñ\n', encoding='utf-8') + with get_dist(tmpdir): + pass + + def test_non_ascii_3(self, tmpdir): + fake_env(tmpdir, '\n' '# -*- coding: invalid\n') + with get_dist(tmpdir): + pass + + def test_non_ascii_4(self, tmpdir): + fake_env( + tmpdir, + '# -*- coding: utf-8\n' '[metadata]\n' 'description = éàïôñ\n', + encoding='utf-8', + ) + with get_dist(tmpdir) as dist: + assert dist.metadata.description == 'éàïôñ' + + def test_not_utf8(self, tmpdir): + """ + Config files encoded not in UTF-8 will fail + """ + fake_env( + tmpdir, + '# vim: set fileencoding=iso-8859-15 :\n' + '[metadata]\n' + 'description = éàïôñ\n', + encoding='iso-8859-15', + ) + with pytest.raises(UnicodeDecodeError): + with get_dist(tmpdir): + pass + + def test_warn_dash_deprecation(self, tmpdir): + # warn_dash_deprecation() is a method in setuptools.dist + # remove this test and the method when no longer needed + fake_env( + tmpdir, + '[metadata]\n' + 'author-email = test@test.com\n' + 'maintainer_email = foo@foo.com\n', + ) + msg = ( + "Usage of dash-separated 'author-email' will not be supported " + "in future versions. " + "Please use the underscore name 'author_email' instead" + ) + with pytest.warns(UserWarning, match=msg): + with get_dist(tmpdir) as dist: + metadata = dist.metadata + + assert metadata.author_email == 'test@test.com' + assert metadata.maintainer_email == 'foo@foo.com' + + def test_make_option_lowercase(self, tmpdir): + # remove this test and the method make_option_lowercase() in setuptools.dist + # when no longer needed + fake_env( + tmpdir, '[metadata]\n' 'Name = foo\n' 'description = Some description\n' + ) + msg = ( + "Usage of uppercase key 'Name' in 'metadata' will be deprecated in " + "future versions. " + "Please use lowercase 'name' instead" + ) + with pytest.warns(UserWarning, match=msg): + with get_dist(tmpdir) as dist: + metadata = dist.metadata + + assert metadata.name == 'foo' + assert metadata.description == 'Some description' + + +class TestOptions: + def test_basic(self, tmpdir): + + fake_env( + tmpdir, + '[options]\n' + 'zip_safe = True\n' + 'include_package_data = yes\n' + 'package_dir = b=c, =src\n' + 'packages = pack_a, pack_b.subpack\n' + 'namespace_packages = pack1, pack2\n' + 'scripts = bin/one.py, bin/two.py\n' + 'eager_resources = bin/one.py, bin/two.py\n' + 'install_requires = docutils>=0.3; pack ==1.1, ==1.3; hey\n' + 'tests_require = mock==0.7.2; pytest\n' + 'setup_requires = docutils>=0.3; spack ==1.1, ==1.3; there\n' + 'dependency_links = http://some.com/here/1, ' + 'http://some.com/there/2\n' + 'python_requires = >=1.0, !=2.8\n' + 'py_modules = module1, module2\n', + ) + with get_dist(tmpdir) as dist: + assert dist.zip_safe + assert dist.include_package_data + assert dist.package_dir == {'': 'src', 'b': 'c'} + assert dist.packages == ['pack_a', 'pack_b.subpack'] + assert dist.namespace_packages == ['pack1', 'pack2'] + assert dist.scripts == ['bin/one.py', 'bin/two.py'] + assert dist.dependency_links == ( + ['http://some.com/here/1', 'http://some.com/there/2'] + ) + assert dist.install_requires == ( + ['docutils>=0.3', 'pack==1.1,==1.3', 'hey'] + ) + assert dist.setup_requires == ( + ['docutils>=0.3', 'spack ==1.1, ==1.3', 'there'] + ) + assert dist.tests_require == ['mock==0.7.2', 'pytest'] + assert dist.python_requires == '>=1.0, !=2.8' + assert dist.py_modules == ['module1', 'module2'] + + def test_multiline(self, tmpdir): + fake_env( + tmpdir, + '[options]\n' + 'package_dir = \n' + ' b=c\n' + ' =src\n' + 'packages = \n' + ' pack_a\n' + ' pack_b.subpack\n' + 'namespace_packages = \n' + ' pack1\n' + ' pack2\n' + 'scripts = \n' + ' bin/one.py\n' + ' bin/two.py\n' + 'eager_resources = \n' + ' bin/one.py\n' + ' bin/two.py\n' + 'install_requires = \n' + ' docutils>=0.3\n' + ' pack ==1.1, ==1.3\n' + ' hey\n' + 'tests_require = \n' + ' mock==0.7.2\n' + ' pytest\n' + 'setup_requires = \n' + ' docutils>=0.3\n' + ' spack ==1.1, ==1.3\n' + ' there\n' + 'dependency_links = \n' + ' http://some.com/here/1\n' + ' http://some.com/there/2\n', + ) + with get_dist(tmpdir) as dist: + assert dist.package_dir == {'': 'src', 'b': 'c'} + assert dist.packages == ['pack_a', 'pack_b.subpack'] + assert dist.namespace_packages == ['pack1', 'pack2'] + assert dist.scripts == ['bin/one.py', 'bin/two.py'] + assert dist.dependency_links == ( + ['http://some.com/here/1', 'http://some.com/there/2'] + ) + assert dist.install_requires == ( + ['docutils>=0.3', 'pack==1.1,==1.3', 'hey'] + ) + assert dist.setup_requires == ( + ['docutils>=0.3', 'spack ==1.1, ==1.3', 'there'] + ) + assert dist.tests_require == ['mock==0.7.2', 'pytest'] + + def test_package_dir_fail(self, tmpdir): + fake_env(tmpdir, '[options]\n' 'package_dir = a b\n') + with get_dist(tmpdir, parse=False) as dist: + with pytest.raises(DistutilsOptionError): + dist.parse_config_files() + + def test_package_data(self, tmpdir): + fake_env( + tmpdir, + '[options.package_data]\n' + '* = *.txt, *.rst\n' + 'hello = *.msg\n' + '\n' + '[options.exclude_package_data]\n' + '* = fake1.txt, fake2.txt\n' + 'hello = *.dat\n', + ) + + with get_dist(tmpdir) as dist: + assert dist.package_data == { + '': ['*.txt', '*.rst'], + 'hello': ['*.msg'], + } + assert dist.exclude_package_data == { + '': ['fake1.txt', 'fake2.txt'], + 'hello': ['*.dat'], + } + + def test_packages(self, tmpdir): + fake_env(tmpdir, '[options]\n' 'packages = find:\n') + + with get_dist(tmpdir) as dist: + assert dist.packages == ['fake_package'] + + def test_find_directive(self, tmpdir): + dir_package, config = fake_env(tmpdir, '[options]\n' 'packages = find:\n') + + dir_sub_one, _ = make_package_dir('sub_one', dir_package) + dir_sub_two, _ = make_package_dir('sub_two', dir_package) + + with get_dist(tmpdir) as dist: + assert set(dist.packages) == set( + ['fake_package', 'fake_package.sub_two', 'fake_package.sub_one'] + ) + + config.write( + '[options]\n' + 'packages = find:\n' + '\n' + '[options.packages.find]\n' + 'where = .\n' + 'include =\n' + ' fake_package.sub_one\n' + ' two\n' + ) + with get_dist(tmpdir) as dist: + assert dist.packages == ['fake_package.sub_one'] + + config.write( + '[options]\n' + 'packages = find:\n' + '\n' + '[options.packages.find]\n' + 'exclude =\n' + ' fake_package.sub_one\n' + ) + with get_dist(tmpdir) as dist: + assert set(dist.packages) == set(['fake_package', 'fake_package.sub_two']) + + def test_find_namespace_directive(self, tmpdir): + dir_package, config = fake_env( + tmpdir, '[options]\n' 'packages = find_namespace:\n' + ) + + dir_sub_one, _ = make_package_dir('sub_one', dir_package) + dir_sub_two, _ = make_package_dir('sub_two', dir_package, ns=True) + + with get_dist(tmpdir) as dist: + assert set(dist.packages) == { + 'fake_package', + 'fake_package.sub_two', + 'fake_package.sub_one', + } + + config.write( + '[options]\n' + 'packages = find_namespace:\n' + '\n' + '[options.packages.find]\n' + 'where = .\n' + 'include =\n' + ' fake_package.sub_one\n' + ' two\n' + ) + with get_dist(tmpdir) as dist: + assert dist.packages == ['fake_package.sub_one'] + + config.write( + '[options]\n' + 'packages = find_namespace:\n' + '\n' + '[options.packages.find]\n' + 'exclude =\n' + ' fake_package.sub_one\n' + ) + with get_dist(tmpdir) as dist: + assert set(dist.packages) == {'fake_package', 'fake_package.sub_two'} + + def test_extras_require(self, tmpdir): + fake_env( + tmpdir, + '[options.extras_require]\n' + 'pdf = ReportLab>=1.2; RXP\n' + 'rest = \n' + ' docutils>=0.3\n' + ' pack ==1.1, ==1.3\n', + ) + + with get_dist(tmpdir) as dist: + assert dist.extras_require == { + 'pdf': ['ReportLab>=1.2', 'RXP'], + 'rest': ['docutils>=0.3', 'pack==1.1,==1.3'], + } + assert dist.metadata.provides_extras == set(['pdf', 'rest']) + + def test_dash_preserved_extras_require(self, tmpdir): + fake_env(tmpdir, '[options.extras_require]\n' 'foo-a = foo\n' 'foo_b = test\n') + + with get_dist(tmpdir) as dist: + assert dist.extras_require == {'foo-a': ['foo'], 'foo_b': ['test']} + + def test_entry_points(self, tmpdir): + _, config = fake_env( + tmpdir, + '[options.entry_points]\n' + 'group1 = point1 = pack.module:func, ' + '.point2 = pack.module2:func_rest [rest]\n' + 'group2 = point3 = pack.module:func2\n', + ) + + with get_dist(tmpdir) as dist: + assert dist.entry_points == { + 'group1': [ + 'point1 = pack.module:func', + '.point2 = pack.module2:func_rest [rest]', + ], + 'group2': ['point3 = pack.module:func2'], + } + + expected = ( + '[blogtool.parsers]\n' + '.rst = some.nested.module:SomeClass.some_classmethod[reST]\n' + ) + + tmpdir.join('entry_points').write(expected) + + # From file. + config.write('[options]\n' 'entry_points = file: entry_points\n') + + with get_dist(tmpdir) as dist: + assert dist.entry_points == expected + + def test_case_sensitive_entry_points(self, tmpdir): + _, config = fake_env( + tmpdir, + '[options.entry_points]\n' + 'GROUP1 = point1 = pack.module:func, ' + '.point2 = pack.module2:func_rest [rest]\n' + 'group2 = point3 = pack.module:func2\n', + ) + + with get_dist(tmpdir) as dist: + assert dist.entry_points == { + 'GROUP1': [ + 'point1 = pack.module:func', + '.point2 = pack.module2:func_rest [rest]', + ], + 'group2': ['point3 = pack.module:func2'], + } + + def test_data_files(self, tmpdir): + fake_env( + tmpdir, + '[options.data_files]\n' + 'cfg =\n' + ' a/b.conf\n' + ' c/d.conf\n' + 'data = e/f.dat, g/h.dat\n', + ) + + with get_dist(tmpdir) as dist: + expected = [ + ('cfg', ['a/b.conf', 'c/d.conf']), + ('data', ['e/f.dat', 'g/h.dat']), + ] + assert sorted(dist.data_files) == sorted(expected) + + def test_data_files_globby(self, tmpdir): + fake_env( + tmpdir, + '[options.data_files]\n' + 'cfg =\n' + ' a/b.conf\n' + ' c/d.conf\n' + 'data = *.dat\n' + 'icons = \n' + ' *.ico\n' + 'audio = \n' + ' *.wav\n' + ' sounds.db\n' + ) + + # Create dummy files for glob()'s sake: + tmpdir.join('a.dat').write('') + tmpdir.join('b.dat').write('') + tmpdir.join('c.dat').write('') + tmpdir.join('a.ico').write('') + tmpdir.join('b.ico').write('') + tmpdir.join('c.ico').write('') + tmpdir.join('beep.wav').write('') + tmpdir.join('boop.wav').write('') + tmpdir.join('sounds.db').write('') + + with get_dist(tmpdir) as dist: + expected = [ + ('cfg', ['a/b.conf', 'c/d.conf']), + ('data', ['a.dat', 'b.dat', 'c.dat']), + ('icons', ['a.ico', 'b.ico', 'c.ico']), + ('audio', ['beep.wav', 'boop.wav', 'sounds.db']), + ] + assert sorted(dist.data_files) == sorted(expected) + + def test_python_requires_simple(self, tmpdir): + fake_env( + tmpdir, + DALS( + """ + [options] + python_requires=>=2.7 + """ + ), + ) + with get_dist(tmpdir) as dist: + dist.parse_config_files() + + def test_python_requires_compound(self, tmpdir): + fake_env( + tmpdir, + DALS( + """ + [options] + python_requires=>=2.7,!=3.0.* + """ + ), + ) + with get_dist(tmpdir) as dist: + dist.parse_config_files() + + def test_python_requires_invalid(self, tmpdir): + fake_env( + tmpdir, + DALS( + """ + [options] + python_requires=invalid + """ + ), + ) + with pytest.raises(Exception): + with get_dist(tmpdir) as dist: + dist.parse_config_files() + + def test_cmdclass(self, tmpdir): + class CustomCmd(Command): + pass + + m = types.ModuleType('custom_build', 'test package') + + m.__dict__['CustomCmd'] = CustomCmd + + sys.modules['custom_build'] = m + + fake_env( + tmpdir, + '[options]\n' 'cmdclass =\n' ' customcmd = custom_build.CustomCmd\n', + ) + + with get_dist(tmpdir) as dist: + assert dist.cmdclass == {'customcmd': CustomCmd} + + +saved_dist_init = _Distribution.__init__ + + +class TestExternalSetters: + # During creation of the setuptools Distribution() object, we call + # the init of the parent distutils Distribution object via + # _Distribution.__init__ (). + # + # It's possible distutils calls out to various keyword + # implementations (i.e. distutils.setup_keywords entry points) + # that may set a range of variables. + # + # This wraps distutil's Distribution.__init__ and simulates + # pbr or something else setting these values. + def _fake_distribution_init(self, dist, attrs): + saved_dist_init(dist, attrs) + # see self._DISTUTUILS_UNSUPPORTED_METADATA + setattr(dist.metadata, 'long_description_content_type', 'text/something') + # Test overwrite setup() args + setattr( + dist.metadata, + 'project_urls', + { + 'Link One': 'https://example.com/one/', + 'Link Two': 'https://example.com/two/', + }, + ) + return None + + @patch.object(_Distribution, '__init__', autospec=True) + def test_external_setters(self, mock_parent_init, tmpdir): + mock_parent_init.side_effect = self._fake_distribution_init + + dist = Distribution(attrs={'project_urls': {'will_be': 'ignored'}}) + + assert dist.metadata.long_description_content_type == 'text/something' + assert dist.metadata.project_urls == { + 'Link One': 'https://example.com/one/', + 'Link Two': 'https://example.com/two/', + } diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py deleted file mode 100644 index 005742e4..00000000 --- a/setuptools/tests/test_config.py +++ /dev/null @@ -1,919 +0,0 @@ -import types -import sys - -import contextlib -import configparser - -import pytest - -from distutils.errors import DistutilsOptionError, DistutilsFileError -from mock import patch -from setuptools.dist import Distribution, _Distribution -from setuptools.config import ConfigHandler, read_configuration -from distutils.core import Command -from .textwrap import DALS - - -class ErrConfigHandler(ConfigHandler): - """Erroneous handler. Fails to implement required methods.""" - - -def make_package_dir(name, base_dir, ns=False): - dir_package = base_dir - for dir_name in name.split('/'): - dir_package = dir_package.mkdir(dir_name) - init_file = None - if not ns: - init_file = dir_package.join('__init__.py') - init_file.write('') - return dir_package, init_file - - -def fake_env( - tmpdir, setup_cfg, setup_py=None, encoding='ascii', package_path='fake_package' -): - - if setup_py is None: - setup_py = 'from setuptools import setup\n' 'setup()\n' - - tmpdir.join('setup.py').write(setup_py) - config = tmpdir.join('setup.cfg') - config.write(setup_cfg.encode(encoding), mode='wb') - - package_dir, init_file = make_package_dir(package_path, tmpdir) - - init_file.write( - 'VERSION = (1, 2, 3)\n' - '\n' - 'VERSION_MAJOR = 1' - '\n' - 'def get_version():\n' - ' return [3, 4, 5, "dev"]\n' - '\n' - ) - - return package_dir, config - - -@contextlib.contextmanager -def get_dist(tmpdir, kwargs_initial=None, parse=True): - kwargs_initial = kwargs_initial or {} - - with tmpdir.as_cwd(): - dist = Distribution(kwargs_initial) - dist.script_name = 'setup.py' - parse and dist.parse_config_files() - - yield dist - - -def test_parsers_implemented(): - - with pytest.raises(NotImplementedError): - handler = ErrConfigHandler(None, {}) - handler.parsers - - -class TestConfigurationReader: - def test_basic(self, tmpdir): - _, config = fake_env( - tmpdir, - '[metadata]\n' - 'version = 10.1.1\n' - 'keywords = one, two\n' - '\n' - '[options]\n' - 'scripts = bin/a.py, bin/b.py\n', - ) - config_dict = read_configuration('%s' % config) - assert config_dict['metadata']['version'] == '10.1.1' - assert config_dict['metadata']['keywords'] == ['one', 'two'] - assert config_dict['options']['scripts'] == ['bin/a.py', 'bin/b.py'] - - def test_no_config(self, tmpdir): - with pytest.raises(DistutilsFileError): - read_configuration('%s' % tmpdir.join('setup.cfg')) - - def test_ignore_errors(self, tmpdir): - _, config = fake_env( - tmpdir, - '[metadata]\n' 'version = attr: none.VERSION\n' 'keywords = one, two\n', - ) - with pytest.raises(ImportError): - read_configuration('%s' % config) - - config_dict = read_configuration('%s' % config, ignore_option_errors=True) - - assert config_dict['metadata']['keywords'] == ['one', 'two'] - assert 'version' not in config_dict['metadata'] - - config.remove() - - -class TestMetadata: - def test_basic(self, tmpdir): - - fake_env( - tmpdir, - '[metadata]\n' - 'version = 10.1.1\n' - 'description = Some description\n' - 'long_description_content_type = text/something\n' - 'long_description = file: README\n' - 'name = fake_name\n' - 'keywords = one, two\n' - 'provides = package, package.sub\n' - 'license = otherlic\n' - 'download_url = http://test.test.com/test/\n' - 'maintainer_email = test@test.com\n', - ) - - tmpdir.join('README').write('readme contents\nline2') - - meta_initial = { - # This will be used so `otherlic` won't replace it. - 'license': 'BSD 3-Clause License', - } - - with get_dist(tmpdir, meta_initial) as dist: - metadata = dist.metadata - - assert metadata.version == '10.1.1' - assert metadata.description == 'Some description' - assert metadata.long_description_content_type == 'text/something' - assert metadata.long_description == 'readme contents\nline2' - assert metadata.provides == ['package', 'package.sub'] - assert metadata.license == 'BSD 3-Clause License' - assert metadata.name == 'fake_name' - assert metadata.keywords == ['one', 'two'] - assert metadata.download_url == 'http://test.test.com/test/' - assert metadata.maintainer_email == 'test@test.com' - - def test_license_cfg(self, tmpdir): - fake_env( - tmpdir, - DALS( - """ - [metadata] - name=foo - version=0.0.1 - license=Apache 2.0 - """ - ), - ) - - with get_dist(tmpdir) as dist: - metadata = dist.metadata - - assert metadata.name == "foo" - assert metadata.version == "0.0.1" - assert metadata.license == "Apache 2.0" - - def test_file_mixed(self, tmpdir): - - fake_env( - tmpdir, - '[metadata]\n' 'long_description = file: README.rst, CHANGES.rst\n' '\n', - ) - - tmpdir.join('README.rst').write('readme contents\nline2') - tmpdir.join('CHANGES.rst').write('changelog contents\nand stuff') - - with get_dist(tmpdir) as dist: - assert dist.metadata.long_description == ( - 'readme contents\nline2\n' 'changelog contents\nand stuff' - ) - - def test_file_sandboxed(self, tmpdir): - - fake_env(tmpdir, '[metadata]\n' 'long_description = file: ../../README\n') - - with get_dist(tmpdir, parse=False) as dist: - with pytest.raises(DistutilsOptionError): - dist.parse_config_files() # file: out of sandbox - - def test_aliases(self, tmpdir): - - fake_env( - tmpdir, - '[metadata]\n' - 'author_email = test@test.com\n' - 'home_page = http://test.test.com/test/\n' - 'summary = Short summary\n' - 'platform = a, b\n' - 'classifier =\n' - ' Framework :: Django\n' - ' Programming Language :: Python :: 3.5\n', - ) - - with get_dist(tmpdir) as dist: - metadata = dist.metadata - assert metadata.author_email == 'test@test.com' - assert metadata.url == 'http://test.test.com/test/' - assert metadata.description == 'Short summary' - assert metadata.platforms == ['a', 'b'] - assert metadata.classifiers == [ - 'Framework :: Django', - 'Programming Language :: Python :: 3.5', - ] - - def test_multiline(self, tmpdir): - - fake_env( - tmpdir, - '[metadata]\n' - 'name = fake_name\n' - 'keywords =\n' - ' one\n' - ' two\n' - 'classifiers =\n' - ' Framework :: Django\n' - ' Programming Language :: Python :: 3.5\n', - ) - with get_dist(tmpdir) as dist: - metadata = dist.metadata - assert metadata.keywords == ['one', 'two'] - assert metadata.classifiers == [ - 'Framework :: Django', - 'Programming Language :: Python :: 3.5', - ] - - def test_dict(self, tmpdir): - - fake_env( - tmpdir, - '[metadata]\n' - 'project_urls =\n' - ' Link One = https://example.com/one/\n' - ' Link Two = https://example.com/two/\n', - ) - with get_dist(tmpdir) as dist: - metadata = dist.metadata - assert metadata.project_urls == { - 'Link One': 'https://example.com/one/', - 'Link Two': 'https://example.com/two/', - } - - def test_version(self, tmpdir): - - package_dir, config = fake_env( - tmpdir, '[metadata]\n' 'version = attr: fake_package.VERSION\n' - ) - - sub_a = package_dir.mkdir('subpkg_a') - sub_a.join('__init__.py').write('') - sub_a.join('mod.py').write('VERSION = (2016, 11, 26)') - - sub_b = package_dir.mkdir('subpkg_b') - sub_b.join('__init__.py').write('') - sub_b.join('mod.py').write( - 'import third_party_module\n' 'VERSION = (2016, 11, 26)' - ) - - with get_dist(tmpdir) as dist: - assert dist.metadata.version == '1.2.3' - - config.write('[metadata]\n' 'version = attr: fake_package.get_version\n') - with get_dist(tmpdir) as dist: - assert dist.metadata.version == '3.4.5.dev' - - config.write('[metadata]\n' 'version = attr: fake_package.VERSION_MAJOR\n') - with get_dist(tmpdir) as dist: - assert dist.metadata.version == '1' - - config.write( - '[metadata]\n' 'version = attr: fake_package.subpkg_a.mod.VERSION\n' - ) - with get_dist(tmpdir) as dist: - assert dist.metadata.version == '2016.11.26' - - config.write( - '[metadata]\n' 'version = attr: fake_package.subpkg_b.mod.VERSION\n' - ) - with get_dist(tmpdir) as dist: - assert dist.metadata.version == '2016.11.26' - - def test_version_file(self, tmpdir): - - _, config = fake_env( - tmpdir, '[metadata]\n' 'version = file: fake_package/version.txt\n' - ) - tmpdir.join('fake_package', 'version.txt').write('1.2.3\n') - - with get_dist(tmpdir) as dist: - assert dist.metadata.version == '1.2.3' - - tmpdir.join('fake_package', 'version.txt').write('1.2.3\n4.5.6\n') - with pytest.raises(DistutilsOptionError): - with get_dist(tmpdir) as dist: - dist.metadata.version - - def test_version_with_package_dir_simple(self, tmpdir): - - _, config = fake_env( - tmpdir, - '[metadata]\n' - 'version = attr: fake_package_simple.VERSION\n' - '[options]\n' - 'package_dir =\n' - ' = src\n', - package_path='src/fake_package_simple', - ) - - with get_dist(tmpdir) as dist: - assert dist.metadata.version == '1.2.3' - - def test_version_with_package_dir_rename(self, tmpdir): - - _, config = fake_env( - tmpdir, - '[metadata]\n' - 'version = attr: fake_package_rename.VERSION\n' - '[options]\n' - 'package_dir =\n' - ' fake_package_rename = fake_dir\n', - package_path='fake_dir', - ) - - with get_dist(tmpdir) as dist: - assert dist.metadata.version == '1.2.3' - - def test_version_with_package_dir_complex(self, tmpdir): - - _, config = fake_env( - tmpdir, - '[metadata]\n' - 'version = attr: fake_package_complex.VERSION\n' - '[options]\n' - 'package_dir =\n' - ' fake_package_complex = src/fake_dir\n', - package_path='src/fake_dir', - ) - - with get_dist(tmpdir) as dist: - assert dist.metadata.version == '1.2.3' - - def test_unknown_meta_item(self, tmpdir): - - fake_env(tmpdir, '[metadata]\n' 'name = fake_name\n' 'unknown = some\n') - with get_dist(tmpdir, parse=False) as dist: - dist.parse_config_files() # Skip unknown. - - def test_usupported_section(self, tmpdir): - - fake_env(tmpdir, '[metadata.some]\n' 'key = val\n') - with get_dist(tmpdir, parse=False) as dist: - with pytest.raises(DistutilsOptionError): - dist.parse_config_files() - - def test_classifiers(self, tmpdir): - expected = set( - [ - 'Framework :: Django', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - ] - ) - - # From file. - _, config = fake_env(tmpdir, '[metadata]\n' 'classifiers = file: classifiers\n') - - tmpdir.join('classifiers').write( - 'Framework :: Django\n' - 'Programming Language :: Python :: 3\n' - 'Programming Language :: Python :: 3.5\n' - ) - - with get_dist(tmpdir) as dist: - assert set(dist.metadata.classifiers) == expected - - # From list notation - config.write( - '[metadata]\n' - 'classifiers =\n' - ' Framework :: Django\n' - ' Programming Language :: Python :: 3\n' - ' Programming Language :: Python :: 3.5\n' - ) - with get_dist(tmpdir) as dist: - assert set(dist.metadata.classifiers) == expected - - def test_deprecated_config_handlers(self, tmpdir): - fake_env( - tmpdir, - '[metadata]\n' - 'version = 10.1.1\n' - 'description = Some description\n' - 'requires = some, requirement\n', - ) - - with pytest.deprecated_call(): - with get_dist(tmpdir) as dist: - metadata = dist.metadata - - assert metadata.version == '10.1.1' - assert metadata.description == 'Some description' - assert metadata.requires == ['some', 'requirement'] - - def test_interpolation(self, tmpdir): - fake_env(tmpdir, '[metadata]\n' 'description = %(message)s\n') - with pytest.raises(configparser.InterpolationMissingOptionError): - with get_dist(tmpdir): - pass - - def test_non_ascii_1(self, tmpdir): - fake_env(tmpdir, '[metadata]\n' 'description = éàïôñ\n', encoding='utf-8') - with get_dist(tmpdir): - pass - - def test_non_ascii_3(self, tmpdir): - fake_env(tmpdir, '\n' '# -*- coding: invalid\n') - with get_dist(tmpdir): - pass - - def test_non_ascii_4(self, tmpdir): - fake_env( - tmpdir, - '# -*- coding: utf-8\n' '[metadata]\n' 'description = éàïôñ\n', - encoding='utf-8', - ) - with get_dist(tmpdir) as dist: - assert dist.metadata.description == 'éàïôñ' - - def test_not_utf8(self, tmpdir): - """ - Config files encoded not in UTF-8 will fail - """ - fake_env( - tmpdir, - '# vim: set fileencoding=iso-8859-15 :\n' - '[metadata]\n' - 'description = éàïôñ\n', - encoding='iso-8859-15', - ) - with pytest.raises(UnicodeDecodeError): - with get_dist(tmpdir): - pass - - def test_warn_dash_deprecation(self, tmpdir): - # warn_dash_deprecation() is a method in setuptools.dist - # remove this test and the method when no longer needed - fake_env( - tmpdir, - '[metadata]\n' - 'author-email = test@test.com\n' - 'maintainer_email = foo@foo.com\n', - ) - msg = ( - "Usage of dash-separated 'author-email' will not be supported " - "in future versions. " - "Please use the underscore name 'author_email' instead" - ) - with pytest.warns(UserWarning, match=msg): - with get_dist(tmpdir) as dist: - metadata = dist.metadata - - assert metadata.author_email == 'test@test.com' - assert metadata.maintainer_email == 'foo@foo.com' - - def test_make_option_lowercase(self, tmpdir): - # remove this test and the method make_option_lowercase() in setuptools.dist - # when no longer needed - fake_env( - tmpdir, '[metadata]\n' 'Name = foo\n' 'description = Some description\n' - ) - msg = ( - "Usage of uppercase key 'Name' in 'metadata' will be deprecated in " - "future versions. " - "Please use lowercase 'name' instead" - ) - with pytest.warns(UserWarning, match=msg): - with get_dist(tmpdir) as dist: - metadata = dist.metadata - - assert metadata.name == 'foo' - assert metadata.description == 'Some description' - - -class TestOptions: - def test_basic(self, tmpdir): - - fake_env( - tmpdir, - '[options]\n' - 'zip_safe = True\n' - 'include_package_data = yes\n' - 'package_dir = b=c, =src\n' - 'packages = pack_a, pack_b.subpack\n' - 'namespace_packages = pack1, pack2\n' - 'scripts = bin/one.py, bin/two.py\n' - 'eager_resources = bin/one.py, bin/two.py\n' - 'install_requires = docutils>=0.3; pack ==1.1, ==1.3; hey\n' - 'tests_require = mock==0.7.2; pytest\n' - 'setup_requires = docutils>=0.3; spack ==1.1, ==1.3; there\n' - 'dependency_links = http://some.com/here/1, ' - 'http://some.com/there/2\n' - 'python_requires = >=1.0, !=2.8\n' - 'py_modules = module1, module2\n', - ) - with get_dist(tmpdir) as dist: - assert dist.zip_safe - assert dist.include_package_data - assert dist.package_dir == {'': 'src', 'b': 'c'} - assert dist.packages == ['pack_a', 'pack_b.subpack'] - assert dist.namespace_packages == ['pack1', 'pack2'] - assert dist.scripts == ['bin/one.py', 'bin/two.py'] - assert dist.dependency_links == ( - ['http://some.com/here/1', 'http://some.com/there/2'] - ) - assert dist.install_requires == ( - ['docutils>=0.3', 'pack==1.1,==1.3', 'hey'] - ) - assert dist.setup_requires == ( - ['docutils>=0.3', 'spack ==1.1, ==1.3', 'there'] - ) - assert dist.tests_require == ['mock==0.7.2', 'pytest'] - assert dist.python_requires == '>=1.0, !=2.8' - assert dist.py_modules == ['module1', 'module2'] - - def test_multiline(self, tmpdir): - fake_env( - tmpdir, - '[options]\n' - 'package_dir = \n' - ' b=c\n' - ' =src\n' - 'packages = \n' - ' pack_a\n' - ' pack_b.subpack\n' - 'namespace_packages = \n' - ' pack1\n' - ' pack2\n' - 'scripts = \n' - ' bin/one.py\n' - ' bin/two.py\n' - 'eager_resources = \n' - ' bin/one.py\n' - ' bin/two.py\n' - 'install_requires = \n' - ' docutils>=0.3\n' - ' pack ==1.1, ==1.3\n' - ' hey\n' - 'tests_require = \n' - ' mock==0.7.2\n' - ' pytest\n' - 'setup_requires = \n' - ' docutils>=0.3\n' - ' spack ==1.1, ==1.3\n' - ' there\n' - 'dependency_links = \n' - ' http://some.com/here/1\n' - ' http://some.com/there/2\n', - ) - with get_dist(tmpdir) as dist: - assert dist.package_dir == {'': 'src', 'b': 'c'} - assert dist.packages == ['pack_a', 'pack_b.subpack'] - assert dist.namespace_packages == ['pack1', 'pack2'] - assert dist.scripts == ['bin/one.py', 'bin/two.py'] - assert dist.dependency_links == ( - ['http://some.com/here/1', 'http://some.com/there/2'] - ) - assert dist.install_requires == ( - ['docutils>=0.3', 'pack==1.1,==1.3', 'hey'] - ) - assert dist.setup_requires == ( - ['docutils>=0.3', 'spack ==1.1, ==1.3', 'there'] - ) - assert dist.tests_require == ['mock==0.7.2', 'pytest'] - - def test_package_dir_fail(self, tmpdir): - fake_env(tmpdir, '[options]\n' 'package_dir = a b\n') - with get_dist(tmpdir, parse=False) as dist: - with pytest.raises(DistutilsOptionError): - dist.parse_config_files() - - def test_package_data(self, tmpdir): - fake_env( - tmpdir, - '[options.package_data]\n' - '* = *.txt, *.rst\n' - 'hello = *.msg\n' - '\n' - '[options.exclude_package_data]\n' - '* = fake1.txt, fake2.txt\n' - 'hello = *.dat\n', - ) - - with get_dist(tmpdir) as dist: - assert dist.package_data == { - '': ['*.txt', '*.rst'], - 'hello': ['*.msg'], - } - assert dist.exclude_package_data == { - '': ['fake1.txt', 'fake2.txt'], - 'hello': ['*.dat'], - } - - def test_packages(self, tmpdir): - fake_env(tmpdir, '[options]\n' 'packages = find:\n') - - with get_dist(tmpdir) as dist: - assert dist.packages == ['fake_package'] - - def test_find_directive(self, tmpdir): - dir_package, config = fake_env(tmpdir, '[options]\n' 'packages = find:\n') - - dir_sub_one, _ = make_package_dir('sub_one', dir_package) - dir_sub_two, _ = make_package_dir('sub_two', dir_package) - - with get_dist(tmpdir) as dist: - assert set(dist.packages) == set( - ['fake_package', 'fake_package.sub_two', 'fake_package.sub_one'] - ) - - config.write( - '[options]\n' - 'packages = find:\n' - '\n' - '[options.packages.find]\n' - 'where = .\n' - 'include =\n' - ' fake_package.sub_one\n' - ' two\n' - ) - with get_dist(tmpdir) as dist: - assert dist.packages == ['fake_package.sub_one'] - - config.write( - '[options]\n' - 'packages = find:\n' - '\n' - '[options.packages.find]\n' - 'exclude =\n' - ' fake_package.sub_one\n' - ) - with get_dist(tmpdir) as dist: - assert set(dist.packages) == set(['fake_package', 'fake_package.sub_two']) - - def test_find_namespace_directive(self, tmpdir): - dir_package, config = fake_env( - tmpdir, '[options]\n' 'packages = find_namespace:\n' - ) - - dir_sub_one, _ = make_package_dir('sub_one', dir_package) - dir_sub_two, _ = make_package_dir('sub_two', dir_package, ns=True) - - with get_dist(tmpdir) as dist: - assert set(dist.packages) == { - 'fake_package', - 'fake_package.sub_two', - 'fake_package.sub_one', - } - - config.write( - '[options]\n' - 'packages = find_namespace:\n' - '\n' - '[options.packages.find]\n' - 'where = .\n' - 'include =\n' - ' fake_package.sub_one\n' - ' two\n' - ) - with get_dist(tmpdir) as dist: - assert dist.packages == ['fake_package.sub_one'] - - config.write( - '[options]\n' - 'packages = find_namespace:\n' - '\n' - '[options.packages.find]\n' - 'exclude =\n' - ' fake_package.sub_one\n' - ) - with get_dist(tmpdir) as dist: - assert set(dist.packages) == {'fake_package', 'fake_package.sub_two'} - - def test_extras_require(self, tmpdir): - fake_env( - tmpdir, - '[options.extras_require]\n' - 'pdf = ReportLab>=1.2; RXP\n' - 'rest = \n' - ' docutils>=0.3\n' - ' pack ==1.1, ==1.3\n', - ) - - with get_dist(tmpdir) as dist: - assert dist.extras_require == { - 'pdf': ['ReportLab>=1.2', 'RXP'], - 'rest': ['docutils>=0.3', 'pack==1.1,==1.3'], - } - assert dist.metadata.provides_extras == set(['pdf', 'rest']) - - def test_dash_preserved_extras_require(self, tmpdir): - fake_env(tmpdir, '[options.extras_require]\n' 'foo-a = foo\n' 'foo_b = test\n') - - with get_dist(tmpdir) as dist: - assert dist.extras_require == {'foo-a': ['foo'], 'foo_b': ['test']} - - def test_entry_points(self, tmpdir): - _, config = fake_env( - tmpdir, - '[options.entry_points]\n' - 'group1 = point1 = pack.module:func, ' - '.point2 = pack.module2:func_rest [rest]\n' - 'group2 = point3 = pack.module:func2\n', - ) - - with get_dist(tmpdir) as dist: - assert dist.entry_points == { - 'group1': [ - 'point1 = pack.module:func', - '.point2 = pack.module2:func_rest [rest]', - ], - 'group2': ['point3 = pack.module:func2'], - } - - expected = ( - '[blogtool.parsers]\n' - '.rst = some.nested.module:SomeClass.some_classmethod[reST]\n' - ) - - tmpdir.join('entry_points').write(expected) - - # From file. - config.write('[options]\n' 'entry_points = file: entry_points\n') - - with get_dist(tmpdir) as dist: - assert dist.entry_points == expected - - def test_case_sensitive_entry_points(self, tmpdir): - _, config = fake_env( - tmpdir, - '[options.entry_points]\n' - 'GROUP1 = point1 = pack.module:func, ' - '.point2 = pack.module2:func_rest [rest]\n' - 'group2 = point3 = pack.module:func2\n', - ) - - with get_dist(tmpdir) as dist: - assert dist.entry_points == { - 'GROUP1': [ - 'point1 = pack.module:func', - '.point2 = pack.module2:func_rest [rest]', - ], - 'group2': ['point3 = pack.module:func2'], - } - - def test_data_files(self, tmpdir): - fake_env( - tmpdir, - '[options.data_files]\n' - 'cfg =\n' - ' a/b.conf\n' - ' c/d.conf\n' - 'data = e/f.dat, g/h.dat\n', - ) - - with get_dist(tmpdir) as dist: - expected = [ - ('cfg', ['a/b.conf', 'c/d.conf']), - ('data', ['e/f.dat', 'g/h.dat']), - ] - assert sorted(dist.data_files) == sorted(expected) - - def test_data_files_globby(self, tmpdir): - fake_env( - tmpdir, - '[options.data_files]\n' - 'cfg =\n' - ' a/b.conf\n' - ' c/d.conf\n' - 'data = *.dat\n' - 'icons = \n' - ' *.ico\n' - 'audio = \n' - ' *.wav\n' - ' sounds.db\n' - ) - - # Create dummy files for glob()'s sake: - tmpdir.join('a.dat').write('') - tmpdir.join('b.dat').write('') - tmpdir.join('c.dat').write('') - tmpdir.join('a.ico').write('') - tmpdir.join('b.ico').write('') - tmpdir.join('c.ico').write('') - tmpdir.join('beep.wav').write('') - tmpdir.join('boop.wav').write('') - tmpdir.join('sounds.db').write('') - - with get_dist(tmpdir) as dist: - expected = [ - ('cfg', ['a/b.conf', 'c/d.conf']), - ('data', ['a.dat', 'b.dat', 'c.dat']), - ('icons', ['a.ico', 'b.ico', 'c.ico']), - ('audio', ['beep.wav', 'boop.wav', 'sounds.db']), - ] - assert sorted(dist.data_files) == sorted(expected) - - def test_python_requires_simple(self, tmpdir): - fake_env( - tmpdir, - DALS( - """ - [options] - python_requires=>=2.7 - """ - ), - ) - with get_dist(tmpdir) as dist: - dist.parse_config_files() - - def test_python_requires_compound(self, tmpdir): - fake_env( - tmpdir, - DALS( - """ - [options] - python_requires=>=2.7,!=3.0.* - """ - ), - ) - with get_dist(tmpdir) as dist: - dist.parse_config_files() - - def test_python_requires_invalid(self, tmpdir): - fake_env( - tmpdir, - DALS( - """ - [options] - python_requires=invalid - """ - ), - ) - with pytest.raises(Exception): - with get_dist(tmpdir) as dist: - dist.parse_config_files() - - def test_cmdclass(self, tmpdir): - class CustomCmd(Command): - pass - - m = types.ModuleType('custom_build', 'test package') - - m.__dict__['CustomCmd'] = CustomCmd - - sys.modules['custom_build'] = m - - fake_env( - tmpdir, - '[options]\n' 'cmdclass =\n' ' customcmd = custom_build.CustomCmd\n', - ) - - with get_dist(tmpdir) as dist: - assert dist.cmdclass == {'customcmd': CustomCmd} - - -saved_dist_init = _Distribution.__init__ - - -class TestExternalSetters: - # During creation of the setuptools Distribution() object, we call - # the init of the parent distutils Distribution object via - # _Distribution.__init__ (). - # - # It's possible distutils calls out to various keyword - # implementations (i.e. distutils.setup_keywords entry points) - # that may set a range of variables. - # - # This wraps distutil's Distribution.__init__ and simulates - # pbr or something else setting these values. - def _fake_distribution_init(self, dist, attrs): - saved_dist_init(dist, attrs) - # see self._DISTUTUILS_UNSUPPORTED_METADATA - setattr(dist.metadata, 'long_description_content_type', 'text/something') - # Test overwrite setup() args - setattr( - dist.metadata, - 'project_urls', - { - 'Link One': 'https://example.com/one/', - 'Link Two': 'https://example.com/two/', - }, - ) - return None - - @patch.object(_Distribution, '__init__', autospec=True) - def test_external_setters(self, mock_parent_init, tmpdir): - mock_parent_init.side_effect = self._fake_distribution_init - - dist = Distribution(attrs={'project_urls': {'will_be': 'ignored'}}) - - assert dist.metadata.long_description_content_type == 'text/something' - assert dist.metadata.project_urls == { - 'Link One': 'https://example.com/one/', - 'Link Two': 'https://example.com/two/', - } -- cgit v1.2.1 From f866876c6a9da2ed5a3255a38d8ff2bddf7767bd Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 2 Dec 2021 13:44:45 +0000 Subject: Extract post-processing functions from config We can split the process of interpreting configuration files into 2 steps: 1. The parsing the file contents from strings to value objects that can be understand by Python (for example a string with a comma separated list of keywords into an actual Python list of strings). 2. The expansion (or post-processing) of these values according to the semantics ``setuptools`` assign to them (for example a configuration field with the ``file:`` directive should be expanded from a list of file paths to a single string with the contents of those files concatenated) The idea of this change is to extract the functions responsible for (2.) into a new module, so they can be reused between different config file formats. --- setuptools/config/expand.py | 249 +++++++++++++++++++++++++++++++++ setuptools/config/setupcfg.py | 168 ++-------------------- setuptools/tests/config/test_expand.py | 83 +++++++++++ 3 files changed, 345 insertions(+), 155 deletions(-) create mode 100644 setuptools/config/expand.py create mode 100644 setuptools/tests/config/test_expand.py diff --git a/setuptools/config/expand.py b/setuptools/config/expand.py new file mode 100644 index 00000000..529ab0fa --- /dev/null +++ b/setuptools/config/expand.py @@ -0,0 +1,249 @@ +"""Utility functions to expand configuration directives or special values +(such glob patterns). + +We can split the process of interpreting configuration files into 2 steps: + +1. The parsing the file contents from strings to value objects + that can be understand by Python (for example a string with a comma + separated list of keywords into an actual Python list of strings). + +2. The expansion (or post-processing) of these values according to the + semantics ``setuptools`` assign to them (for example a configuration field + with the ``file:`` directive should be expanded from a list of file paths to + a single string with the contents of those files concatenated) + +This module focus on the second step, and therefore allow sharing the expansion +functions among several configuration file formats. +""" +import ast +import contextlib +import importlib +import io +import os +import sys +from glob import iglob +from itertools import chain + +from distutils.errors import DistutilsOptionError + +chain_iter = chain.from_iterable + + +class StaticModule: + """ + Attempt to load the module by the name + """ + + def __init__(self, name): + spec = importlib.util.find_spec(name) + if spec is None: + raise ModuleNotFoundError(name) + with open(spec.origin) as strm: + src = strm.read() + module = ast.parse(src) + vars(self).update(locals()) + del self.self + + def __getattr__(self, attr): + try: + return next( + ast.literal_eval(statement.value) + for statement in self.module.body + if isinstance(statement, ast.Assign) + for target in statement.targets + if isinstance(target, ast.Name) and target.id == attr + ) + except Exception as e: + raise AttributeError( + "{self.name} has no attribute {attr}".format(**locals()) + ) from e + + +@contextlib.contextmanager +def patch_path(path): + """ + Add path to front of sys.path for the duration of the context. + """ + try: + sys.path.insert(0, path) + yield + finally: + sys.path.remove(path) + + +def glob_relative(patterns): + """Expand the list of glob patterns, but preserving relative paths. + + :param list[str] patterns: List of glob patterns + :rtype: list + """ + glob_characters = ('*', '?', '[', ']', '{', '}') + expanded_values = [] + root_dir = os.getcwd() + for value in patterns: + + # Has globby characters? + if any(char in value for char in glob_characters): + # then expand the glob pattern while keeping paths *relative*: + expanded_values.extend(sorted( + os.path.relpath(path, root_dir) + for path in iglob(os.path.abspath(value), recursive=True))) + + else: + # take the value as-is: + expanded_values.append(value) + + return expanded_values + + +def read_files(filepaths): + """Return the content of the files concatenated using ``\n`` as str + + This function is sandboxed and won't reach anything outside the directory + with ``setup.py``. + """ + root_dir = os.getcwd() + return '\n'.join( + _read_file(path) + for path in filepaths + if _assert_local(path, root_dir) and os.path.isfile(path) + ) + + +def _read_file(filepath): + with io.open(filepath, encoding='utf-8') as f: + return f.read() + + +def _assert_local(filepath, root_dir): + if not os.path.abspath(filepath).startswith(os.path.abspath(root_dir)): + raise DistutilsOptionError(f'Cannot access {filepath!r}') + + return True + + +def read_attr(attr_desc, package_dir=None): + """Reads the value of an attribute from a module. + + This function will try to read the attributed statically first + (via :func:`ast.literal_eval`), and only evaluate the module if it fails. + + Examples: + read_attr("package.attr") + read_attr("package.module.attr") + + :param str attr_desc: Dot-separated string describing how to reach the + attribute (see examples above) + :param dict[str, str] package_dir: Mapping of package names to their + location in disk. + :rtype: str + """ + root_dir = os.getcwd() + attrs_path = attr_desc.strip().split('.') + attr_name = attrs_path.pop() + + module_name = '.'.join(attrs_path) + module_name = module_name or '__init__' + + parent_path = root_dir + if package_dir: + if attrs_path[0] in package_dir: + # A custom path was specified for the module we want to import + custom_path = package_dir[attrs_path[0]] + parts = custom_path.rsplit('/', 1) + if len(parts) > 1: + parent_path = os.path.join(root_dir, parts[0]) + parent_module = parts[1] + else: + parent_module = custom_path + module_name = ".".join([parent_module, *attrs_path[1:]]) + elif '' in package_dir: + # A custom parent directory was specified for all root modules + parent_path = os.path.join(root_dir, package_dir['']) + + with patch_path(parent_path): + try: + # attempt to load value statically + return getattr(StaticModule(module_name), attr_name) + except Exception: + # fallback to simple import + module = importlib.import_module(module_name) + + return getattr(module, attr_name) + + +def resolve_class(qualified_class_name): + """Given a qualified class name, return the associated class object""" + idx = qualified_class_name.rfind('.') + class_name = qualified_class_name[idx + 1 :] + pkg_name = qualified_class_name[:idx] + module = importlib.import_module(pkg_name) + return getattr(module, class_name) + + +def cmdclass(values): + """Given a dictionary mapping command names to strings for qualified class + names, apply :func:`resolve_class` to the dict values. + """ + return {k: resolve_class(v) for k, v in values.items()} + + +def find_packages(namespaces=False, **kwargs): + """Works similarly to :func:`setuptools.find_packages`, but with all + arguments given as keyword arguments. Moreover, ``where`` can be given + as a list (the results will be simply concatenated). + + When the additional keyword argument ``namespaces`` is ``True``, it will + behave like :func:`setuptools.find_namespace_packages`` (i.e. include + implicit namespaces as per :pep:`420`). + + :rtype: list + """ + + if namespaces: + from setuptools import PEP420PackageFinder as PackageFinder + else: + from setuptools import PackageFinder + + where = kwargs.pop('where', ['.']) + if isinstance(where, str): + where = [where] + + return list(chain_iter(PackageFinder.find(x, **kwargs) for x in where)) + + +def version(value): + """When getting the version directly from an attribute, + it should be normalised to string. + """ + if callable(value): + value = value() + + if not isinstance(value, str): + if hasattr(value, '__iter__'): + value = '.'.join(map(str, value)) + else: + value = '%s' % value + + return value + + +def canonic_package_data(package_data): + if "*" in package_data: + package_data[""] = package_data.pop("*") + return package_data + + +def canonic_data_files(data_files, root_dir=None): + """For compatibility with ``setup.py``, ``data_files`` should be a list + of pairs instead of a dict. + + This function also expands glob patterns. + """ + if isinstance(data_files, list): + return data_files + + return [ + (dest, glob_relative(patterns, root_dir)) + for dest, patterns in data_files.items() + ] diff --git a/setuptools/config/setupcfg.py b/setuptools/config/setupcfg.py index b4e968e5..457033d4 100644 --- a/setuptools/config/setupcfg.py +++ b/setuptools/config/setupcfg.py @@ -1,60 +1,16 @@ -import ast -import io +"""Load setuptools configuration from ``setup.cfg`` files""" import os -import sys import warnings import functools -import importlib from collections import defaultdict from functools import partial from functools import wraps -from glob import iglob -import contextlib from distutils.errors import DistutilsOptionError, DistutilsFileError from setuptools.extern.packaging.version import Version, InvalidVersion from setuptools.extern.packaging.specifiers import SpecifierSet - - -class StaticModule: - """ - Attempt to load the module by the name - """ - - def __init__(self, name): - spec = importlib.util.find_spec(name) - with open(spec.origin) as strm: - src = strm.read() - module = ast.parse(src) - vars(self).update(locals()) - del self.self - - def __getattr__(self, attr): - try: - return next( - ast.literal_eval(statement.value) - for statement in self.module.body - if isinstance(statement, ast.Assign) - for target in statement.targets - if isinstance(target, ast.Name) and target.id == attr - ) - except Exception as e: - raise AttributeError( - "{self.name} has no attribute {attr}".format(**locals()) - ) from e - - -@contextlib.contextmanager -def patch_path(path): - """ - Add path to front of sys.path for the duration of the context. - """ - try: - sys.path.insert(0, path) - yield - finally: - sys.path.remove(path) +from setuptools.config import expand def read_configuration(filepath, find_others=False, ignore_option_errors=False): @@ -257,34 +213,6 @@ class ConfigHandler: return [chunk.strip() for chunk in value if chunk.strip()] - @classmethod - def _parse_list_glob(cls, value, separator=','): - """Equivalent to _parse_list() but expands any glob patterns using glob(). - - However, unlike with glob() calls, the results remain relative paths. - - :param value: - :param separator: List items separator character. - :rtype: list - """ - glob_characters = ('*', '?', '[', ']', '{', '}') - values = cls._parse_list(value, separator=separator) - expanded_values = [] - for value in values: - - # Has globby characters? - if any(char in value for char in glob_characters): - # then expand the glob pattern while keeping paths *relative*: - expanded_values.extend(sorted( - os.path.relpath(path, os.getcwd()) - for path in iglob(os.path.abspath(value)))) - - else: - # take the value as-is: - expanded_values.append(value) - - return expanded_values - @classmethod def _parse_dict(cls, value): """Represents value as a dict. @@ -361,21 +289,7 @@ class ConfigHandler: spec = value[len(include_directive) :] filepaths = (os.path.abspath(path.strip()) for path in spec.split(',')) - return '\n'.join( - cls._read_file(path) - for path in filepaths - if (cls._assert_local(path) or True) and os.path.isfile(path) - ) - - @staticmethod - def _assert_local(filepath): - if not filepath.startswith(os.getcwd()): - raise DistutilsOptionError('`file:` directive can not access %s' % filepath) - - @staticmethod - def _read_file(filepath): - with io.open(filepath, encoding='utf-8') as f: - return f.read() + return expand.read_files(filepaths) @classmethod def _parse_attr(cls, value, package_dir=None): @@ -392,36 +306,8 @@ class ConfigHandler: if not value.startswith(attr_directive): return value - attrs_path = value.replace(attr_directive, '').strip().split('.') - attr_name = attrs_path.pop() - - module_name = '.'.join(attrs_path) - module_name = module_name or '__init__' - - parent_path = os.getcwd() - if package_dir: - if attrs_path[0] in package_dir: - # A custom path was specified for the module we want to import - custom_path = package_dir[attrs_path[0]] - parts = custom_path.rsplit('/', 1) - if len(parts) > 1: - parent_path = os.path.join(os.getcwd(), parts[0]) - module_name = parts[1] - else: - module_name = custom_path - elif '' in package_dir: - # A custom parent directory was specified for all root modules - parent_path = os.path.join(os.getcwd(), package_dir['']) - - with patch_path(parent_path): - try: - # attempt to load value statically - return getattr(StaticModule(module_name), attr_name) - except Exception: - # fallback to simple import - module = importlib.import_module(module_name) - - return getattr(module, attr_name) + attr_desc = value.replace(attr_directive, '') + return expand.read_attr(attr_desc, package_dir) @classmethod def _get_parser_compound(cls, *parse_methods): @@ -596,18 +482,7 @@ class ConfigMetadataHandler(ConfigHandler): return version - version = self._parse_attr(value, self.package_dir) - - if callable(version): - version = version() - - if not isinstance(version, str): - if hasattr(version, '__iter__'): - version = '.'.join(map(str, version)) - else: - version = '%s' % version - - return version + return expand.version(self._parse_attr(value, self.package_dir)) class ConfigOptionsHandler(ConfigHandler): @@ -642,16 +517,7 @@ class ConfigOptionsHandler(ConfigHandler): } def _parse_cmdclass(self, value): - def resolve_class(qualified_class_name): - idx = qualified_class_name.rfind('.') - class_name = qualified_class_name[idx + 1 :] - pkg_name = qualified_class_name[:idx] - - module = __import__(pkg_name) - - return getattr(module, class_name) - - return {k: resolve_class(v) for k, v in self._parse_dict(value).items()} + return expand.cmdclass(self._parse_dict(value)) def _parse_packages(self, value): """Parses `packages` option value. @@ -673,11 +539,9 @@ class ConfigOptionsHandler(ConfigHandler): ) if findns: - from setuptools import find_namespace_packages as find_packages - else: - from setuptools import find_packages + find_kwargs["namespaces"] = True - return find_packages(**find_kwargs) + return expand.find_packages(**find_kwargs) def parse_section_packages__find(self, section_options): """Parses `packages.find` configuration file section. @@ -709,14 +573,8 @@ class ConfigOptionsHandler(ConfigHandler): self['entry_points'] = parsed def _parse_package_data(self, section_options): - parsed = self._parse_section_to_dict(section_options, self._parse_list) - - root = parsed.get('*') - if root: - parsed[''] = root - del parsed['*'] - - return parsed + package_data = self._parse_section_to_dict(section_options, self._parse_list) + return expand.canonic_package_data(package_data) def parse_section_package_data(self, section_options): """Parses `package_data` configuration file section. @@ -747,5 +605,5 @@ class ConfigOptionsHandler(ConfigHandler): :param dict section_options: """ - parsed = self._parse_section_to_dict(section_options, self._parse_list_glob) - self['data_files'] = [(k, v) for k, v in parsed.items()] + parsed = self._parse_section_to_dict(section_options, self._parse_list) + self['data_files'] = expand.canonic_data_files(parsed) diff --git a/setuptools/tests/config/test_expand.py b/setuptools/tests/config/test_expand.py new file mode 100644 index 00000000..03ee6841 --- /dev/null +++ b/setuptools/tests/config/test_expand.py @@ -0,0 +1,83 @@ +import pytest + +from distutils.errors import DistutilsOptionError +from setuptools.config import expand +from setuptools.sandbox import pushd + + +def write_files(files, root_dir): + for file, content in files.items(): + path = root_dir / file + path.parent.mkdir(exist_ok=True, parents=True) + path.write_text(content) + + +def test_glob_relative(tmp_path): + files = { + os.path.join("dir1", "dir2", "dir3", "file1.txt"), + os.path.join("dir1", "dir2", "file2.txt"), + os.path.join("dir1", "file3.txt"), + os.path.join("a.ini"), + os.path.join("b.ini"), + os.path.join("dir1", "c.ini"), + os.path.join("dir1", "dir2", "a.ini"), + } + + write_files({k: "" for k in files}, tmp_path) + patterns = ["**/*.txt", "[ab].*", "**/[ac].ini"] + with pushd(tmp_path): + assert set(expand.glob_relative(patterns)) == files + + +def test_read_files(tmp_path): + files = { + "a.txt": "a", + "dir1/b.txt": "b", + "dir1/dir2/c.txt": "c" + } + write_files(files, tmp_path) + with pushd(tmp_path): + assert expand.read_files(list(files)) == "a\nb\nc" + + with pushd(tmp_path / "dir1"), pytest.raises(DistutilsOptionError): + expand.read_files(["../a.txt"]) + + +def test_read_attr(tmp_path): + files = { + "pkg/__init__.py": "", + "pkg/sub/__init__.py": "VERSION = '0.1.1'", + "pkg/sub/mod.py": ( + "VALUES = {'a': 0, 'b': {42}, 'c': (0, 1, 1)}\n" + "raise SystemExit(1)" + ), + } + write_files(files, tmp_path) + # Make sure it can read the attr statically without evaluating the module + with pushd(tmp_path): + assert expand.read_attr('pkg.sub.VERSION') == '0.1.1' + values = expand.read_attr('lib.mod.VALUES', {'lib': 'pkg/sub'}) + assert values['a'] == 0 + assert values['b'] == {42} + assert values['c'] == (0, 1, 1) + + +def test_resolve_class(): + from setuptools.command.sdist import sdist + assert expand.resolve_class("setuptools.command.sdist.sdist") == sdist + + +def test_find_packages(tmp_path): + files = { + "pkg/__init__.py", + "other/__init__.py", + "dir1/dir2/__init__.py", + } + + write_files({k: "" for k in files}, tmp_path) + with pushd(tmp_path): + assert set(expand.find_packages(where=['.'])) == {"pkg", "other"} + expected = {"pkg", "other", "dir2"} + assert set(expand.find_packages(where=['.', "dir1"])) == expected + expected = {"pkg", "other", "dir1", "dir1.dir2"} + assert set(expand.find_packages(namespaces="True")) == expected -- cgit v1.2.1 From 7d9ecc02a2574452750fafeedbec40175bb52216 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 2 Dec 2021 14:11:53 +0000 Subject: Allow root_dir to be explicit in config.expand functions --- setuptools/config/expand.py | 140 +++++++++++++++++++++------------ setuptools/config/setupcfg.py | 2 +- setuptools/tests/config/test_expand.py | 37 +++++++-- 3 files changed, 119 insertions(+), 60 deletions(-) diff --git a/setuptools/config/expand.py b/setuptools/config/expand.py index 529ab0fa..e96578bd 100644 --- a/setuptools/config/expand.py +++ b/setuptools/config/expand.py @@ -16,7 +16,6 @@ This module focus on the second step, and therefore allow sharing the expansion functions among several configuration file formats. """ import ast -import contextlib import importlib import io import os @@ -34,10 +33,7 @@ class StaticModule: Attempt to load the module by the name """ - def __init__(self, name): - spec = importlib.util.find_spec(name) - if spec is None: - raise ModuleNotFoundError(name) + def __init__(self, name, spec): with open(spec.origin) as strm: src = strm.read() module = ast.parse(src) @@ -59,53 +55,47 @@ class StaticModule: ) from e -@contextlib.contextmanager -def patch_path(path): - """ - Add path to front of sys.path for the duration of the context. - """ - try: - sys.path.insert(0, path) - yield - finally: - sys.path.remove(path) - - -def glob_relative(patterns): +def glob_relative(patterns, root_dir=None): """Expand the list of glob patterns, but preserving relative paths. :param list[str] patterns: List of glob patterns + :param str root_dir: Path to which globs should be relative + (current directory by default) :rtype: list """ glob_characters = ('*', '?', '[', ']', '{', '}') expanded_values = [] - root_dir = os.getcwd() + root_dir = root_dir or os.getcwd() for value in patterns: # Has globby characters? if any(char in value for char in glob_characters): # then expand the glob pattern while keeping paths *relative*: + glob_path = os.path.abspath(os.path.join(root_dir, value)) expanded_values.extend(sorted( - os.path.relpath(path, root_dir) - for path in iglob(os.path.abspath(value), recursive=True))) + os.path.relpath(path, root_dir).replace(os.sep, "/") + for path in iglob(glob_path, recursive=True))) else: - # take the value as-is: - expanded_values.append(value) + # take the value as-is + path = os.path.relpath(value, root_dir).replace(os.sep, "/") + expanded_values.append(path) return expanded_values -def read_files(filepaths): +def read_files(filepaths, root_dir=None): """Return the content of the files concatenated using ``\n`` as str - This function is sandboxed and won't reach anything outside the directory - with ``setup.py``. + This function is sandboxed and won't reach anything outside ``root_dir`` + + (By default ``root_dir`` is the current directory). """ - root_dir = os.getcwd() + root_dir = os.path.abspath(root_dir or os.getcwd()) + _filepaths = (os.path.join(root_dir, path) for path in filepaths) return '\n'.join( _read_file(path) - for path in filepaths + for path in _filepaths if _assert_local(path, root_dir) and os.path.isfile(path) ) @@ -116,13 +106,14 @@ def _read_file(filepath): def _assert_local(filepath, root_dir): - if not os.path.abspath(filepath).startswith(os.path.abspath(root_dir)): - raise DistutilsOptionError(f'Cannot access {filepath!r}') + if not os.path.abspath(filepath).startswith(root_dir): + msg = f"Cannot access {filepath!r} (or anything outside {root_dir!r})" + raise DistutilsOptionError(msg) return True -def read_attr(attr_desc, package_dir=None): +def read_attr(attr_desc, package_dir=None, root_dir=None): """Reads the value of an attribute from a module. This function will try to read the attributed statically first @@ -135,60 +126,99 @@ def read_attr(attr_desc, package_dir=None): :param str attr_desc: Dot-separated string describing how to reach the attribute (see examples above) :param dict[str, str] package_dir: Mapping of package names to their - location in disk. + location in disk (represented by paths relative to ``root_dir``). + :param str root_dir: Path to directory containing all the packages in + ``package_dir`` (current directory by default). :rtype: str """ - root_dir = os.getcwd() + root_dir = root_dir or os.getcwd() attrs_path = attr_desc.strip().split('.') attr_name = attrs_path.pop() - module_name = '.'.join(attrs_path) module_name = module_name or '__init__' + parent_path, path, module_name = _find_module(module_name, package_dir, root_dir) + spec = _find_spec(module_name, path, parent_path) + + try: + return getattr(StaticModule(module_name, spec), attr_name) + except Exception: + # fallback to evaluate module + module = _load_spec(spec, module_name) + return getattr(module, attr_name) + + +def _find_spec(module_name, module_path, parent_path): + spec = importlib.util.spec_from_file_location(module_name, module_path) + spec = spec or importlib.util.find_spec(module_name) + if spec is None: + raise ModuleNotFoundError(module_name) + + return spec + + +def _load_spec(spec, module_name): + name = getattr(spec, "__name__", module_name) + if name in sys.modules: + return sys.modules[name] + module = importlib.util.module_from_spec(spec) + sys.modules[name] = module # cache (it also ensures `==` works on loaded items) + spec.loader.exec_module(module) + return module + + +def _find_module(module_name, package_dir, root_dir): + """Given a module (that could normally be imported by ``module_name`` + after the build is complete), find the path to the parent directory where + it is contained and the canonical name that could be used to import it + considering the ``package_dir`` in the build configuration and ``root_dir`` + """ parent_path = root_dir + module_parts = module_name.split('.') if package_dir: - if attrs_path[0] in package_dir: + if module_parts[0] in package_dir: # A custom path was specified for the module we want to import - custom_path = package_dir[attrs_path[0]] + custom_path = package_dir[module_parts[0]] parts = custom_path.rsplit('/', 1) if len(parts) > 1: parent_path = os.path.join(root_dir, parts[0]) parent_module = parts[1] else: parent_module = custom_path - module_name = ".".join([parent_module, *attrs_path[1:]]) + module_name = ".".join([parent_module, *module_parts[1:]]) elif '' in package_dir: # A custom parent directory was specified for all root modules parent_path = os.path.join(root_dir, package_dir['']) - with patch_path(parent_path): - try: - # attempt to load value statically - return getattr(StaticModule(module_name), attr_name) - except Exception: - # fallback to simple import - module = importlib.import_module(module_name) - - return getattr(module, attr_name) + path_start = os.path.join(parent_path, *module_name.split(".")) + candidates = chain( + (f"{path_start}.py", os.path.join(path_start, "__init__.py")), + iglob(f"{path_start}.*") + ) + module_path = next((x for x in candidates if os.path.isfile(x)), None) + return parent_path, module_path, module_name -def resolve_class(qualified_class_name): +def resolve_class(qualified_class_name, package_dir=None, root_dir=None): """Given a qualified class name, return the associated class object""" + root_dir = root_dir or os.getcwd() idx = qualified_class_name.rfind('.') class_name = qualified_class_name[idx + 1 :] pkg_name = qualified_class_name[:idx] - module = importlib.import_module(pkg_name) + + parent_path, path, module_name = _find_module(pkg_name, package_dir, root_dir) + module = _load_spec(_find_spec(module_name, path, parent_path), module_name) return getattr(module, class_name) -def cmdclass(values): +def cmdclass(values, package_dir=None, root_dir=None): """Given a dictionary mapping command names to strings for qualified class names, apply :func:`resolve_class` to the dict values. """ - return {k: resolve_class(v) for k, v in values.items()} + return {k: resolve_class(v, package_dir, root_dir) for k, v in values.items()} -def find_packages(namespaces=False, **kwargs): +def find_packages(*, namespaces=False, root_dir=None, **kwargs): """Works similarly to :func:`setuptools.find_packages`, but with all arguments given as keyword arguments. Moreover, ``where`` can be given as a list (the results will be simply concatenated). @@ -205,11 +235,17 @@ def find_packages(namespaces=False, **kwargs): else: from setuptools import PackageFinder + root_dir = root_dir or "." where = kwargs.pop('where', ['.']) if isinstance(where, str): where = [where] + target = [_nest_path(root_dir, path) for path in where] + return list(chain_iter(PackageFinder.find(x, **kwargs) for x in target)) + - return list(chain_iter(PackageFinder.find(x, **kwargs) for x in where)) +def _nest_path(parent, path): + path = parent if path == "." else os.path.join(parent, path) + return os.path.normpath(path) def version(value): diff --git a/setuptools/config/setupcfg.py b/setuptools/config/setupcfg.py index 457033d4..80cf4541 100644 --- a/setuptools/config/setupcfg.py +++ b/setuptools/config/setupcfg.py @@ -288,7 +288,7 @@ class ConfigHandler: return value spec = value[len(include_directive) :] - filepaths = (os.path.abspath(path.strip()) for path in spec.split(',')) + filepaths = (path.strip() for path in spec.split(',')) return expand.read_files(filepaths) @classmethod diff --git a/setuptools/tests/config/test_expand.py b/setuptools/tests/config/test_expand.py index 03ee6841..11dc74aa 100644 --- a/setuptools/tests/config/test_expand.py +++ b/setuptools/tests/config/test_expand.py @@ -1,3 +1,5 @@ +import os + import pytest from distutils.errors import DistutilsOptionError @@ -14,19 +16,21 @@ def write_files(files, root_dir): def test_glob_relative(tmp_path): files = { - os.path.join("dir1", "dir2", "dir3", "file1.txt"), - os.path.join("dir1", "dir2", "file2.txt"), - os.path.join("dir1", "file3.txt"), - os.path.join("a.ini"), - os.path.join("b.ini"), - os.path.join("dir1", "c.ini"), - os.path.join("dir1", "dir2", "a.ini"), + "dir1/dir2/dir3/file1.txt", + "dir1/dir2/file2.txt", + "dir1/file3.txt", + "a.ini", + "b.ini", + "dir1/c.ini", + "dir1/dir2/a.ini", } write_files({k: "" for k in files}, tmp_path) patterns = ["**/*.txt", "[ab].*", "**/[ac].ini"] with pushd(tmp_path): assert set(expand.glob_relative(patterns)) == files + # Make sure the same APIs work outside cwd + assert set(expand.glob_relative(patterns, tmp_path)) == files def test_read_files(tmp_path): @@ -42,6 +46,11 @@ def test_read_files(tmp_path): with pushd(tmp_path / "dir1"), pytest.raises(DistutilsOptionError): expand.read_files(["../a.txt"]) + # Make sure the same APIs work outside cwd + assert expand.read_files(list(files), tmp_path) == "a\nb\nc" + with pytest.raises(DistutilsOptionError): + expand.read_files(["../a.txt"], tmp_path) + def test_read_attr(tmp_path): files = { @@ -59,6 +68,10 @@ def test_read_attr(tmp_path): values = expand.read_attr('lib.mod.VALUES', {'lib': 'pkg/sub'}) assert values['a'] == 0 assert values['b'] == {42} + + # Make sure the same APIs work outside cwd + assert expand.read_attr('pkg.sub.VERSION', root_dir=tmp_path) == '0.1.1' + values = expand.read_attr('lib.mod.VALUES', {'lib': 'pkg/sub'}, tmp_path) assert values['c'] == (0, 1, 1) @@ -81,3 +94,13 @@ def test_find_packages(tmp_path): assert set(expand.find_packages(where=['.', "dir1"])) == expected expected = {"pkg", "other", "dir1", "dir1.dir2"} assert set(expand.find_packages(namespaces="True")) == expected + + # Make sure the same APIs work outside cwd + path = str(tmp_path).replace(os.sep, '/') # ensure posix-style paths + dir1_path = str(tmp_path / "dir1").replace(os.sep, '/') + + assert set(expand.find_packages(where=[path])) == {"pkg", "other"} + expected = {"pkg", "other", "dir2"} + assert set(expand.find_packages(where=[path, dir1_path])) == expected + expected = {"pkg", "other", "dir1", "dir1.dir2"} + assert set(expand.find_packages(where=[path], namespaces="True")) == expected -- cgit v1.2.1 From 83300405987a8525cfdcc44d9db92503435ac1fe Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 3 Dec 2021 11:22:18 +0000 Subject: Allow single strings in config.expand.read_files --- setuptools/config/expand.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setuptools/config/expand.py b/setuptools/config/expand.py index e96578bd..352db0c3 100644 --- a/setuptools/config/expand.py +++ b/setuptools/config/expand.py @@ -91,6 +91,9 @@ def read_files(filepaths, root_dir=None): (By default ``root_dir`` is the current directory). """ + if isinstance(filepaths, (str, bytes)): + filepaths = [filepaths] + root_dir = os.path.abspath(root_dir or os.getcwd()) _filepaths = (os.path.join(root_dir, path) for path in filepaths) return '\n'.join( -- cgit v1.2.1 From a148c337fc1174b45b34695500a1f50d39997b5d Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 22 Dec 2021 17:41:45 +0000 Subject: Adequate test_setupcfg to latest changes in setupcfg --- setuptools/tests/config/test_setupcfg.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/setuptools/tests/config/test_setupcfg.py b/setuptools/tests/config/test_setupcfg.py index af4b69bc..268cf91d 100644 --- a/setuptools/tests/config/test_setupcfg.py +++ b/setuptools/tests/config/test_setupcfg.py @@ -1,16 +1,14 @@ -import types -import sys - -import contextlib import configparser +import contextlib +import importlib +import os +from unittest.mock import patch import pytest from distutils.errors import DistutilsOptionError, DistutilsFileError -from mock import patch from setuptools.dist import Distribution, _Distribution from setuptools.config.setupcfg import ConfigHandler, read_configuration -from distutils.core import Command from ..textwrap import DALS @@ -858,23 +856,23 @@ class TestOptions: with get_dist(tmpdir) as dist: dist.parse_config_files() - def test_cmdclass(self, tmpdir): - class CustomCmd(Command): - pass - - m = types.ModuleType('custom_build', 'test package') - - m.__dict__['CustomCmd'] = CustomCmd - - sys.modules['custom_build'] = m + def test_cmdclass(self, tmpdir, monkeypatch): + module_path = os.path.join(tmpdir, "custom_build.py") + with open(module_path, "w") as f: + f.write("from distutils.core import Command\n") + f.write("class CustomCmd(Command): pass\n") fake_env( tmpdir, '[options]\n' 'cmdclass =\n' ' customcmd = custom_build.CustomCmd\n', ) + with monkeypatch.context() as m: + m.syspath_prepend(tmpdir) + custom_build = importlib.import_module("custom_build") + with get_dist(tmpdir) as dist: - assert dist.cmdclass == {'customcmd': CustomCmd} + assert dist.cmdclass == {'customcmd': custom_build.CustomCmd} saved_dist_init = _Distribution.__init__ -- cgit v1.2.1 From d96e8bf57fd6ae7551b12530438d88ba1696c727 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 1 Feb 2022 12:17:44 +0000 Subject: Add news fragment --- changelog.d/3065.change.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelog.d/3065.change.rst diff --git a/changelog.d/3065.change.rst b/changelog.d/3065.change.rst new file mode 100644 index 00000000..31b9d59c --- /dev/null +++ b/changelog.d/3065.change.rst @@ -0,0 +1,4 @@ +Refactored ``setuptools.config`` by separating configuration parsing (specific +to the configuration file format, e.g. ``setup.cfg``) and post-processing +(which includes directives such as ``file:`` that can be used across different +configuration formats). -- cgit v1.2.1 From 61a416b97f2b48496df6bebe29a9eac6c90d6f69 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 9 Feb 2022 14:47:50 +0000 Subject: Make __all__ immutable in setuptools.config --- changelog.d/3065.change.rst | 4 ---- changelog.d/3065.misc.rst | 4 ++++ setuptools/config/__init__.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) delete mode 100644 changelog.d/3065.change.rst create mode 100644 changelog.d/3065.misc.rst diff --git a/changelog.d/3065.change.rst b/changelog.d/3065.change.rst deleted file mode 100644 index 31b9d59c..00000000 --- a/changelog.d/3065.change.rst +++ /dev/null @@ -1,4 +0,0 @@ -Refactored ``setuptools.config`` by separating configuration parsing (specific -to the configuration file format, e.g. ``setup.cfg``) and post-processing -(which includes directives such as ``file:`` that can be used across different -configuration formats). diff --git a/changelog.d/3065.misc.rst b/changelog.d/3065.misc.rst new file mode 100644 index 00000000..31b9d59c --- /dev/null +++ b/changelog.d/3065.misc.rst @@ -0,0 +1,4 @@ +Refactored ``setuptools.config`` by separating configuration parsing (specific +to the configuration file format, e.g. ``setup.cfg``) and post-processing +(which includes directives such as ``file:`` that can be used across different +configuration formats). diff --git a/setuptools/config/__init__.py b/setuptools/config/__init__.py index 0d190ecf..fa48907a 100644 --- a/setuptools/config/__init__.py +++ b/setuptools/config/__init__.py @@ -5,7 +5,7 @@ from setuptools.config.setupcfg import ( read_configuration, ) -__all__ = [ +__all__ = ( 'parse_configuration', 'read_configuration' -] +) -- cgit v1.2.1 From ec2071adb27e6dc5918fc5268f9ad6d247f19b6d Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 9 Feb 2022 15:09:56 +0000 Subject: Split complex generator expression in setuptools.config.expand --- setuptools/config/expand.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/setuptools/config/expand.py b/setuptools/config/expand.py index 352db0c3..b7ffb30d 100644 --- a/setuptools/config/expand.py +++ b/setuptools/config/expand.py @@ -29,9 +29,7 @@ chain_iter = chain.from_iterable class StaticModule: - """ - Attempt to load the module by the name - """ + """Proxy to a module object that avoids executing arbitrary code.""" def __init__(self, name, spec): with open(spec.origin) as strm: @@ -41,14 +39,24 @@ class StaticModule: del self.self def __getattr__(self, attr): + """Attempt to load an attribute "statically", via :func:`ast.literal_eval`.""" try: - return next( - ast.literal_eval(statement.value) + assignment_expressions = ( + statement for statement in self.module.body if isinstance(statement, ast.Assign) + ) + expressions_with_target = ( + (statement, target) + for statement in assignment_expressions for target in statement.targets + ) + matching_values = ( + statement.value + for statement, target in expressions_with_target if isinstance(target, ast.Name) and target.id == attr ) + return next(ast.literal_eval(value) for value in matching_values) except Exception as e: raise AttributeError( "{self.name} has no attribute {attr}".format(**locals()) -- cgit v1.2.1 From 25612c5557a2d693214903bae0f8ff6bf405a7eb Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 9 Feb 2022 15:16:00 +0000 Subject: Adopt review suggestions Co-authored-by: Sviatoslav Sydorenko --- setuptools/config/expand.py | 6 ++---- setuptools/tests/config/test_expand.py | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/setuptools/config/expand.py b/setuptools/config/expand.py index b7ffb30d..feb55be1 100644 --- a/setuptools/config/expand.py +++ b/setuptools/config/expand.py @@ -58,9 +58,7 @@ class StaticModule: ) return next(ast.literal_eval(value) for value in matching_values) except Exception as e: - raise AttributeError( - "{self.name} has no attribute {attr}".format(**locals()) - ) from e + raise AttributeError(f"{self.name} has no attribute {attr}") from e def glob_relative(patterns, root_dir=None): @@ -71,7 +69,7 @@ def glob_relative(patterns, root_dir=None): (current directory by default) :rtype: list """ - glob_characters = ('*', '?', '[', ']', '{', '}') + glob_characters = {'*', '?', '[', ']', '{', '}'} expanded_values = [] root_dir = root_dir or os.getcwd() for value in patterns: diff --git a/setuptools/tests/config/test_expand.py b/setuptools/tests/config/test_expand.py index 11dc74aa..9fc256f0 100644 --- a/setuptools/tests/config/test_expand.py +++ b/setuptools/tests/config/test_expand.py @@ -3,6 +3,7 @@ import os import pytest from distutils.errors import DistutilsOptionError +from setuptools.command.sdist import sdist from setuptools.config import expand from setuptools.sandbox import pushd @@ -76,7 +77,6 @@ def test_read_attr(tmp_path): def test_resolve_class(): - from setuptools.command.sdist import sdist assert expand.resolve_class("setuptools.command.sdist.sdist") == sdist -- cgit v1.2.1 From 81c3faaca72550e36809d4bbd9ea3922e89225cf Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 9 Feb 2022 15:32:03 +0000 Subject: Replace pushd with monkeypatch.chdir in test_expand --- setuptools/tests/config/test_expand.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/setuptools/tests/config/test_expand.py b/setuptools/tests/config/test_expand.py index 9fc256f0..4ca23bdc 100644 --- a/setuptools/tests/config/test_expand.py +++ b/setuptools/tests/config/test_expand.py @@ -5,7 +5,6 @@ import pytest from distutils.errors import DistutilsOptionError from setuptools.command.sdist import sdist from setuptools.config import expand -from setuptools.sandbox import pushd def write_files(files, root_dir): @@ -15,7 +14,7 @@ def write_files(files, root_dir): path.write_text(content) -def test_glob_relative(tmp_path): +def test_glob_relative(tmp_path, monkeypatch): files = { "dir1/dir2/dir3/file1.txt", "dir1/dir2/file2.txt", @@ -28,24 +27,26 @@ def test_glob_relative(tmp_path): write_files({k: "" for k in files}, tmp_path) patterns = ["**/*.txt", "[ab].*", "**/[ac].ini"] - with pushd(tmp_path): - assert set(expand.glob_relative(patterns)) == files + monkeypatch.chdir(tmp_path) + assert set(expand.glob_relative(patterns)) == files # Make sure the same APIs work outside cwd assert set(expand.glob_relative(patterns, tmp_path)) == files -def test_read_files(tmp_path): +def test_read_files(tmp_path, monkeypatch): files = { "a.txt": "a", "dir1/b.txt": "b", "dir1/dir2/c.txt": "c" } write_files(files, tmp_path) - with pushd(tmp_path): + + with monkeypatch.context() as m: + m.chdir(tmp_path) assert expand.read_files(list(files)) == "a\nb\nc" - with pushd(tmp_path / "dir1"), pytest.raises(DistutilsOptionError): - expand.read_files(["../a.txt"]) + with pytest.raises(DistutilsOptionError): + expand.read_files(["../a.txt"]) # Make sure the same APIs work outside cwd assert expand.read_files(list(files), tmp_path) == "a\nb\nc" @@ -53,7 +54,7 @@ def test_read_files(tmp_path): expand.read_files(["../a.txt"], tmp_path) -def test_read_attr(tmp_path): +def test_read_attr(tmp_path, monkeypatch): files = { "pkg/__init__.py": "", "pkg/sub/__init__.py": "VERSION = '0.1.1'", @@ -63,10 +64,13 @@ def test_read_attr(tmp_path): ), } write_files(files, tmp_path) - # Make sure it can read the attr statically without evaluating the module - with pushd(tmp_path): + + with monkeypatch.context() as m: + m.chdir(tmp_path) + # Make sure it can read the attr statically without evaluating the module assert expand.read_attr('pkg.sub.VERSION') == '0.1.1' values = expand.read_attr('lib.mod.VALUES', {'lib': 'pkg/sub'}) + assert values['a'] == 0 assert values['b'] == {42} @@ -80,7 +84,7 @@ def test_resolve_class(): assert expand.resolve_class("setuptools.command.sdist.sdist") == sdist -def test_find_packages(tmp_path): +def test_find_packages(tmp_path, monkeypatch): files = { "pkg/__init__.py", "other/__init__.py", @@ -88,7 +92,9 @@ def test_find_packages(tmp_path): } write_files({k: "" for k in files}, tmp_path) - with pushd(tmp_path): + + with monkeypatch.context() as m: + m.chdir(tmp_path) assert set(expand.find_packages(where=['.'])) == {"pkg", "other"} expected = {"pkg", "other", "dir2"} assert set(expand.find_packages(where=['.', "dir1"])) == expected -- cgit v1.2.1 From 82779f9ccf44e0d6cb4e52f960a9fe66e6c0dc01 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 9 Feb 2022 15:36:56 +0000 Subject: Ensure proper exception matching in test_expand --- setuptools/tests/config/test_expand.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setuptools/tests/config/test_expand.py b/setuptools/tests/config/test_expand.py index 4ca23bdc..c33565a0 100644 --- a/setuptools/tests/config/test_expand.py +++ b/setuptools/tests/config/test_expand.py @@ -45,12 +45,13 @@ def test_read_files(tmp_path, monkeypatch): m.chdir(tmp_path) assert expand.read_files(list(files)) == "a\nb\nc" - with pytest.raises(DistutilsOptionError): + cannot_access_msg = r"Cannot access '.*\.\..a\.txt'" + with pytest.raises(DistutilsOptionError, match=cannot_access_msg): expand.read_files(["../a.txt"]) # Make sure the same APIs work outside cwd assert expand.read_files(list(files), tmp_path) == "a\nb\nc" - with pytest.raises(DistutilsOptionError): + with pytest.raises(DistutilsOptionError, match=cannot_access_msg): expand.read_files(["../a.txt"], tmp_path) -- cgit v1.2.1 From e5d2bc8607988f776187bd6f805e56556437bd04 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 9 Feb 2022 15:52:37 +0000 Subject: Parametrize test_expand.test_find_packages --- setuptools/tests/config/test_expand.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/setuptools/tests/config/test_expand.py b/setuptools/tests/config/test_expand.py index c33565a0..1898792b 100644 --- a/setuptools/tests/config/test_expand.py +++ b/setuptools/tests/config/test_expand.py @@ -85,29 +85,30 @@ def test_resolve_class(): assert expand.resolve_class("setuptools.command.sdist.sdist") == sdist -def test_find_packages(tmp_path, monkeypatch): +@pytest.mark.parametrize( + 'args, pkgs', + [ + ({"where": ["."]}, {"pkg", "other"}), + ({"where": [".", "dir1"]}, {"pkg", "other", "dir2"}), + ({"namespaces": True}, {"pkg", "other", "dir1", "dir1.dir2"}), + ] +) +def test_find_packages(tmp_path, monkeypatch, args, pkgs): files = { "pkg/__init__.py", "other/__init__.py", "dir1/dir2/__init__.py", } - write_files({k: "" for k in files}, tmp_path) with monkeypatch.context() as m: m.chdir(tmp_path) - assert set(expand.find_packages(where=['.'])) == {"pkg", "other"} - expected = {"pkg", "other", "dir2"} - assert set(expand.find_packages(where=['.', "dir1"])) == expected - expected = {"pkg", "other", "dir1", "dir1.dir2"} - assert set(expand.find_packages(namespaces="True")) == expected + assert set(expand.find_packages(**args)) == pkgs # Make sure the same APIs work outside cwd - path = str(tmp_path).replace(os.sep, '/') # ensure posix-style paths - dir1_path = str(tmp_path / "dir1").replace(os.sep, '/') - - assert set(expand.find_packages(where=[path])) == {"pkg", "other"} - expected = {"pkg", "other", "dir2"} - assert set(expand.find_packages(where=[path, dir1_path])) == expected - expected = {"pkg", "other", "dir1", "dir1.dir2"} - assert set(expand.find_packages(where=[path], namespaces="True")) == expected + where = [ + str((tmp_path / p).resolve()).replace(os.sep, "/") # ensure posix-style paths + for p in args.pop("where", ["."]) + ] + + assert set(expand.find_packages(where=where, **args)) == pkgs -- cgit v1.2.1 From 099ac60fba6f63d9658733f10c5525ecfb390eee Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Thu, 2 Dec 2021 19:23:34 +0000 Subject: Add `tomli` as vendorised dependency This eventually will allow reading project metadata directly from `pyproject.toml` --- setuptools/_vendor/tomli/LICENSE | 21 ++ setuptools/_vendor/tomli/__init__.py | 9 + setuptools/_vendor/tomli/_parser.py | 663 +++++++++++++++++++++++++++++++++++ setuptools/_vendor/tomli/_re.py | 101 ++++++ setuptools/_vendor/tomli/_types.py | 6 + setuptools/_vendor/tomli/py.typed | 1 + setuptools/_vendor/vendored.txt | 1 + setuptools/extern/__init__.py | 2 +- 8 files changed, 803 insertions(+), 1 deletion(-) create mode 100644 setuptools/_vendor/tomli/LICENSE create mode 100644 setuptools/_vendor/tomli/__init__.py create mode 100644 setuptools/_vendor/tomli/_parser.py create mode 100644 setuptools/_vendor/tomli/_re.py create mode 100644 setuptools/_vendor/tomli/_types.py create mode 100644 setuptools/_vendor/tomli/py.typed diff --git a/setuptools/_vendor/tomli/LICENSE b/setuptools/_vendor/tomli/LICENSE new file mode 100644 index 00000000..e859590f --- /dev/null +++ b/setuptools/_vendor/tomli/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Taneli Hukkinen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/setuptools/_vendor/tomli/__init__.py b/setuptools/_vendor/tomli/__init__.py new file mode 100644 index 00000000..60f792af --- /dev/null +++ b/setuptools/_vendor/tomli/__init__.py @@ -0,0 +1,9 @@ +"""A lil' TOML parser.""" + +__all__ = ("loads", "load", "TOMLDecodeError") +__version__ = "1.2.3" # DO NOT EDIT THIS LINE MANUALLY. LET bump2version UTILITY DO IT + +from tomli._parser import TOMLDecodeError, load, loads + +# Pretend this exception was created here. +TOMLDecodeError.__module__ = "tomli" diff --git a/setuptools/_vendor/tomli/_parser.py b/setuptools/_vendor/tomli/_parser.py new file mode 100644 index 00000000..89e81c3b --- /dev/null +++ b/setuptools/_vendor/tomli/_parser.py @@ -0,0 +1,663 @@ +import string +from types import MappingProxyType +from typing import Any, BinaryIO, Dict, FrozenSet, Iterable, NamedTuple, Optional, Tuple +import warnings + +from tomli._re import ( + RE_DATETIME, + RE_LOCALTIME, + RE_NUMBER, + match_to_datetime, + match_to_localtime, + match_to_number, +) +from tomli._types import Key, ParseFloat, Pos + +ASCII_CTRL = frozenset(chr(i) for i in range(32)) | frozenset(chr(127)) + +# Neither of these sets include quotation mark or backslash. They are +# currently handled as separate cases in the parser functions. +ILLEGAL_BASIC_STR_CHARS = ASCII_CTRL - frozenset("\t") +ILLEGAL_MULTILINE_BASIC_STR_CHARS = ASCII_CTRL - frozenset("\t\n") + +ILLEGAL_LITERAL_STR_CHARS = ILLEGAL_BASIC_STR_CHARS +ILLEGAL_MULTILINE_LITERAL_STR_CHARS = ILLEGAL_MULTILINE_BASIC_STR_CHARS + +ILLEGAL_COMMENT_CHARS = ILLEGAL_BASIC_STR_CHARS + +TOML_WS = frozenset(" \t") +TOML_WS_AND_NEWLINE = TOML_WS | frozenset("\n") +BARE_KEY_CHARS = frozenset(string.ascii_letters + string.digits + "-_") +KEY_INITIAL_CHARS = BARE_KEY_CHARS | frozenset("\"'") +HEXDIGIT_CHARS = frozenset(string.hexdigits) + +BASIC_STR_ESCAPE_REPLACEMENTS = MappingProxyType( + { + "\\b": "\u0008", # backspace + "\\t": "\u0009", # tab + "\\n": "\u000A", # linefeed + "\\f": "\u000C", # form feed + "\\r": "\u000D", # carriage return + '\\"': "\u0022", # quote + "\\\\": "\u005C", # backslash + } +) + + +class TOMLDecodeError(ValueError): + """An error raised if a document is not valid TOML.""" + + +def load(fp: BinaryIO, *, parse_float: ParseFloat = float) -> Dict[str, Any]: + """Parse TOML from a binary file object.""" + s_bytes = fp.read() + try: + s = s_bytes.decode() + except AttributeError: + warnings.warn( + "Text file object support is deprecated in favor of binary file objects." + ' Use `open("foo.toml", "rb")` to open the file in binary mode.', + DeprecationWarning, + stacklevel=2, + ) + s = s_bytes # type: ignore[assignment] + return loads(s, parse_float=parse_float) + + +def loads(s: str, *, parse_float: ParseFloat = float) -> Dict[str, Any]: # noqa: C901 + """Parse TOML from a string.""" + + # The spec allows converting "\r\n" to "\n", even in string + # literals. Let's do so to simplify parsing. + src = s.replace("\r\n", "\n") + pos = 0 + out = Output(NestedDict(), Flags()) + header: Key = () + + # Parse one statement at a time + # (typically means one line in TOML source) + while True: + # 1. Skip line leading whitespace + pos = skip_chars(src, pos, TOML_WS) + + # 2. Parse rules. Expect one of the following: + # - end of file + # - end of line + # - comment + # - key/value pair + # - append dict to list (and move to its namespace) + # - create dict (and move to its namespace) + # Skip trailing whitespace when applicable. + try: + char = src[pos] + except IndexError: + break + if char == "\n": + pos += 1 + continue + if char in KEY_INITIAL_CHARS: + pos = key_value_rule(src, pos, out, header, parse_float) + pos = skip_chars(src, pos, TOML_WS) + elif char == "[": + try: + second_char: Optional[str] = src[pos + 1] + except IndexError: + second_char = None + if second_char == "[": + pos, header = create_list_rule(src, pos, out) + else: + pos, header = create_dict_rule(src, pos, out) + pos = skip_chars(src, pos, TOML_WS) + elif char != "#": + raise suffixed_err(src, pos, "Invalid statement") + + # 3. Skip comment + pos = skip_comment(src, pos) + + # 4. Expect end of line or end of file + try: + char = src[pos] + except IndexError: + break + if char != "\n": + raise suffixed_err( + src, pos, "Expected newline or end of document after a statement" + ) + pos += 1 + + return out.data.dict + + +class Flags: + """Flags that map to parsed keys/namespaces.""" + + # Marks an immutable namespace (inline array or inline table). + FROZEN = 0 + # Marks a nest that has been explicitly created and can no longer + # be opened using the "[table]" syntax. + EXPLICIT_NEST = 1 + + def __init__(self) -> None: + self._flags: Dict[str, dict] = {} + + def unset_all(self, key: Key) -> None: + cont = self._flags + for k in key[:-1]: + if k not in cont: + return + cont = cont[k]["nested"] + cont.pop(key[-1], None) + + def set_for_relative_key(self, head_key: Key, rel_key: Key, flag: int) -> None: + cont = self._flags + for k in head_key: + if k not in cont: + cont[k] = {"flags": set(), "recursive_flags": set(), "nested": {}} + cont = cont[k]["nested"] + for k in rel_key: + if k in cont: + cont[k]["flags"].add(flag) + else: + cont[k] = {"flags": {flag}, "recursive_flags": set(), "nested": {}} + cont = cont[k]["nested"] + + def set(self, key: Key, flag: int, *, recursive: bool) -> None: # noqa: A003 + cont = self._flags + key_parent, key_stem = key[:-1], key[-1] + for k in key_parent: + if k not in cont: + cont[k] = {"flags": set(), "recursive_flags": set(), "nested": {}} + cont = cont[k]["nested"] + if key_stem not in cont: + cont[key_stem] = {"flags": set(), "recursive_flags": set(), "nested": {}} + cont[key_stem]["recursive_flags" if recursive else "flags"].add(flag) + + def is_(self, key: Key, flag: int) -> bool: + if not key: + return False # document root has no flags + cont = self._flags + for k in key[:-1]: + if k not in cont: + return False + inner_cont = cont[k] + if flag in inner_cont["recursive_flags"]: + return True + cont = inner_cont["nested"] + key_stem = key[-1] + if key_stem in cont: + cont = cont[key_stem] + return flag in cont["flags"] or flag in cont["recursive_flags"] + return False + + +class NestedDict: + def __init__(self) -> None: + # The parsed content of the TOML document + self.dict: Dict[str, Any] = {} + + def get_or_create_nest( + self, + key: Key, + *, + access_lists: bool = True, + ) -> dict: + cont: Any = self.dict + for k in key: + if k not in cont: + cont[k] = {} + cont = cont[k] + if access_lists and isinstance(cont, list): + cont = cont[-1] + if not isinstance(cont, dict): + raise KeyError("There is no nest behind this key") + return cont + + def append_nest_to_list(self, key: Key) -> None: + cont = self.get_or_create_nest(key[:-1]) + last_key = key[-1] + if last_key in cont: + list_ = cont[last_key] + try: + list_.append({}) + except AttributeError: + raise KeyError("An object other than list found behind this key") + else: + cont[last_key] = [{}] + + +class Output(NamedTuple): + data: NestedDict + flags: Flags + + +def skip_chars(src: str, pos: Pos, chars: Iterable[str]) -> Pos: + try: + while src[pos] in chars: + pos += 1 + except IndexError: + pass + return pos + + +def skip_until( + src: str, + pos: Pos, + expect: str, + *, + error_on: FrozenSet[str], + error_on_eof: bool, +) -> Pos: + try: + new_pos = src.index(expect, pos) + except ValueError: + new_pos = len(src) + if error_on_eof: + raise suffixed_err(src, new_pos, f"Expected {expect!r}") from None + + if not error_on.isdisjoint(src[pos:new_pos]): + while src[pos] not in error_on: + pos += 1 + raise suffixed_err(src, pos, f"Found invalid character {src[pos]!r}") + return new_pos + + +def skip_comment(src: str, pos: Pos) -> Pos: + try: + char: Optional[str] = src[pos] + except IndexError: + char = None + if char == "#": + return skip_until( + src, pos + 1, "\n", error_on=ILLEGAL_COMMENT_CHARS, error_on_eof=False + ) + return pos + + +def skip_comments_and_array_ws(src: str, pos: Pos) -> Pos: + while True: + pos_before_skip = pos + pos = skip_chars(src, pos, TOML_WS_AND_NEWLINE) + pos = skip_comment(src, pos) + if pos == pos_before_skip: + return pos + + +def create_dict_rule(src: str, pos: Pos, out: Output) -> Tuple[Pos, Key]: + pos += 1 # Skip "[" + pos = skip_chars(src, pos, TOML_WS) + pos, key = parse_key(src, pos) + + if out.flags.is_(key, Flags.EXPLICIT_NEST) or out.flags.is_(key, Flags.FROZEN): + raise suffixed_err(src, pos, f"Can not declare {key} twice") + out.flags.set(key, Flags.EXPLICIT_NEST, recursive=False) + try: + out.data.get_or_create_nest(key) + except KeyError: + raise suffixed_err(src, pos, "Can not overwrite a value") from None + + if not src.startswith("]", pos): + raise suffixed_err(src, pos, 'Expected "]" at the end of a table declaration') + return pos + 1, key + + +def create_list_rule(src: str, pos: Pos, out: Output) -> Tuple[Pos, Key]: + pos += 2 # Skip "[[" + pos = skip_chars(src, pos, TOML_WS) + pos, key = parse_key(src, pos) + + if out.flags.is_(key, Flags.FROZEN): + raise suffixed_err(src, pos, f"Can not mutate immutable namespace {key}") + # Free the namespace now that it points to another empty list item... + out.flags.unset_all(key) + # ...but this key precisely is still prohibited from table declaration + out.flags.set(key, Flags.EXPLICIT_NEST, recursive=False) + try: + out.data.append_nest_to_list(key) + except KeyError: + raise suffixed_err(src, pos, "Can not overwrite a value") from None + + if not src.startswith("]]", pos): + raise suffixed_err(src, pos, 'Expected "]]" at the end of an array declaration') + return pos + 2, key + + +def key_value_rule( + src: str, pos: Pos, out: Output, header: Key, parse_float: ParseFloat +) -> Pos: + pos, key, value = parse_key_value_pair(src, pos, parse_float) + key_parent, key_stem = key[:-1], key[-1] + abs_key_parent = header + key_parent + + if out.flags.is_(abs_key_parent, Flags.FROZEN): + raise suffixed_err( + src, pos, f"Can not mutate immutable namespace {abs_key_parent}" + ) + # Containers in the relative path can't be opened with the table syntax after this + out.flags.set_for_relative_key(header, key, Flags.EXPLICIT_NEST) + try: + nest = out.data.get_or_create_nest(abs_key_parent) + except KeyError: + raise suffixed_err(src, pos, "Can not overwrite a value") from None + if key_stem in nest: + raise suffixed_err(src, pos, "Can not overwrite a value") + # Mark inline table and array namespaces recursively immutable + if isinstance(value, (dict, list)): + out.flags.set(header + key, Flags.FROZEN, recursive=True) + nest[key_stem] = value + return pos + + +def parse_key_value_pair( + src: str, pos: Pos, parse_float: ParseFloat +) -> Tuple[Pos, Key, Any]: + pos, key = parse_key(src, pos) + try: + char: Optional[str] = src[pos] + except IndexError: + char = None + if char != "=": + raise suffixed_err(src, pos, 'Expected "=" after a key in a key/value pair') + pos += 1 + pos = skip_chars(src, pos, TOML_WS) + pos, value = parse_value(src, pos, parse_float) + return pos, key, value + + +def parse_key(src: str, pos: Pos) -> Tuple[Pos, Key]: + pos, key_part = parse_key_part(src, pos) + key: Key = (key_part,) + pos = skip_chars(src, pos, TOML_WS) + while True: + try: + char: Optional[str] = src[pos] + except IndexError: + char = None + if char != ".": + return pos, key + pos += 1 + pos = skip_chars(src, pos, TOML_WS) + pos, key_part = parse_key_part(src, pos) + key += (key_part,) + pos = skip_chars(src, pos, TOML_WS) + + +def parse_key_part(src: str, pos: Pos) -> Tuple[Pos, str]: + try: + char: Optional[str] = src[pos] + except IndexError: + char = None + if char in BARE_KEY_CHARS: + start_pos = pos + pos = skip_chars(src, pos, BARE_KEY_CHARS) + return pos, src[start_pos:pos] + if char == "'": + return parse_literal_str(src, pos) + if char == '"': + return parse_one_line_basic_str(src, pos) + raise suffixed_err(src, pos, "Invalid initial character for a key part") + + +def parse_one_line_basic_str(src: str, pos: Pos) -> Tuple[Pos, str]: + pos += 1 + return parse_basic_str(src, pos, multiline=False) + + +def parse_array(src: str, pos: Pos, parse_float: ParseFloat) -> Tuple[Pos, list]: + pos += 1 + array: list = [] + + pos = skip_comments_and_array_ws(src, pos) + if src.startswith("]", pos): + return pos + 1, array + while True: + pos, val = parse_value(src, pos, parse_float) + array.append(val) + pos = skip_comments_and_array_ws(src, pos) + + c = src[pos : pos + 1] + if c == "]": + return pos + 1, array + if c != ",": + raise suffixed_err(src, pos, "Unclosed array") + pos += 1 + + pos = skip_comments_and_array_ws(src, pos) + if src.startswith("]", pos): + return pos + 1, array + + +def parse_inline_table(src: str, pos: Pos, parse_float: ParseFloat) -> Tuple[Pos, dict]: + pos += 1 + nested_dict = NestedDict() + flags = Flags() + + pos = skip_chars(src, pos, TOML_WS) + if src.startswith("}", pos): + return pos + 1, nested_dict.dict + while True: + pos, key, value = parse_key_value_pair(src, pos, parse_float) + key_parent, key_stem = key[:-1], key[-1] + if flags.is_(key, Flags.FROZEN): + raise suffixed_err(src, pos, f"Can not mutate immutable namespace {key}") + try: + nest = nested_dict.get_or_create_nest(key_parent, access_lists=False) + except KeyError: + raise suffixed_err(src, pos, "Can not overwrite a value") from None + if key_stem in nest: + raise suffixed_err(src, pos, f"Duplicate inline table key {key_stem!r}") + nest[key_stem] = value + pos = skip_chars(src, pos, TOML_WS) + c = src[pos : pos + 1] + if c == "}": + return pos + 1, nested_dict.dict + if c != ",": + raise suffixed_err(src, pos, "Unclosed inline table") + if isinstance(value, (dict, list)): + flags.set(key, Flags.FROZEN, recursive=True) + pos += 1 + pos = skip_chars(src, pos, TOML_WS) + + +def parse_basic_str_escape( # noqa: C901 + src: str, pos: Pos, *, multiline: bool = False +) -> Tuple[Pos, str]: + escape_id = src[pos : pos + 2] + pos += 2 + if multiline and escape_id in {"\\ ", "\\\t", "\\\n"}: + # Skip whitespace until next non-whitespace character or end of + # the doc. Error if non-whitespace is found before newline. + if escape_id != "\\\n": + pos = skip_chars(src, pos, TOML_WS) + try: + char = src[pos] + except IndexError: + return pos, "" + if char != "\n": + raise suffixed_err(src, pos, 'Unescaped "\\" in a string') + pos += 1 + pos = skip_chars(src, pos, TOML_WS_AND_NEWLINE) + return pos, "" + if escape_id == "\\u": + return parse_hex_char(src, pos, 4) + if escape_id == "\\U": + return parse_hex_char(src, pos, 8) + try: + return pos, BASIC_STR_ESCAPE_REPLACEMENTS[escape_id] + except KeyError: + if len(escape_id) != 2: + raise suffixed_err(src, pos, "Unterminated string") from None + raise suffixed_err(src, pos, 'Unescaped "\\" in a string') from None + + +def parse_basic_str_escape_multiline(src: str, pos: Pos) -> Tuple[Pos, str]: + return parse_basic_str_escape(src, pos, multiline=True) + + +def parse_hex_char(src: str, pos: Pos, hex_len: int) -> Tuple[Pos, str]: + hex_str = src[pos : pos + hex_len] + if len(hex_str) != hex_len or not HEXDIGIT_CHARS.issuperset(hex_str): + raise suffixed_err(src, pos, "Invalid hex value") + pos += hex_len + hex_int = int(hex_str, 16) + if not is_unicode_scalar_value(hex_int): + raise suffixed_err(src, pos, "Escaped character is not a Unicode scalar value") + return pos, chr(hex_int) + + +def parse_literal_str(src: str, pos: Pos) -> Tuple[Pos, str]: + pos += 1 # Skip starting apostrophe + start_pos = pos + pos = skip_until( + src, pos, "'", error_on=ILLEGAL_LITERAL_STR_CHARS, error_on_eof=True + ) + return pos + 1, src[start_pos:pos] # Skip ending apostrophe + + +def parse_multiline_str(src: str, pos: Pos, *, literal: bool) -> Tuple[Pos, str]: + pos += 3 + if src.startswith("\n", pos): + pos += 1 + + if literal: + delim = "'" + end_pos = skip_until( + src, + pos, + "'''", + error_on=ILLEGAL_MULTILINE_LITERAL_STR_CHARS, + error_on_eof=True, + ) + result = src[pos:end_pos] + pos = end_pos + 3 + else: + delim = '"' + pos, result = parse_basic_str(src, pos, multiline=True) + + # Add at maximum two extra apostrophes/quotes if the end sequence + # is 4 or 5 chars long instead of just 3. + if not src.startswith(delim, pos): + return pos, result + pos += 1 + if not src.startswith(delim, pos): + return pos, result + delim + pos += 1 + return pos, result + (delim * 2) + + +def parse_basic_str(src: str, pos: Pos, *, multiline: bool) -> Tuple[Pos, str]: + if multiline: + error_on = ILLEGAL_MULTILINE_BASIC_STR_CHARS + parse_escapes = parse_basic_str_escape_multiline + else: + error_on = ILLEGAL_BASIC_STR_CHARS + parse_escapes = parse_basic_str_escape + result = "" + start_pos = pos + while True: + try: + char = src[pos] + except IndexError: + raise suffixed_err(src, pos, "Unterminated string") from None + if char == '"': + if not multiline: + return pos + 1, result + src[start_pos:pos] + if src.startswith('"""', pos): + return pos + 3, result + src[start_pos:pos] + pos += 1 + continue + if char == "\\": + result += src[start_pos:pos] + pos, parsed_escape = parse_escapes(src, pos) + result += parsed_escape + start_pos = pos + continue + if char in error_on: + raise suffixed_err(src, pos, f"Illegal character {char!r}") + pos += 1 + + +def parse_value( # noqa: C901 + src: str, pos: Pos, parse_float: ParseFloat +) -> Tuple[Pos, Any]: + try: + char: Optional[str] = src[pos] + except IndexError: + char = None + + # Basic strings + if char == '"': + if src.startswith('"""', pos): + return parse_multiline_str(src, pos, literal=False) + return parse_one_line_basic_str(src, pos) + + # Literal strings + if char == "'": + if src.startswith("'''", pos): + return parse_multiline_str(src, pos, literal=True) + return parse_literal_str(src, pos) + + # Booleans + if char == "t": + if src.startswith("true", pos): + return pos + 4, True + if char == "f": + if src.startswith("false", pos): + return pos + 5, False + + # Dates and times + datetime_match = RE_DATETIME.match(src, pos) + if datetime_match: + try: + datetime_obj = match_to_datetime(datetime_match) + except ValueError as e: + raise suffixed_err(src, pos, "Invalid date or datetime") from e + return datetime_match.end(), datetime_obj + localtime_match = RE_LOCALTIME.match(src, pos) + if localtime_match: + return localtime_match.end(), match_to_localtime(localtime_match) + + # Integers and "normal" floats. + # The regex will greedily match any type starting with a decimal + # char, so needs to be located after handling of dates and times. + number_match = RE_NUMBER.match(src, pos) + if number_match: + return number_match.end(), match_to_number(number_match, parse_float) + + # Arrays + if char == "[": + return parse_array(src, pos, parse_float) + + # Inline tables + if char == "{": + return parse_inline_table(src, pos, parse_float) + + # Special floats + first_three = src[pos : pos + 3] + if first_three in {"inf", "nan"}: + return pos + 3, parse_float(first_three) + first_four = src[pos : pos + 4] + if first_four in {"-inf", "+inf", "-nan", "+nan"}: + return pos + 4, parse_float(first_four) + + raise suffixed_err(src, pos, "Invalid value") + + +def suffixed_err(src: str, pos: Pos, msg: str) -> TOMLDecodeError: + """Return a `TOMLDecodeError` where error message is suffixed with + coordinates in source.""" + + def coord_repr(src: str, pos: Pos) -> str: + if pos >= len(src): + return "end of document" + line = src.count("\n", 0, pos) + 1 + if line == 1: + column = pos + 1 + else: + column = pos - src.rindex("\n", 0, pos) + return f"line {line}, column {column}" + + return TOMLDecodeError(f"{msg} (at {coord_repr(src, pos)})") + + +def is_unicode_scalar_value(codepoint: int) -> bool: + return (0 <= codepoint <= 55295) or (57344 <= codepoint <= 1114111) diff --git a/setuptools/_vendor/tomli/_re.py b/setuptools/_vendor/tomli/_re.py new file mode 100644 index 00000000..9dc9e903 --- /dev/null +++ b/setuptools/_vendor/tomli/_re.py @@ -0,0 +1,101 @@ +from datetime import date, datetime, time, timedelta, timezone, tzinfo +from functools import lru_cache +import re +from typing import Any, Optional, Union + +from tomli._types import ParseFloat + +# E.g. +# - 00:32:00.999999 +# - 00:32:00 +_TIME_RE_STR = r"([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(?:\.([0-9]{1,6})[0-9]*)?" + +RE_NUMBER = re.compile( + r""" +0 +(?: + x[0-9A-Fa-f](?:_?[0-9A-Fa-f])* # hex + | + b[01](?:_?[01])* # bin + | + o[0-7](?:_?[0-7])* # oct +) +| +[+-]?(?:0|[1-9](?:_?[0-9])*) # dec, integer part +(?P + (?:\.[0-9](?:_?[0-9])*)? # optional fractional part + (?:[eE][+-]?[0-9](?:_?[0-9])*)? # optional exponent part +) +""", + flags=re.VERBOSE, +) +RE_LOCALTIME = re.compile(_TIME_RE_STR) +RE_DATETIME = re.compile( + fr""" +([0-9]{{4}})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]) # date, e.g. 1988-10-27 +(?: + [Tt ] + {_TIME_RE_STR} + (?:([Zz])|([+-])([01][0-9]|2[0-3]):([0-5][0-9]))? # optional time offset +)? +""", + flags=re.VERBOSE, +) + + +def match_to_datetime(match: "re.Match") -> Union[datetime, date]: + """Convert a `RE_DATETIME` match to `datetime.datetime` or `datetime.date`. + + Raises ValueError if the match does not correspond to a valid date + or datetime. + """ + ( + year_str, + month_str, + day_str, + hour_str, + minute_str, + sec_str, + micros_str, + zulu_time, + offset_sign_str, + offset_hour_str, + offset_minute_str, + ) = match.groups() + year, month, day = int(year_str), int(month_str), int(day_str) + if hour_str is None: + return date(year, month, day) + hour, minute, sec = int(hour_str), int(minute_str), int(sec_str) + micros = int(micros_str.ljust(6, "0")) if micros_str else 0 + if offset_sign_str: + tz: Optional[tzinfo] = cached_tz( + offset_hour_str, offset_minute_str, offset_sign_str + ) + elif zulu_time: + tz = timezone.utc + else: # local date-time + tz = None + return datetime(year, month, day, hour, minute, sec, micros, tzinfo=tz) + + +@lru_cache(maxsize=None) +def cached_tz(hour_str: str, minute_str: str, sign_str: str) -> timezone: + sign = 1 if sign_str == "+" else -1 + return timezone( + timedelta( + hours=sign * int(hour_str), + minutes=sign * int(minute_str), + ) + ) + + +def match_to_localtime(match: "re.Match") -> time: + hour_str, minute_str, sec_str, micros_str = match.groups() + micros = int(micros_str.ljust(6, "0")) if micros_str else 0 + return time(int(hour_str), int(minute_str), int(sec_str), micros) + + +def match_to_number(match: "re.Match", parse_float: "ParseFloat") -> Any: + if match.group("floatpart"): + return parse_float(match.group()) + return int(match.group(), 0) diff --git a/setuptools/_vendor/tomli/_types.py b/setuptools/_vendor/tomli/_types.py new file mode 100644 index 00000000..e37cc808 --- /dev/null +++ b/setuptools/_vendor/tomli/_types.py @@ -0,0 +1,6 @@ +from typing import Any, Callable, Tuple + +# Type annotations +ParseFloat = Callable[[str], Any] +Key = Tuple[str, ...] +Pos = int diff --git a/setuptools/_vendor/tomli/py.typed b/setuptools/_vendor/tomli/py.typed new file mode 100644 index 00000000..7632ecf7 --- /dev/null +++ b/setuptools/_vendor/tomli/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561 diff --git a/setuptools/_vendor/vendored.txt b/setuptools/_vendor/vendored.txt index db24b402..d10e196a 100644 --- a/setuptools/_vendor/vendored.txt +++ b/setuptools/_vendor/vendored.txt @@ -9,3 +9,4 @@ importlib_metadata==4.11.1 typing_extensions==4.0.1 # required for importlib_resources and _metadata on older Pythons zipp==3.7.0 +tomli==1.2.3 diff --git a/setuptools/extern/__init__.py b/setuptools/extern/__init__.py index 98235a4b..d3a6dc99 100644 --- a/setuptools/extern/__init__.py +++ b/setuptools/extern/__init__.py @@ -71,6 +71,6 @@ class VendorImporter: names = ( 'packaging', 'pyparsing', 'ordered_set', 'more_itertools', 'importlib_metadata', - 'zipp', 'importlib_resources', 'jaraco', 'typing_extensions', + 'zipp', 'importlib_resources', 'jaraco', 'typing_extensions', 'tomli', ) VendorImporter(__name__, names, 'setuptools._vendor').install() -- cgit v1.2.1 From 771488dabe71374a735b266c38e6b8c1fd94a02d Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 3 Dec 2021 10:04:00 +0000 Subject: Add `validate-pyproject` as a vendored dependency In order to minimise dependencies, `validate-pyproject` has the ability to "dump" only the code necessary to run the validations to a given directory. This special strategy is used instead of the default `pip install -t`. The idea of using JSONSchema for validation was suggested in #2671, and the rationale for that approach is further discussed in https://github.com/abravalheri/validate-pyproject/blob/main/docs/faq.rst Using a library such as `validate-pyproject` has the advantage of incentive sing reuse and collaboration with other projects. Currently `validate-pyproject` ships a JSONSchema for the proposed use of `pyproject.toml` as means of configuration for setuptools. In the future, if there is interest, setuptools could also ship its own schema and just use the shared infrastructure of `validate-pyproject` (by advertising the schemas via entry-points). --- setuptools/_vendor/_validate_pyproject/NOTICE | 439 +++++++++ setuptools/_vendor/_validate_pyproject/__init__.py | 31 + .../_validate_pyproject/extra_validations.py | 36 + .../fastjsonschema_exceptions.py | 51 + .../fastjsonschema_validations.py | 1002 ++++++++++++++++++++ setuptools/_vendor/_validate_pyproject/formats.py | 202 ++++ setuptools/_vendor/vendored.txt | 1 + setuptools/extern/__init__.py | 1 + tools/vendored.py | 39 + 9 files changed, 1802 insertions(+) create mode 100644 setuptools/_vendor/_validate_pyproject/NOTICE create mode 100644 setuptools/_vendor/_validate_pyproject/__init__.py create mode 100644 setuptools/_vendor/_validate_pyproject/extra_validations.py create mode 100644 setuptools/_vendor/_validate_pyproject/fastjsonschema_exceptions.py create mode 100644 setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py create mode 100644 setuptools/_vendor/_validate_pyproject/formats.py diff --git a/setuptools/_vendor/_validate_pyproject/NOTICE b/setuptools/_vendor/_validate_pyproject/NOTICE new file mode 100644 index 00000000..020083ac --- /dev/null +++ b/setuptools/_vendor/_validate_pyproject/NOTICE @@ -0,0 +1,439 @@ +The code contained in this directory was automatically generated using the +following command: + + python -m validate_pyproject.vendoring --output-dir setuptools/_vendor/_validate_pyproject --enable-plugins setuptools distutils --very-verbose + +Please avoid changing it manually. + + +You can report issues or suggest changes directly to `validate-pyproject` +(or to the relevant plugin repository) + +- https://github.com/abravalheri/validate-pyproject/issues + + +*** + +The following files include code from opensource projects +(either as direct copies or modified versions): + +- `fastjsonschema_exceptions.py`: + - project: `fastjsonschema` - licensed under BSD-3-Clause + (https://github.com/horejsek/python-fastjsonschema) +- `extra_validations.py` and `format.py`: + - project: `validate-pyproject` - licensed under MPL-2.0 + (https://github.com/abravalheri/validate-pyproject) + + +Additionally the following files are automatically generated by tools provided +by the same projects: + +- `__init__.py` +- `fastjsonschema_validations.py` + +The relevant copyright notes and licenses are included bellow. + + +*** + +`fastjsonschema` +================ + +Copyright (c) 2018, Michal Horejsek +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + Neither the name of the {organization} nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + +*** + +`validate-pyproject` +==================== + +Mozilla Public License, version 2.0 + +1. Definitions + +1.1. "Contributor" + + means each individual or legal entity that creates, contributes to the + creation of, or owns Covered Software. + +1.2. "Contributor Version" + + means the combination of the Contributions of others (if any) used by a + Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + + means Source Code Form to which the initial Contributor has attached the + notice in Exhibit A, the Executable Form of such Source Code Form, and + Modifications of such Source Code Form, in each case including portions + thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + a. that the initial Contributor has attached the notice described in + Exhibit B to the Covered Software; or + + b. that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the terms of + a Secondary License. + +1.6. "Executable Form" + + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + + means a work that combines Covered Software with other material, in a + separate file or files, that is not Covered Software. + +1.8. "License" + + means this document. + +1.9. "Licensable" + + means having the right to grant, to the maximum extent possible, whether + at the time of the initial grant or subsequently, any and all of the + rights conveyed by this License. + +1.10. "Modifications" + + means any of the following: + + a. any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered Software; or + + b. any new file in Source Code Form that contains any Covered Software. + +1.11. "Patent Claims" of a Contributor + + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the License, + by the making, using, selling, offering for sale, having made, import, + or transfer of either its Contributions or its Contributor Version. + +1.12. "Secondary License" + + means either the GNU General Public License, Version 2.0, the GNU Lesser + General Public License, Version 2.1, the GNU Affero General Public + License, Version 3.0, or any later versions of those licenses. + +1.13. "Source Code Form" + + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that controls, is + controlled by, or is under common control with You. For purposes of this + definition, "control" means (a) the power, direct or indirect, to cause + the direction or management of such entity, whether by contract or + otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity. + + +2. License Grants and Conditions + +2.1. Grants + + Each Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + + a. under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + + b. under Patent Claims of such Contributor to make, use, sell, offer for + sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + + The licenses granted in Section 2.1 with respect to any Contribution + become effective for each Contribution on the date the Contributor first + distributes such Contribution. + +2.3. Limitations on Grant Scope + + The licenses granted in this Section 2 are the only rights granted under + this License. No additional rights or licenses will be implied from the + distribution or licensing of Covered Software under this License. + Notwithstanding Section 2.1(b) above, no patent license is granted by a + Contributor: + + a. for any code that a Contributor has removed from Covered Software; or + + b. for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + + c. under Patent Claims infringed by Covered Software in the absence of + its Contributions. + + This License does not grant any rights in the trademarks, service marks, + or logos of any Contributor (except as may be necessary to comply with + the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + + No Contributor makes additional grants as a result of Your choice to + distribute the Covered Software under a subsequent version of this + License (see Section 10.2) or under the terms of a Secondary License (if + permitted under the terms of Section 3.3). + +2.5. Representation + + Each Contributor represents that the Contributor believes its + Contributions are its original creation(s) or it has sufficient rights to + grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + + This License is not intended to limit any rights You have under + applicable copyright doctrines of fair use, fair dealing, or other + equivalents. + +2.7. Conditions + + Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in + Section 2.1. + + +3. Responsibilities + +3.1. Distribution of Source Form + + All distribution of Covered Software in Source Code Form, including any + Modifications that You create or to which You contribute, must be under + the terms of this License. You must inform recipients that the Source + Code Form of the Covered Software is governed by the terms of this + License, and how they can obtain a copy of this License. You may not + attempt to alter or restrict the recipients' rights in the Source Code + Form. + +3.2. Distribution of Executable Form + + If You distribute Covered Software in Executable Form then: + + a. such Covered Software must also be made available in Source Code Form, + as described in Section 3.1, and You must inform recipients of the + Executable Form how they can obtain a copy of such Source Code Form by + reasonable means in a timely manner, at a charge no more than the cost + of distribution to the recipient; and + + b. You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter the + recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + + You may create and distribute a Larger Work under terms of Your choice, + provided that You also comply with the requirements of this License for + the Covered Software. If the Larger Work is a combination of Covered + Software with a work governed by one or more Secondary Licenses, and the + Covered Software is not Incompatible With Secondary Licenses, this + License permits You to additionally distribute such Covered Software + under the terms of such Secondary License(s), so that the recipient of + the Larger Work may, at their option, further distribute the Covered + Software under the terms of either this License or such Secondary + License(s). + +3.4. Notices + + You may not remove or alter the substance of any license notices + (including copyright notices, patent notices, disclaimers of warranty, or + limitations of liability) contained within the Source Code Form of the + Covered Software, except that You may alter any license notices to the + extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + + You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, You may do so only on Your own behalf, and not on + behalf of any Contributor. You must make it absolutely clear that any + such warranty, support, indemnity, or liability obligation is offered by + You alone, and You hereby agree to indemnify every Contributor for any + liability incurred by such Contributor as a result of warranty, support, + indemnity or liability terms You offer. You may include additional + disclaimers of warranty and limitations of liability specific to any + jurisdiction. + +4. Inability to Comply Due to Statute or Regulation + + If it is impossible for You to comply with any of the terms of this License + with respect to some or all of the Covered Software due to statute, + judicial order, or regulation then You must: (a) comply with the terms of + this License to the maximum extent possible; and (b) describe the + limitations and the code they affect. Such description must be placed in a + text file included with all distributions of the Covered Software under + this License. Except to the extent prohibited by statute or regulation, + such description must be sufficiently detailed for a recipient of ordinary + skill to be able to understand it. + +5. Termination + +5.1. The rights granted under this License will terminate automatically if You + fail to comply with any of its terms. However, if You become compliant, + then the rights granted under this License from a particular Contributor + are reinstated (a) provisionally, unless and until such Contributor + explicitly and finally terminates Your grants, and (b) on an ongoing + basis, if such Contributor fails to notify You of the non-compliance by + some reasonable means prior to 60 days after You have come back into + compliance. Moreover, Your grants from a particular Contributor are + reinstated on an ongoing basis if such Contributor notifies You of the + non-compliance by some reasonable means, this is the first time You have + received notice of non-compliance with this License from such + Contributor, and You become compliant prior to 30 days after Your receipt + of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent + infringement claim (excluding declaratory judgment actions, + counter-claims, and cross-claims) alleging that a Contributor Version + directly or indirectly infringes any patent, then the rights granted to + You by any and all Contributors for the Covered Software under Section + 2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user + license agreements (excluding distributors and resellers) which have been + validly granted by You or Your distributors under this License prior to + termination shall survive termination. + +6. Disclaimer of Warranty + + Covered Software is provided under this License on an "as is" basis, + without warranty of any kind, either expressed, implied, or statutory, + including, without limitation, warranties that the Covered Software is free + of defects, merchantable, fit for a particular purpose or non-infringing. + The entire risk as to the quality and performance of the Covered Software + is with You. Should any Covered Software prove defective in any respect, + You (not any Contributor) assume the cost of any necessary servicing, + repair, or correction. This disclaimer of warranty constitutes an essential + part of this License. No use of any Covered Software is authorized under + this License except under this disclaimer. + +7. Limitation of Liability + + Under no circumstances and under no legal theory, whether tort (including + negligence), contract, or otherwise, shall any Contributor, or anyone who + distributes Covered Software as permitted above, be liable to You for any + direct, indirect, special, incidental, or consequential damages of any + character including, without limitation, damages for lost profits, loss of + goodwill, work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses, even if such party shall have been + informed of the possibility of such damages. This limitation of liability + shall not apply to liability for death or personal injury resulting from + such party's negligence to the extent applicable law prohibits such + limitation. Some jurisdictions do not allow the exclusion or limitation of + incidental or consequential damages, so this exclusion and limitation may + not apply to You. + +8. Litigation + + Any litigation relating to this License may be brought only in the courts + of a jurisdiction where the defendant maintains its principal place of + business and such litigation shall be governed by laws of that + jurisdiction, without reference to its conflict-of-law provisions. Nothing + in this Section shall prevent a party's ability to bring cross-claims or + counter-claims. + +9. Miscellaneous + + This License represents the complete agreement concerning the subject + matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the extent + necessary to make it enforceable. Any law or regulation which provides that + the language of a contract shall be construed against the drafter shall not + be used to construe this License against a Contributor. + + +10. Versions of the License + +10.1. New Versions + + Mozilla Foundation is the license steward. Except as provided in Section + 10.3, no one other than the license steward has the right to modify or + publish new versions of this License. Each version will be given a + distinguishing version number. + +10.2. Effect of New Versions + + You may distribute the Covered Software under the terms of the version + of the License under which You originally received the Covered Software, + or under the terms of any subsequent version published by the license + steward. + +10.3. Modified Versions + + If you create software not governed by this License, and you want to + create a new license for such software, you may create and use a + modified version of this License if you rename the license and remove + any references to the name of the license steward (except to note that + such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary + Licenses If You choose to distribute Source Code Form that is + Incompatible With Secondary Licenses under the terms of this version of + the License, the notice described in Exhibit B of this License must be + attached. + +Exhibit A - Source Code Form License Notice + + This Source Code Form is subject to the + terms of the Mozilla Public License, v. + 2.0. If a copy of the MPL was not + distributed with this file, You can + obtain one at + https://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular file, +then You may include the notice in a location (such as a LICENSE file in a +relevant directory) where a recipient would be likely to look for such a +notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice + + This Source Code Form is "Incompatible + With Secondary Licenses", as defined by + the Mozilla Public License, v. 2.0. + diff --git a/setuptools/_vendor/_validate_pyproject/__init__.py b/setuptools/_vendor/_validate_pyproject/__init__.py new file mode 100644 index 00000000..2b1e77f3 --- /dev/null +++ b/setuptools/_vendor/_validate_pyproject/__init__.py @@ -0,0 +1,31 @@ +from functools import reduce +from typing import Any, Callable, Dict + +from . import formats +from .extra_validations import EXTRA_VALIDATIONS +from .fastjsonschema_exceptions import JsonSchemaException, JsonSchemaValueException +from .fastjsonschema_validations import validate as _validate + +__all__ = [ + "validate", + "FORMAT_FUNCTIONS", + "EXTRA_VALIDATIONS", + "JsonSchemaException", + "JsonSchemaValueException", +] + + +FORMAT_FUNCTIONS: Dict[str, Callable[[str], bool]] = { + fn.__name__.replace("_", "-"): fn + for fn in formats.__dict__.values() + if callable(fn) and not fn.__name__.startswith("_") +} + + +def validate(data: Any) -> bool: + """Validate the given ``data`` object using JSON Schema + This function raises ``JsonSchemaValueException`` if ``data`` is invalid. + """ + _validate(data, custom_formats=FORMAT_FUNCTIONS) + reduce(lambda acc, fn: fn(acc), EXTRA_VALIDATIONS, data) + return True diff --git a/setuptools/_vendor/_validate_pyproject/extra_validations.py b/setuptools/_vendor/_validate_pyproject/extra_validations.py new file mode 100644 index 00000000..d7d5b39d --- /dev/null +++ b/setuptools/_vendor/_validate_pyproject/extra_validations.py @@ -0,0 +1,36 @@ +"""The purpose of this module is implement PEP 621 validations that are +difficult to express as a JSON Schema (or that are not supported by the current +JSON Schema library). +""" + +from typing import Mapping, TypeVar + +from .fastjsonschema_exceptions import JsonSchemaValueException + +T = TypeVar("T", bound=Mapping) + + +class RedefiningStaticFieldAsDynamic(JsonSchemaValueException): + """According to PEP 621: + + Build back-ends MUST raise an error if the metadata specifies a field + statically as well as being listed in dynamic. + """ + + +def validate_project_dynamic(pyproject: T) -> T: + project_table = pyproject.get("project", {}) + dynamic = project_table.get("dynamic", []) + + for field in dynamic: + if field in project_table: + msg = f"You cannot provided a value for `project.{field}` and " + msg += "list it under `project.dynamic` at the same time" + name = f"data.project.{field}" + value = {field: project_table[field], "...": " # ...", "dynamic": dynamic} + raise RedefiningStaticFieldAsDynamic(msg, value, name, rule="PEP 621") + + return pyproject + + +EXTRA_VALIDATIONS = (validate_project_dynamic,) diff --git a/setuptools/_vendor/_validate_pyproject/fastjsonschema_exceptions.py b/setuptools/_vendor/_validate_pyproject/fastjsonschema_exceptions.py new file mode 100644 index 00000000..63d98199 --- /dev/null +++ b/setuptools/_vendor/_validate_pyproject/fastjsonschema_exceptions.py @@ -0,0 +1,51 @@ +import re + + +SPLIT_RE = re.compile(r'[\.\[\]]+') + + +class JsonSchemaException(ValueError): + """ + Base exception of ``fastjsonschema`` library. + """ + + +class JsonSchemaValueException(JsonSchemaException): + """ + Exception raised by validation function. Available properties: + + * ``message`` containing human-readable information what is wrong (e.g. ``data.property[index] must be smaller than or equal to 42``), + * invalid ``value`` (e.g. ``60``), + * ``name`` of a path in the data structure (e.g. ``data.propery[index]``), + * ``path`` as an array in the data structure (e.g. ``['data', 'propery', 'index']``), + * the whole ``definition`` which the ``value`` has to fulfil (e.g. ``{'type': 'number', 'maximum': 42}``), + * ``rule`` which the ``value`` is breaking (e.g. ``maximum``) + * and ``rule_definition`` (e.g. ``42``). + + .. versionchanged:: 2.14.0 + Added all extra properties. + """ + + def __init__(self, message, value=None, name=None, definition=None, rule=None): + super().__init__(message) + self.message = message + self.value = value + self.name = name + self.definition = definition + self.rule = rule + + @property + def path(self): + return [item for item in SPLIT_RE.split(self.name) if item != ''] + + @property + def rule_definition(self): + if not self.rule or not self.definition: + return None + return self.definition.get(self.rule) + + +class JsonSchemaDefinitionException(JsonSchemaException): + """ + Exception raised by generator of validation function. + """ diff --git a/setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py b/setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py new file mode 100644 index 00000000..d409b2a5 --- /dev/null +++ b/setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py @@ -0,0 +1,1002 @@ +# noqa +# type: ignore +# flake8: noqa +# pylint: skip-file +# mypy: ignore-errors +# yapf: disable +# pylama:skip=1 + + +# *** PLEASE DO NOT MODIFY DIRECTLY: Automatically generated code *** + + +VERSION = "2.15.2" +import re +from .fastjsonschema_exceptions import JsonSchemaValueException + + +REGEX_PATTERNS = { + '^.*$': re.compile('^.*$'), + '.+': re.compile('.+'), + '^.+$': re.compile('^.+$'), + 'idn-email_re_pattern': re.compile('^[^@]+@[^@]+\\.[^@]+\\Z') +} + +NoneType = type(None) + +def validate(data, custom_formats={}): + validate_https___www_python_org_dev_peps_pep_0517(data, custom_formats) + return data + +def validate_https___www_python_org_dev_peps_pep_0517(data, custom_formats={}): + if not isinstance(data, (dict)): + raise JsonSchemaValueException("data must be object", value=data, name="data", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://www.python.org/dev/peps/pep-0517/', 'title': 'Data structure for ``pyproject.toml`` files', '$$description': [':pep:`517` defines a build-system independent format for source trees', 'while :pep:`518` provides a way of specifying the minimum system requirements', 'for Python projects.', 'Please notice the ``project`` table (as defined in :pep:`621`) is not included', 'in this schema and should be considered separately.'], 'type': 'object', 'additionalProperties': False, 'properties': {'build-system': {'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, 'project': {'$ref': 'https://www.python.org/dev/peps/pep-0621/'}, 'tool': {'type': 'object', 'properties': {'distutils': {'$ref': 'https://docs.python.org/3/install/'}, 'setuptools': {'$ref': 'https://setuptools.pypa.io/en/latest/references/keywords.html'}}}}, 'project': {'$ref': 'https://www.python.org/dev/peps/pep-0621/'}}, rule='type') + data_is_dict = isinstance(data, dict) + if data_is_dict: + data_keys = set(data.keys()) + if "build-system" in data_keys: + data_keys.remove("build-system") + data__buildsystem = data["build-system"] + if not isinstance(data__buildsystem, (dict)): + raise JsonSchemaValueException("data.build-system must be object", value=data__buildsystem, name="data.build-system", definition={'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, rule='type') + data__buildsystem_is_dict = isinstance(data__buildsystem, dict) + if data__buildsystem_is_dict: + data__buildsystem_len = len(data__buildsystem) + if not all(prop in data__buildsystem for prop in ['requires']): + raise JsonSchemaValueException("data.build-system must contain ['requires'] properties", value=data__buildsystem, name="data.build-system", definition={'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, rule='required') + data__buildsystem_keys = set(data__buildsystem.keys()) + if "requires" in data__buildsystem_keys: + data__buildsystem_keys.remove("requires") + data__buildsystem__requires = data__buildsystem["requires"] + if not isinstance(data__buildsystem__requires, (list, tuple)): + raise JsonSchemaValueException("data.build-system.requires must be array", value=data__buildsystem__requires, name="data.build-system.requires", definition={'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, rule='type') + data__buildsystem__requires_is_list = isinstance(data__buildsystem__requires, (list, tuple)) + if data__buildsystem__requires_is_list: + data__buildsystem__requires_len = len(data__buildsystem__requires) + for data__buildsystem__requires_x, data__buildsystem__requires_item in enumerate(data__buildsystem__requires): + if not isinstance(data__buildsystem__requires_item, (str)): + raise JsonSchemaValueException(""+"data.build-system.requires[{data__buildsystem__requires_x}]".format(**locals())+" must be string", value=data__buildsystem__requires_item, name=""+"data.build-system.requires[{data__buildsystem__requires_x}]".format(**locals())+"", definition={'type': 'string'}, rule='type') + if "build-backend" in data__buildsystem_keys: + data__buildsystem_keys.remove("build-backend") + data__buildsystem__buildbackend = data__buildsystem["build-backend"] + if not isinstance(data__buildsystem__buildbackend, (str)): + raise JsonSchemaValueException("data.build-system.build-backend must be string", value=data__buildsystem__buildbackend, name="data.build-system.build-backend", definition={'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, rule='type') + if isinstance(data__buildsystem__buildbackend, str): + if not custom_formats["pep517-backend-reference"](data__buildsystem__buildbackend): + raise JsonSchemaValueException("data.build-system.build-backend must be pep517-backend-reference", value=data__buildsystem__buildbackend, name="data.build-system.build-backend", definition={'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, rule='format') + if "backend-path" in data__buildsystem_keys: + data__buildsystem_keys.remove("backend-path") + data__buildsystem__backendpath = data__buildsystem["backend-path"] + if not isinstance(data__buildsystem__backendpath, (list, tuple)): + raise JsonSchemaValueException("data.build-system.backend-path must be array", value=data__buildsystem__backendpath, name="data.build-system.backend-path", definition={'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}, rule='type') + data__buildsystem__backendpath_is_list = isinstance(data__buildsystem__backendpath, (list, tuple)) + if data__buildsystem__backendpath_is_list: + data__buildsystem__backendpath_len = len(data__buildsystem__backendpath) + for data__buildsystem__backendpath_x, data__buildsystem__backendpath_item in enumerate(data__buildsystem__backendpath): + if not isinstance(data__buildsystem__backendpath_item, (str)): + raise JsonSchemaValueException(""+"data.build-system.backend-path[{data__buildsystem__backendpath_x}]".format(**locals())+" must be string", value=data__buildsystem__backendpath_item, name=""+"data.build-system.backend-path[{data__buildsystem__backendpath_x}]".format(**locals())+"", definition={'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}, rule='type') + if data__buildsystem_keys: + raise JsonSchemaValueException("data.build-system must not contain "+str(data__buildsystem_keys)+" properties", value=data__buildsystem, name="data.build-system", definition={'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, rule='additionalProperties') + if "project" in data_keys: + data_keys.remove("project") + data__project = data["project"] + validate_https___www_python_org_dev_peps_pep_0621(data__project, custom_formats) + if "tool" in data_keys: + data_keys.remove("tool") + data__tool = data["tool"] + if not isinstance(data__tool, (dict)): + raise JsonSchemaValueException("data.tool must be object", value=data__tool, name="data.tool", definition={'type': 'object', 'properties': {'distutils': {'$ref': 'https://docs.python.org/3/install/'}, 'setuptools': {'$ref': 'https://setuptools.pypa.io/en/latest/references/keywords.html'}}}, rule='type') + data__tool_is_dict = isinstance(data__tool, dict) + if data__tool_is_dict: + data__tool_keys = set(data__tool.keys()) + if "distutils" in data__tool_keys: + data__tool_keys.remove("distutils") + data__tool__distutils = data__tool["distutils"] + validate_https___docs_python_org_3_install(data__tool__distutils, custom_formats) + if "setuptools" in data__tool_keys: + data__tool_keys.remove("setuptools") + data__tool__setuptools = data__tool["setuptools"] + validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data__tool__setuptools, custom_formats) + if data_keys: + raise JsonSchemaValueException("data must not contain "+str(data_keys)+" properties", value=data, name="data", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://www.python.org/dev/peps/pep-0517/', 'title': 'Data structure for ``pyproject.toml`` files', '$$description': [':pep:`517` defines a build-system independent format for source trees', 'while :pep:`518` provides a way of specifying the minimum system requirements', 'for Python projects.', 'Please notice the ``project`` table (as defined in :pep:`621`) is not included', 'in this schema and should be considered separately.'], 'type': 'object', 'additionalProperties': False, 'properties': {'build-system': {'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, 'project': {'$ref': 'https://www.python.org/dev/peps/pep-0621/'}, 'tool': {'type': 'object', 'properties': {'distutils': {'$ref': 'https://docs.python.org/3/install/'}, 'setuptools': {'$ref': 'https://setuptools.pypa.io/en/latest/references/keywords.html'}}}}, 'project': {'$ref': 'https://www.python.org/dev/peps/pep-0621/'}}, rule='additionalProperties') + return data + +def validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data, custom_formats={}): + if not isinstance(data, (dict)): + raise JsonSchemaValueException("data must be object", value=data, name="data", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$ref': '#/definitions/find-directive'}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', ' cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'$ref': '#/definitions/attr-directive'}, {'$ref': '#/definitions/file-directive'}]}, 'classifiers': {'$ref': '#/definitions/file-directive'}, 'description': {'$ref': '#/definitions/file-directive'}, 'entry-points': {'$ref': '#/definitions/file-directive'}, 'readme': {'anyOf': [{'$ref': '#/definitions/file-directive'}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}, 'license': {'type': 'string', '$$description': ['PROVISIONAL: A string specifying the license of the package', '(might change with PEP 639)'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-expression``?'}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might change with PEP 639)'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-files.glob``?'}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}, rule='type') + data_is_dict = isinstance(data, dict) + if data_is_dict: + data_keys = set(data.keys()) + if "platforms" in data_keys: + data_keys.remove("platforms") + data__platforms = data["platforms"] + if not isinstance(data__platforms, (list, tuple)): + raise JsonSchemaValueException("data.platforms must be array", value=data__platforms, name="data.platforms", definition={'type': 'array', 'items': {'type': 'string'}}, rule='type') + data__platforms_is_list = isinstance(data__platforms, (list, tuple)) + if data__platforms_is_list: + data__platforms_len = len(data__platforms) + for data__platforms_x, data__platforms_item in enumerate(data__platforms): + if not isinstance(data__platforms_item, (str)): + raise JsonSchemaValueException(""+"data.platforms[{data__platforms_x}]".format(**locals())+" must be string", value=data__platforms_item, name=""+"data.platforms[{data__platforms_x}]".format(**locals())+"", definition={'type': 'string'}, rule='type') + if "provides" in data_keys: + data_keys.remove("provides") + data__provides = data["provides"] + if not isinstance(data__provides, (list, tuple)): + raise JsonSchemaValueException("data.provides must be array", value=data__provides, name="data.provides", definition={'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, rule='type') + data__provides_is_list = isinstance(data__provides, (list, tuple)) + if data__provides_is_list: + data__provides_len = len(data__provides) + for data__provides_x, data__provides_item in enumerate(data__provides): + if not isinstance(data__provides_item, (str)): + raise JsonSchemaValueException(""+"data.provides[{data__provides_x}]".format(**locals())+" must be string", value=data__provides_item, name=""+"data.provides[{data__provides_x}]".format(**locals())+"", definition={'type': 'string', 'format': 'pep508-identifier'}, rule='type') + if isinstance(data__provides_item, str): + if not custom_formats["pep508-identifier"](data__provides_item): + raise JsonSchemaValueException(""+"data.provides[{data__provides_x}]".format(**locals())+" must be pep508-identifier", value=data__provides_item, name=""+"data.provides[{data__provides_x}]".format(**locals())+"", definition={'type': 'string', 'format': 'pep508-identifier'}, rule='format') + if "obsoletes" in data_keys: + data_keys.remove("obsoletes") + data__obsoletes = data["obsoletes"] + if not isinstance(data__obsoletes, (list, tuple)): + raise JsonSchemaValueException("data.obsoletes must be array", value=data__obsoletes, name="data.obsoletes", definition={'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, rule='type') + data__obsoletes_is_list = isinstance(data__obsoletes, (list, tuple)) + if data__obsoletes_is_list: + data__obsoletes_len = len(data__obsoletes) + for data__obsoletes_x, data__obsoletes_item in enumerate(data__obsoletes): + if not isinstance(data__obsoletes_item, (str)): + raise JsonSchemaValueException(""+"data.obsoletes[{data__obsoletes_x}]".format(**locals())+" must be string", value=data__obsoletes_item, name=""+"data.obsoletes[{data__obsoletes_x}]".format(**locals())+"", definition={'type': 'string', 'format': 'pep508-identifier'}, rule='type') + if isinstance(data__obsoletes_item, str): + if not custom_formats["pep508-identifier"](data__obsoletes_item): + raise JsonSchemaValueException(""+"data.obsoletes[{data__obsoletes_x}]".format(**locals())+" must be pep508-identifier", value=data__obsoletes_item, name=""+"data.obsoletes[{data__obsoletes_x}]".format(**locals())+"", definition={'type': 'string', 'format': 'pep508-identifier'}, rule='format') + if "zip-safe" in data_keys: + data_keys.remove("zip-safe") + data__zipsafe = data["zip-safe"] + if not isinstance(data__zipsafe, (bool)): + raise JsonSchemaValueException("data.zip-safe must be boolean", value=data__zipsafe, name="data.zip-safe", definition={'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, rule='type') + if "script-files" in data_keys: + data_keys.remove("script-files") + data__scriptfiles = data["script-files"] + if not isinstance(data__scriptfiles, (list, tuple)): + raise JsonSchemaValueException("data.script-files must be array", value=data__scriptfiles, name="data.script-files", definition={'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, rule='type') + data__scriptfiles_is_list = isinstance(data__scriptfiles, (list, tuple)) + if data__scriptfiles_is_list: + data__scriptfiles_len = len(data__scriptfiles) + for data__scriptfiles_x, data__scriptfiles_item in enumerate(data__scriptfiles): + if not isinstance(data__scriptfiles_item, (str)): + raise JsonSchemaValueException(""+"data.script-files[{data__scriptfiles_x}]".format(**locals())+" must be string", value=data__scriptfiles_item, name=""+"data.script-files[{data__scriptfiles_x}]".format(**locals())+"", definition={'type': 'string'}, rule='type') + if "eager-resources" in data_keys: + data_keys.remove("eager-resources") + data__eagerresources = data["eager-resources"] + if not isinstance(data__eagerresources, (list, tuple)): + raise JsonSchemaValueException("data.eager-resources must be array", value=data__eagerresources, name="data.eager-resources", definition={'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, rule='type') + data__eagerresources_is_list = isinstance(data__eagerresources, (list, tuple)) + if data__eagerresources_is_list: + data__eagerresources_len = len(data__eagerresources) + for data__eagerresources_x, data__eagerresources_item in enumerate(data__eagerresources): + if not isinstance(data__eagerresources_item, (str)): + raise JsonSchemaValueException(""+"data.eager-resources[{data__eagerresources_x}]".format(**locals())+" must be string", value=data__eagerresources_item, name=""+"data.eager-resources[{data__eagerresources_x}]".format(**locals())+"", definition={'type': 'string'}, rule='type') + if "packages" in data_keys: + data_keys.remove("packages") + data__packages = data["packages"] + data__packages_one_of_count1 = 0 + if data__packages_one_of_count1 < 2: + try: + if not isinstance(data__packages, (list, tuple)): + raise JsonSchemaValueException("data.packages must be array", value=data__packages, name="data.packages", definition={'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, rule='type') + data__packages_is_list = isinstance(data__packages, (list, tuple)) + if data__packages_is_list: + data__packages_len = len(data__packages) + for data__packages_x, data__packages_item in enumerate(data__packages): + if not isinstance(data__packages_item, (str)): + raise JsonSchemaValueException(""+"data.packages[{data__packages_x}]".format(**locals())+" must be string", value=data__packages_item, name=""+"data.packages[{data__packages_x}]".format(**locals())+"", definition={'type': 'string', 'format': 'python-module-name'}, rule='type') + if isinstance(data__packages_item, str): + if not custom_formats["python-module-name"](data__packages_item): + raise JsonSchemaValueException(""+"data.packages[{data__packages_x}]".format(**locals())+" must be python-module-name", value=data__packages_item, name=""+"data.packages[{data__packages_x}]".format(**locals())+"", definition={'type': 'string', 'format': 'python-module-name'}, rule='format') + data__packages_one_of_count1 += 1 + except JsonSchemaValueException: pass + if data__packages_one_of_count1 < 2: + try: + validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_find_directive(data__packages, custom_formats) + data__packages_one_of_count1 += 1 + except JsonSchemaValueException: pass + if data__packages_one_of_count1 != 1: + raise JsonSchemaValueException("data.packages must be valid exactly by one of oneOf definition", value=data__packages, name="data.packages", definition={'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$ref': '#/definitions/find-directive'}]}, rule='oneOf') + if "package-dir" in data_keys: + data_keys.remove("package-dir") + data__packagedir = data["package-dir"] + if not isinstance(data__packagedir, (dict)): + raise JsonSchemaValueException("data.package-dir must be object", value=data__packagedir, name="data.package-dir", definition={'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, rule='type') + data__packagedir_is_dict = isinstance(data__packagedir, dict) + if data__packagedir_is_dict: + data__packagedir_keys = set(data__packagedir.keys()) + for data__packagedir_key, data__packagedir_val in data__packagedir.items(): + if REGEX_PATTERNS['^.*$'].search(data__packagedir_key): + if data__packagedir_key in data__packagedir_keys: + data__packagedir_keys.remove(data__packagedir_key) + if not isinstance(data__packagedir_val, (str)): + raise JsonSchemaValueException(""+"data.package-dir.{data__packagedir_key}".format(**locals())+" must be string", value=data__packagedir_val, name=""+"data.package-dir.{data__packagedir_key}".format(**locals())+"", definition={'type': 'string'}, rule='type') + if data__packagedir_keys: + raise JsonSchemaValueException("data.package-dir must not contain "+str(data__packagedir_keys)+" properties", value=data__packagedir, name="data.package-dir", definition={'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, rule='additionalProperties') + data__packagedir_len = len(data__packagedir) + if data__packagedir_len != 0: + data__packagedir_property_names = True + for data__packagedir_key in data__packagedir: + try: + data__packagedir_key_one_of_count2 = 0 + if data__packagedir_key_one_of_count2 < 2: + try: + if isinstance(data__packagedir_key, str): + if not custom_formats["python-module-name"](data__packagedir_key): + raise JsonSchemaValueException("data.package-dir must be python-module-name", value=data__packagedir_key, name="data.package-dir", definition={'format': 'python-module-name'}, rule='format') + data__packagedir_key_one_of_count2 += 1 + except JsonSchemaValueException: pass + if data__packagedir_key_one_of_count2 < 2: + try: + if data__packagedir_key != "": + raise JsonSchemaValueException("data.package-dir must be same as const definition: ", value=data__packagedir_key, name="data.package-dir", definition={'const': ''}, rule='const') + data__packagedir_key_one_of_count2 += 1 + except JsonSchemaValueException: pass + if data__packagedir_key_one_of_count2 != 1: + raise JsonSchemaValueException("data.package-dir must be valid exactly by one of oneOf definition", value=data__packagedir_key, name="data.package-dir", definition={'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, rule='oneOf') + except JsonSchemaValueException: + data__packagedir_property_names = False + if not data__packagedir_property_names: + raise JsonSchemaValueException("data.package-dir must be named by propertyName definition", value=data__packagedir, name="data.package-dir", definition={'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, rule='propertyNames') + if "package-data" in data_keys: + data_keys.remove("package-data") + data__packagedata = data["package-data"] + if not isinstance(data__packagedata, (dict)): + raise JsonSchemaValueException("data.package-data must be object", value=data__packagedata, name="data.package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='type') + data__packagedata_is_dict = isinstance(data__packagedata, dict) + if data__packagedata_is_dict: + data__packagedata_keys = set(data__packagedata.keys()) + for data__packagedata_key, data__packagedata_val in data__packagedata.items(): + if REGEX_PATTERNS['^.*$'].search(data__packagedata_key): + if data__packagedata_key in data__packagedata_keys: + data__packagedata_keys.remove(data__packagedata_key) + if not isinstance(data__packagedata_val, (list, tuple)): + raise JsonSchemaValueException(""+"data.package-data.{data__packagedata_key}".format(**locals())+" must be array", value=data__packagedata_val, name=""+"data.package-data.{data__packagedata_key}".format(**locals())+"", definition={'type': 'array', 'items': {'type': 'string'}}, rule='type') + data__packagedata_val_is_list = isinstance(data__packagedata_val, (list, tuple)) + if data__packagedata_val_is_list: + data__packagedata_val_len = len(data__packagedata_val) + for data__packagedata_val_x, data__packagedata_val_item in enumerate(data__packagedata_val): + if not isinstance(data__packagedata_val_item, (str)): + raise JsonSchemaValueException(""+"data.package-data.{data__packagedata_key}[{data__packagedata_val_x}]".format(**locals())+" must be string", value=data__packagedata_val_item, name=""+"data.package-data.{data__packagedata_key}[{data__packagedata_val_x}]".format(**locals())+"", definition={'type': 'string'}, rule='type') + if data__packagedata_keys: + raise JsonSchemaValueException("data.package-data must not contain "+str(data__packagedata_keys)+" properties", value=data__packagedata, name="data.package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='additionalProperties') + data__packagedata_len = len(data__packagedata) + if data__packagedata_len != 0: + data__packagedata_property_names = True + for data__packagedata_key in data__packagedata: + try: + data__packagedata_key_one_of_count3 = 0 + if data__packagedata_key_one_of_count3 < 2: + try: + if isinstance(data__packagedata_key, str): + if not custom_formats["python-module-name"](data__packagedata_key): + raise JsonSchemaValueException("data.package-data must be python-module-name", value=data__packagedata_key, name="data.package-data", definition={'format': 'python-module-name'}, rule='format') + data__packagedata_key_one_of_count3 += 1 + except JsonSchemaValueException: pass + if data__packagedata_key_one_of_count3 < 2: + try: + if data__packagedata_key != "*": + raise JsonSchemaValueException("data.package-data must be same as const definition: *", value=data__packagedata_key, name="data.package-data", definition={'const': '*'}, rule='const') + data__packagedata_key_one_of_count3 += 1 + except JsonSchemaValueException: pass + if data__packagedata_key_one_of_count3 != 1: + raise JsonSchemaValueException("data.package-data must be valid exactly by one of oneOf definition", value=data__packagedata_key, name="data.package-data", definition={'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, rule='oneOf') + except JsonSchemaValueException: + data__packagedata_property_names = False + if not data__packagedata_property_names: + raise JsonSchemaValueException("data.package-data must be named by propertyName definition", value=data__packagedata, name="data.package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='propertyNames') + if "include-package-data" in data_keys: + data_keys.remove("include-package-data") + data__includepackagedata = data["include-package-data"] + if not isinstance(data__includepackagedata, (bool)): + raise JsonSchemaValueException("data.include-package-data must be boolean", value=data__includepackagedata, name="data.include-package-data", definition={'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, rule='type') + if "exclude-package-data" in data_keys: + data_keys.remove("exclude-package-data") + data__excludepackagedata = data["exclude-package-data"] + if not isinstance(data__excludepackagedata, (dict)): + raise JsonSchemaValueException("data.exclude-package-data must be object", value=data__excludepackagedata, name="data.exclude-package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='type') + data__excludepackagedata_is_dict = isinstance(data__excludepackagedata, dict) + if data__excludepackagedata_is_dict: + data__excludepackagedata_keys = set(data__excludepackagedata.keys()) + for data__excludepackagedata_key, data__excludepackagedata_val in data__excludepackagedata.items(): + if REGEX_PATTERNS['^.*$'].search(data__excludepackagedata_key): + if data__excludepackagedata_key in data__excludepackagedata_keys: + data__excludepackagedata_keys.remove(data__excludepackagedata_key) + if not isinstance(data__excludepackagedata_val, (list, tuple)): + raise JsonSchemaValueException(""+"data.exclude-package-data.{data__excludepackagedata_key}".format(**locals())+" must be array", value=data__excludepackagedata_val, name=""+"data.exclude-package-data.{data__excludepackagedata_key}".format(**locals())+"", definition={'type': 'array', 'items': {'type': 'string'}}, rule='type') + data__excludepackagedata_val_is_list = isinstance(data__excludepackagedata_val, (list, tuple)) + if data__excludepackagedata_val_is_list: + data__excludepackagedata_val_len = len(data__excludepackagedata_val) + for data__excludepackagedata_val_x, data__excludepackagedata_val_item in enumerate(data__excludepackagedata_val): + if not isinstance(data__excludepackagedata_val_item, (str)): + raise JsonSchemaValueException(""+"data.exclude-package-data.{data__excludepackagedata_key}[{data__excludepackagedata_val_x}]".format(**locals())+" must be string", value=data__excludepackagedata_val_item, name=""+"data.exclude-package-data.{data__excludepackagedata_key}[{data__excludepackagedata_val_x}]".format(**locals())+"", definition={'type': 'string'}, rule='type') + if data__excludepackagedata_keys: + raise JsonSchemaValueException("data.exclude-package-data must not contain "+str(data__excludepackagedata_keys)+" properties", value=data__excludepackagedata, name="data.exclude-package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='additionalProperties') + data__excludepackagedata_len = len(data__excludepackagedata) + if data__excludepackagedata_len != 0: + data__excludepackagedata_property_names = True + for data__excludepackagedata_key in data__excludepackagedata: + try: + data__excludepackagedata_key_one_of_count4 = 0 + if data__excludepackagedata_key_one_of_count4 < 2: + try: + if isinstance(data__excludepackagedata_key, str): + if not custom_formats["python-module-name"](data__excludepackagedata_key): + raise JsonSchemaValueException("data.exclude-package-data must be python-module-name", value=data__excludepackagedata_key, name="data.exclude-package-data", definition={'format': 'python-module-name'}, rule='format') + data__excludepackagedata_key_one_of_count4 += 1 + except JsonSchemaValueException: pass + if data__excludepackagedata_key_one_of_count4 < 2: + try: + if data__excludepackagedata_key != "*": + raise JsonSchemaValueException("data.exclude-package-data must be same as const definition: *", value=data__excludepackagedata_key, name="data.exclude-package-data", definition={'const': '*'}, rule='const') + data__excludepackagedata_key_one_of_count4 += 1 + except JsonSchemaValueException: pass + if data__excludepackagedata_key_one_of_count4 != 1: + raise JsonSchemaValueException("data.exclude-package-data must be valid exactly by one of oneOf definition", value=data__excludepackagedata_key, name="data.exclude-package-data", definition={'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, rule='oneOf') + except JsonSchemaValueException: + data__excludepackagedata_property_names = False + if not data__excludepackagedata_property_names: + raise JsonSchemaValueException("data.exclude-package-data must be named by propertyName definition", value=data__excludepackagedata, name="data.exclude-package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='propertyNames') + if "namespace-packages" in data_keys: + data_keys.remove("namespace-packages") + data__namespacepackages = data["namespace-packages"] + if not isinstance(data__namespacepackages, (list, tuple)): + raise JsonSchemaValueException("data.namespace-packages must be array", value=data__namespacepackages, name="data.namespace-packages", definition={'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, rule='type') + data__namespacepackages_is_list = isinstance(data__namespacepackages, (list, tuple)) + if data__namespacepackages_is_list: + data__namespacepackages_len = len(data__namespacepackages) + for data__namespacepackages_x, data__namespacepackages_item in enumerate(data__namespacepackages): + if not isinstance(data__namespacepackages_item, (str)): + raise JsonSchemaValueException(""+"data.namespace-packages[{data__namespacepackages_x}]".format(**locals())+" must be string", value=data__namespacepackages_item, name=""+"data.namespace-packages[{data__namespacepackages_x}]".format(**locals())+"", definition={'type': 'string', 'format': 'python-module-name'}, rule='type') + if isinstance(data__namespacepackages_item, str): + if not custom_formats["python-module-name"](data__namespacepackages_item): + raise JsonSchemaValueException(""+"data.namespace-packages[{data__namespacepackages_x}]".format(**locals())+" must be python-module-name", value=data__namespacepackages_item, name=""+"data.namespace-packages[{data__namespacepackages_x}]".format(**locals())+"", definition={'type': 'string', 'format': 'python-module-name'}, rule='format') + if "py-modules" in data_keys: + data_keys.remove("py-modules") + data__pymodules = data["py-modules"] + if not isinstance(data__pymodules, (list, tuple)): + raise JsonSchemaValueException("data.py-modules must be array", value=data__pymodules, name="data.py-modules", definition={'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, rule='type') + data__pymodules_is_list = isinstance(data__pymodules, (list, tuple)) + if data__pymodules_is_list: + data__pymodules_len = len(data__pymodules) + for data__pymodules_x, data__pymodules_item in enumerate(data__pymodules): + if not isinstance(data__pymodules_item, (str)): + raise JsonSchemaValueException(""+"data.py-modules[{data__pymodules_x}]".format(**locals())+" must be string", value=data__pymodules_item, name=""+"data.py-modules[{data__pymodules_x}]".format(**locals())+"", definition={'type': 'string', 'format': 'python-module-name'}, rule='type') + if isinstance(data__pymodules_item, str): + if not custom_formats["python-module-name"](data__pymodules_item): + raise JsonSchemaValueException(""+"data.py-modules[{data__pymodules_x}]".format(**locals())+" must be python-module-name", value=data__pymodules_item, name=""+"data.py-modules[{data__pymodules_x}]".format(**locals())+"", definition={'type': 'string', 'format': 'python-module-name'}, rule='format') + if "data-files" in data_keys: + data_keys.remove("data-files") + data__datafiles = data["data-files"] + if not isinstance(data__datafiles, (dict)): + raise JsonSchemaValueException("data.data-files must be object", value=data__datafiles, name="data.data-files", definition={'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='type') + data__datafiles_is_dict = isinstance(data__datafiles, dict) + if data__datafiles_is_dict: + data__datafiles_keys = set(data__datafiles.keys()) + for data__datafiles_key, data__datafiles_val in data__datafiles.items(): + if REGEX_PATTERNS['^.*$'].search(data__datafiles_key): + if data__datafiles_key in data__datafiles_keys: + data__datafiles_keys.remove(data__datafiles_key) + if not isinstance(data__datafiles_val, (list, tuple)): + raise JsonSchemaValueException(""+"data.data-files.{data__datafiles_key}".format(**locals())+" must be array", value=data__datafiles_val, name=""+"data.data-files.{data__datafiles_key}".format(**locals())+"", definition={'type': 'array', 'items': {'type': 'string'}}, rule='type') + data__datafiles_val_is_list = isinstance(data__datafiles_val, (list, tuple)) + if data__datafiles_val_is_list: + data__datafiles_val_len = len(data__datafiles_val) + for data__datafiles_val_x, data__datafiles_val_item in enumerate(data__datafiles_val): + if not isinstance(data__datafiles_val_item, (str)): + raise JsonSchemaValueException(""+"data.data-files.{data__datafiles_key}[{data__datafiles_val_x}]".format(**locals())+" must be string", value=data__datafiles_val_item, name=""+"data.data-files.{data__datafiles_key}[{data__datafiles_val_x}]".format(**locals())+"", definition={'type': 'string'}, rule='type') + if "cmdclass" in data_keys: + data_keys.remove("cmdclass") + data__cmdclass = data["cmdclass"] + if not isinstance(data__cmdclass, (dict)): + raise JsonSchemaValueException("data.cmdclass must be object", value=data__cmdclass, name="data.cmdclass", definition={'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', ' cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, rule='type') + data__cmdclass_is_dict = isinstance(data__cmdclass, dict) + if data__cmdclass_is_dict: + data__cmdclass_keys = set(data__cmdclass.keys()) + for data__cmdclass_key, data__cmdclass_val in data__cmdclass.items(): + if REGEX_PATTERNS['^.*$'].search(data__cmdclass_key): + if data__cmdclass_key in data__cmdclass_keys: + data__cmdclass_keys.remove(data__cmdclass_key) + if not isinstance(data__cmdclass_val, (str)): + raise JsonSchemaValueException(""+"data.cmdclass.{data__cmdclass_key}".format(**locals())+" must be string", value=data__cmdclass_val, name=""+"data.cmdclass.{data__cmdclass_key}".format(**locals())+"", definition={'type': 'string', 'format': 'python-qualified-identifier'}, rule='type') + if isinstance(data__cmdclass_val, str): + if not custom_formats["python-qualified-identifier"](data__cmdclass_val): + raise JsonSchemaValueException(""+"data.cmdclass.{data__cmdclass_key}".format(**locals())+" must be python-qualified-identifier", value=data__cmdclass_val, name=""+"data.cmdclass.{data__cmdclass_key}".format(**locals())+"", definition={'type': 'string', 'format': 'python-qualified-identifier'}, rule='format') + if "dynamic" in data_keys: + data_keys.remove("dynamic") + data__dynamic = data["dynamic"] + if not isinstance(data__dynamic, (dict)): + raise JsonSchemaValueException("data.dynamic must be object", value=data__dynamic, name="data.dynamic", definition={'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'$ref': '#/definitions/attr-directive'}, {'$ref': '#/definitions/file-directive'}]}, 'classifiers': {'$ref': '#/definitions/file-directive'}, 'description': {'$ref': '#/definitions/file-directive'}, 'entry-points': {'$ref': '#/definitions/file-directive'}, 'readme': {'anyOf': [{'$ref': '#/definitions/file-directive'}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}, 'license': {'type': 'string', '$$description': ['PROVISIONAL: A string specifying the license of the package', '(might change with PEP 639)'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-expression``?'}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might change with PEP 639)'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-files.glob``?'}}}, rule='type') + data__dynamic_is_dict = isinstance(data__dynamic, dict) + if data__dynamic_is_dict: + data__dynamic_keys = set(data__dynamic.keys()) + if "version" in data__dynamic_keys: + data__dynamic_keys.remove("version") + data__dynamic__version = data__dynamic["version"] + data__dynamic__version_one_of_count5 = 0 + if data__dynamic__version_one_of_count5 < 2: + try: + validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_attr_directive(data__dynamic__version, custom_formats) + data__dynamic__version_one_of_count5 += 1 + except JsonSchemaValueException: pass + if data__dynamic__version_one_of_count5 < 2: + try: + validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data__dynamic__version, custom_formats) + data__dynamic__version_one_of_count5 += 1 + except JsonSchemaValueException: pass + if data__dynamic__version_one_of_count5 != 1: + raise JsonSchemaValueException("data.dynamic.version must be valid exactly by one of oneOf definition", value=data__dynamic__version, name="data.dynamic.version", definition={'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'$ref': '#/definitions/attr-directive'}, {'$ref': '#/definitions/file-directive'}]}, rule='oneOf') + if "classifiers" in data__dynamic_keys: + data__dynamic_keys.remove("classifiers") + data__dynamic__classifiers = data__dynamic["classifiers"] + validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data__dynamic__classifiers, custom_formats) + if "description" in data__dynamic_keys: + data__dynamic_keys.remove("description") + data__dynamic__description = data__dynamic["description"] + validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data__dynamic__description, custom_formats) + if "entry-points" in data__dynamic_keys: + data__dynamic_keys.remove("entry-points") + data__dynamic__entrypoints = data__dynamic["entry-points"] + validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data__dynamic__entrypoints, custom_formats) + if "readme" in data__dynamic_keys: + data__dynamic_keys.remove("readme") + data__dynamic__readme = data__dynamic["readme"] + data__dynamic__readme_any_of_count6 = 0 + if not data__dynamic__readme_any_of_count6: + try: + validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data__dynamic__readme, custom_formats) + data__dynamic__readme_any_of_count6 += 1 + except JsonSchemaValueException: pass + if not data__dynamic__readme_any_of_count6: + try: + data__dynamic__readme_is_dict = isinstance(data__dynamic__readme, dict) + if data__dynamic__readme_is_dict: + data__dynamic__readme_keys = set(data__dynamic__readme.keys()) + if "content-type" in data__dynamic__readme_keys: + data__dynamic__readme_keys.remove("content-type") + data__dynamic__readme__contenttype = data__dynamic__readme["content-type"] + if not isinstance(data__dynamic__readme__contenttype, (str)): + raise JsonSchemaValueException("data.dynamic.readme.content-type must be string", value=data__dynamic__readme__contenttype, name="data.dynamic.readme.content-type", definition={'type': 'string'}, rule='type') + data__dynamic__readme_any_of_count6 += 1 + except JsonSchemaValueException: pass + if not data__dynamic__readme_any_of_count6: + raise JsonSchemaValueException("data.dynamic.readme must be valid by one of anyOf definition", value=data__dynamic__readme, name="data.dynamic.readme", definition={'anyOf': [{'$ref': '#/definitions/file-directive'}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}, rule='anyOf') + data__dynamic__readme_is_dict = isinstance(data__dynamic__readme, dict) + if data__dynamic__readme_is_dict: + data__dynamic__readme_len = len(data__dynamic__readme) + if not all(prop in data__dynamic__readme for prop in ['file']): + raise JsonSchemaValueException("data.dynamic.readme must contain ['file'] properties", value=data__dynamic__readme, name="data.dynamic.readme", definition={'anyOf': [{'$ref': '#/definitions/file-directive'}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}, rule='required') + if "license" in data__dynamic_keys: + data__dynamic_keys.remove("license") + data__dynamic__license = data__dynamic["license"] + if not isinstance(data__dynamic__license, (str)): + raise JsonSchemaValueException("data.dynamic.license must be string", value=data__dynamic__license, name="data.dynamic.license", definition={'type': 'string', '$$description': ['PROVISIONAL: A string specifying the license of the package', '(might change with PEP 639)'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-expression``?'}, rule='type') + if "license-files" in data__dynamic_keys: + data__dynamic_keys.remove("license-files") + data__dynamic__licensefiles = data__dynamic["license-files"] + if not isinstance(data__dynamic__licensefiles, (list, tuple)): + raise JsonSchemaValueException("data.dynamic.license-files must be array", value=data__dynamic__licensefiles, name="data.dynamic.license-files", definition={'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might change with PEP 639)'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-files.glob``?'}, rule='type') + data__dynamic__licensefiles_is_list = isinstance(data__dynamic__licensefiles, (list, tuple)) + if data__dynamic__licensefiles_is_list: + data__dynamic__licensefiles_len = len(data__dynamic__licensefiles) + for data__dynamic__licensefiles_x, data__dynamic__licensefiles_item in enumerate(data__dynamic__licensefiles): + if not isinstance(data__dynamic__licensefiles_item, (str)): + raise JsonSchemaValueException(""+"data.dynamic.license-files[{data__dynamic__licensefiles_x}]".format(**locals())+" must be string", value=data__dynamic__licensefiles_item, name=""+"data.dynamic.license-files[{data__dynamic__licensefiles_x}]".format(**locals())+"", definition={'type': 'string'}, rule='type') + else: data__dynamic["license-files"] = ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'] + if data_keys: + raise JsonSchemaValueException("data must not contain "+str(data_keys)+" properties", value=data, name="data", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$ref': '#/definitions/find-directive'}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', ' cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'$ref': '#/definitions/attr-directive'}, {'$ref': '#/definitions/file-directive'}]}, 'classifiers': {'$ref': '#/definitions/file-directive'}, 'description': {'$ref': '#/definitions/file-directive'}, 'entry-points': {'$ref': '#/definitions/file-directive'}, 'readme': {'anyOf': [{'$ref': '#/definitions/file-directive'}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}, 'license': {'type': 'string', '$$description': ['PROVISIONAL: A string specifying the license of the package', '(might change with PEP 639)'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-expression``?'}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might change with PEP 639)'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-files.glob``?'}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}, rule='additionalProperties') + return data + +def validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data, custom_formats={}): + if not isinstance(data, (dict)): + raise JsonSchemaValueException("data must be object", value=data, name="data", definition={'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, rule='type') + data_is_dict = isinstance(data, dict) + if data_is_dict: + data_len = len(data) + if not all(prop in data for prop in ['file']): + raise JsonSchemaValueException("data must contain ['file'] properties", value=data, name="data", definition={'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, rule='required') + data_keys = set(data.keys()) + if "file" in data_keys: + data_keys.remove("file") + data__file = data["file"] + data__file_one_of_count7 = 0 + if data__file_one_of_count7 < 2: + try: + if not isinstance(data__file, (str)): + raise JsonSchemaValueException("data.file must be string", value=data__file, name="data.file", definition={'type': 'string'}, rule='type') + data__file_one_of_count7 += 1 + except JsonSchemaValueException: pass + if data__file_one_of_count7 < 2: + try: + if not isinstance(data__file, (list, tuple)): + raise JsonSchemaValueException("data.file must be array", value=data__file, name="data.file", definition={'type': 'array', 'items': {'type': 'string'}}, rule='type') + data__file_is_list = isinstance(data__file, (list, tuple)) + if data__file_is_list: + data__file_len = len(data__file) + for data__file_x, data__file_item in enumerate(data__file): + if not isinstance(data__file_item, (str)): + raise JsonSchemaValueException(""+"data.file[{data__file_x}]".format(**locals())+" must be string", value=data__file_item, name=""+"data.file[{data__file_x}]".format(**locals())+"", definition={'type': 'string'}, rule='type') + data__file_one_of_count7 += 1 + except JsonSchemaValueException: pass + if data__file_one_of_count7 != 1: + raise JsonSchemaValueException("data.file must be valid exactly by one of oneOf definition", value=data__file, name="data.file", definition={'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}, rule='oneOf') + if data_keys: + raise JsonSchemaValueException("data must not contain "+str(data_keys)+" properties", value=data, name="data", definition={'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, rule='additionalProperties') + return data + +def validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_attr_directive(data, custom_formats={}): + if not isinstance(data, (dict)): + raise JsonSchemaValueException("data must be object", value=data, name="data", definition={'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, rule='type') + data_is_dict = isinstance(data, dict) + if data_is_dict: + data_len = len(data) + if not all(prop in data for prop in ['attr']): + raise JsonSchemaValueException("data must contain ['attr'] properties", value=data, name="data", definition={'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, rule='required') + data_keys = set(data.keys()) + if "attr" in data_keys: + data_keys.remove("attr") + data__attr = data["attr"] + if not isinstance(data__attr, (str)): + raise JsonSchemaValueException("data.attr must be string", value=data__attr, name="data.attr", definition={'type': 'string'}, rule='type') + if data_keys: + raise JsonSchemaValueException("data must not contain "+str(data_keys)+" properties", value=data, name="data", definition={'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, rule='additionalProperties') + return data + +def validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_find_directive(data, custom_formats={}): + if not isinstance(data, (dict)): + raise JsonSchemaValueException("data must be object", value=data, name="data", definition={'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}, rule='type') + data_is_dict = isinstance(data, dict) + if data_is_dict: + data_keys = set(data.keys()) + if "find" in data_keys: + data_keys.remove("find") + data__find = data["find"] + if not isinstance(data__find, (dict)): + raise JsonSchemaValueException("data.find must be object", value=data__find, name="data.find", definition={'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}, rule='type') + data__find_is_dict = isinstance(data__find, dict) + if data__find_is_dict: + data__find_keys = set(data__find.keys()) + if "where" in data__find_keys: + data__find_keys.remove("where") + data__find__where = data__find["where"] + if not isinstance(data__find__where, (list, tuple)): + raise JsonSchemaValueException("data.find.where must be array", value=data__find__where, name="data.find.where", definition={'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, rule='type') + data__find__where_is_list = isinstance(data__find__where, (list, tuple)) + if data__find__where_is_list: + data__find__where_len = len(data__find__where) + for data__find__where_x, data__find__where_item in enumerate(data__find__where): + if not isinstance(data__find__where_item, (str)): + raise JsonSchemaValueException(""+"data.find.where[{data__find__where_x}]".format(**locals())+" must be string", value=data__find__where_item, name=""+"data.find.where[{data__find__where_x}]".format(**locals())+"", definition={'type': 'string'}, rule='type') + if "exclude" in data__find_keys: + data__find_keys.remove("exclude") + data__find__exclude = data__find["exclude"] + if not isinstance(data__find__exclude, (list, tuple)): + raise JsonSchemaValueException("data.find.exclude must be array", value=data__find__exclude, name="data.find.exclude", definition={'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, rule='type') + data__find__exclude_is_list = isinstance(data__find__exclude, (list, tuple)) + if data__find__exclude_is_list: + data__find__exclude_len = len(data__find__exclude) + for data__find__exclude_x, data__find__exclude_item in enumerate(data__find__exclude): + if not isinstance(data__find__exclude_item, (str)): + raise JsonSchemaValueException(""+"data.find.exclude[{data__find__exclude_x}]".format(**locals())+" must be string", value=data__find__exclude_item, name=""+"data.find.exclude[{data__find__exclude_x}]".format(**locals())+"", definition={'type': 'string'}, rule='type') + if "include" in data__find_keys: + data__find_keys.remove("include") + data__find__include = data__find["include"] + if not isinstance(data__find__include, (list, tuple)): + raise JsonSchemaValueException("data.find.include must be array", value=data__find__include, name="data.find.include", definition={'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, rule='type') + data__find__include_is_list = isinstance(data__find__include, (list, tuple)) + if data__find__include_is_list: + data__find__include_len = len(data__find__include) + for data__find__include_x, data__find__include_item in enumerate(data__find__include): + if not isinstance(data__find__include_item, (str)): + raise JsonSchemaValueException(""+"data.find.include[{data__find__include_x}]".format(**locals())+" must be string", value=data__find__include_item, name=""+"data.find.include[{data__find__include_x}]".format(**locals())+"", definition={'type': 'string'}, rule='type') + if "namespaces" in data__find_keys: + data__find_keys.remove("namespaces") + data__find__namespaces = data__find["namespaces"] + if not isinstance(data__find__namespaces, (bool)): + raise JsonSchemaValueException("data.find.namespaces must be boolean", value=data__find__namespaces, name="data.find.namespaces", definition={'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}, rule='type') + if data__find_keys: + raise JsonSchemaValueException("data.find must not contain "+str(data__find_keys)+" properties", value=data__find, name="data.find", definition={'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}, rule='additionalProperties') + if data_keys: + raise JsonSchemaValueException("data must not contain "+str(data_keys)+" properties", value=data, name="data", definition={'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}, rule='additionalProperties') + return data + +def validate_https___docs_python_org_3_install(data, custom_formats={}): + if not isinstance(data, (dict)): + raise JsonSchemaValueException("data must be object", value=data, name="data", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://docs.python.org/3/install/', 'title': '``tool.distutils`` table', '$$description': ['Originally, ``distutils`` allowed developers to configure arguments for', '``setup.py`` scripts via `distutils configuration files', '`_.', '``tool.distutils`` subtables could be used with the same purpose', '(NOT CURRENTLY IMPLEMENTED).'], 'type': 'object', 'properties': {'global': {'type': 'object', 'description': 'Global options applied to all ``distutils`` commands'}}, 'patternProperties': {'.+': {'type': 'object'}}, '$comment': 'TODO: Is there a practical way of making this schema more specific?'}, rule='type') + data_is_dict = isinstance(data, dict) + if data_is_dict: + data_keys = set(data.keys()) + if "global" in data_keys: + data_keys.remove("global") + data__global = data["global"] + if not isinstance(data__global, (dict)): + raise JsonSchemaValueException("data.global must be object", value=data__global, name="data.global", definition={'type': 'object', 'description': 'Global options applied to all ``distutils`` commands'}, rule='type') + for data_key, data_val in data.items(): + if REGEX_PATTERNS['.+'].search(data_key): + if data_key in data_keys: + data_keys.remove(data_key) + if not isinstance(data_val, (dict)): + raise JsonSchemaValueException(""+"data.{data_key}".format(**locals())+" must be object", value=data_val, name=""+"data.{data_key}".format(**locals())+"", definition={'type': 'object'}, rule='type') + return data + +def validate_https___www_python_org_dev_peps_pep_0621(data, custom_formats={}): + if not isinstance(data, (dict)): + raise JsonSchemaValueException("data must be object", value=data, name="data", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://www.python.org/dev/peps/pep-0621/', 'title': '``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', ' If the core metadata specification lists a field as "Required", then', ' the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', ' The required fields are: Metadata-Version, Name, Version.', ' All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, rule='type') + data_is_dict = isinstance(data, dict) + if data_is_dict: + data_len = len(data) + if not all(prop in data for prop in ['name']): + raise JsonSchemaValueException("data must contain ['name'] properties", value=data, name="data", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://www.python.org/dev/peps/pep-0621/', 'title': '``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', ' If the core metadata specification lists a field as "Required", then', ' the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', ' The required fields are: Metadata-Version, Name, Version.', ' All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, rule='required') + data_keys = set(data.keys()) + if "name" in data_keys: + data_keys.remove("name") + data__name = data["name"] + if not isinstance(data__name, (str)): + raise JsonSchemaValueException("data.name must be string", value=data__name, name="data.name", definition={'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, rule='type') + if isinstance(data__name, str): + if not custom_formats["pep508-identifier"](data__name): + raise JsonSchemaValueException("data.name must be pep508-identifier", value=data__name, name="data.name", definition={'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, rule='format') + if "version" in data_keys: + data_keys.remove("version") + data__version = data["version"] + if not isinstance(data__version, (str)): + raise JsonSchemaValueException("data.version must be string", value=data__version, name="data.version", definition={'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, rule='type') + if isinstance(data__version, str): + if not custom_formats["pep440"](data__version): + raise JsonSchemaValueException("data.version must be pep440", value=data__version, name="data.version", definition={'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, rule='format') + if "description" in data_keys: + data_keys.remove("description") + data__description = data["description"] + if not isinstance(data__description, (str)): + raise JsonSchemaValueException("data.description must be string", value=data__description, name="data.description", definition={'type': 'string', '$$description': ['The `summary description of the project', '`_']}, rule='type') + if "readme" in data_keys: + data_keys.remove("readme") + data__readme = data["readme"] + data__readme_one_of_count8 = 0 + if data__readme_one_of_count8 < 2: + try: + if not isinstance(data__readme, (str)): + raise JsonSchemaValueException("data.readme must be string", value=data__readme, name="data.readme", definition={'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, rule='type') + data__readme_one_of_count8 += 1 + except JsonSchemaValueException: pass + if data__readme_one_of_count8 < 2: + try: + if not isinstance(data__readme, (dict)): + raise JsonSchemaValueException("data.readme must be object", value=data__readme, name="data.readme", definition={'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}, rule='type') + data__readme_any_of_count9 = 0 + if not data__readme_any_of_count9: + try: + data__readme_is_dict = isinstance(data__readme, dict) + if data__readme_is_dict: + data__readme_len = len(data__readme) + if not all(prop in data__readme for prop in ['file']): + raise JsonSchemaValueException("data.readme must contain ['file'] properties", value=data__readme, name="data.readme", definition={'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, rule='required') + data__readme_keys = set(data__readme.keys()) + if "file" in data__readme_keys: + data__readme_keys.remove("file") + data__readme__file = data__readme["file"] + if not isinstance(data__readme__file, (str)): + raise JsonSchemaValueException("data.readme.file must be string", value=data__readme__file, name="data.readme.file", definition={'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}, rule='type') + data__readme_any_of_count9 += 1 + except JsonSchemaValueException: pass + if not data__readme_any_of_count9: + try: + data__readme_is_dict = isinstance(data__readme, dict) + if data__readme_is_dict: + data__readme_len = len(data__readme) + if not all(prop in data__readme for prop in ['text']): + raise JsonSchemaValueException("data.readme must contain ['text'] properties", value=data__readme, name="data.readme", definition={'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}, rule='required') + data__readme_keys = set(data__readme.keys()) + if "text" in data__readme_keys: + data__readme_keys.remove("text") + data__readme__text = data__readme["text"] + if not isinstance(data__readme__text, (str)): + raise JsonSchemaValueException("data.readme.text must be string", value=data__readme__text, name="data.readme.text", definition={'type': 'string', 'description': 'Full text describing the project.'}, rule='type') + data__readme_any_of_count9 += 1 + except JsonSchemaValueException: pass + if not data__readme_any_of_count9: + raise JsonSchemaValueException("data.readme must be valid by one of anyOf definition", value=data__readme, name="data.readme", definition={'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, rule='anyOf') + data__readme_is_dict = isinstance(data__readme, dict) + if data__readme_is_dict: + data__readme_len = len(data__readme) + if not all(prop in data__readme for prop in ['content-type']): + raise JsonSchemaValueException("data.readme must contain ['content-type'] properties", value=data__readme, name="data.readme", definition={'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}, rule='required') + data__readme_keys = set(data__readme.keys()) + if "content-type" in data__readme_keys: + data__readme_keys.remove("content-type") + data__readme__contenttype = data__readme["content-type"] + if not isinstance(data__readme__contenttype, (str)): + raise JsonSchemaValueException("data.readme.content-type must be string", value=data__readme__contenttype, name="data.readme.content-type", definition={'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}, rule='type') + data__readme_one_of_count8 += 1 + except JsonSchemaValueException: pass + if data__readme_one_of_count8 != 1: + raise JsonSchemaValueException("data.readme must be valid exactly by one of oneOf definition", value=data__readme, name="data.readme", definition={'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, rule='oneOf') + if "requires-python" in data_keys: + data_keys.remove("requires-python") + data__requirespython = data["requires-python"] + if not isinstance(data__requirespython, (str)): + raise JsonSchemaValueException("data.requires-python must be string", value=data__requirespython, name="data.requires-python", definition={'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, rule='type') + if isinstance(data__requirespython, str): + if not custom_formats["pep508-versionspec"](data__requirespython): + raise JsonSchemaValueException("data.requires-python must be pep508-versionspec", value=data__requirespython, name="data.requires-python", definition={'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, rule='format') + if "license" in data_keys: + data_keys.remove("license") + data__license = data["license"] + data__license_one_of_count10 = 0 + if data__license_one_of_count10 < 2: + try: + data__license_is_dict = isinstance(data__license, dict) + if data__license_is_dict: + data__license_len = len(data__license) + if not all(prop in data__license for prop in ['file']): + raise JsonSchemaValueException("data.license must contain ['file'] properties", value=data__license, name="data.license", definition={'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, rule='required') + data__license_keys = set(data__license.keys()) + if "file" in data__license_keys: + data__license_keys.remove("file") + data__license__file = data__license["file"] + if not isinstance(data__license__file, (str)): + raise JsonSchemaValueException("data.license.file must be string", value=data__license__file, name="data.license.file", definition={'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}, rule='type') + data__license_one_of_count10 += 1 + except JsonSchemaValueException: pass + if data__license_one_of_count10 < 2: + try: + data__license_is_dict = isinstance(data__license, dict) + if data__license_is_dict: + data__license_len = len(data__license) + if not all(prop in data__license for prop in ['text']): + raise JsonSchemaValueException("data.license must contain ['text'] properties", value=data__license, name="data.license", definition={'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}, rule='required') + data__license_keys = set(data__license.keys()) + if "text" in data__license_keys: + data__license_keys.remove("text") + data__license__text = data__license["text"] + if not isinstance(data__license__text, (str)): + raise JsonSchemaValueException("data.license.text must be string", value=data__license__text, name="data.license.text", definition={'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}, rule='type') + data__license_one_of_count10 += 1 + except JsonSchemaValueException: pass + if data__license_one_of_count10 != 1: + raise JsonSchemaValueException("data.license must be valid exactly by one of oneOf definition", value=data__license, name="data.license", definition={'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, rule='oneOf') + if "authors" in data_keys: + data_keys.remove("authors") + data__authors = data["authors"] + if not isinstance(data__authors, (list, tuple)): + raise JsonSchemaValueException("data.authors must be array", value=data__authors, name="data.authors", definition={'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, rule='type') + data__authors_is_list = isinstance(data__authors, (list, tuple)) + if data__authors_is_list: + data__authors_len = len(data__authors) + for data__authors_x, data__authors_item in enumerate(data__authors): + validate_https___www_python_org_dev_peps_pep_0621___definitions_author(data__authors_item, custom_formats) + if "maintainers" in data_keys: + data_keys.remove("maintainers") + data__maintainers = data["maintainers"] + if not isinstance(data__maintainers, (list, tuple)): + raise JsonSchemaValueException("data.maintainers must be array", value=data__maintainers, name="data.maintainers", definition={'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, rule='type') + data__maintainers_is_list = isinstance(data__maintainers, (list, tuple)) + if data__maintainers_is_list: + data__maintainers_len = len(data__maintainers) + for data__maintainers_x, data__maintainers_item in enumerate(data__maintainers): + validate_https___www_python_org_dev_peps_pep_0621___definitions_author(data__maintainers_item, custom_formats) + if "keywords" in data_keys: + data_keys.remove("keywords") + data__keywords = data["keywords"] + if not isinstance(data__keywords, (list, tuple)): + raise JsonSchemaValueException("data.keywords must be array", value=data__keywords, name="data.keywords", definition={'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, rule='type') + data__keywords_is_list = isinstance(data__keywords, (list, tuple)) + if data__keywords_is_list: + data__keywords_len = len(data__keywords) + for data__keywords_x, data__keywords_item in enumerate(data__keywords): + if not isinstance(data__keywords_item, (str)): + raise JsonSchemaValueException(""+"data.keywords[{data__keywords_x}]".format(**locals())+" must be string", value=data__keywords_item, name=""+"data.keywords[{data__keywords_x}]".format(**locals())+"", definition={'type': 'string'}, rule='type') + if "classifiers" in data_keys: + data_keys.remove("classifiers") + data__classifiers = data["classifiers"] + if not isinstance(data__classifiers, (list, tuple)): + raise JsonSchemaValueException("data.classifiers must be array", value=data__classifiers, name="data.classifiers", definition={'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, rule='type') + data__classifiers_is_list = isinstance(data__classifiers, (list, tuple)) + if data__classifiers_is_list: + data__classifiers_len = len(data__classifiers) + for data__classifiers_x, data__classifiers_item in enumerate(data__classifiers): + if not isinstance(data__classifiers_item, (str)): + raise JsonSchemaValueException(""+"data.classifiers[{data__classifiers_x}]".format(**locals())+" must be string", value=data__classifiers_item, name=""+"data.classifiers[{data__classifiers_x}]".format(**locals())+"", definition={'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, rule='type') + if isinstance(data__classifiers_item, str): + if not custom_formats["trove-classifier"](data__classifiers_item): + raise JsonSchemaValueException(""+"data.classifiers[{data__classifiers_x}]".format(**locals())+" must be trove-classifier", value=data__classifiers_item, name=""+"data.classifiers[{data__classifiers_x}]".format(**locals())+"", definition={'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, rule='format') + if "urls" in data_keys: + data_keys.remove("urls") + data__urls = data["urls"] + if not isinstance(data__urls, (dict)): + raise JsonSchemaValueException("data.urls must be object", value=data__urls, name="data.urls", definition={'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, rule='type') + data__urls_is_dict = isinstance(data__urls, dict) + if data__urls_is_dict: + data__urls_keys = set(data__urls.keys()) + for data__urls_key, data__urls_val in data__urls.items(): + if REGEX_PATTERNS['^.+$'].search(data__urls_key): + if data__urls_key in data__urls_keys: + data__urls_keys.remove(data__urls_key) + if not isinstance(data__urls_val, (str)): + raise JsonSchemaValueException(""+"data.urls.{data__urls_key}".format(**locals())+" must be string", value=data__urls_val, name=""+"data.urls.{data__urls_key}".format(**locals())+"", definition={'type': 'string', 'format': 'url'}, rule='type') + if isinstance(data__urls_val, str): + if not custom_formats["url"](data__urls_val): + raise JsonSchemaValueException(""+"data.urls.{data__urls_key}".format(**locals())+" must be url", value=data__urls_val, name=""+"data.urls.{data__urls_key}".format(**locals())+"", definition={'type': 'string', 'format': 'url'}, rule='format') + if data__urls_keys: + raise JsonSchemaValueException("data.urls must not contain "+str(data__urls_keys)+" properties", value=data__urls, name="data.urls", definition={'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, rule='additionalProperties') + if "scripts" in data_keys: + data_keys.remove("scripts") + data__scripts = data["scripts"] + validate_https___www_python_org_dev_peps_pep_0621___definitions_entry_point_group(data__scripts, custom_formats) + if "gui-scripts" in data_keys: + data_keys.remove("gui-scripts") + data__guiscripts = data["gui-scripts"] + validate_https___www_python_org_dev_peps_pep_0621___definitions_entry_point_group(data__guiscripts, custom_formats) + if "entry-points" in data_keys: + data_keys.remove("entry-points") + data__entrypoints = data["entry-points"] + data__entrypoints_is_dict = isinstance(data__entrypoints, dict) + if data__entrypoints_is_dict: + data__entrypoints_keys = set(data__entrypoints.keys()) + for data__entrypoints_key, data__entrypoints_val in data__entrypoints.items(): + if REGEX_PATTERNS['^.+$'].search(data__entrypoints_key): + if data__entrypoints_key in data__entrypoints_keys: + data__entrypoints_keys.remove(data__entrypoints_key) + validate_https___www_python_org_dev_peps_pep_0621___definitions_entry_point_group(data__entrypoints_val, custom_formats) + if data__entrypoints_keys: + raise JsonSchemaValueException("data.entry-points must not contain "+str(data__entrypoints_keys)+" properties", value=data__entrypoints, name="data.entry-points", definition={'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, rule='additionalProperties') + data__entrypoints_len = len(data__entrypoints) + if data__entrypoints_len != 0: + data__entrypoints_property_names = True + for data__entrypoints_key in data__entrypoints: + try: + if isinstance(data__entrypoints_key, str): + if not custom_formats["python-entrypoint-group"](data__entrypoints_key): + raise JsonSchemaValueException("data.entry-points must be python-entrypoint-group", value=data__entrypoints_key, name="data.entry-points", definition={'format': 'python-entrypoint-group'}, rule='format') + except JsonSchemaValueException: + data__entrypoints_property_names = False + if not data__entrypoints_property_names: + raise JsonSchemaValueException("data.entry-points must be named by propertyName definition", value=data__entrypoints, name="data.entry-points", definition={'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, rule='propertyNames') + if "dependencies" in data_keys: + data_keys.remove("dependencies") + data__dependencies = data["dependencies"] + if not isinstance(data__dependencies, (list, tuple)): + raise JsonSchemaValueException("data.dependencies must be array", value=data__dependencies, name="data.dependencies", definition={'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, rule='type') + data__dependencies_is_list = isinstance(data__dependencies, (list, tuple)) + if data__dependencies_is_list: + data__dependencies_len = len(data__dependencies) + for data__dependencies_x, data__dependencies_item in enumerate(data__dependencies): + validate_https___www_python_org_dev_peps_pep_0621___definitions_dependency(data__dependencies_item, custom_formats) + if "optional-dependencies" in data_keys: + data_keys.remove("optional-dependencies") + data__optionaldependencies = data["optional-dependencies"] + if not isinstance(data__optionaldependencies, (dict)): + raise JsonSchemaValueException("data.optional-dependencies must be object", value=data__optionaldependencies, name="data.optional-dependencies", definition={'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, rule='type') + data__optionaldependencies_is_dict = isinstance(data__optionaldependencies, dict) + if data__optionaldependencies_is_dict: + data__optionaldependencies_keys = set(data__optionaldependencies.keys()) + for data__optionaldependencies_key, data__optionaldependencies_val in data__optionaldependencies.items(): + if REGEX_PATTERNS['^.+$'].search(data__optionaldependencies_key): + if data__optionaldependencies_key in data__optionaldependencies_keys: + data__optionaldependencies_keys.remove(data__optionaldependencies_key) + if not isinstance(data__optionaldependencies_val, (list, tuple)): + raise JsonSchemaValueException(""+"data.optional-dependencies.{data__optionaldependencies_key}".format(**locals())+" must be array", value=data__optionaldependencies_val, name=""+"data.optional-dependencies.{data__optionaldependencies_key}".format(**locals())+"", definition={'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}, rule='type') + data__optionaldependencies_val_is_list = isinstance(data__optionaldependencies_val, (list, tuple)) + if data__optionaldependencies_val_is_list: + data__optionaldependencies_val_len = len(data__optionaldependencies_val) + for data__optionaldependencies_val_x, data__optionaldependencies_val_item in enumerate(data__optionaldependencies_val): + validate_https___www_python_org_dev_peps_pep_0621___definitions_dependency(data__optionaldependencies_val_item, custom_formats) + if data__optionaldependencies_keys: + raise JsonSchemaValueException("data.optional-dependencies must not contain "+str(data__optionaldependencies_keys)+" properties", value=data__optionaldependencies, name="data.optional-dependencies", definition={'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, rule='additionalProperties') + data__optionaldependencies_len = len(data__optionaldependencies) + if data__optionaldependencies_len != 0: + data__optionaldependencies_property_names = True + for data__optionaldependencies_key in data__optionaldependencies: + try: + if isinstance(data__optionaldependencies_key, str): + if not custom_formats["pep508-identifier"](data__optionaldependencies_key): + raise JsonSchemaValueException("data.optional-dependencies must be pep508-identifier", value=data__optionaldependencies_key, name="data.optional-dependencies", definition={'format': 'pep508-identifier'}, rule='format') + except JsonSchemaValueException: + data__optionaldependencies_property_names = False + if not data__optionaldependencies_property_names: + raise JsonSchemaValueException("data.optional-dependencies must be named by propertyName definition", value=data__optionaldependencies, name="data.optional-dependencies", definition={'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, rule='propertyNames') + if "dynamic" in data_keys: + data_keys.remove("dynamic") + data__dynamic = data["dynamic"] + if not isinstance(data__dynamic, (list, tuple)): + raise JsonSchemaValueException("data.dynamic must be array", value=data__dynamic, name="data.dynamic", definition={'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}, rule='type') + data__dynamic_is_list = isinstance(data__dynamic, (list, tuple)) + if data__dynamic_is_list: + data__dynamic_len = len(data__dynamic) + for data__dynamic_x, data__dynamic_item in enumerate(data__dynamic): + if data__dynamic_item not in ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']: + raise JsonSchemaValueException(""+"data.dynamic[{data__dynamic_x}]".format(**locals())+" must be one of ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']", value=data__dynamic_item, name=""+"data.dynamic[{data__dynamic_x}]".format(**locals())+"", definition={'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}, rule='enum') + try: + try: + data_is_dict = isinstance(data, dict) + if data_is_dict: + data_len = len(data) + if not all(prop in data for prop in ['version']): + raise JsonSchemaValueException("data must contain ['version'] properties", value=data, name="data", definition={'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, rule='required') + except JsonSchemaValueException: pass + else: + raise JsonSchemaValueException("data must not be valid by not definition", value=data, name="data", definition={'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', ' If the core metadata specification lists a field as "Required", then', ' the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', ' The required fields are: Metadata-Version, Name, Version.', ' All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, rule='not') + except JsonSchemaValueException: + pass + else: + data_is_dict = isinstance(data, dict) + if data_is_dict: + data_keys = set(data.keys()) + if "dynamic" in data_keys: + data_keys.remove("dynamic") + data__dynamic = data["dynamic"] + data__dynamic_is_list = isinstance(data__dynamic, (list, tuple)) + if data__dynamic_is_list: + data__dynamic_contains = False + for data__dynamic_key in data__dynamic: + try: + if data__dynamic_key != "version": + raise JsonSchemaValueException("data.dynamic must be same as const definition: version", value=data__dynamic_key, name="data.dynamic", definition={'const': 'version'}, rule='const') + data__dynamic_contains = True + break + except JsonSchemaValueException: pass + if not data__dynamic_contains: + raise JsonSchemaValueException("data.dynamic must contain one of contains definition", value=data__dynamic, name="data.dynamic", definition={'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}, rule='contains') + return data + +def validate_https___www_python_org_dev_peps_pep_0621___definitions_dependency(data, custom_formats={}): + if not isinstance(data, (str)): + raise JsonSchemaValueException("data must be string", value=data, name="data", definition={'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}, rule='type') + if isinstance(data, str): + if not custom_formats["pep508"](data): + raise JsonSchemaValueException("data must be pep508", value=data, name="data", definition={'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}, rule='format') + return data + +def validate_https___www_python_org_dev_peps_pep_0621___definitions_entry_point_group(data, custom_formats={}): + if not isinstance(data, (dict)): + raise JsonSchemaValueException("data must be object", value=data, name="data", definition={'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, rule='type') + data_is_dict = isinstance(data, dict) + if data_is_dict: + data_keys = set(data.keys()) + for data_key, data_val in data.items(): + if REGEX_PATTERNS['^.+$'].search(data_key): + if data_key in data_keys: + data_keys.remove(data_key) + if not isinstance(data_val, (str)): + raise JsonSchemaValueException(""+"data.{data_key}".format(**locals())+" must be string", value=data_val, name=""+"data.{data_key}".format(**locals())+"", definition={'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}, rule='type') + if isinstance(data_val, str): + if not custom_formats["python-entrypoint-reference"](data_val): + raise JsonSchemaValueException(""+"data.{data_key}".format(**locals())+" must be python-entrypoint-reference", value=data_val, name=""+"data.{data_key}".format(**locals())+"", definition={'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}, rule='format') + if data_keys: + raise JsonSchemaValueException("data must not contain "+str(data_keys)+" properties", value=data, name="data", definition={'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, rule='additionalProperties') + data_len = len(data) + if data_len != 0: + data_property_names = True + for data_key in data: + try: + if isinstance(data_key, str): + if not custom_formats["python-entrypoint-name"](data_key): + raise JsonSchemaValueException("data must be python-entrypoint-name", value=data_key, name="data", definition={'format': 'python-entrypoint-name'}, rule='format') + except JsonSchemaValueException: + data_property_names = False + if not data_property_names: + raise JsonSchemaValueException("data must be named by propertyName definition", value=data, name="data", definition={'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, rule='propertyNames') + return data + +def validate_https___www_python_org_dev_peps_pep_0621___definitions_author(data, custom_formats={}): + if not isinstance(data, (dict)): + raise JsonSchemaValueException("data must be object", value=data, name="data", definition={'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, rule='type') + data_is_dict = isinstance(data, dict) + if data_is_dict: + data_keys = set(data.keys()) + if "name" in data_keys: + data_keys.remove("name") + data__name = data["name"] + if not isinstance(data__name, (str)): + raise JsonSchemaValueException("data.name must be string", value=data__name, name="data.name", definition={'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, rule='type') + if "email" in data_keys: + data_keys.remove("email") + data__email = data["email"] + if not isinstance(data__email, (str)): + raise JsonSchemaValueException("data.email must be string", value=data__email, name="data.email", definition={'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}, rule='type') + if isinstance(data__email, str): + if not REGEX_PATTERNS["idn-email_re_pattern"].match(data__email): + raise JsonSchemaValueException("data.email must be idn-email", value=data__email, name="data.email", definition={'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}, rule='format') + return data \ No newline at end of file diff --git a/setuptools/_vendor/_validate_pyproject/formats.py b/setuptools/_vendor/_validate_pyproject/formats.py new file mode 100644 index 00000000..cc8566af --- /dev/null +++ b/setuptools/_vendor/_validate_pyproject/formats.py @@ -0,0 +1,202 @@ +import logging +import re +import string +from itertools import chain +from urllib.parse import urlparse + +_logger = logging.getLogger(__name__) + +# ------------------------------------------------------------------------------------- +# PEP 440 + +VERSION_PATTERN = r""" + v? + (?: + (?:(?P[0-9]+)!)? # epoch + (?P[0-9]+(?:\.[0-9]+)*) # release segment + (?P
                                          # pre-release
+            [-_\.]?
+            (?P(a|b|c|rc|alpha|beta|pre|preview))
+            [-_\.]?
+            (?P[0-9]+)?
+        )?
+        (?P                                         # post release
+            (?:-(?P[0-9]+))
+            |
+            (?:
+                [-_\.]?
+                (?Ppost|rev|r)
+                [-_\.]?
+                (?P[0-9]+)?
+            )
+        )?
+        (?P                                          # dev release
+            [-_\.]?
+            (?Pdev)
+            [-_\.]?
+            (?P[0-9]+)?
+        )?
+    )
+    (?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
+"""
+
+VERSION_REGEX = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.X | re.I)
+
+
+def pep440(version: str) -> bool:
+    return VERSION_REGEX.match(version) is not None
+
+
+# -------------------------------------------------------------------------------------
+# PEP 508
+
+PEP508_IDENTIFIER_PATTERN = r"([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])"
+PEP508_IDENTIFIER_REGEX = re.compile(f"^{PEP508_IDENTIFIER_PATTERN}$", re.I)
+
+
+def pep508_identifier(name: str) -> bool:
+    return PEP508_IDENTIFIER_REGEX.match(name) is not None
+
+
+try:
+    try:
+        from packaging import requirements as _req
+    except ImportError:  # pragma: no cover
+        # let's try setuptools vendored version
+        from setuptools._vendor.packaging import requirements as _req  # type: ignore
+
+    def pep508(value: str) -> bool:
+        try:
+            _req.Requirement(value)
+            return True
+        except _req.InvalidRequirement:
+            return False
+
+
+except ImportError:  # pragma: no cover
+    _logger.warning(
+        "Could not find an installation of `packaging`. Requirements, dependencies and "
+        "versions might not be validated. "
+        "To enforce validation, please install `packaging`."
+    )
+
+    def pep508(value: str) -> bool:
+        return True
+
+
+def pep508_versionspec(value: str) -> bool:
+    """Expression that can be used to specify/lock versions (including ranges)"""
+    if any(c in value for c in (";", "]", "@")):
+        # In PEP 508:
+        # conditional markers, extras and URL specs are not included in the
+        # versionspec
+        return False
+    # Let's pretend we have a dependency called `requirement` with the given
+    # version spec, then we can re-use the pep508 function for validation:
+    return pep508(f"requirement{value}")
+
+
+# -------------------------------------------------------------------------------------
+# PEP 517
+
+
+def pep517_backend_reference(value: str) -> bool:
+    module, _, obj = value.partition(":")
+    identifiers = (i.strip() for i in chain(module.split("."), obj.split(".")))
+    return all(python_identifier(i) for i in identifiers if i)
+
+
+# -------------------------------------------------------------------------------------
+# Classifiers - PEP 301
+
+
+try:
+    from trove_classifiers import classifiers as _trove_classifiers
+
+    def trove_classifier(value: str) -> bool:
+        return value in _trove_classifiers
+
+
+except ImportError:  # pragma: no cover
+
+    class _TroveClassifier:
+        def __init__(self):
+            self._warned = False
+            self.__name__ = "trove-classifier"
+
+        def __call__(self, value: str) -> bool:
+            if self._warned is False:
+                self._warned = True
+                _logger.warning("Install ``trove-classifiers`` to ensure validation.")
+            return True
+
+    trove_classifier = _TroveClassifier()
+
+
+# -------------------------------------------------------------------------------------
+# Non-PEP related
+
+
+def url(value: str) -> bool:
+    try:
+        parts = urlparse(value)
+        return bool(parts.scheme and parts.netloc)
+        # ^  TODO: should we enforce schema to be http(s)?
+    except Exception:
+        return False
+
+
+# https://packaging.python.org/specifications/entry-points/
+ENTRYPOINT_PATTERN = r"[^\[\s=]([^=]*[^\s=])?"
+ENTRYPOINT_REGEX = re.compile(f"^{ENTRYPOINT_PATTERN}$", re.I)
+RECOMMEDED_ENTRYPOINT_PATTERN = r"[\w.-]+"
+RECOMMEDED_ENTRYPOINT_REGEX = re.compile(f"^{RECOMMEDED_ENTRYPOINT_PATTERN}$", re.I)
+ENTRYPOINT_GROUP_PATTERN = r"\w+(\.\w+)*"
+ENTRYPOINT_GROUP_REGEX = re.compile(f"^{ENTRYPOINT_GROUP_PATTERN}$", re.I)
+
+
+def python_identifier(value: str) -> bool:
+    return value.isidentifier()
+
+
+def python_qualified_identifier(value: str) -> bool:
+    if value.startswith(".") or value.endswith("."):
+        return False
+    return all(python_identifier(m) for m in value.split("."))
+
+
+def python_module_name(value: str) -> bool:
+    return python_qualified_identifier(value)
+
+
+def python_entrypoint_group(value: str) -> bool:
+    return ENTRYPOINT_GROUP_REGEX.match(value) is not None
+
+
+def python_entrypoint_name(value: str) -> bool:
+    if not ENTRYPOINT_REGEX.match(value):
+        return False
+    if not RECOMMEDED_ENTRYPOINT_REGEX.match(value):
+        msg = f"Entry point `{value}` does not follow recommended pattern: "
+        msg += RECOMMEDED_ENTRYPOINT_PATTERN
+        _logger.warning(msg)
+    return True
+
+
+def python_entrypoint_reference(value: str) -> bool:
+    if ":" not in value:
+        return False
+    module, _, rest = value.partition(":")
+    if "[" in rest:
+        obj, _, extras_ = rest.partition("[")
+        if extras_.strip()[-1] != "]":
+            return False
+        extras = (x.strip() for x in extras_.strip(string.whitespace + "[]").split(","))
+        if not all(pep508_identifier(e) for e in extras):
+            return False
+        _logger.warning(f"`{value}` - using extras for entry points is not recommended")
+    else:
+        obj = rest
+
+    identifiers = chain(module.split("."), obj.split("."))
+    return all(python_identifier(i.strip()) for i in identifiers)
diff --git a/setuptools/_vendor/vendored.txt b/setuptools/_vendor/vendored.txt
index d10e196a..35c33c01 100644
--- a/setuptools/_vendor/vendored.txt
+++ b/setuptools/_vendor/vendored.txt
@@ -10,3 +10,4 @@ typing_extensions==4.0.1
 # required for importlib_resources and _metadata on older Pythons
 zipp==3.7.0
 tomli==1.2.3
+# validate-pyproject[all]==0.3.2  # Special handling, don't remove
diff --git a/setuptools/extern/__init__.py b/setuptools/extern/__init__.py
index d3a6dc99..90736e21 100644
--- a/setuptools/extern/__init__.py
+++ b/setuptools/extern/__init__.py
@@ -72,5 +72,6 @@ class VendorImporter:
 names = (
     'packaging', 'pyparsing', 'ordered_set', 'more_itertools', 'importlib_metadata',
     'zipp', 'importlib_resources', 'jaraco', 'typing_extensions', 'tomli',
+    '_validate_pyproject',
 )
 VendorImporter(__name__, names, 'setuptools._vendor').install()
diff --git a/tools/vendored.py b/tools/vendored.py
index 8a122ad7..53185437 100644
--- a/tools/vendored.py
+++ b/tools/vendored.py
@@ -1,6 +1,11 @@
+import os
 import re
 import sys
+import shutil
+import string
 import subprocess
+import venv
+from tempfile import TemporaryDirectory
 
 from path import Path
 
@@ -127,6 +132,7 @@ def update_pkg_resources():
 def update_setuptools():
     vendor = Path('setuptools/_vendor')
     install(vendor)
+    install_validate_pyproject(vendor)
     rewrite_packaging(vendor / 'packaging', 'setuptools.extern')
     rewrite_jaraco_text(vendor / 'jaraco/text', 'setuptools.extern')
     rewrite_jaraco(vendor / 'jaraco', 'setuptools.extern')
@@ -135,4 +141,37 @@ def update_setuptools():
     rewrite_more_itertools(vendor / "more_itertools")
 
 
+def install_validate_pyproject(vendor):
+    """``validate-pyproject`` can be vendorized to remove all dependencies"""
+    req = next(
+        (x for x in (vendor / "vendored.txt").lines() if 'validate-pyproject' in x),
+        "validate-pyproject[all]"
+    )
+
+    pkg, _, _ = req.strip(string.whitespace + "#").partition("#")
+    pkg = pkg.strip()
+
+    opts = {}
+    if sys.version_info[:2] >= (3, 10):
+        opts["ignore_cleanup_errors"] = True
+
+    with TemporaryDirectory(**opts) as tmp:
+        venv.create(tmp, with_pip=True)
+        path = os.pathsep.join(Path(tmp).glob("*"))
+        venv_python = shutil.which("python", path=path)
+        subprocess.check_call([venv_python, "-m", "pip", "install", pkg])
+        cmd = [
+            venv_python,
+            "-m",
+            "validate_pyproject.vendoring",
+            "--output-dir",
+            str(vendor / "_validate_pyproject"),
+            "--enable-plugins",
+            "setuptools",
+            "distutils",
+            "--very-verbose"
+        ]
+        subprocess.check_call(cmd)
+
+
 __name__ == '__main__' and update_vendored()
-- 
cgit v1.2.1


From 78dc27828702345bf9f0a9895f8f1ecd2838d1d6 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 1 Feb 2022 12:25:48 +0000
Subject: Add news fragment

---
 changelog.d/3066.change.rst | 3 +++
 1 file changed, 3 insertions(+)
 create mode 100644 changelog.d/3066.change.rst

diff --git a/changelog.d/3066.change.rst b/changelog.d/3066.change.rst
new file mode 100644
index 00000000..e672351f
--- /dev/null
+++ b/changelog.d/3066.change.rst
@@ -0,0 +1,3 @@
+Added vendored dependencies for :pypi:`tomli`, :pypi:`validate-pyproject`.
+
+These dependencies are used to read ``pyproject.toml`` files and validate them.
-- 
cgit v1.2.1


From 73715080cf0fbd5ae65b4f0f5af0e651adda7234 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 1 Feb 2022 15:30:39 +0000
Subject: Make comment in setuptools/_vendor/vendored.txt more clear

---
 setuptools/_vendor/vendored.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setuptools/_vendor/vendored.txt b/setuptools/_vendor/vendored.txt
index 35c33c01..fe05dc1a 100644
--- a/setuptools/_vendor/vendored.txt
+++ b/setuptools/_vendor/vendored.txt
@@ -10,4 +10,4 @@ typing_extensions==4.0.1
 # required for importlib_resources and _metadata on older Pythons
 zipp==3.7.0
 tomli==1.2.3
-# validate-pyproject[all]==0.3.2  # Special handling, don't remove
+# validate-pyproject[all]==0.3.2  # Special handling in tools/vendored, don't uncomment or remove
-- 
cgit v1.2.1


From ccd2f073171065ad8fe65215ff837644689c6d85 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 4 Feb 2022 11:16:12 +0000
Subject: Ensure relative imports for vendorised tomli

---
 setuptools/_vendor/tomli/__init__.py |  4 ++--
 setuptools/_vendor/tomli/_parser.py  |  4 ++--
 setuptools/_vendor/tomli/_re.py      |  2 +-
 tools/vendored.py                    | 11 +++++++++++
 4 files changed, 16 insertions(+), 5 deletions(-)

diff --git a/setuptools/_vendor/tomli/__init__.py b/setuptools/_vendor/tomli/__init__.py
index 60f792af..0ac89c82 100644
--- a/setuptools/_vendor/tomli/__init__.py
+++ b/setuptools/_vendor/tomli/__init__.py
@@ -3,7 +3,7 @@
 __all__ = ("loads", "load", "TOMLDecodeError")
 __version__ = "1.2.3"  # DO NOT EDIT THIS LINE MANUALLY. LET bump2version UTILITY DO IT
 
-from tomli._parser import TOMLDecodeError, load, loads
+from ._parser import TOMLDecodeError, load, loads
 
 # Pretend this exception was created here.
-TOMLDecodeError.__module__ = "tomli"
+TOMLDecodeError.__module__ = "setuptools.extern.tomli"
diff --git a/setuptools/_vendor/tomli/_parser.py b/setuptools/_vendor/tomli/_parser.py
index 89e81c3b..093afe50 100644
--- a/setuptools/_vendor/tomli/_parser.py
+++ b/setuptools/_vendor/tomli/_parser.py
@@ -3,7 +3,7 @@ from types import MappingProxyType
 from typing import Any, BinaryIO, Dict, FrozenSet, Iterable, NamedTuple, Optional, Tuple
 import warnings
 
-from tomli._re import (
+from ._re import (
     RE_DATETIME,
     RE_LOCALTIME,
     RE_NUMBER,
@@ -11,7 +11,7 @@ from tomli._re import (
     match_to_localtime,
     match_to_number,
 )
-from tomli._types import Key, ParseFloat, Pos
+from ._types import Key, ParseFloat, Pos
 
 ASCII_CTRL = frozenset(chr(i) for i in range(32)) | frozenset(chr(127))
 
diff --git a/setuptools/_vendor/tomli/_re.py b/setuptools/_vendor/tomli/_re.py
index 9dc9e903..45e17e2c 100644
--- a/setuptools/_vendor/tomli/_re.py
+++ b/setuptools/_vendor/tomli/_re.py
@@ -3,7 +3,7 @@ from functools import lru_cache
 import re
 from typing import Any, Optional, Union
 
-from tomli._types import ParseFloat
+from ._types import ParseFloat
 
 # E.g.
 # - 00:32:00.999999
diff --git a/tools/vendored.py b/tools/vendored.py
index 53185437..c1839711 100644
--- a/tools/vendored.py
+++ b/tools/vendored.py
@@ -94,6 +94,16 @@ def rewrite_more_itertools(pkg_files: Path):
     more_file.write_text(text)
 
 
+def rewrite_tomli(pkg_files, new_root):
+    """
+    Rewrite imports in tomli to use the relative form.
+    """
+    for file in pkg_files.glob('*.py'):
+        text = file.read_text().replace('tomli.', '.')
+        text = text.replace('tomli', f'{new_root}.tomli')
+        file.write_text(text)
+
+
 def clean(vendor):
     """
     Remove all files out of the vendor directory except the meta
@@ -139,6 +149,7 @@ def update_setuptools():
     rewrite_importlib_resources(vendor / 'importlib_resources', 'setuptools.extern')
     rewrite_importlib_metadata(vendor / 'importlib_metadata', 'setuptools.extern')
     rewrite_more_itertools(vendor / "more_itertools")
+    rewrite_tomli(vendor / 'tomli', 'setuptools.extern')
 
 
 def install_validate_pyproject(vendor):
-- 
cgit v1.2.1


From e0d61d45eaf94b55a57a68c6cd65b3e508aee5a9 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 8 Feb 2022 11:39:25 +0000
Subject: Update vendored tomli to 2.0.1

Enforcing local imports is no longer needed.
---
 .../fastjsonschema_validations.py                  |   2 +-
 setuptools/_vendor/tomli-2.0.1.dist-info/INSTALLER |   1 +
 setuptools/_vendor/tomli-2.0.1.dist-info/LICENSE   |  21 +++
 setuptools/_vendor/tomli-2.0.1.dist-info/METADATA  | 206 +++++++++++++++++++++
 setuptools/_vendor/tomli-2.0.1.dist-info/RECORD    |  15 ++
 setuptools/_vendor/tomli-2.0.1.dist-info/REQUESTED |   0
 setuptools/_vendor/tomli-2.0.1.dist-info/WHEEL     |   4 +
 setuptools/_vendor/tomli/LICENSE                   |  21 ---
 setuptools/_vendor/tomli/__init__.py               |   8 +-
 setuptools/_vendor/tomli/_parser.py                | 190 +++++++++++--------
 setuptools/_vendor/tomli/_re.py                    |  18 +-
 setuptools/_vendor/tomli/_types.py                 |   4 +
 setuptools/_vendor/vendored.txt                    |   2 +-
 tools/vendored.py                                  |  11 --
 14 files changed, 379 insertions(+), 124 deletions(-)
 create mode 100644 setuptools/_vendor/tomli-2.0.1.dist-info/INSTALLER
 create mode 100644 setuptools/_vendor/tomli-2.0.1.dist-info/LICENSE
 create mode 100644 setuptools/_vendor/tomli-2.0.1.dist-info/METADATA
 create mode 100644 setuptools/_vendor/tomli-2.0.1.dist-info/RECORD
 create mode 100644 setuptools/_vendor/tomli-2.0.1.dist-info/REQUESTED
 create mode 100644 setuptools/_vendor/tomli-2.0.1.dist-info/WHEEL
 delete mode 100644 setuptools/_vendor/tomli/LICENSE

diff --git a/setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py b/setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py
index d409b2a5..8bfd8809 100644
--- a/setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py
+++ b/setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py
@@ -10,7 +10,7 @@
 # *** PLEASE DO NOT MODIFY DIRECTLY: Automatically generated code *** 
 
 
-VERSION = "2.15.2"
+VERSION = "2.15.3"
 import re
 from .fastjsonschema_exceptions import JsonSchemaValueException
 
diff --git a/setuptools/_vendor/tomli-2.0.1.dist-info/INSTALLER b/setuptools/_vendor/tomli-2.0.1.dist-info/INSTALLER
new file mode 100644
index 00000000..a1b589e3
--- /dev/null
+++ b/setuptools/_vendor/tomli-2.0.1.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/setuptools/_vendor/tomli-2.0.1.dist-info/LICENSE b/setuptools/_vendor/tomli-2.0.1.dist-info/LICENSE
new file mode 100644
index 00000000..e859590f
--- /dev/null
+++ b/setuptools/_vendor/tomli-2.0.1.dist-info/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Taneli Hukkinen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/setuptools/_vendor/tomli-2.0.1.dist-info/METADATA b/setuptools/_vendor/tomli-2.0.1.dist-info/METADATA
new file mode 100644
index 00000000..efd87ecc
--- /dev/null
+++ b/setuptools/_vendor/tomli-2.0.1.dist-info/METADATA
@@ -0,0 +1,206 @@
+Metadata-Version: 2.1
+Name: tomli
+Version: 2.0.1
+Summary: A lil' TOML parser
+Keywords: toml
+Author-email: Taneli Hukkinen 
+Requires-Python: >=3.7
+Description-Content-Type: text/markdown
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Operating System :: MacOS
+Classifier: Operating System :: Microsoft :: Windows
+Classifier: Operating System :: POSIX :: Linux
+Classifier: Programming Language :: Python :: 3 :: Only
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Typing :: Typed
+Project-URL: Changelog, https://github.com/hukkin/tomli/blob/master/CHANGELOG.md
+Project-URL: Homepage, https://github.com/hukkin/tomli
+
+[![Build Status](https://github.com/hukkin/tomli/workflows/Tests/badge.svg?branch=master)](https://github.com/hukkin/tomli/actions?query=workflow%3ATests+branch%3Amaster+event%3Apush)
+[![codecov.io](https://codecov.io/gh/hukkin/tomli/branch/master/graph/badge.svg)](https://codecov.io/gh/hukkin/tomli)
+[![PyPI version](https://img.shields.io/pypi/v/tomli)](https://pypi.org/project/tomli)
+
+# Tomli
+
+> A lil' TOML parser
+
+**Table of Contents**  *generated with [mdformat-toc](https://github.com/hukkin/mdformat-toc)*
+
+
+
+- [Intro](#intro)
+- [Installation](#installation)
+- [Usage](#usage)
+  - [Parse a TOML string](#parse-a-toml-string)
+  - [Parse a TOML file](#parse-a-toml-file)
+  - [Handle invalid TOML](#handle-invalid-toml)
+  - [Construct `decimal.Decimal`s from TOML floats](#construct-decimaldecimals-from-toml-floats)
+- [FAQ](#faq)
+  - [Why this parser?](#why-this-parser)
+  - [Is comment preserving round-trip parsing supported?](#is-comment-preserving-round-trip-parsing-supported)
+  - [Is there a `dumps`, `write` or `encode` function?](#is-there-a-dumps-write-or-encode-function)
+  - [How do TOML types map into Python types?](#how-do-toml-types-map-into-python-types)
+- [Performance](#performance)
+
+
+
+## Intro
+
+Tomli is a Python library for parsing [TOML](https://toml.io).
+Tomli is fully compatible with [TOML v1.0.0](https://toml.io/en/v1.0.0).
+
+## Installation
+
+```bash
+pip install tomli
+```
+
+## Usage
+
+### Parse a TOML string
+
+```python
+import tomli
+
+toml_str = """
+           gretzky = 99
+
+           [kurri]
+           jari = 17
+           """
+
+toml_dict = tomli.loads(toml_str)
+assert toml_dict == {"gretzky": 99, "kurri": {"jari": 17}}
+```
+
+### Parse a TOML file
+
+```python
+import tomli
+
+with open("path_to_file/conf.toml", "rb") as f:
+    toml_dict = tomli.load(f)
+```
+
+The file must be opened in binary mode (with the `"rb"` flag).
+Binary mode will enforce decoding the file as UTF-8 with universal newlines disabled,
+both of which are required to correctly parse TOML.
+
+### Handle invalid TOML
+
+```python
+import tomli
+
+try:
+    toml_dict = tomli.loads("]] this is invalid TOML [[")
+except tomli.TOMLDecodeError:
+    print("Yep, definitely not valid.")
+```
+
+Note that error messages are considered informational only.
+They should not be assumed to stay constant across Tomli versions.
+
+### Construct `decimal.Decimal`s from TOML floats
+
+```python
+from decimal import Decimal
+import tomli
+
+toml_dict = tomli.loads("precision-matters = 0.982492", parse_float=Decimal)
+assert toml_dict["precision-matters"] == Decimal("0.982492")
+```
+
+Note that `decimal.Decimal` can be replaced with another callable that converts a TOML float from string to a Python type.
+The `decimal.Decimal` is, however, a practical choice for use cases where float inaccuracies can not be tolerated.
+
+Illegal types are `dict` and `list`, and their subtypes.
+A `ValueError` will be raised if `parse_float` produces illegal types.
+
+## FAQ
+
+### Why this parser?
+
+- it's lil'
+- pure Python with zero dependencies
+- the fastest pure Python parser [\*](#performance):
+  15x as fast as [tomlkit](https://pypi.org/project/tomlkit/),
+  2.4x as fast as [toml](https://pypi.org/project/toml/)
+- outputs [basic data types](#how-do-toml-types-map-into-python-types) only
+- 100% spec compliant: passes all tests in
+  [a test set](https://github.com/toml-lang/compliance/pull/8)
+  soon to be merged to the official
+  [compliance tests for TOML](https://github.com/toml-lang/compliance)
+  repository
+- thoroughly tested: 100% branch coverage
+
+### Is comment preserving round-trip parsing supported?
+
+No.
+
+The `tomli.loads` function returns a plain `dict` that is populated with builtin types and types from the standard library only.
+Preserving comments requires a custom type to be returned so will not be supported,
+at least not by the `tomli.loads` and `tomli.load` functions.
+
+Look into [TOML Kit](https://github.com/sdispater/tomlkit) if preservation of style is what you need.
+
+### Is there a `dumps`, `write` or `encode` function?
+
+[Tomli-W](https://github.com/hukkin/tomli-w) is the write-only counterpart of Tomli, providing `dump` and `dumps` functions.
+
+The core library does not include write capability, as most TOML use cases are read-only, and Tomli intends to be minimal.
+
+### How do TOML types map into Python types?
+
+| TOML type        | Python type         | Details                                                      |
+| ---------------- | ------------------- | ------------------------------------------------------------ |
+| Document Root    | `dict`              |                                                              |
+| Key              | `str`               |                                                              |
+| String           | `str`               |                                                              |
+| Integer          | `int`               |                                                              |
+| Float            | `float`             |                                                              |
+| Boolean          | `bool`              |                                                              |
+| Offset Date-Time | `datetime.datetime` | `tzinfo` attribute set to an instance of `datetime.timezone` |
+| Local Date-Time  | `datetime.datetime` | `tzinfo` attribute set to `None`                             |
+| Local Date       | `datetime.date`     |                                                              |
+| Local Time       | `datetime.time`     |                                                              |
+| Array            | `list`              |                                                              |
+| Table            | `dict`              |                                                              |
+| Inline Table     | `dict`              |                                                              |
+
+## Performance
+
+The `benchmark/` folder in this repository contains a performance benchmark for comparing the various Python TOML parsers.
+The benchmark can be run with `tox -e benchmark-pypi`.
+Running the benchmark on my personal computer output the following:
+
+```console
+foo@bar:~/dev/tomli$ tox -e benchmark-pypi
+benchmark-pypi installed: attrs==19.3.0,click==7.1.2,pytomlpp==1.0.2,qtoml==0.3.0,rtoml==0.7.0,toml==0.10.2,tomli==1.1.0,tomlkit==0.7.2
+benchmark-pypi run-test-pre: PYTHONHASHSEED='2658546909'
+benchmark-pypi run-test: commands[0] | python -c 'import datetime; print(datetime.date.today())'
+2021-07-23
+benchmark-pypi run-test: commands[1] | python --version
+Python 3.8.10
+benchmark-pypi run-test: commands[2] | python benchmark/run.py
+Parsing data.toml 5000 times:
+------------------------------------------------------
+    parser |  exec time | performance (more is better)
+-----------+------------+-----------------------------
+     rtoml |    0.901 s | baseline (100%)
+  pytomlpp |     1.08 s | 83.15%
+     tomli |     3.89 s | 23.15%
+      toml |     9.36 s | 9.63%
+     qtoml |     11.5 s | 7.82%
+   tomlkit |     56.8 s | 1.59%
+```
+
+The parsers are ordered from fastest to slowest, using the fastest parser as baseline.
+Tomli performed the best out of all pure Python TOML parsers,
+losing only to pytomlpp (wraps C++) and rtoml (wraps Rust).
+
diff --git a/setuptools/_vendor/tomli-2.0.1.dist-info/RECORD b/setuptools/_vendor/tomli-2.0.1.dist-info/RECORD
new file mode 100644
index 00000000..2d93fa2c
--- /dev/null
+++ b/setuptools/_vendor/tomli-2.0.1.dist-info/RECORD
@@ -0,0 +1,15 @@
+tomli-2.0.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+tomli-2.0.1.dist-info/LICENSE,sha256=uAgWsNUwuKzLTCIReDeQmEpuO2GSLCte6S8zcqsnQv4,1072
+tomli-2.0.1.dist-info/METADATA,sha256=zPDceKmPwJGLWtZykrHixL7WVXWmJGzZ1jyRT5lCoPI,8875
+tomli-2.0.1.dist-info/RECORD,,
+tomli-2.0.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+tomli-2.0.1.dist-info/WHEEL,sha256=jPMR_Dzkc4X4icQtmz81lnNY_kAsfog7ry7qoRvYLXw,81
+tomli/__init__.py,sha256=JhUwV66DB1g4Hvt1UQCVMdfCu-IgAV8FXmvDU9onxd4,396
+tomli/__pycache__/__init__.cpython-38.pyc,,
+tomli/__pycache__/_parser.cpython-38.pyc,,
+tomli/__pycache__/_re.cpython-38.pyc,,
+tomli/__pycache__/_types.cpython-38.pyc,,
+tomli/_parser.py,sha256=g9-ENaALS-B8dokYpCuzUFalWlog7T-SIYMjLZSWrtM,22633
+tomli/_re.py,sha256=dbjg5ChZT23Ka9z9DHOXfdtSpPwUfdgMXnj8NOoly-w,2943
+tomli/_types.py,sha256=-GTG2VUqkpxwMqzmVO4F7ybKddIbAnuAHXfmWQcTi3Q,254
+tomli/py.typed,sha256=8PjyZ1aVoQpRVvt71muvuq5qE-jTFZkK-GLHkhdebmc,26
diff --git a/setuptools/_vendor/tomli-2.0.1.dist-info/REQUESTED b/setuptools/_vendor/tomli-2.0.1.dist-info/REQUESTED
new file mode 100644
index 00000000..e69de29b
diff --git a/setuptools/_vendor/tomli-2.0.1.dist-info/WHEEL b/setuptools/_vendor/tomli-2.0.1.dist-info/WHEEL
new file mode 100644
index 00000000..c727d148
--- /dev/null
+++ b/setuptools/_vendor/tomli-2.0.1.dist-info/WHEEL
@@ -0,0 +1,4 @@
+Wheel-Version: 1.0
+Generator: flit 3.6.0
+Root-Is-Purelib: true
+Tag: py3-none-any
diff --git a/setuptools/_vendor/tomli/LICENSE b/setuptools/_vendor/tomli/LICENSE
deleted file mode 100644
index e859590f..00000000
--- a/setuptools/_vendor/tomli/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2021 Taneli Hukkinen
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/setuptools/_vendor/tomli/__init__.py b/setuptools/_vendor/tomli/__init__.py
index 0ac89c82..4c6ec97e 100644
--- a/setuptools/_vendor/tomli/__init__.py
+++ b/setuptools/_vendor/tomli/__init__.py
@@ -1,9 +1,11 @@
-"""A lil' TOML parser."""
+# SPDX-License-Identifier: MIT
+# SPDX-FileCopyrightText: 2021 Taneli Hukkinen
+# Licensed to PSF under a Contributor Agreement.
 
 __all__ = ("loads", "load", "TOMLDecodeError")
-__version__ = "1.2.3"  # DO NOT EDIT THIS LINE MANUALLY. LET bump2version UTILITY DO IT
+__version__ = "2.0.1"  # DO NOT EDIT THIS LINE MANUALLY. LET bump2version UTILITY DO IT
 
 from ._parser import TOMLDecodeError, load, loads
 
 # Pretend this exception was created here.
-TOMLDecodeError.__module__ = "setuptools.extern.tomli"
+TOMLDecodeError.__module__ = __name__
diff --git a/setuptools/_vendor/tomli/_parser.py b/setuptools/_vendor/tomli/_parser.py
index 093afe50..f1bb0aa1 100644
--- a/setuptools/_vendor/tomli/_parser.py
+++ b/setuptools/_vendor/tomli/_parser.py
@@ -1,7 +1,13 @@
+# SPDX-License-Identifier: MIT
+# SPDX-FileCopyrightText: 2021 Taneli Hukkinen
+# Licensed to PSF under a Contributor Agreement.
+
+from __future__ import annotations
+
+from collections.abc import Iterable
 import string
 from types import MappingProxyType
-from typing import Any, BinaryIO, Dict, FrozenSet, Iterable, NamedTuple, Optional, Tuple
-import warnings
+from typing import Any, BinaryIO, NamedTuple
 
 from ._re import (
     RE_DATETIME,
@@ -48,31 +54,28 @@ class TOMLDecodeError(ValueError):
     """An error raised if a document is not valid TOML."""
 
 
-def load(fp: BinaryIO, *, parse_float: ParseFloat = float) -> Dict[str, Any]:
+def load(__fp: BinaryIO, *, parse_float: ParseFloat = float) -> dict[str, Any]:
     """Parse TOML from a binary file object."""
-    s_bytes = fp.read()
+    b = __fp.read()
     try:
-        s = s_bytes.decode()
+        s = b.decode()
     except AttributeError:
-        warnings.warn(
-            "Text file object support is deprecated in favor of binary file objects."
-            ' Use `open("foo.toml", "rb")` to open the file in binary mode.',
-            DeprecationWarning,
-            stacklevel=2,
-        )
-        s = s_bytes  # type: ignore[assignment]
+        raise TypeError(
+            "File must be opened in binary mode, e.g. use `open('foo.toml', 'rb')`"
+        ) from None
     return loads(s, parse_float=parse_float)
 
 
-def loads(s: str, *, parse_float: ParseFloat = float) -> Dict[str, Any]:  # noqa: C901
+def loads(__s: str, *, parse_float: ParseFloat = float) -> dict[str, Any]:  # noqa: C901
     """Parse TOML from a string."""
 
     # The spec allows converting "\r\n" to "\n", even in string
     # literals. Let's do so to simplify parsing.
-    src = s.replace("\r\n", "\n")
+    src = __s.replace("\r\n", "\n")
     pos = 0
     out = Output(NestedDict(), Flags())
     header: Key = ()
+    parse_float = make_safe_parse_float(parse_float)
 
     # Parse one statement at a time
     # (typically means one line in TOML source)
@@ -100,9 +103,10 @@ def loads(s: str, *, parse_float: ParseFloat = float) -> Dict[str, Any]:  # noqa
             pos = skip_chars(src, pos, TOML_WS)
         elif char == "[":
             try:
-                second_char: Optional[str] = src[pos + 1]
+                second_char: str | None = src[pos + 1]
             except IndexError:
                 second_char = None
+            out.flags.finalize_pending()
             if second_char == "[":
                 pos, header = create_list_rule(src, pos, out)
             else:
@@ -138,7 +142,16 @@ class Flags:
     EXPLICIT_NEST = 1
 
     def __init__(self) -> None:
-        self._flags: Dict[str, dict] = {}
+        self._flags: dict[str, dict] = {}
+        self._pending_flags: set[tuple[Key, int]] = set()
+
+    def add_pending(self, key: Key, flag: int) -> None:
+        self._pending_flags.add((key, flag))
+
+    def finalize_pending(self) -> None:
+        for key, flag in self._pending_flags:
+            self.set(key, flag, recursive=False)
+        self._pending_flags.clear()
 
     def unset_all(self, key: Key) -> None:
         cont = self._flags
@@ -148,19 +161,6 @@ class Flags:
             cont = cont[k]["nested"]
         cont.pop(key[-1], None)
 
-    def set_for_relative_key(self, head_key: Key, rel_key: Key, flag: int) -> None:
-        cont = self._flags
-        for k in head_key:
-            if k not in cont:
-                cont[k] = {"flags": set(), "recursive_flags": set(), "nested": {}}
-            cont = cont[k]["nested"]
-        for k in rel_key:
-            if k in cont:
-                cont[k]["flags"].add(flag)
-            else:
-                cont[k] = {"flags": {flag}, "recursive_flags": set(), "nested": {}}
-            cont = cont[k]["nested"]
-
     def set(self, key: Key, flag: int, *, recursive: bool) -> None:  # noqa: A003
         cont = self._flags
         key_parent, key_stem = key[:-1], key[-1]
@@ -193,7 +193,7 @@ class Flags:
 class NestedDict:
     def __init__(self) -> None:
         # The parsed content of the TOML document
-        self.dict: Dict[str, Any] = {}
+        self.dict: dict[str, Any] = {}
 
     def get_or_create_nest(
         self,
@@ -217,10 +217,9 @@ class NestedDict:
         last_key = key[-1]
         if last_key in cont:
             list_ = cont[last_key]
-            try:
-                list_.append({})
-            except AttributeError:
+            if not isinstance(list_, list):
                 raise KeyError("An object other than list found behind this key")
+            list_.append({})
         else:
             cont[last_key] = [{}]
 
@@ -244,7 +243,7 @@ def skip_until(
     pos: Pos,
     expect: str,
     *,
-    error_on: FrozenSet[str],
+    error_on: frozenset[str],
     error_on_eof: bool,
 ) -> Pos:
     try:
@@ -263,7 +262,7 @@ def skip_until(
 
 def skip_comment(src: str, pos: Pos) -> Pos:
     try:
-        char: Optional[str] = src[pos]
+        char: str | None = src[pos]
     except IndexError:
         char = None
     if char == "#":
@@ -282,31 +281,31 @@ def skip_comments_and_array_ws(src: str, pos: Pos) -> Pos:
             return pos
 
 
-def create_dict_rule(src: str, pos: Pos, out: Output) -> Tuple[Pos, Key]:
+def create_dict_rule(src: str, pos: Pos, out: Output) -> tuple[Pos, Key]:
     pos += 1  # Skip "["
     pos = skip_chars(src, pos, TOML_WS)
     pos, key = parse_key(src, pos)
 
     if out.flags.is_(key, Flags.EXPLICIT_NEST) or out.flags.is_(key, Flags.FROZEN):
-        raise suffixed_err(src, pos, f"Can not declare {key} twice")
+        raise suffixed_err(src, pos, f"Cannot declare {key} twice")
     out.flags.set(key, Flags.EXPLICIT_NEST, recursive=False)
     try:
         out.data.get_or_create_nest(key)
     except KeyError:
-        raise suffixed_err(src, pos, "Can not overwrite a value") from None
+        raise suffixed_err(src, pos, "Cannot overwrite a value") from None
 
     if not src.startswith("]", pos):
-        raise suffixed_err(src, pos, 'Expected "]" at the end of a table declaration')
+        raise suffixed_err(src, pos, "Expected ']' at the end of a table declaration")
     return pos + 1, key
 
 
-def create_list_rule(src: str, pos: Pos, out: Output) -> Tuple[Pos, Key]:
+def create_list_rule(src: str, pos: Pos, out: Output) -> tuple[Pos, Key]:
     pos += 2  # Skip "[["
     pos = skip_chars(src, pos, TOML_WS)
     pos, key = parse_key(src, pos)
 
     if out.flags.is_(key, Flags.FROZEN):
-        raise suffixed_err(src, pos, f"Can not mutate immutable namespace {key}")
+        raise suffixed_err(src, pos, f"Cannot mutate immutable namespace {key}")
     # Free the namespace now that it points to another empty list item...
     out.flags.unset_all(key)
     # ...but this key precisely is still prohibited from table declaration
@@ -314,10 +313,10 @@ def create_list_rule(src: str, pos: Pos, out: Output) -> Tuple[Pos, Key]:
     try:
         out.data.append_nest_to_list(key)
     except KeyError:
-        raise suffixed_err(src, pos, "Can not overwrite a value") from None
+        raise suffixed_err(src, pos, "Cannot overwrite a value") from None
 
     if not src.startswith("]]", pos):
-        raise suffixed_err(src, pos, 'Expected "]]" at the end of an array declaration')
+        raise suffixed_err(src, pos, "Expected ']]' at the end of an array declaration")
     return pos + 2, key
 
 
@@ -328,18 +327,26 @@ def key_value_rule(
     key_parent, key_stem = key[:-1], key[-1]
     abs_key_parent = header + key_parent
 
+    relative_path_cont_keys = (header + key[:i] for i in range(1, len(key)))
+    for cont_key in relative_path_cont_keys:
+        # Check that dotted key syntax does not redefine an existing table
+        if out.flags.is_(cont_key, Flags.EXPLICIT_NEST):
+            raise suffixed_err(src, pos, f"Cannot redefine namespace {cont_key}")
+        # Containers in the relative path can't be opened with the table syntax or
+        # dotted key/value syntax in following table sections.
+        out.flags.add_pending(cont_key, Flags.EXPLICIT_NEST)
+
     if out.flags.is_(abs_key_parent, Flags.FROZEN):
         raise suffixed_err(
-            src, pos, f"Can not mutate immutable namespace {abs_key_parent}"
+            src, pos, f"Cannot mutate immutable namespace {abs_key_parent}"
         )
-    # Containers in the relative path can't be opened with the table syntax after this
-    out.flags.set_for_relative_key(header, key, Flags.EXPLICIT_NEST)
+
     try:
         nest = out.data.get_or_create_nest(abs_key_parent)
     except KeyError:
-        raise suffixed_err(src, pos, "Can not overwrite a value") from None
+        raise suffixed_err(src, pos, "Cannot overwrite a value") from None
     if key_stem in nest:
-        raise suffixed_err(src, pos, "Can not overwrite a value")
+        raise suffixed_err(src, pos, "Cannot overwrite a value")
     # Mark inline table and array namespaces recursively immutable
     if isinstance(value, (dict, list)):
         out.flags.set(header + key, Flags.FROZEN, recursive=True)
@@ -349,27 +356,27 @@ def key_value_rule(
 
 def parse_key_value_pair(
     src: str, pos: Pos, parse_float: ParseFloat
-) -> Tuple[Pos, Key, Any]:
+) -> tuple[Pos, Key, Any]:
     pos, key = parse_key(src, pos)
     try:
-        char: Optional[str] = src[pos]
+        char: str | None = src[pos]
     except IndexError:
         char = None
     if char != "=":
-        raise suffixed_err(src, pos, 'Expected "=" after a key in a key/value pair')
+        raise suffixed_err(src, pos, "Expected '=' after a key in a key/value pair")
     pos += 1
     pos = skip_chars(src, pos, TOML_WS)
     pos, value = parse_value(src, pos, parse_float)
     return pos, key, value
 
 
-def parse_key(src: str, pos: Pos) -> Tuple[Pos, Key]:
+def parse_key(src: str, pos: Pos) -> tuple[Pos, Key]:
     pos, key_part = parse_key_part(src, pos)
     key: Key = (key_part,)
     pos = skip_chars(src, pos, TOML_WS)
     while True:
         try:
-            char: Optional[str] = src[pos]
+            char: str | None = src[pos]
         except IndexError:
             char = None
         if char != ".":
@@ -381,9 +388,9 @@ def parse_key(src: str, pos: Pos) -> Tuple[Pos, Key]:
         pos = skip_chars(src, pos, TOML_WS)
 
 
-def parse_key_part(src: str, pos: Pos) -> Tuple[Pos, str]:
+def parse_key_part(src: str, pos: Pos) -> tuple[Pos, str]:
     try:
-        char: Optional[str] = src[pos]
+        char: str | None = src[pos]
     except IndexError:
         char = None
     if char in BARE_KEY_CHARS:
@@ -397,12 +404,12 @@ def parse_key_part(src: str, pos: Pos) -> Tuple[Pos, str]:
     raise suffixed_err(src, pos, "Invalid initial character for a key part")
 
 
-def parse_one_line_basic_str(src: str, pos: Pos) -> Tuple[Pos, str]:
+def parse_one_line_basic_str(src: str, pos: Pos) -> tuple[Pos, str]:
     pos += 1
     return parse_basic_str(src, pos, multiline=False)
 
 
-def parse_array(src: str, pos: Pos, parse_float: ParseFloat) -> Tuple[Pos, list]:
+def parse_array(src: str, pos: Pos, parse_float: ParseFloat) -> tuple[Pos, list]:
     pos += 1
     array: list = []
 
@@ -426,7 +433,7 @@ def parse_array(src: str, pos: Pos, parse_float: ParseFloat) -> Tuple[Pos, list]
             return pos + 1, array
 
 
-def parse_inline_table(src: str, pos: Pos, parse_float: ParseFloat) -> Tuple[Pos, dict]:
+def parse_inline_table(src: str, pos: Pos, parse_float: ParseFloat) -> tuple[Pos, dict]:
     pos += 1
     nested_dict = NestedDict()
     flags = Flags()
@@ -438,11 +445,11 @@ def parse_inline_table(src: str, pos: Pos, parse_float: ParseFloat) -> Tuple[Pos
         pos, key, value = parse_key_value_pair(src, pos, parse_float)
         key_parent, key_stem = key[:-1], key[-1]
         if flags.is_(key, Flags.FROZEN):
-            raise suffixed_err(src, pos, f"Can not mutate immutable namespace {key}")
+            raise suffixed_err(src, pos, f"Cannot mutate immutable namespace {key}")
         try:
             nest = nested_dict.get_or_create_nest(key_parent, access_lists=False)
         except KeyError:
-            raise suffixed_err(src, pos, "Can not overwrite a value") from None
+            raise suffixed_err(src, pos, "Cannot overwrite a value") from None
         if key_stem in nest:
             raise suffixed_err(src, pos, f"Duplicate inline table key {key_stem!r}")
         nest[key_stem] = value
@@ -458,9 +465,9 @@ def parse_inline_table(src: str, pos: Pos, parse_float: ParseFloat) -> Tuple[Pos
         pos = skip_chars(src, pos, TOML_WS)
 
 
-def parse_basic_str_escape(  # noqa: C901
+def parse_basic_str_escape(
     src: str, pos: Pos, *, multiline: bool = False
-) -> Tuple[Pos, str]:
+) -> tuple[Pos, str]:
     escape_id = src[pos : pos + 2]
     pos += 2
     if multiline and escape_id in {"\\ ", "\\\t", "\\\n"}:
@@ -473,7 +480,7 @@ def parse_basic_str_escape(  # noqa: C901
             except IndexError:
                 return pos, ""
             if char != "\n":
-                raise suffixed_err(src, pos, 'Unescaped "\\" in a string')
+                raise suffixed_err(src, pos, "Unescaped '\\' in a string")
             pos += 1
         pos = skip_chars(src, pos, TOML_WS_AND_NEWLINE)
         return pos, ""
@@ -484,16 +491,14 @@ def parse_basic_str_escape(  # noqa: C901
     try:
         return pos, BASIC_STR_ESCAPE_REPLACEMENTS[escape_id]
     except KeyError:
-        if len(escape_id) != 2:
-            raise suffixed_err(src, pos, "Unterminated string") from None
-        raise suffixed_err(src, pos, 'Unescaped "\\" in a string') from None
+        raise suffixed_err(src, pos, "Unescaped '\\' in a string") from None
 
 
-def parse_basic_str_escape_multiline(src: str, pos: Pos) -> Tuple[Pos, str]:
+def parse_basic_str_escape_multiline(src: str, pos: Pos) -> tuple[Pos, str]:
     return parse_basic_str_escape(src, pos, multiline=True)
 
 
-def parse_hex_char(src: str, pos: Pos, hex_len: int) -> Tuple[Pos, str]:
+def parse_hex_char(src: str, pos: Pos, hex_len: int) -> tuple[Pos, str]:
     hex_str = src[pos : pos + hex_len]
     if len(hex_str) != hex_len or not HEXDIGIT_CHARS.issuperset(hex_str):
         raise suffixed_err(src, pos, "Invalid hex value")
@@ -504,7 +509,7 @@ def parse_hex_char(src: str, pos: Pos, hex_len: int) -> Tuple[Pos, str]:
     return pos, chr(hex_int)
 
 
-def parse_literal_str(src: str, pos: Pos) -> Tuple[Pos, str]:
+def parse_literal_str(src: str, pos: Pos) -> tuple[Pos, str]:
     pos += 1  # Skip starting apostrophe
     start_pos = pos
     pos = skip_until(
@@ -513,7 +518,7 @@ def parse_literal_str(src: str, pos: Pos) -> Tuple[Pos, str]:
     return pos + 1, src[start_pos:pos]  # Skip ending apostrophe
 
 
-def parse_multiline_str(src: str, pos: Pos, *, literal: bool) -> Tuple[Pos, str]:
+def parse_multiline_str(src: str, pos: Pos, *, literal: bool) -> tuple[Pos, str]:
     pos += 3
     if src.startswith("\n", pos):
         pos += 1
@@ -544,7 +549,7 @@ def parse_multiline_str(src: str, pos: Pos, *, literal: bool) -> Tuple[Pos, str]
     return pos, result + (delim * 2)
 
 
-def parse_basic_str(src: str, pos: Pos, *, multiline: bool) -> Tuple[Pos, str]:
+def parse_basic_str(src: str, pos: Pos, *, multiline: bool) -> tuple[Pos, str]:
     if multiline:
         error_on = ILLEGAL_MULTILINE_BASIC_STR_CHARS
         parse_escapes = parse_basic_str_escape_multiline
@@ -578,12 +583,14 @@ def parse_basic_str(src: str, pos: Pos, *, multiline: bool) -> Tuple[Pos, str]:
 
 def parse_value(  # noqa: C901
     src: str, pos: Pos, parse_float: ParseFloat
-) -> Tuple[Pos, Any]:
+) -> tuple[Pos, Any]:
     try:
-        char: Optional[str] = src[pos]
+        char: str | None = src[pos]
     except IndexError:
         char = None
 
+    # IMPORTANT: order conditions based on speed of checking and likelihood
+
     # Basic strings
     if char == '"':
         if src.startswith('"""', pos):
@@ -604,6 +611,14 @@ def parse_value(  # noqa: C901
         if src.startswith("false", pos):
             return pos + 5, False
 
+    # Arrays
+    if char == "[":
+        return parse_array(src, pos, parse_float)
+
+    # Inline tables
+    if char == "{":
+        return parse_inline_table(src, pos, parse_float)
+
     # Dates and times
     datetime_match = RE_DATETIME.match(src, pos)
     if datetime_match:
@@ -623,14 +638,6 @@ def parse_value(  # noqa: C901
     if number_match:
         return number_match.end(), match_to_number(number_match, parse_float)
 
-    # Arrays
-    if char == "[":
-        return parse_array(src, pos, parse_float)
-
-    # Inline tables
-    if char == "{":
-        return parse_inline_table(src, pos, parse_float)
-
     # Special floats
     first_three = src[pos : pos + 3]
     if first_three in {"inf", "nan"}:
@@ -661,3 +668,24 @@ def suffixed_err(src: str, pos: Pos, msg: str) -> TOMLDecodeError:
 
 def is_unicode_scalar_value(codepoint: int) -> bool:
     return (0 <= codepoint <= 55295) or (57344 <= codepoint <= 1114111)
+
+
+def make_safe_parse_float(parse_float: ParseFloat) -> ParseFloat:
+    """A decorator to make `parse_float` safe.
+
+    `parse_float` must not return dicts or lists, because these types
+    would be mixed with parsed TOML tables and arrays, thus confusing
+    the parser. The returned decorated callable raises `ValueError`
+    instead of returning illegal types.
+    """
+    # The default `float` callable never returns illegal types. Optimize it.
+    if parse_float is float:  # type: ignore[comparison-overlap]
+        return float
+
+    def safe_parse_float(float_str: str) -> Any:
+        float_value = parse_float(float_str)
+        if isinstance(float_value, (dict, list)):
+            raise ValueError("parse_float must not return dicts or lists")
+        return float_value
+
+    return safe_parse_float
diff --git a/setuptools/_vendor/tomli/_re.py b/setuptools/_vendor/tomli/_re.py
index 45e17e2c..994bb749 100644
--- a/setuptools/_vendor/tomli/_re.py
+++ b/setuptools/_vendor/tomli/_re.py
@@ -1,7 +1,13 @@
+# SPDX-License-Identifier: MIT
+# SPDX-FileCopyrightText: 2021 Taneli Hukkinen
+# Licensed to PSF under a Contributor Agreement.
+
+from __future__ import annotations
+
 from datetime import date, datetime, time, timedelta, timezone, tzinfo
 from functools import lru_cache
 import re
-from typing import Any, Optional, Union
+from typing import Any
 
 from ._types import ParseFloat
 
@@ -31,7 +37,7 @@ RE_NUMBER = re.compile(
 )
 RE_LOCALTIME = re.compile(_TIME_RE_STR)
 RE_DATETIME = re.compile(
-    fr"""
+    rf"""
 ([0-9]{{4}})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])  # date, e.g. 1988-10-27
 (?:
     [Tt ]
@@ -43,7 +49,7 @@ RE_DATETIME = re.compile(
 )
 
 
-def match_to_datetime(match: "re.Match") -> Union[datetime, date]:
+def match_to_datetime(match: re.Match) -> datetime | date:
     """Convert a `RE_DATETIME` match to `datetime.datetime` or `datetime.date`.
 
     Raises ValueError if the match does not correspond to a valid date
@@ -68,7 +74,7 @@ def match_to_datetime(match: "re.Match") -> Union[datetime, date]:
     hour, minute, sec = int(hour_str), int(minute_str), int(sec_str)
     micros = int(micros_str.ljust(6, "0")) if micros_str else 0
     if offset_sign_str:
-        tz: Optional[tzinfo] = cached_tz(
+        tz: tzinfo | None = cached_tz(
             offset_hour_str, offset_minute_str, offset_sign_str
         )
     elif zulu_time:
@@ -89,13 +95,13 @@ def cached_tz(hour_str: str, minute_str: str, sign_str: str) -> timezone:
     )
 
 
-def match_to_localtime(match: "re.Match") -> time:
+def match_to_localtime(match: re.Match) -> time:
     hour_str, minute_str, sec_str, micros_str = match.groups()
     micros = int(micros_str.ljust(6, "0")) if micros_str else 0
     return time(int(hour_str), int(minute_str), int(sec_str), micros)
 
 
-def match_to_number(match: "re.Match", parse_float: "ParseFloat") -> Any:
+def match_to_number(match: re.Match, parse_float: ParseFloat) -> Any:
     if match.group("floatpart"):
         return parse_float(match.group())
     return int(match.group(), 0)
diff --git a/setuptools/_vendor/tomli/_types.py b/setuptools/_vendor/tomli/_types.py
index e37cc808..d949412e 100644
--- a/setuptools/_vendor/tomli/_types.py
+++ b/setuptools/_vendor/tomli/_types.py
@@ -1,3 +1,7 @@
+# SPDX-License-Identifier: MIT
+# SPDX-FileCopyrightText: 2021 Taneli Hukkinen
+# Licensed to PSF under a Contributor Agreement.
+
 from typing import Any, Callable, Tuple
 
 # Type annotations
diff --git a/setuptools/_vendor/vendored.txt b/setuptools/_vendor/vendored.txt
index fe05dc1a..38d1f70f 100644
--- a/setuptools/_vendor/vendored.txt
+++ b/setuptools/_vendor/vendored.txt
@@ -9,5 +9,5 @@ importlib_metadata==4.11.1
 typing_extensions==4.0.1
 # required for importlib_resources and _metadata on older Pythons
 zipp==3.7.0
-tomli==1.2.3
+tomli==2.0.1
 # validate-pyproject[all]==0.3.2  # Special handling in tools/vendored, don't uncomment or remove
diff --git a/tools/vendored.py b/tools/vendored.py
index c1839711..53185437 100644
--- a/tools/vendored.py
+++ b/tools/vendored.py
@@ -94,16 +94,6 @@ def rewrite_more_itertools(pkg_files: Path):
     more_file.write_text(text)
 
 
-def rewrite_tomli(pkg_files, new_root):
-    """
-    Rewrite imports in tomli to use the relative form.
-    """
-    for file in pkg_files.glob('*.py'):
-        text = file.read_text().replace('tomli.', '.')
-        text = text.replace('tomli', f'{new_root}.tomli')
-        file.write_text(text)
-
-
 def clean(vendor):
     """
     Remove all files out of the vendor directory except the meta
@@ -149,7 +139,6 @@ def update_setuptools():
     rewrite_importlib_resources(vendor / 'importlib_resources', 'setuptools.extern')
     rewrite_importlib_metadata(vendor / 'importlib_metadata', 'setuptools.extern')
     rewrite_more_itertools(vendor / "more_itertools")
-    rewrite_tomli(vendor / 'tomli', 'setuptools.extern')
 
 
 def install_validate_pyproject(vendor):
-- 
cgit v1.2.1


From 74c73411b24a32a8d030f4339ff8a96c3d26d6fc Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 9 Feb 2022 16:27:53 +0000
Subject: Improve custom vendoring logic for validate-pyproject

Co-authored-by: Sviatoslav Sydorenko 
---
 setuptools/_vendor/_validate_pyproject/NOTICE |  2 +-
 tools/vendored.py                             | 13 ++++++-------
 2 files changed, 7 insertions(+), 8 deletions(-)

diff --git a/setuptools/_vendor/_validate_pyproject/NOTICE b/setuptools/_vendor/_validate_pyproject/NOTICE
index 020083ac..003d646f 100644
--- a/setuptools/_vendor/_validate_pyproject/NOTICE
+++ b/setuptools/_vendor/_validate_pyproject/NOTICE
@@ -1,7 +1,7 @@
 The code contained in this directory was automatically generated using the
 following command:
 
-    python -m validate_pyproject.vendoring --output-dir setuptools/_vendor/_validate_pyproject --enable-plugins setuptools distutils --very-verbose
+    python -m validate_pyproject.vendoring --output-dir=setuptools/_vendor/_validate_pyproject --enable-plugins setuptools distutils --very-verbose
 
 Please avoid changing it manually.
 
diff --git a/tools/vendored.py b/tools/vendored.py
index 53185437..83ab200f 100644
--- a/tools/vendored.py
+++ b/tools/vendored.py
@@ -1,7 +1,5 @@
-import os
 import re
 import sys
-import shutil
 import string
 import subprocess
 import venv
@@ -156,16 +154,17 @@ def install_validate_pyproject(vendor):
         opts["ignore_cleanup_errors"] = True
 
     with TemporaryDirectory(**opts) as tmp:
-        venv.create(tmp, with_pip=True)
-        path = os.pathsep.join(Path(tmp).glob("*"))
-        venv_python = shutil.which("python", path=path)
+        env_builder = venv.EnvBuilder(with_pip=True)
+        env_builder.create(tmp)
+        context = env_builder.ensure_directories(tmp)
+        venv_python = getattr(context, 'env_exec_cmd', context.env_exe)
+
         subprocess.check_call([venv_python, "-m", "pip", "install", pkg])
         cmd = [
             venv_python,
             "-m",
             "validate_pyproject.vendoring",
-            "--output-dir",
-            str(vendor / "_validate_pyproject"),
+            f"--output-dir={vendor / '_validate_pyproject' !s}",
             "--enable-plugins",
             "setuptools",
             "distutils",
-- 
cgit v1.2.1


From e2f07dc092a08b5cbc445519fdc7bf3a049b3894 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 10 Feb 2022 19:59:00 +0000
Subject: Update vendored validate-pyproject to 0.4

---
 .../fastjsonschema_validations.py                  | 38 ++++++++++++----------
 setuptools/_vendor/_validate_pyproject/formats.py  |  2 --
 setuptools/_vendor/vendored.txt                    |  2 +-
 3 files changed, 21 insertions(+), 21 deletions(-)

diff --git a/setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py b/setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py
index 8bfd8809..e171c0d9 100644
--- a/setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py
+++ b/setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py
@@ -25,12 +25,12 @@ REGEX_PATTERNS = {
 NoneType = type(None)
 
 def validate(data, custom_formats={}):
-    validate_https___www_python_org_dev_peps_pep_0517(data, custom_formats)
+    validate_https___packaging_python_org_en_latest_specifications_declaring_build_dependencies(data, custom_formats)
     return data
 
-def validate_https___www_python_org_dev_peps_pep_0517(data, custom_formats={}):
+def validate_https___packaging_python_org_en_latest_specifications_declaring_build_dependencies(data, custom_formats={}):
     if not isinstance(data, (dict)):
-        raise JsonSchemaValueException("data must be object", value=data, name="data", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://www.python.org/dev/peps/pep-0517/', 'title': 'Data structure for ``pyproject.toml`` files', '$$description': [':pep:`517` defines a build-system independent format for source trees', 'while :pep:`518` provides a way of specifying the minimum system requirements', 'for Python projects.', 'Please notice the ``project`` table (as defined in  :pep:`621`) is not included', 'in this schema and should be considered separately.'], 'type': 'object', 'additionalProperties': False, 'properties': {'build-system': {'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, 'project': {'$ref': 'https://www.python.org/dev/peps/pep-0621/'}, 'tool': {'type': 'object', 'properties': {'distutils': {'$ref': 'https://docs.python.org/3/install/'}, 'setuptools': {'$ref': 'https://setuptools.pypa.io/en/latest/references/keywords.html'}}}}, 'project': {'$ref': 'https://www.python.org/dev/peps/pep-0621/'}}, rule='type')
+        raise JsonSchemaValueException("data must be object", value=data, name="data", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-build-dependencies/', 'title': 'Data structure for ``pyproject.toml`` files', '$$description': ['File format containing build-time configurations for the Python ecosystem. ', ':pep:`517` initially defined a build-system independent format for source trees', 'which was complemented by :pep:`518` to provide a way of specifying dependencies ', 'for building Python projects.', 'Please notice the ``project`` table (as initially defined in  :pep:`621`) is not included', 'in this schema and should be considered separately.'], 'type': 'object', 'additionalProperties': False, 'properties': {'build-system': {'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, 'project': {'$ref': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/'}, 'tool': {'type': 'object', 'properties': {'distutils': {'$ref': 'https://docs.python.org/3/install/'}, 'setuptools': {'$ref': 'https://setuptools.pypa.io/en/latest/references/keywords.html'}}}}, 'project': {'$ref': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/'}}, rule='type')
     data_is_dict = isinstance(data, dict)
     if data_is_dict:
         data_keys = set(data.keys())
@@ -80,7 +80,7 @@ def validate_https___www_python_org_dev_peps_pep_0517(data, custom_formats={}):
         if "project" in data_keys:
             data_keys.remove("project")
             data__project = data["project"]
-            validate_https___www_python_org_dev_peps_pep_0621(data__project, custom_formats)
+            validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata(data__project, custom_formats)
         if "tool" in data_keys:
             data_keys.remove("tool")
             data__tool = data["tool"]
@@ -98,7 +98,7 @@ def validate_https___www_python_org_dev_peps_pep_0517(data, custom_formats={}):
                     data__tool__setuptools = data__tool["setuptools"]
                     validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data__tool__setuptools, custom_formats)
         if data_keys:
-            raise JsonSchemaValueException("data must not contain "+str(data_keys)+" properties", value=data, name="data", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://www.python.org/dev/peps/pep-0517/', 'title': 'Data structure for ``pyproject.toml`` files', '$$description': [':pep:`517` defines a build-system independent format for source trees', 'while :pep:`518` provides a way of specifying the minimum system requirements', 'for Python projects.', 'Please notice the ``project`` table (as defined in  :pep:`621`) is not included', 'in this schema and should be considered separately.'], 'type': 'object', 'additionalProperties': False, 'properties': {'build-system': {'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, 'project': {'$ref': 'https://www.python.org/dev/peps/pep-0621/'}, 'tool': {'type': 'object', 'properties': {'distutils': {'$ref': 'https://docs.python.org/3/install/'}, 'setuptools': {'$ref': 'https://setuptools.pypa.io/en/latest/references/keywords.html'}}}}, 'project': {'$ref': 'https://www.python.org/dev/peps/pep-0621/'}}, rule='additionalProperties')
+            raise JsonSchemaValueException("data must not contain "+str(data_keys)+" properties", value=data, name="data", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-build-dependencies/', 'title': 'Data structure for ``pyproject.toml`` files', '$$description': ['File format containing build-time configurations for the Python ecosystem. ', ':pep:`517` initially defined a build-system independent format for source trees', 'which was complemented by :pep:`518` to provide a way of specifying dependencies ', 'for building Python projects.', 'Please notice the ``project`` table (as initially defined in  :pep:`621`) is not included', 'in this schema and should be considered separately.'], 'type': 'object', 'additionalProperties': False, 'properties': {'build-system': {'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, 'project': {'$ref': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/'}, 'tool': {'type': 'object', 'properties': {'distutils': {'$ref': 'https://docs.python.org/3/install/'}, 'setuptools': {'$ref': 'https://setuptools.pypa.io/en/latest/references/keywords.html'}}}}, 'project': {'$ref': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/'}}, rule='additionalProperties')
     return data
 
 def validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data, custom_formats={}):
@@ -621,14 +621,14 @@ def validate_https___docs_python_org_3_install(data, custom_formats={}):
                     raise JsonSchemaValueException(""+"data.{data_key}".format(**locals())+" must be object", value=data_val, name=""+"data.{data_key}".format(**locals())+"", definition={'type': 'object'}, rule='type')
     return data
 
-def validate_https___www_python_org_dev_peps_pep_0621(data, custom_formats={}):
+def validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata(data, custom_formats={}):
     if not isinstance(data, (dict)):
-        raise JsonSchemaValueException("data must be object", value=data, name="data", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://www.python.org/dev/peps/pep-0621/', 'title': '``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, rule='type')
+        raise JsonSchemaValueException("data must be object", value=data, name="data", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, rule='type')
     data_is_dict = isinstance(data, dict)
     if data_is_dict:
         data_len = len(data)
         if not all(prop in data for prop in ['name']):
-            raise JsonSchemaValueException("data must contain ['name'] properties", value=data, name="data", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://www.python.org/dev/peps/pep-0621/', 'title': '``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, rule='required')
+            raise JsonSchemaValueException("data must contain ['name'] properties", value=data, name="data", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, rule='required')
         data_keys = set(data.keys())
         if "name" in data_keys:
             data_keys.remove("name")
@@ -766,7 +766,7 @@ def validate_https___www_python_org_dev_peps_pep_0621(data, custom_formats={}):
             if data__authors_is_list:
                 data__authors_len = len(data__authors)
                 for data__authors_x, data__authors_item in enumerate(data__authors):
-                    validate_https___www_python_org_dev_peps_pep_0621___definitions_author(data__authors_item, custom_formats)
+                    validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_author(data__authors_item, custom_formats)
         if "maintainers" in data_keys:
             data_keys.remove("maintainers")
             data__maintainers = data["maintainers"]
@@ -776,7 +776,7 @@ def validate_https___www_python_org_dev_peps_pep_0621(data, custom_formats={}):
             if data__maintainers_is_list:
                 data__maintainers_len = len(data__maintainers)
                 for data__maintainers_x, data__maintainers_item in enumerate(data__maintainers):
-                    validate_https___www_python_org_dev_peps_pep_0621___definitions_author(data__maintainers_item, custom_formats)
+                    validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_author(data__maintainers_item, custom_formats)
         if "keywords" in data_keys:
             data_keys.remove("keywords")
             data__keywords = data["keywords"]
@@ -824,11 +824,11 @@ def validate_https___www_python_org_dev_peps_pep_0621(data, custom_formats={}):
         if "scripts" in data_keys:
             data_keys.remove("scripts")
             data__scripts = data["scripts"]
-            validate_https___www_python_org_dev_peps_pep_0621___definitions_entry_point_group(data__scripts, custom_formats)
+            validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_entry_point_group(data__scripts, custom_formats)
         if "gui-scripts" in data_keys:
             data_keys.remove("gui-scripts")
             data__guiscripts = data["gui-scripts"]
-            validate_https___www_python_org_dev_peps_pep_0621___definitions_entry_point_group(data__guiscripts, custom_formats)
+            validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_entry_point_group(data__guiscripts, custom_formats)
         if "entry-points" in data_keys:
             data_keys.remove("entry-points")
             data__entrypoints = data["entry-points"]
@@ -839,7 +839,7 @@ def validate_https___www_python_org_dev_peps_pep_0621(data, custom_formats={}):
                     if REGEX_PATTERNS['^.+$'].search(data__entrypoints_key):
                         if data__entrypoints_key in data__entrypoints_keys:
                             data__entrypoints_keys.remove(data__entrypoints_key)
-                        validate_https___www_python_org_dev_peps_pep_0621___definitions_entry_point_group(data__entrypoints_val, custom_formats)
+                        validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_entry_point_group(data__entrypoints_val, custom_formats)
                 if data__entrypoints_keys:
                     raise JsonSchemaValueException("data.entry-points must not contain "+str(data__entrypoints_keys)+" properties", value=data__entrypoints, name="data.entry-points", definition={'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, rule='additionalProperties')
                 data__entrypoints_len = len(data__entrypoints)
@@ -863,7 +863,7 @@ def validate_https___www_python_org_dev_peps_pep_0621(data, custom_formats={}):
             if data__dependencies_is_list:
                 data__dependencies_len = len(data__dependencies)
                 for data__dependencies_x, data__dependencies_item in enumerate(data__dependencies):
-                    validate_https___www_python_org_dev_peps_pep_0621___definitions_dependency(data__dependencies_item, custom_formats)
+                    validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_dependency(data__dependencies_item, custom_formats)
         if "optional-dependencies" in data_keys:
             data_keys.remove("optional-dependencies")
             data__optionaldependencies = data["optional-dependencies"]
@@ -882,7 +882,7 @@ def validate_https___www_python_org_dev_peps_pep_0621(data, custom_formats={}):
                         if data__optionaldependencies_val_is_list:
                             data__optionaldependencies_val_len = len(data__optionaldependencies_val)
                             for data__optionaldependencies_val_x, data__optionaldependencies_val_item in enumerate(data__optionaldependencies_val):
-                                validate_https___www_python_org_dev_peps_pep_0621___definitions_dependency(data__optionaldependencies_val_item, custom_formats)
+                                validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_dependency(data__optionaldependencies_val_item, custom_formats)
                 if data__optionaldependencies_keys:
                     raise JsonSchemaValueException("data.optional-dependencies must not contain "+str(data__optionaldependencies_keys)+" properties", value=data__optionaldependencies, name="data.optional-dependencies", definition={'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, rule='additionalProperties')
                 data__optionaldependencies_len = len(data__optionaldependencies)
@@ -908,6 +908,8 @@ def validate_https___www_python_org_dev_peps_pep_0621(data, custom_formats={}):
                 for data__dynamic_x, data__dynamic_item in enumerate(data__dynamic):
                     if data__dynamic_item not in ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']:
                         raise JsonSchemaValueException(""+"data.dynamic[{data__dynamic_x}]".format(**locals())+" must be one of ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']", value=data__dynamic_item, name=""+"data.dynamic[{data__dynamic_x}]".format(**locals())+"", definition={'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}, rule='enum')
+        if data_keys:
+            raise JsonSchemaValueException("data must not contain "+str(data_keys)+" properties", value=data, name="data", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, rule='additionalProperties')
     try:
         try:
             data_is_dict = isinstance(data, dict)
@@ -941,7 +943,7 @@ def validate_https___www_python_org_dev_peps_pep_0621(data, custom_formats={}):
                         raise JsonSchemaValueException("data.dynamic must contain one of contains definition", value=data__dynamic, name="data.dynamic", definition={'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}, rule='contains')
     return data
 
-def validate_https___www_python_org_dev_peps_pep_0621___definitions_dependency(data, custom_formats={}):
+def validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_dependency(data, custom_formats={}):
     if not isinstance(data, (str)):
         raise JsonSchemaValueException("data must be string", value=data, name="data", definition={'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}, rule='type')
     if isinstance(data, str):
@@ -949,7 +951,7 @@ def validate_https___www_python_org_dev_peps_pep_0621___definitions_dependency(d
             raise JsonSchemaValueException("data must be pep508", value=data, name="data", definition={'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}, rule='format')
     return data
 
-def validate_https___www_python_org_dev_peps_pep_0621___definitions_entry_point_group(data, custom_formats={}):
+def validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_entry_point_group(data, custom_formats={}):
     if not isinstance(data, (dict)):
         raise JsonSchemaValueException("data must be object", value=data, name="data", definition={'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, rule='type')
     data_is_dict = isinstance(data, dict)
@@ -980,7 +982,7 @@ def validate_https___www_python_org_dev_peps_pep_0621___definitions_entry_point_
                 raise JsonSchemaValueException("data must be named by propertyName definition", value=data, name="data", definition={'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, rule='propertyNames')
     return data
 
-def validate_https___www_python_org_dev_peps_pep_0621___definitions_author(data, custom_formats={}):
+def validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_author(data, custom_formats={}):
     if not isinstance(data, (dict)):
         raise JsonSchemaValueException("data must be object", value=data, name="data", definition={'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, rule='type')
     data_is_dict = isinstance(data, dict)
diff --git a/setuptools/_vendor/_validate_pyproject/formats.py b/setuptools/_vendor/_validate_pyproject/formats.py
index cc8566af..8ab8596c 100644
--- a/setuptools/_vendor/_validate_pyproject/formats.py
+++ b/setuptools/_vendor/_validate_pyproject/formats.py
@@ -72,7 +72,6 @@ try:
         except _req.InvalidRequirement:
             return False
 
-
 except ImportError:  # pragma: no cover
     _logger.warning(
         "Could not find an installation of `packaging`. Requirements, dependencies and "
@@ -116,7 +115,6 @@ try:
     def trove_classifier(value: str) -> bool:
         return value in _trove_classifiers
 
-
 except ImportError:  # pragma: no cover
 
     class _TroveClassifier:
diff --git a/setuptools/_vendor/vendored.txt b/setuptools/_vendor/vendored.txt
index 38d1f70f..1a71366d 100644
--- a/setuptools/_vendor/vendored.txt
+++ b/setuptools/_vendor/vendored.txt
@@ -10,4 +10,4 @@ typing_extensions==4.0.1
 # required for importlib_resources and _metadata on older Pythons
 zipp==3.7.0
 tomli==2.0.1
-# validate-pyproject[all]==0.3.2  # Special handling in tools/vendored, don't uncomment or remove
+# validate-pyproject[all]==0.4  # Special handling in tools/vendored, don't uncomment or remove
-- 
cgit v1.2.1


From 7f68bb4978fad697142dbdad716ad0dfbf850081 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 5 Mar 2022 10:07:32 +0000
Subject: Update vendored validate-pyproject to 0.5.2

---
 setuptools/_vendor/_validate_pyproject/NOTICE      |   2 +-
 setuptools/_vendor/_validate_pyproject/__init__.py |   7 +-
 .../_vendor/_validate_pyproject/error_reporting.py | 318 +++++++++++++++++
 .../_validate_pyproject/extra_validations.py       |   2 +-
 .../fastjsonschema_exceptions.py                   |   4 +-
 .../fastjsonschema_validations.py                  | 384 ++++++++++-----------
 setuptools/_vendor/_validate_pyproject/formats.py  |  90 ++++-
 setuptools/_vendor/vendored.txt                    |   2 +-
 8 files changed, 591 insertions(+), 218 deletions(-)
 create mode 100644 setuptools/_vendor/_validate_pyproject/error_reporting.py

diff --git a/setuptools/_vendor/_validate_pyproject/NOTICE b/setuptools/_vendor/_validate_pyproject/NOTICE
index 003d646f..fd64608b 100644
--- a/setuptools/_vendor/_validate_pyproject/NOTICE
+++ b/setuptools/_vendor/_validate_pyproject/NOTICE
@@ -20,7 +20,7 @@ The following files include code from opensource projects
 - `fastjsonschema_exceptions.py`:
     - project: `fastjsonschema` - licensed under BSD-3-Clause
       (https://github.com/horejsek/python-fastjsonschema)
-- `extra_validations.py` and `format.py`:
+- `extra_validations.py` and `format.py`, `error_reporting.py`:
     - project: `validate-pyproject` - licensed under MPL-2.0
       (https://github.com/abravalheri/validate-pyproject)
 
diff --git a/setuptools/_vendor/_validate_pyproject/__init__.py b/setuptools/_vendor/_validate_pyproject/__init__.py
index 2b1e77f3..dbe6cb4c 100644
--- a/setuptools/_vendor/_validate_pyproject/__init__.py
+++ b/setuptools/_vendor/_validate_pyproject/__init__.py
@@ -2,6 +2,7 @@ from functools import reduce
 from typing import Any, Callable, Dict
 
 from . import formats
+from .error_reporting import detailed_errors, ValidationError
 from .extra_validations import EXTRA_VALIDATIONS
 from .fastjsonschema_exceptions import JsonSchemaException, JsonSchemaValueException
 from .fastjsonschema_validations import validate as _validate
@@ -10,6 +11,7 @@ __all__ = [
     "validate",
     "FORMAT_FUNCTIONS",
     "EXTRA_VALIDATIONS",
+    "ValidationError",
     "JsonSchemaException",
     "JsonSchemaValueException",
 ]
@@ -24,8 +26,9 @@ FORMAT_FUNCTIONS: Dict[str, Callable[[str], bool]] = {
 
 def validate(data: Any) -> bool:
     """Validate the given ``data`` object using JSON Schema
-    This function raises ``JsonSchemaValueException`` if ``data`` is invalid.
+    This function raises ``ValidationError`` if ``data`` is invalid.
     """
-    _validate(data, custom_formats=FORMAT_FUNCTIONS)
+    with detailed_errors():
+        _validate(data, custom_formats=FORMAT_FUNCTIONS)
     reduce(lambda acc, fn: fn(acc), EXTRA_VALIDATIONS, data)
     return True
diff --git a/setuptools/_vendor/_validate_pyproject/error_reporting.py b/setuptools/_vendor/_validate_pyproject/error_reporting.py
new file mode 100644
index 00000000..3a4d4e9e
--- /dev/null
+++ b/setuptools/_vendor/_validate_pyproject/error_reporting.py
@@ -0,0 +1,318 @@
+import io
+import json
+import logging
+import os
+import re
+from contextlib import contextmanager
+from textwrap import indent, wrap
+from typing import Any, Dict, Iterator, List, Optional, Sequence, Union, cast
+
+from .fastjsonschema_exceptions import JsonSchemaValueException
+
+_logger = logging.getLogger(__name__)
+
+_MESSAGE_REPLACEMENTS = {
+    "must be named by propertyName definition": "keys must be named by",
+    "one of contains definition": "at least one item that matches",
+    " same as const definition:": "",
+    "only specified items": "only items matching the definition",
+}
+
+_SKIP_DETAILS = (
+    "must not be empty",
+    "is always invalid",
+    "must not be there",
+)
+
+_NEED_DETAILS = {"anyOf", "oneOf", "anyOf", "contains", "propertyNames", "not", "items"}
+
+_CAMEL_CASE_SPLITTER = re.compile(r"\W+|([A-Z][^A-Z\W]*)")
+_IDENTIFIER = re.compile(r"^[\w_]+$", re.I)
+
+_TOML_JARGON = {
+    "object": "table",
+    "property": "key",
+    "properties": "keys",
+    "property names": "keys",
+}
+
+
+class ValidationError(JsonSchemaValueException):
+    """Report violations of a given JSON schema.
+
+    This class extends :exc:`~fastjsonschema.JsonSchemaValueException`
+    by adding the following properties:
+
+    - ``summary``: an improved version of the ``JsonSchemaValueException`` error message
+      with only the necessary information)
+
+    - ``details``: more contextual information about the error like the failing schema
+      itself and the value that violates the schema.
+
+    Depending on the level of the verbosity of the ``logging`` configuration
+    the exception message will be only ``summary`` (default) or a combination of
+    ``summary`` and ``details`` (when the logging level is set to :obj:`logging.DEBUG`).
+    """
+
+    summary = ""
+    details = ""
+    _original_message = ""
+
+    @classmethod
+    def _from_jsonschema(cls, ex: JsonSchemaValueException):
+        formatter = _ErrorFormatting(ex)
+        obj = cls(str(formatter), ex.value, formatter.name, ex.definition, ex.rule)
+        debug_code = os.getenv("JSONSCHEMA_DEBUG_CODE_GENERATION", "false").lower()
+        if debug_code != "false":  # pragma: no cover
+            obj.__cause__, obj.__traceback__ = ex.__cause__, ex.__traceback__
+        obj._original_message = ex.message
+        obj.summary = formatter.summary
+        obj.details = formatter.details
+        return obj
+
+
+@contextmanager
+def detailed_errors():
+    try:
+        yield
+    except JsonSchemaValueException as ex:
+        raise ValidationError._from_jsonschema(ex) from None
+
+
+class _ErrorFormatting:
+    def __init__(self, ex: JsonSchemaValueException):
+        self.ex = ex
+        self.name = f"`{self._simplify_name(ex.name)}`"
+        self._original_message = self.ex.message.replace(ex.name, self.name)
+        self._summary = ""
+        self._details = ""
+
+    def __str__(self) -> str:
+        if _logger.getEffectiveLevel() <= logging.DEBUG and self.details:
+            return f"{self.summary}\n\n{self.details}"
+
+        return self.summary
+
+    @property
+    def summary(self) -> str:
+        if not self._summary:
+            self._summary = self._expand_summary()
+
+        return self._summary
+
+    @property
+    def details(self) -> str:
+        if not self._details:
+            self._details = self._expand_details()
+
+        return self._details
+
+    def _simplify_name(self, name):
+        x = len("data.")
+        return name[x:] if name.startswith("data.") else name
+
+    def _expand_summary(self):
+        msg = self._original_message
+
+        for bad, repl in _MESSAGE_REPLACEMENTS.items():
+            msg = msg.replace(bad, repl)
+
+        if any(substring in msg for substring in _SKIP_DETAILS):
+            return msg
+
+        schema = self.ex.rule_definition
+        if self.ex.rule in _NEED_DETAILS and schema:
+            summary = _SummaryWriter(_TOML_JARGON)
+            return f"{msg}:\n\n{indent(summary(schema), '    ')}"
+
+        return msg
+
+    def _expand_details(self) -> str:
+        optional = []
+        desc_lines = self.ex.definition.pop("$$description", [])
+        desc = self.ex.definition.pop("description", None) or " ".join(desc_lines)
+        if desc:
+            description = "\n".join(
+                wrap(
+                    desc,
+                    width=80,
+                    initial_indent="    ",
+                    subsequent_indent="    ",
+                    break_long_words=False,
+                )
+            )
+            optional.append(f"DESCRIPTION:\n{description}")
+        schema = json.dumps(self.ex.definition, indent=4)
+        value = json.dumps(self.ex.value, indent=4)
+        defaults = [
+            f"GIVEN VALUE:\n{indent(value, '    ')}",
+            f"OFFENDING RULE: {self.ex.rule!r}",
+            f"DEFINITION:\n{indent(schema, '    ')}",
+        ]
+        return "\n\n".join(optional + defaults)
+
+
+class _SummaryWriter:
+    _IGNORE = {"description", "default", "title", "examples"}
+
+    def __init__(self, jargon: Optional[Dict[str, str]] = None):
+        self.jargon: Dict[str, str] = jargon or {}
+        # Clarify confusing terms
+        self._terms = {
+            "anyOf": "at least one of the following",
+            "oneOf": "exactly one of the following",
+            "allOf": "all of the following",
+            "not": "(*NOT* the following)",
+            "prefixItems": f"{self._jargon('items')} (in order)",
+            "items": "items",
+            "contains": "contains at least one of",
+            "propertyNames": (
+                f"non-predefined acceptable {self._jargon('property names')}"
+            ),
+            "patternProperties": f"{self._jargon('properties')} named via pattern",
+            "const": "predefined value",
+            "enum": "one of",
+        }
+        # Attributes that indicate that the definition is easy and can be done
+        # inline (e.g. string and number)
+        self._guess_inline_defs = [
+            "enum",
+            "const",
+            "maxLength",
+            "minLength",
+            "pattern",
+            "format",
+            "minimum",
+            "maximum",
+            "exclusiveMinimum",
+            "exclusiveMaximum",
+            "multipleOf",
+        ]
+
+    def _jargon(self, term: Union[str, List[str]]) -> Union[str, List[str]]:
+        if isinstance(term, list):
+            return [self.jargon.get(t, t) for t in term]
+        return self.jargon.get(term, term)
+
+    def __call__(
+        self,
+        schema: Union[dict, List[dict]],
+        prefix: str = "",
+        *,
+        _path: Sequence[str] = (),
+    ) -> str:
+        if isinstance(schema, list):
+            return self._handle_list(schema, prefix, _path)
+
+        filtered = self._filter_unecessary(schema, _path)
+        simple = self._handle_simple_dict(filtered, _path)
+        if simple:
+            return f"{prefix}{simple}"
+
+        child_prefix = self._child_prefix(prefix, "  ")
+        item_prefix = self._child_prefix(prefix, "- ")
+        indent = len(prefix) * " "
+        with io.StringIO() as buffer:
+            for i, (key, value) in enumerate(filtered.items()):
+                child_path = [*_path, key]
+                line_prefix = prefix if i == 0 else indent
+                buffer.write(f"{line_prefix}{self._label(child_path)}:")
+                # ^  just the first item should receive the complete prefix
+                if isinstance(value, dict):
+                    filtered = self._filter_unecessary(value, child_path)
+                    simple = self._handle_simple_dict(filtered, child_path)
+                    buffer.write(
+                        f" {simple}"
+                        if simple
+                        else f"\n{self(value, child_prefix, _path=child_path)}"
+                    )
+                elif isinstance(value, list) and (
+                    key != "type" or self._is_property(child_path)
+                ):
+                    children = self._handle_list(value, item_prefix, child_path)
+                    sep = " " if children.startswith("[") else "\n"
+                    buffer.write(f"{sep}{children}")
+                else:
+                    buffer.write(f" {self._value(value, child_path)}\n")
+            return buffer.getvalue()
+
+    def _is_unecessary(self, path: Sequence[str]) -> bool:
+        if self._is_property(path) or not path:  # empty path => instruction @ root
+            return False
+        key = path[-1]
+        return any(key.startswith(k) for k in "$_") or key in self._IGNORE
+
+    def _filter_unecessary(self, schema: dict, path: Sequence[str]):
+        return {
+            key: value
+            for key, value in schema.items()
+            if not self._is_unecessary([*path, key])
+        }
+
+    def _handle_simple_dict(self, value: dict, path: Sequence[str]) -> Optional[str]:
+        inline = any(p in value for p in self._guess_inline_defs)
+        simple = not any(isinstance(v, (list, dict)) for v in value.values())
+        if inline or simple:
+            return f"{{{', '.join(self._inline_attrs(value, path))}}}\n"
+        return None
+
+    def _handle_list(
+        self, schemas: list, prefix: str = "", path: Sequence[str] = ()
+    ) -> str:
+        if self._is_unecessary(path):
+            return ""
+
+        repr_ = repr(schemas)
+        if all(not isinstance(e, (dict, list)) for e in schemas) and len(repr_) < 60:
+            return f"{repr_}\n"
+
+        item_prefix = self._child_prefix(prefix, "- ")
+        return "".join(
+            self(v, item_prefix, _path=[*path, f"[{i}]"]) for i, v in enumerate(schemas)
+        )
+
+    def _is_property(self, path: Sequence[str]):
+        """Check if the given path can correspond to an arbitrarily named property"""
+        counter = 0
+        for key in path[-2::-1]:
+            if key not in {"properties", "patternProperties"}:
+                break
+            counter += 1
+
+        # If the counter if even, the path correspond to a JSON Schema keyword
+        # otherwise it can be any arbitrary string naming a property
+        return counter % 2 == 1
+
+    def _label(self, path: Sequence[str]) -> str:
+        *parents, key = path
+        if not self._is_property(path):
+            norm_key = _separate_terms(key)
+            return self._terms.get(key) or " ".join(self._jargon(norm_key))
+
+        if parents[-1] == "patternProperties":
+            return f"(regex {key!r})"
+        return repr(key)  # property name
+
+    def _value(self, value: Any, path: Sequence[str]) -> str:
+        if path[-1] == "type" and not self._is_property(path):
+            type_ = self._jargon(value)
+            return (
+                f"[{', '.join(type_)}]" if isinstance(value, list) else cast(str, type_)
+            )
+        return repr(value)
+
+    def _inline_attrs(self, schema: dict, path: Sequence[str]) -> Iterator[str]:
+        for key, value in schema.items():
+            child_path = [*path, key]
+            yield f"{self._label(child_path)}: {self._value(value, child_path)}"
+
+    def _child_prefix(self, parent_prefix: str, child_prefix: str) -> str:
+        return len(parent_prefix) * " " + child_prefix
+
+
+def _separate_terms(word: str) -> List[str]:
+    """
+    >>> _separate_terms("FooBar-foo")
+    "foo bar foo"
+    """
+    return [w.lower() for w in _CAMEL_CASE_SPLITTER.split(word) if w]
diff --git a/setuptools/_vendor/_validate_pyproject/extra_validations.py b/setuptools/_vendor/_validate_pyproject/extra_validations.py
index d7d5b39d..48c4e257 100644
--- a/setuptools/_vendor/_validate_pyproject/extra_validations.py
+++ b/setuptools/_vendor/_validate_pyproject/extra_validations.py
@@ -24,7 +24,7 @@ def validate_project_dynamic(pyproject: T) -> T:
 
     for field in dynamic:
         if field in project_table:
-            msg = f"You cannot provided a value for `project.{field}` and "
+            msg = f"You cannot provide a value for `project.{field}` and "
             msg += "list it under `project.dynamic` at the same time"
             name = f"data.project.{field}"
             value = {field: project_table[field], "...": " # ...", "dynamic": dynamic}
diff --git a/setuptools/_vendor/_validate_pyproject/fastjsonschema_exceptions.py b/setuptools/_vendor/_validate_pyproject/fastjsonschema_exceptions.py
index 63d98199..d2dddd6a 100644
--- a/setuptools/_vendor/_validate_pyproject/fastjsonschema_exceptions.py
+++ b/setuptools/_vendor/_validate_pyproject/fastjsonschema_exceptions.py
@@ -16,8 +16,8 @@ class JsonSchemaValueException(JsonSchemaException):
 
      * ``message`` containing human-readable information what is wrong (e.g. ``data.property[index] must be smaller than or equal to 42``),
      * invalid ``value`` (e.g. ``60``),
-     * ``name`` of a path in the data structure (e.g. ``data.propery[index]``),
-     * ``path`` as an array in the data structure (e.g. ``['data', 'propery', 'index']``),
+     * ``name`` of a path in the data structure (e.g. ``data.property[index]``),
+     * ``path`` as an array in the data structure (e.g. ``['data', 'property', 'index']``),
      * the whole ``definition`` which the ``value`` has to fulfil (e.g. ``{'type': 'number', 'maximum': 42}``),
      * ``rule`` which the ``value`` is breaking (e.g. ``maximum``)
      * and ``rule_definition`` (e.g. ``42``).
diff --git a/setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py b/setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py
index e171c0d9..556e6fed 100644
--- a/setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py
+++ b/setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py
@@ -24,13 +24,13 @@ REGEX_PATTERNS = {
 
 NoneType = type(None)
 
-def validate(data, custom_formats={}):
-    validate_https___packaging_python_org_en_latest_specifications_declaring_build_dependencies(data, custom_formats)
+def validate(data, custom_formats={}, name_prefix=None):
+    validate_https___packaging_python_org_en_latest_specifications_declaring_build_dependencies(data, custom_formats, (name_prefix or "data") + "")
     return data
 
-def validate_https___packaging_python_org_en_latest_specifications_declaring_build_dependencies(data, custom_formats={}):
+def validate_https___packaging_python_org_en_latest_specifications_declaring_build_dependencies(data, custom_formats={}, name_prefix=None):
     if not isinstance(data, (dict)):
-        raise JsonSchemaValueException("data must be object", value=data, name="data", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-build-dependencies/', 'title': 'Data structure for ``pyproject.toml`` files', '$$description': ['File format containing build-time configurations for the Python ecosystem. ', ':pep:`517` initially defined a build-system independent format for source trees', 'which was complemented by :pep:`518` to provide a way of specifying dependencies ', 'for building Python projects.', 'Please notice the ``project`` table (as initially defined in  :pep:`621`) is not included', 'in this schema and should be considered separately.'], 'type': 'object', 'additionalProperties': False, 'properties': {'build-system': {'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, 'project': {'$ref': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/'}, 'tool': {'type': 'object', 'properties': {'distutils': {'$ref': 'https://docs.python.org/3/install/'}, 'setuptools': {'$ref': 'https://setuptools.pypa.io/en/latest/references/keywords.html'}}}}, 'project': {'$ref': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/'}}, rule='type')
+        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-build-dependencies/', 'title': 'Data structure for ``pyproject.toml`` files', '$$description': ['File format containing build-time configurations for the Python ecosystem. ', ':pep:`517` initially defined a build-system independent format for source trees', 'which was complemented by :pep:`518` to provide a way of specifying dependencies ', 'for building Python projects.', 'Please notice the ``project`` table (as initially defined in  :pep:`621`) is not included', 'in this schema and should be considered separately.'], 'type': 'object', 'additionalProperties': False, 'properties': {'build-system': {'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, 'tool': {'type': 'object', 'properties': {'distutils': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://docs.python.org/3/install/', 'title': '``tool.distutils`` table', '$$description': ['Originally, ``distutils`` allowed developers to configure arguments for', '``setup.py`` scripts via `distutils configuration files', '`_.', '``tool.distutils`` subtables could be used with the same purpose', '(NOT CURRENTLY IMPLEMENTED).'], 'type': 'object', 'properties': {'global': {'type': 'object', 'description': 'Global options applied to all ``distutils`` commands'}}, 'patternProperties': {'.+': {'type': 'object'}}, '$comment': 'TODO: Is there a practical way of making this schema more specific?'}, 'setuptools': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$ref': '#/definitions/find-directive'}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'$ref': '#/definitions/attr-directive'}, {'$ref': '#/definitions/file-directive'}]}, 'classifiers': {'$ref': '#/definitions/file-directive'}, 'description': {'$ref': '#/definitions/file-directive'}, 'entry-points': {'$ref': '#/definitions/file-directive'}, 'readme': {'anyOf': [{'$ref': '#/definitions/file-directive'}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}, 'license': {'type': 'string', '$$description': ['PROVISIONAL: A string specifying the license of the package', '(might change with PEP 639)'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-expression``?'}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might change with PEP 639)'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-files.glob``?'}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}}}}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, rule='type')
     data_is_dict = isinstance(data, dict)
     if data_is_dict:
         data_keys = set(data.keys())
@@ -38,72 +38,72 @@ def validate_https___packaging_python_org_en_latest_specifications_declaring_bui
             data_keys.remove("build-system")
             data__buildsystem = data["build-system"]
             if not isinstance(data__buildsystem, (dict)):
-                raise JsonSchemaValueException("data.build-system must be object", value=data__buildsystem, name="data.build-system", definition={'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".build-system must be object", value=data__buildsystem, name="" + (name_prefix or "data") + ".build-system", definition={'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, rule='type')
             data__buildsystem_is_dict = isinstance(data__buildsystem, dict)
             if data__buildsystem_is_dict:
                 data__buildsystem_len = len(data__buildsystem)
                 if not all(prop in data__buildsystem for prop in ['requires']):
-                    raise JsonSchemaValueException("data.build-system must contain ['requires'] properties", value=data__buildsystem, name="data.build-system", definition={'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, rule='required')
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".build-system must contain ['requires'] properties", value=data__buildsystem, name="" + (name_prefix or "data") + ".build-system", definition={'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, rule='required')
                 data__buildsystem_keys = set(data__buildsystem.keys())
                 if "requires" in data__buildsystem_keys:
                     data__buildsystem_keys.remove("requires")
                     data__buildsystem__requires = data__buildsystem["requires"]
                     if not isinstance(data__buildsystem__requires, (list, tuple)):
-                        raise JsonSchemaValueException("data.build-system.requires must be array", value=data__buildsystem__requires, name="data.build-system.requires", definition={'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, rule='type')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".build-system.requires must be array", value=data__buildsystem__requires, name="" + (name_prefix or "data") + ".build-system.requires", definition={'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, rule='type')
                     data__buildsystem__requires_is_list = isinstance(data__buildsystem__requires, (list, tuple))
                     if data__buildsystem__requires_is_list:
                         data__buildsystem__requires_len = len(data__buildsystem__requires)
                         for data__buildsystem__requires_x, data__buildsystem__requires_item in enumerate(data__buildsystem__requires):
                             if not isinstance(data__buildsystem__requires_item, (str)):
-                                raise JsonSchemaValueException(""+"data.build-system.requires[{data__buildsystem__requires_x}]".format(**locals())+" must be string", value=data__buildsystem__requires_item, name=""+"data.build-system.requires[{data__buildsystem__requires_x}]".format(**locals())+"", definition={'type': 'string'}, rule='type')
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".build-system.requires[{data__buildsystem__requires_x}]".format(**locals()) + " must be string", value=data__buildsystem__requires_item, name="" + (name_prefix or "data") + ".build-system.requires[{data__buildsystem__requires_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
                 if "build-backend" in data__buildsystem_keys:
                     data__buildsystem_keys.remove("build-backend")
                     data__buildsystem__buildbackend = data__buildsystem["build-backend"]
                     if not isinstance(data__buildsystem__buildbackend, (str)):
-                        raise JsonSchemaValueException("data.build-system.build-backend must be string", value=data__buildsystem__buildbackend, name="data.build-system.build-backend", definition={'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, rule='type')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".build-system.build-backend must be string", value=data__buildsystem__buildbackend, name="" + (name_prefix or "data") + ".build-system.build-backend", definition={'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, rule='type')
                     if isinstance(data__buildsystem__buildbackend, str):
                         if not custom_formats["pep517-backend-reference"](data__buildsystem__buildbackend):
-                            raise JsonSchemaValueException("data.build-system.build-backend must be pep517-backend-reference", value=data__buildsystem__buildbackend, name="data.build-system.build-backend", definition={'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, rule='format')
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".build-system.build-backend must be pep517-backend-reference", value=data__buildsystem__buildbackend, name="" + (name_prefix or "data") + ".build-system.build-backend", definition={'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, rule='format')
                 if "backend-path" in data__buildsystem_keys:
                     data__buildsystem_keys.remove("backend-path")
                     data__buildsystem__backendpath = data__buildsystem["backend-path"]
                     if not isinstance(data__buildsystem__backendpath, (list, tuple)):
-                        raise JsonSchemaValueException("data.build-system.backend-path must be array", value=data__buildsystem__backendpath, name="data.build-system.backend-path", definition={'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}, rule='type')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".build-system.backend-path must be array", value=data__buildsystem__backendpath, name="" + (name_prefix or "data") + ".build-system.backend-path", definition={'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}, rule='type')
                     data__buildsystem__backendpath_is_list = isinstance(data__buildsystem__backendpath, (list, tuple))
                     if data__buildsystem__backendpath_is_list:
                         data__buildsystem__backendpath_len = len(data__buildsystem__backendpath)
                         for data__buildsystem__backendpath_x, data__buildsystem__backendpath_item in enumerate(data__buildsystem__backendpath):
                             if not isinstance(data__buildsystem__backendpath_item, (str)):
-                                raise JsonSchemaValueException(""+"data.build-system.backend-path[{data__buildsystem__backendpath_x}]".format(**locals())+" must be string", value=data__buildsystem__backendpath_item, name=""+"data.build-system.backend-path[{data__buildsystem__backendpath_x}]".format(**locals())+"", definition={'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}, rule='type')
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".build-system.backend-path[{data__buildsystem__backendpath_x}]".format(**locals()) + " must be string", value=data__buildsystem__backendpath_item, name="" + (name_prefix or "data") + ".build-system.backend-path[{data__buildsystem__backendpath_x}]".format(**locals()) + "", definition={'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}, rule='type')
                 if data__buildsystem_keys:
-                    raise JsonSchemaValueException("data.build-system must not contain "+str(data__buildsystem_keys)+" properties", value=data__buildsystem, name="data.build-system", definition={'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, rule='additionalProperties')
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".build-system must not contain "+str(data__buildsystem_keys)+" properties", value=data__buildsystem, name="" + (name_prefix or "data") + ".build-system", definition={'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, rule='additionalProperties')
         if "project" in data_keys:
             data_keys.remove("project")
             data__project = data["project"]
-            validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata(data__project, custom_formats)
+            validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata(data__project, custom_formats, (name_prefix or "data") + ".project")
         if "tool" in data_keys:
             data_keys.remove("tool")
             data__tool = data["tool"]
             if not isinstance(data__tool, (dict)):
-                raise JsonSchemaValueException("data.tool must be object", value=data__tool, name="data.tool", definition={'type': 'object', 'properties': {'distutils': {'$ref': 'https://docs.python.org/3/install/'}, 'setuptools': {'$ref': 'https://setuptools.pypa.io/en/latest/references/keywords.html'}}}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".tool must be object", value=data__tool, name="" + (name_prefix or "data") + ".tool", definition={'type': 'object', 'properties': {'distutils': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://docs.python.org/3/install/', 'title': '``tool.distutils`` table', '$$description': ['Originally, ``distutils`` allowed developers to configure arguments for', '``setup.py`` scripts via `distutils configuration files', '`_.', '``tool.distutils`` subtables could be used with the same purpose', '(NOT CURRENTLY IMPLEMENTED).'], 'type': 'object', 'properties': {'global': {'type': 'object', 'description': 'Global options applied to all ``distutils`` commands'}}, 'patternProperties': {'.+': {'type': 'object'}}, '$comment': 'TODO: Is there a practical way of making this schema more specific?'}, 'setuptools': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$ref': '#/definitions/find-directive'}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'$ref': '#/definitions/attr-directive'}, {'$ref': '#/definitions/file-directive'}]}, 'classifiers': {'$ref': '#/definitions/file-directive'}, 'description': {'$ref': '#/definitions/file-directive'}, 'entry-points': {'$ref': '#/definitions/file-directive'}, 'readme': {'anyOf': [{'$ref': '#/definitions/file-directive'}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}, 'license': {'type': 'string', '$$description': ['PROVISIONAL: A string specifying the license of the package', '(might change with PEP 639)'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-expression``?'}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might change with PEP 639)'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-files.glob``?'}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}}}, rule='type')
             data__tool_is_dict = isinstance(data__tool, dict)
             if data__tool_is_dict:
                 data__tool_keys = set(data__tool.keys())
                 if "distutils" in data__tool_keys:
                     data__tool_keys.remove("distutils")
                     data__tool__distutils = data__tool["distutils"]
-                    validate_https___docs_python_org_3_install(data__tool__distutils, custom_formats)
+                    validate_https___docs_python_org_3_install(data__tool__distutils, custom_formats, (name_prefix or "data") + ".tool.distutils")
                 if "setuptools" in data__tool_keys:
                     data__tool_keys.remove("setuptools")
                     data__tool__setuptools = data__tool["setuptools"]
-                    validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data__tool__setuptools, custom_formats)
+                    validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data__tool__setuptools, custom_formats, (name_prefix or "data") + ".tool.setuptools")
         if data_keys:
-            raise JsonSchemaValueException("data must not contain "+str(data_keys)+" properties", value=data, name="data", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-build-dependencies/', 'title': 'Data structure for ``pyproject.toml`` files', '$$description': ['File format containing build-time configurations for the Python ecosystem. ', ':pep:`517` initially defined a build-system independent format for source trees', 'which was complemented by :pep:`518` to provide a way of specifying dependencies ', 'for building Python projects.', 'Please notice the ``project`` table (as initially defined in  :pep:`621`) is not included', 'in this schema and should be considered separately.'], 'type': 'object', 'additionalProperties': False, 'properties': {'build-system': {'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, 'project': {'$ref': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/'}, 'tool': {'type': 'object', 'properties': {'distutils': {'$ref': 'https://docs.python.org/3/install/'}, 'setuptools': {'$ref': 'https://setuptools.pypa.io/en/latest/references/keywords.html'}}}}, 'project': {'$ref': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/'}}, rule='additionalProperties')
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must not contain "+str(data_keys)+" properties", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-build-dependencies/', 'title': 'Data structure for ``pyproject.toml`` files', '$$description': ['File format containing build-time configurations for the Python ecosystem. ', ':pep:`517` initially defined a build-system independent format for source trees', 'which was complemented by :pep:`518` to provide a way of specifying dependencies ', 'for building Python projects.', 'Please notice the ``project`` table (as initially defined in  :pep:`621`) is not included', 'in this schema and should be considered separately.'], 'type': 'object', 'additionalProperties': False, 'properties': {'build-system': {'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, 'tool': {'type': 'object', 'properties': {'distutils': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://docs.python.org/3/install/', 'title': '``tool.distutils`` table', '$$description': ['Originally, ``distutils`` allowed developers to configure arguments for', '``setup.py`` scripts via `distutils configuration files', '`_.', '``tool.distutils`` subtables could be used with the same purpose', '(NOT CURRENTLY IMPLEMENTED).'], 'type': 'object', 'properties': {'global': {'type': 'object', 'description': 'Global options applied to all ``distutils`` commands'}}, 'patternProperties': {'.+': {'type': 'object'}}, '$comment': 'TODO: Is there a practical way of making this schema more specific?'}, 'setuptools': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$ref': '#/definitions/find-directive'}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'$ref': '#/definitions/attr-directive'}, {'$ref': '#/definitions/file-directive'}]}, 'classifiers': {'$ref': '#/definitions/file-directive'}, 'description': {'$ref': '#/definitions/file-directive'}, 'entry-points': {'$ref': '#/definitions/file-directive'}, 'readme': {'anyOf': [{'$ref': '#/definitions/file-directive'}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}, 'license': {'type': 'string', '$$description': ['PROVISIONAL: A string specifying the license of the package', '(might change with PEP 639)'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-expression``?'}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might change with PEP 639)'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-files.glob``?'}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}}}}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, rule='additionalProperties')
     return data
 
-def validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data, custom_formats={}):
+def validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data, custom_formats={}, name_prefix=None):
     if not isinstance(data, (dict)):
-        raise JsonSchemaValueException("data must be object", value=data, name="data", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$ref': '#/definitions/find-directive'}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'$ref': '#/definitions/attr-directive'}, {'$ref': '#/definitions/file-directive'}]}, 'classifiers': {'$ref': '#/definitions/file-directive'}, 'description': {'$ref': '#/definitions/file-directive'}, 'entry-points': {'$ref': '#/definitions/file-directive'}, 'readme': {'anyOf': [{'$ref': '#/definitions/file-directive'}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}, 'license': {'type': 'string', '$$description': ['PROVISIONAL: A string specifying the license of the package', '(might change with PEP 639)'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-expression``?'}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might change with PEP 639)'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-files.glob``?'}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}, rule='type')
+        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}]}, 'classifiers': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'description': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'entry-points': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'readme': {'anyOf': [{'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}, 'license': {'type': 'string', '$$description': ['PROVISIONAL: A string specifying the license of the package', '(might change with PEP 639)'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-expression``?'}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might change with PEP 639)'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-files.glob``?'}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}, rule='type')
     data_is_dict = isinstance(data, dict)
     if data_is_dict:
         data_keys = set(data.keys())
@@ -111,68 +111,68 @@ def validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data,
             data_keys.remove("platforms")
             data__platforms = data["platforms"]
             if not isinstance(data__platforms, (list, tuple)):
-                raise JsonSchemaValueException("data.platforms must be array", value=data__platforms, name="data.platforms", definition={'type': 'array', 'items': {'type': 'string'}}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".platforms must be array", value=data__platforms, name="" + (name_prefix or "data") + ".platforms", definition={'type': 'array', 'items': {'type': 'string'}}, rule='type')
             data__platforms_is_list = isinstance(data__platforms, (list, tuple))
             if data__platforms_is_list:
                 data__platforms_len = len(data__platforms)
                 for data__platforms_x, data__platforms_item in enumerate(data__platforms):
                     if not isinstance(data__platforms_item, (str)):
-                        raise JsonSchemaValueException(""+"data.platforms[{data__platforms_x}]".format(**locals())+" must be string", value=data__platforms_item, name=""+"data.platforms[{data__platforms_x}]".format(**locals())+"", definition={'type': 'string'}, rule='type')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".platforms[{data__platforms_x}]".format(**locals()) + " must be string", value=data__platforms_item, name="" + (name_prefix or "data") + ".platforms[{data__platforms_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
         if "provides" in data_keys:
             data_keys.remove("provides")
             data__provides = data["provides"]
             if not isinstance(data__provides, (list, tuple)):
-                raise JsonSchemaValueException("data.provides must be array", value=data__provides, name="data.provides", definition={'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".provides must be array", value=data__provides, name="" + (name_prefix or "data") + ".provides", definition={'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, rule='type')
             data__provides_is_list = isinstance(data__provides, (list, tuple))
             if data__provides_is_list:
                 data__provides_len = len(data__provides)
                 for data__provides_x, data__provides_item in enumerate(data__provides):
                     if not isinstance(data__provides_item, (str)):
-                        raise JsonSchemaValueException(""+"data.provides[{data__provides_x}]".format(**locals())+" must be string", value=data__provides_item, name=""+"data.provides[{data__provides_x}]".format(**locals())+"", definition={'type': 'string', 'format': 'pep508-identifier'}, rule='type')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".provides[{data__provides_x}]".format(**locals()) + " must be string", value=data__provides_item, name="" + (name_prefix or "data") + ".provides[{data__provides_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'pep508-identifier'}, rule='type')
                     if isinstance(data__provides_item, str):
                         if not custom_formats["pep508-identifier"](data__provides_item):
-                            raise JsonSchemaValueException(""+"data.provides[{data__provides_x}]".format(**locals())+" must be pep508-identifier", value=data__provides_item, name=""+"data.provides[{data__provides_x}]".format(**locals())+"", definition={'type': 'string', 'format': 'pep508-identifier'}, rule='format')
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".provides[{data__provides_x}]".format(**locals()) + " must be pep508-identifier", value=data__provides_item, name="" + (name_prefix or "data") + ".provides[{data__provides_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'pep508-identifier'}, rule='format')
         if "obsoletes" in data_keys:
             data_keys.remove("obsoletes")
             data__obsoletes = data["obsoletes"]
             if not isinstance(data__obsoletes, (list, tuple)):
-                raise JsonSchemaValueException("data.obsoletes must be array", value=data__obsoletes, name="data.obsoletes", definition={'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".obsoletes must be array", value=data__obsoletes, name="" + (name_prefix or "data") + ".obsoletes", definition={'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, rule='type')
             data__obsoletes_is_list = isinstance(data__obsoletes, (list, tuple))
             if data__obsoletes_is_list:
                 data__obsoletes_len = len(data__obsoletes)
                 for data__obsoletes_x, data__obsoletes_item in enumerate(data__obsoletes):
                     if not isinstance(data__obsoletes_item, (str)):
-                        raise JsonSchemaValueException(""+"data.obsoletes[{data__obsoletes_x}]".format(**locals())+" must be string", value=data__obsoletes_item, name=""+"data.obsoletes[{data__obsoletes_x}]".format(**locals())+"", definition={'type': 'string', 'format': 'pep508-identifier'}, rule='type')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".obsoletes[{data__obsoletes_x}]".format(**locals()) + " must be string", value=data__obsoletes_item, name="" + (name_prefix or "data") + ".obsoletes[{data__obsoletes_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'pep508-identifier'}, rule='type')
                     if isinstance(data__obsoletes_item, str):
                         if not custom_formats["pep508-identifier"](data__obsoletes_item):
-                            raise JsonSchemaValueException(""+"data.obsoletes[{data__obsoletes_x}]".format(**locals())+" must be pep508-identifier", value=data__obsoletes_item, name=""+"data.obsoletes[{data__obsoletes_x}]".format(**locals())+"", definition={'type': 'string', 'format': 'pep508-identifier'}, rule='format')
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".obsoletes[{data__obsoletes_x}]".format(**locals()) + " must be pep508-identifier", value=data__obsoletes_item, name="" + (name_prefix or "data") + ".obsoletes[{data__obsoletes_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'pep508-identifier'}, rule='format')
         if "zip-safe" in data_keys:
             data_keys.remove("zip-safe")
             data__zipsafe = data["zip-safe"]
             if not isinstance(data__zipsafe, (bool)):
-                raise JsonSchemaValueException("data.zip-safe must be boolean", value=data__zipsafe, name="data.zip-safe", definition={'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".zip-safe must be boolean", value=data__zipsafe, name="" + (name_prefix or "data") + ".zip-safe", definition={'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, rule='type')
         if "script-files" in data_keys:
             data_keys.remove("script-files")
             data__scriptfiles = data["script-files"]
             if not isinstance(data__scriptfiles, (list, tuple)):
-                raise JsonSchemaValueException("data.script-files must be array", value=data__scriptfiles, name="data.script-files", definition={'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".script-files must be array", value=data__scriptfiles, name="" + (name_prefix or "data") + ".script-files", definition={'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, rule='type')
             data__scriptfiles_is_list = isinstance(data__scriptfiles, (list, tuple))
             if data__scriptfiles_is_list:
                 data__scriptfiles_len = len(data__scriptfiles)
                 for data__scriptfiles_x, data__scriptfiles_item in enumerate(data__scriptfiles):
                     if not isinstance(data__scriptfiles_item, (str)):
-                        raise JsonSchemaValueException(""+"data.script-files[{data__scriptfiles_x}]".format(**locals())+" must be string", value=data__scriptfiles_item, name=""+"data.script-files[{data__scriptfiles_x}]".format(**locals())+"", definition={'type': 'string'}, rule='type')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".script-files[{data__scriptfiles_x}]".format(**locals()) + " must be string", value=data__scriptfiles_item, name="" + (name_prefix or "data") + ".script-files[{data__scriptfiles_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
         if "eager-resources" in data_keys:
             data_keys.remove("eager-resources")
             data__eagerresources = data["eager-resources"]
             if not isinstance(data__eagerresources, (list, tuple)):
-                raise JsonSchemaValueException("data.eager-resources must be array", value=data__eagerresources, name="data.eager-resources", definition={'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".eager-resources must be array", value=data__eagerresources, name="" + (name_prefix or "data") + ".eager-resources", definition={'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, rule='type')
             data__eagerresources_is_list = isinstance(data__eagerresources, (list, tuple))
             if data__eagerresources_is_list:
                 data__eagerresources_len = len(data__eagerresources)
                 for data__eagerresources_x, data__eagerresources_item in enumerate(data__eagerresources):
                     if not isinstance(data__eagerresources_item, (str)):
-                        raise JsonSchemaValueException(""+"data.eager-resources[{data__eagerresources_x}]".format(**locals())+" must be string", value=data__eagerresources_item, name=""+"data.eager-resources[{data__eagerresources_x}]".format(**locals())+"", definition={'type': 'string'}, rule='type')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".eager-resources[{data__eagerresources_x}]".format(**locals()) + " must be string", value=data__eagerresources_item, name="" + (name_prefix or "data") + ".eager-resources[{data__eagerresources_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
         if "packages" in data_keys:
             data_keys.remove("packages")
             data__packages = data["packages"]
@@ -180,30 +180,30 @@ def validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data,
             if data__packages_one_of_count1 < 2:
                 try:
                     if not isinstance(data__packages, (list, tuple)):
-                        raise JsonSchemaValueException("data.packages must be array", value=data__packages, name="data.packages", definition={'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, rule='type')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".packages must be array", value=data__packages, name="" + (name_prefix or "data") + ".packages", definition={'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, rule='type')
                     data__packages_is_list = isinstance(data__packages, (list, tuple))
                     if data__packages_is_list:
                         data__packages_len = len(data__packages)
                         for data__packages_x, data__packages_item in enumerate(data__packages):
                             if not isinstance(data__packages_item, (str)):
-                                raise JsonSchemaValueException(""+"data.packages[{data__packages_x}]".format(**locals())+" must be string", value=data__packages_item, name=""+"data.packages[{data__packages_x}]".format(**locals())+"", definition={'type': 'string', 'format': 'python-module-name'}, rule='type')
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".packages[{data__packages_x}]".format(**locals()) + " must be string", value=data__packages_item, name="" + (name_prefix or "data") + ".packages[{data__packages_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'python-module-name'}, rule='type')
                             if isinstance(data__packages_item, str):
                                 if not custom_formats["python-module-name"](data__packages_item):
-                                    raise JsonSchemaValueException(""+"data.packages[{data__packages_x}]".format(**locals())+" must be python-module-name", value=data__packages_item, name=""+"data.packages[{data__packages_x}]".format(**locals())+"", definition={'type': 'string', 'format': 'python-module-name'}, rule='format')
+                                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".packages[{data__packages_x}]".format(**locals()) + " must be python-module-name", value=data__packages_item, name="" + (name_prefix or "data") + ".packages[{data__packages_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'python-module-name'}, rule='format')
                     data__packages_one_of_count1 += 1
                 except JsonSchemaValueException: pass
             if data__packages_one_of_count1 < 2:
                 try:
-                    validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_find_directive(data__packages, custom_formats)
+                    validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_find_directive(data__packages, custom_formats, (name_prefix or "data") + ".packages")
                     data__packages_one_of_count1 += 1
                 except JsonSchemaValueException: pass
             if data__packages_one_of_count1 != 1:
-                raise JsonSchemaValueException("data.packages must be valid exactly by one of oneOf definition", value=data__packages, name="data.packages", definition={'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$ref': '#/definitions/find-directive'}]}, rule='oneOf')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".packages must be valid exactly by one definition" + (" (" + str(data__packages_one_of_count1) + " matches found)"), value=data__packages, name="" + (name_prefix or "data") + ".packages", definition={'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}]}, rule='oneOf')
         if "package-dir" in data_keys:
             data_keys.remove("package-dir")
             data__packagedir = data["package-dir"]
             if not isinstance(data__packagedir, (dict)):
-                raise JsonSchemaValueException("data.package-dir must be object", value=data__packagedir, name="data.package-dir", definition={'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-dir must be object", value=data__packagedir, name="" + (name_prefix or "data") + ".package-dir", definition={'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, rule='type')
             data__packagedir_is_dict = isinstance(data__packagedir, dict)
             if data__packagedir_is_dict:
                 data__packagedir_keys = set(data__packagedir.keys())
@@ -212,9 +212,9 @@ def validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data,
                         if data__packagedir_key in data__packagedir_keys:
                             data__packagedir_keys.remove(data__packagedir_key)
                         if not isinstance(data__packagedir_val, (str)):
-                            raise JsonSchemaValueException(""+"data.package-dir.{data__packagedir_key}".format(**locals())+" must be string", value=data__packagedir_val, name=""+"data.package-dir.{data__packagedir_key}".format(**locals())+"", definition={'type': 'string'}, rule='type')
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-dir.{data__packagedir_key}".format(**locals()) + " must be string", value=data__packagedir_val, name="" + (name_prefix or "data") + ".package-dir.{data__packagedir_key}".format(**locals()) + "", definition={'type': 'string'}, rule='type')
                 if data__packagedir_keys:
-                    raise JsonSchemaValueException("data.package-dir must not contain "+str(data__packagedir_keys)+" properties", value=data__packagedir, name="data.package-dir", definition={'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, rule='additionalProperties')
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-dir must not contain "+str(data__packagedir_keys)+" properties", value=data__packagedir, name="" + (name_prefix or "data") + ".package-dir", definition={'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, rule='additionalProperties')
                 data__packagedir_len = len(data__packagedir)
                 if data__packagedir_len != 0:
                     data__packagedir_property_names = True
@@ -225,26 +225,26 @@ def validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data,
                                 try:
                                     if isinstance(data__packagedir_key, str):
                                         if not custom_formats["python-module-name"](data__packagedir_key):
-                                            raise JsonSchemaValueException("data.package-dir must be python-module-name", value=data__packagedir_key, name="data.package-dir", definition={'format': 'python-module-name'}, rule='format')
+                                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-dir must be python-module-name", value=data__packagedir_key, name="" + (name_prefix or "data") + ".package-dir", definition={'format': 'python-module-name'}, rule='format')
                                     data__packagedir_key_one_of_count2 += 1
                                 except JsonSchemaValueException: pass
                             if data__packagedir_key_one_of_count2 < 2:
                                 try:
                                     if data__packagedir_key != "":
-                                        raise JsonSchemaValueException("data.package-dir must be same as const definition: ", value=data__packagedir_key, name="data.package-dir", definition={'const': ''}, rule='const')
+                                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-dir must be same as const definition: ", value=data__packagedir_key, name="" + (name_prefix or "data") + ".package-dir", definition={'const': ''}, rule='const')
                                     data__packagedir_key_one_of_count2 += 1
                                 except JsonSchemaValueException: pass
                             if data__packagedir_key_one_of_count2 != 1:
-                                raise JsonSchemaValueException("data.package-dir must be valid exactly by one of oneOf definition", value=data__packagedir_key, name="data.package-dir", definition={'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, rule='oneOf')
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-dir must be valid exactly by one definition" + (" (" + str(data__packagedir_key_one_of_count2) + " matches found)"), value=data__packagedir_key, name="" + (name_prefix or "data") + ".package-dir", definition={'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, rule='oneOf')
                         except JsonSchemaValueException:
                             data__packagedir_property_names = False
                     if not data__packagedir_property_names:
-                        raise JsonSchemaValueException("data.package-dir must be named by propertyName definition", value=data__packagedir, name="data.package-dir", definition={'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, rule='propertyNames')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-dir must be named by propertyName definition", value=data__packagedir, name="" + (name_prefix or "data") + ".package-dir", definition={'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, rule='propertyNames')
         if "package-data" in data_keys:
             data_keys.remove("package-data")
             data__packagedata = data["package-data"]
             if not isinstance(data__packagedata, (dict)):
-                raise JsonSchemaValueException("data.package-data must be object", value=data__packagedata, name="data.package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-data must be object", value=data__packagedata, name="" + (name_prefix or "data") + ".package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='type')
             data__packagedata_is_dict = isinstance(data__packagedata, dict)
             if data__packagedata_is_dict:
                 data__packagedata_keys = set(data__packagedata.keys())
@@ -253,15 +253,15 @@ def validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data,
                         if data__packagedata_key in data__packagedata_keys:
                             data__packagedata_keys.remove(data__packagedata_key)
                         if not isinstance(data__packagedata_val, (list, tuple)):
-                            raise JsonSchemaValueException(""+"data.package-data.{data__packagedata_key}".format(**locals())+" must be array", value=data__packagedata_val, name=""+"data.package-data.{data__packagedata_key}".format(**locals())+"", definition={'type': 'array', 'items': {'type': 'string'}}, rule='type')
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-data.{data__packagedata_key}".format(**locals()) + " must be array", value=data__packagedata_val, name="" + (name_prefix or "data") + ".package-data.{data__packagedata_key}".format(**locals()) + "", definition={'type': 'array', 'items': {'type': 'string'}}, rule='type')
                         data__packagedata_val_is_list = isinstance(data__packagedata_val, (list, tuple))
                         if data__packagedata_val_is_list:
                             data__packagedata_val_len = len(data__packagedata_val)
                             for data__packagedata_val_x, data__packagedata_val_item in enumerate(data__packagedata_val):
                                 if not isinstance(data__packagedata_val_item, (str)):
-                                    raise JsonSchemaValueException(""+"data.package-data.{data__packagedata_key}[{data__packagedata_val_x}]".format(**locals())+" must be string", value=data__packagedata_val_item, name=""+"data.package-data.{data__packagedata_key}[{data__packagedata_val_x}]".format(**locals())+"", definition={'type': 'string'}, rule='type')
+                                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-data.{data__packagedata_key}[{data__packagedata_val_x}]".format(**locals()) + " must be string", value=data__packagedata_val_item, name="" + (name_prefix or "data") + ".package-data.{data__packagedata_key}[{data__packagedata_val_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
                 if data__packagedata_keys:
-                    raise JsonSchemaValueException("data.package-data must not contain "+str(data__packagedata_keys)+" properties", value=data__packagedata, name="data.package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='additionalProperties')
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-data must not contain "+str(data__packagedata_keys)+" properties", value=data__packagedata, name="" + (name_prefix or "data") + ".package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='additionalProperties')
                 data__packagedata_len = len(data__packagedata)
                 if data__packagedata_len != 0:
                     data__packagedata_property_names = True
@@ -272,31 +272,31 @@ def validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data,
                                 try:
                                     if isinstance(data__packagedata_key, str):
                                         if not custom_formats["python-module-name"](data__packagedata_key):
-                                            raise JsonSchemaValueException("data.package-data must be python-module-name", value=data__packagedata_key, name="data.package-data", definition={'format': 'python-module-name'}, rule='format')
+                                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-data must be python-module-name", value=data__packagedata_key, name="" + (name_prefix or "data") + ".package-data", definition={'format': 'python-module-name'}, rule='format')
                                     data__packagedata_key_one_of_count3 += 1
                                 except JsonSchemaValueException: pass
                             if data__packagedata_key_one_of_count3 < 2:
                                 try:
                                     if data__packagedata_key != "*":
-                                        raise JsonSchemaValueException("data.package-data must be same as const definition: *", value=data__packagedata_key, name="data.package-data", definition={'const': '*'}, rule='const')
+                                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-data must be same as const definition: *", value=data__packagedata_key, name="" + (name_prefix or "data") + ".package-data", definition={'const': '*'}, rule='const')
                                     data__packagedata_key_one_of_count3 += 1
                                 except JsonSchemaValueException: pass
                             if data__packagedata_key_one_of_count3 != 1:
-                                raise JsonSchemaValueException("data.package-data must be valid exactly by one of oneOf definition", value=data__packagedata_key, name="data.package-data", definition={'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, rule='oneOf')
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-data must be valid exactly by one definition" + (" (" + str(data__packagedata_key_one_of_count3) + " matches found)"), value=data__packagedata_key, name="" + (name_prefix or "data") + ".package-data", definition={'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, rule='oneOf')
                         except JsonSchemaValueException:
                             data__packagedata_property_names = False
                     if not data__packagedata_property_names:
-                        raise JsonSchemaValueException("data.package-data must be named by propertyName definition", value=data__packagedata, name="data.package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='propertyNames')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-data must be named by propertyName definition", value=data__packagedata, name="" + (name_prefix or "data") + ".package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='propertyNames')
         if "include-package-data" in data_keys:
             data_keys.remove("include-package-data")
             data__includepackagedata = data["include-package-data"]
             if not isinstance(data__includepackagedata, (bool)):
-                raise JsonSchemaValueException("data.include-package-data must be boolean", value=data__includepackagedata, name="data.include-package-data", definition={'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".include-package-data must be boolean", value=data__includepackagedata, name="" + (name_prefix or "data") + ".include-package-data", definition={'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, rule='type')
         if "exclude-package-data" in data_keys:
             data_keys.remove("exclude-package-data")
             data__excludepackagedata = data["exclude-package-data"]
             if not isinstance(data__excludepackagedata, (dict)):
-                raise JsonSchemaValueException("data.exclude-package-data must be object", value=data__excludepackagedata, name="data.exclude-package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".exclude-package-data must be object", value=data__excludepackagedata, name="" + (name_prefix or "data") + ".exclude-package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='type')
             data__excludepackagedata_is_dict = isinstance(data__excludepackagedata, dict)
             if data__excludepackagedata_is_dict:
                 data__excludepackagedata_keys = set(data__excludepackagedata.keys())
@@ -305,15 +305,15 @@ def validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data,
                         if data__excludepackagedata_key in data__excludepackagedata_keys:
                             data__excludepackagedata_keys.remove(data__excludepackagedata_key)
                         if not isinstance(data__excludepackagedata_val, (list, tuple)):
-                            raise JsonSchemaValueException(""+"data.exclude-package-data.{data__excludepackagedata_key}".format(**locals())+" must be array", value=data__excludepackagedata_val, name=""+"data.exclude-package-data.{data__excludepackagedata_key}".format(**locals())+"", definition={'type': 'array', 'items': {'type': 'string'}}, rule='type')
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".exclude-package-data.{data__excludepackagedata_key}".format(**locals()) + " must be array", value=data__excludepackagedata_val, name="" + (name_prefix or "data") + ".exclude-package-data.{data__excludepackagedata_key}".format(**locals()) + "", definition={'type': 'array', 'items': {'type': 'string'}}, rule='type')
                         data__excludepackagedata_val_is_list = isinstance(data__excludepackagedata_val, (list, tuple))
                         if data__excludepackagedata_val_is_list:
                             data__excludepackagedata_val_len = len(data__excludepackagedata_val)
                             for data__excludepackagedata_val_x, data__excludepackagedata_val_item in enumerate(data__excludepackagedata_val):
                                 if not isinstance(data__excludepackagedata_val_item, (str)):
-                                    raise JsonSchemaValueException(""+"data.exclude-package-data.{data__excludepackagedata_key}[{data__excludepackagedata_val_x}]".format(**locals())+" must be string", value=data__excludepackagedata_val_item, name=""+"data.exclude-package-data.{data__excludepackagedata_key}[{data__excludepackagedata_val_x}]".format(**locals())+"", definition={'type': 'string'}, rule='type')
+                                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".exclude-package-data.{data__excludepackagedata_key}[{data__excludepackagedata_val_x}]".format(**locals()) + " must be string", value=data__excludepackagedata_val_item, name="" + (name_prefix or "data") + ".exclude-package-data.{data__excludepackagedata_key}[{data__excludepackagedata_val_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
                 if data__excludepackagedata_keys:
-                    raise JsonSchemaValueException("data.exclude-package-data must not contain "+str(data__excludepackagedata_keys)+" properties", value=data__excludepackagedata, name="data.exclude-package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='additionalProperties')
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".exclude-package-data must not contain "+str(data__excludepackagedata_keys)+" properties", value=data__excludepackagedata, name="" + (name_prefix or "data") + ".exclude-package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='additionalProperties')
                 data__excludepackagedata_len = len(data__excludepackagedata)
                 if data__excludepackagedata_len != 0:
                     data__excludepackagedata_property_names = True
@@ -324,54 +324,54 @@ def validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data,
                                 try:
                                     if isinstance(data__excludepackagedata_key, str):
                                         if not custom_formats["python-module-name"](data__excludepackagedata_key):
-                                            raise JsonSchemaValueException("data.exclude-package-data must be python-module-name", value=data__excludepackagedata_key, name="data.exclude-package-data", definition={'format': 'python-module-name'}, rule='format')
+                                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".exclude-package-data must be python-module-name", value=data__excludepackagedata_key, name="" + (name_prefix or "data") + ".exclude-package-data", definition={'format': 'python-module-name'}, rule='format')
                                     data__excludepackagedata_key_one_of_count4 += 1
                                 except JsonSchemaValueException: pass
                             if data__excludepackagedata_key_one_of_count4 < 2:
                                 try:
                                     if data__excludepackagedata_key != "*":
-                                        raise JsonSchemaValueException("data.exclude-package-data must be same as const definition: *", value=data__excludepackagedata_key, name="data.exclude-package-data", definition={'const': '*'}, rule='const')
+                                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".exclude-package-data must be same as const definition: *", value=data__excludepackagedata_key, name="" + (name_prefix or "data") + ".exclude-package-data", definition={'const': '*'}, rule='const')
                                     data__excludepackagedata_key_one_of_count4 += 1
                                 except JsonSchemaValueException: pass
                             if data__excludepackagedata_key_one_of_count4 != 1:
-                                raise JsonSchemaValueException("data.exclude-package-data must be valid exactly by one of oneOf definition", value=data__excludepackagedata_key, name="data.exclude-package-data", definition={'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, rule='oneOf')
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".exclude-package-data must be valid exactly by one definition" + (" (" + str(data__excludepackagedata_key_one_of_count4) + " matches found)"), value=data__excludepackagedata_key, name="" + (name_prefix or "data") + ".exclude-package-data", definition={'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, rule='oneOf')
                         except JsonSchemaValueException:
                             data__excludepackagedata_property_names = False
                     if not data__excludepackagedata_property_names:
-                        raise JsonSchemaValueException("data.exclude-package-data must be named by propertyName definition", value=data__excludepackagedata, name="data.exclude-package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='propertyNames')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".exclude-package-data must be named by propertyName definition", value=data__excludepackagedata, name="" + (name_prefix or "data") + ".exclude-package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='propertyNames')
         if "namespace-packages" in data_keys:
             data_keys.remove("namespace-packages")
             data__namespacepackages = data["namespace-packages"]
             if not isinstance(data__namespacepackages, (list, tuple)):
-                raise JsonSchemaValueException("data.namespace-packages must be array", value=data__namespacepackages, name="data.namespace-packages", definition={'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".namespace-packages must be array", value=data__namespacepackages, name="" + (name_prefix or "data") + ".namespace-packages", definition={'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, rule='type')
             data__namespacepackages_is_list = isinstance(data__namespacepackages, (list, tuple))
             if data__namespacepackages_is_list:
                 data__namespacepackages_len = len(data__namespacepackages)
                 for data__namespacepackages_x, data__namespacepackages_item in enumerate(data__namespacepackages):
                     if not isinstance(data__namespacepackages_item, (str)):
-                        raise JsonSchemaValueException(""+"data.namespace-packages[{data__namespacepackages_x}]".format(**locals())+" must be string", value=data__namespacepackages_item, name=""+"data.namespace-packages[{data__namespacepackages_x}]".format(**locals())+"", definition={'type': 'string', 'format': 'python-module-name'}, rule='type')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".namespace-packages[{data__namespacepackages_x}]".format(**locals()) + " must be string", value=data__namespacepackages_item, name="" + (name_prefix or "data") + ".namespace-packages[{data__namespacepackages_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'python-module-name'}, rule='type')
                     if isinstance(data__namespacepackages_item, str):
                         if not custom_formats["python-module-name"](data__namespacepackages_item):
-                            raise JsonSchemaValueException(""+"data.namespace-packages[{data__namespacepackages_x}]".format(**locals())+" must be python-module-name", value=data__namespacepackages_item, name=""+"data.namespace-packages[{data__namespacepackages_x}]".format(**locals())+"", definition={'type': 'string', 'format': 'python-module-name'}, rule='format')
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".namespace-packages[{data__namespacepackages_x}]".format(**locals()) + " must be python-module-name", value=data__namespacepackages_item, name="" + (name_prefix or "data") + ".namespace-packages[{data__namespacepackages_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'python-module-name'}, rule='format')
         if "py-modules" in data_keys:
             data_keys.remove("py-modules")
             data__pymodules = data["py-modules"]
             if not isinstance(data__pymodules, (list, tuple)):
-                raise JsonSchemaValueException("data.py-modules must be array", value=data__pymodules, name="data.py-modules", definition={'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".py-modules must be array", value=data__pymodules, name="" + (name_prefix or "data") + ".py-modules", definition={'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, rule='type')
             data__pymodules_is_list = isinstance(data__pymodules, (list, tuple))
             if data__pymodules_is_list:
                 data__pymodules_len = len(data__pymodules)
                 for data__pymodules_x, data__pymodules_item in enumerate(data__pymodules):
                     if not isinstance(data__pymodules_item, (str)):
-                        raise JsonSchemaValueException(""+"data.py-modules[{data__pymodules_x}]".format(**locals())+" must be string", value=data__pymodules_item, name=""+"data.py-modules[{data__pymodules_x}]".format(**locals())+"", definition={'type': 'string', 'format': 'python-module-name'}, rule='type')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".py-modules[{data__pymodules_x}]".format(**locals()) + " must be string", value=data__pymodules_item, name="" + (name_prefix or "data") + ".py-modules[{data__pymodules_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'python-module-name'}, rule='type')
                     if isinstance(data__pymodules_item, str):
                         if not custom_formats["python-module-name"](data__pymodules_item):
-                            raise JsonSchemaValueException(""+"data.py-modules[{data__pymodules_x}]".format(**locals())+" must be python-module-name", value=data__pymodules_item, name=""+"data.py-modules[{data__pymodules_x}]".format(**locals())+"", definition={'type': 'string', 'format': 'python-module-name'}, rule='format')
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".py-modules[{data__pymodules_x}]".format(**locals()) + " must be python-module-name", value=data__pymodules_item, name="" + (name_prefix or "data") + ".py-modules[{data__pymodules_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'python-module-name'}, rule='format')
         if "data-files" in data_keys:
             data_keys.remove("data-files")
             data__datafiles = data["data-files"]
             if not isinstance(data__datafiles, (dict)):
-                raise JsonSchemaValueException("data.data-files must be object", value=data__datafiles, name="data.data-files", definition={'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".data-files must be object", value=data__datafiles, name="" + (name_prefix or "data") + ".data-files", definition={'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='type')
             data__datafiles_is_dict = isinstance(data__datafiles, dict)
             if data__datafiles_is_dict:
                 data__datafiles_keys = set(data__datafiles.keys())
@@ -380,18 +380,18 @@ def validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data,
                         if data__datafiles_key in data__datafiles_keys:
                             data__datafiles_keys.remove(data__datafiles_key)
                         if not isinstance(data__datafiles_val, (list, tuple)):
-                            raise JsonSchemaValueException(""+"data.data-files.{data__datafiles_key}".format(**locals())+" must be array", value=data__datafiles_val, name=""+"data.data-files.{data__datafiles_key}".format(**locals())+"", definition={'type': 'array', 'items': {'type': 'string'}}, rule='type')
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".data-files.{data__datafiles_key}".format(**locals()) + " must be array", value=data__datafiles_val, name="" + (name_prefix or "data") + ".data-files.{data__datafiles_key}".format(**locals()) + "", definition={'type': 'array', 'items': {'type': 'string'}}, rule='type')
                         data__datafiles_val_is_list = isinstance(data__datafiles_val, (list, tuple))
                         if data__datafiles_val_is_list:
                             data__datafiles_val_len = len(data__datafiles_val)
                             for data__datafiles_val_x, data__datafiles_val_item in enumerate(data__datafiles_val):
                                 if not isinstance(data__datafiles_val_item, (str)):
-                                    raise JsonSchemaValueException(""+"data.data-files.{data__datafiles_key}[{data__datafiles_val_x}]".format(**locals())+" must be string", value=data__datafiles_val_item, name=""+"data.data-files.{data__datafiles_key}[{data__datafiles_val_x}]".format(**locals())+"", definition={'type': 'string'}, rule='type')
+                                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".data-files.{data__datafiles_key}[{data__datafiles_val_x}]".format(**locals()) + " must be string", value=data__datafiles_val_item, name="" + (name_prefix or "data") + ".data-files.{data__datafiles_key}[{data__datafiles_val_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
         if "cmdclass" in data_keys:
             data_keys.remove("cmdclass")
             data__cmdclass = data["cmdclass"]
             if not isinstance(data__cmdclass, (dict)):
-                raise JsonSchemaValueException("data.cmdclass must be object", value=data__cmdclass, name="data.cmdclass", definition={'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".cmdclass must be object", value=data__cmdclass, name="" + (name_prefix or "data") + ".cmdclass", definition={'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, rule='type')
             data__cmdclass_is_dict = isinstance(data__cmdclass, dict)
             if data__cmdclass_is_dict:
                 data__cmdclass_keys = set(data__cmdclass.keys())
@@ -400,15 +400,15 @@ def validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data,
                         if data__cmdclass_key in data__cmdclass_keys:
                             data__cmdclass_keys.remove(data__cmdclass_key)
                         if not isinstance(data__cmdclass_val, (str)):
-                            raise JsonSchemaValueException(""+"data.cmdclass.{data__cmdclass_key}".format(**locals())+" must be string", value=data__cmdclass_val, name=""+"data.cmdclass.{data__cmdclass_key}".format(**locals())+"", definition={'type': 'string', 'format': 'python-qualified-identifier'}, rule='type')
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".cmdclass.{data__cmdclass_key}".format(**locals()) + " must be string", value=data__cmdclass_val, name="" + (name_prefix or "data") + ".cmdclass.{data__cmdclass_key}".format(**locals()) + "", definition={'type': 'string', 'format': 'python-qualified-identifier'}, rule='type')
                         if isinstance(data__cmdclass_val, str):
                             if not custom_formats["python-qualified-identifier"](data__cmdclass_val):
-                                raise JsonSchemaValueException(""+"data.cmdclass.{data__cmdclass_key}".format(**locals())+" must be python-qualified-identifier", value=data__cmdclass_val, name=""+"data.cmdclass.{data__cmdclass_key}".format(**locals())+"", definition={'type': 'string', 'format': 'python-qualified-identifier'}, rule='format')
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".cmdclass.{data__cmdclass_key}".format(**locals()) + " must be python-qualified-identifier", value=data__cmdclass_val, name="" + (name_prefix or "data") + ".cmdclass.{data__cmdclass_key}".format(**locals()) + "", definition={'type': 'string', 'format': 'python-qualified-identifier'}, rule='format')
         if "dynamic" in data_keys:
             data_keys.remove("dynamic")
             data__dynamic = data["dynamic"]
             if not isinstance(data__dynamic, (dict)):
-                raise JsonSchemaValueException("data.dynamic must be object", value=data__dynamic, name="data.dynamic", definition={'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'$ref': '#/definitions/attr-directive'}, {'$ref': '#/definitions/file-directive'}]}, 'classifiers': {'$ref': '#/definitions/file-directive'}, 'description': {'$ref': '#/definitions/file-directive'}, 'entry-points': {'$ref': '#/definitions/file-directive'}, 'readme': {'anyOf': [{'$ref': '#/definitions/file-directive'}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}, 'license': {'type': 'string', '$$description': ['PROVISIONAL: A string specifying the license of the package', '(might change with PEP 639)'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-expression``?'}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might change with PEP 639)'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-files.glob``?'}}}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic must be object", value=data__dynamic, name="" + (name_prefix or "data") + ".dynamic", definition={'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}]}, 'classifiers': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'description': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'entry-points': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'readme': {'anyOf': [{'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}, 'license': {'type': 'string', '$$description': ['PROVISIONAL: A string specifying the license of the package', '(might change with PEP 639)'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-expression``?'}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might change with PEP 639)'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-files.glob``?'}}}, rule='type')
             data__dynamic_is_dict = isinstance(data__dynamic, dict)
             if data__dynamic_is_dict:
                 data__dynamic_keys = set(data__dynamic.keys())
@@ -418,35 +418,35 @@ def validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data,
                     data__dynamic__version_one_of_count5 = 0
                     if data__dynamic__version_one_of_count5 < 2:
                         try:
-                            validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_attr_directive(data__dynamic__version, custom_formats)
+                            validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_attr_directive(data__dynamic__version, custom_formats, (name_prefix or "data") + ".dynamic.version")
                             data__dynamic__version_one_of_count5 += 1
                         except JsonSchemaValueException: pass
                     if data__dynamic__version_one_of_count5 < 2:
                         try:
-                            validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data__dynamic__version, custom_formats)
+                            validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data__dynamic__version, custom_formats, (name_prefix or "data") + ".dynamic.version")
                             data__dynamic__version_one_of_count5 += 1
                         except JsonSchemaValueException: pass
                     if data__dynamic__version_one_of_count5 != 1:
-                        raise JsonSchemaValueException("data.dynamic.version must be valid exactly by one of oneOf definition", value=data__dynamic__version, name="data.dynamic.version", definition={'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'$ref': '#/definitions/attr-directive'}, {'$ref': '#/definitions/file-directive'}]}, rule='oneOf')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic.version must be valid exactly by one definition" + (" (" + str(data__dynamic__version_one_of_count5) + " matches found)"), value=data__dynamic__version, name="" + (name_prefix or "data") + ".dynamic.version", definition={'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}]}, rule='oneOf')
                 if "classifiers" in data__dynamic_keys:
                     data__dynamic_keys.remove("classifiers")
                     data__dynamic__classifiers = data__dynamic["classifiers"]
-                    validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data__dynamic__classifiers, custom_formats)
+                    validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data__dynamic__classifiers, custom_formats, (name_prefix or "data") + ".dynamic.classifiers")
                 if "description" in data__dynamic_keys:
                     data__dynamic_keys.remove("description")
                     data__dynamic__description = data__dynamic["description"]
-                    validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data__dynamic__description, custom_formats)
+                    validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data__dynamic__description, custom_formats, (name_prefix or "data") + ".dynamic.description")
                 if "entry-points" in data__dynamic_keys:
                     data__dynamic_keys.remove("entry-points")
                     data__dynamic__entrypoints = data__dynamic["entry-points"]
-                    validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data__dynamic__entrypoints, custom_formats)
+                    validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data__dynamic__entrypoints, custom_formats, (name_prefix or "data") + ".dynamic.entry-points")
                 if "readme" in data__dynamic_keys:
                     data__dynamic_keys.remove("readme")
                     data__dynamic__readme = data__dynamic["readme"]
                     data__dynamic__readme_any_of_count6 = 0
                     if not data__dynamic__readme_any_of_count6:
                         try:
-                            validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data__dynamic__readme, custom_formats)
+                            validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data__dynamic__readme, custom_formats, (name_prefix or "data") + ".dynamic.readme")
                             data__dynamic__readme_any_of_count6 += 1
                         except JsonSchemaValueException: pass
                     if not data__dynamic__readme_any_of_count6:
@@ -458,45 +458,45 @@ def validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data,
                                     data__dynamic__readme_keys.remove("content-type")
                                     data__dynamic__readme__contenttype = data__dynamic__readme["content-type"]
                                     if not isinstance(data__dynamic__readme__contenttype, (str)):
-                                        raise JsonSchemaValueException("data.dynamic.readme.content-type must be string", value=data__dynamic__readme__contenttype, name="data.dynamic.readme.content-type", definition={'type': 'string'}, rule='type')
+                                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic.readme.content-type must be string", value=data__dynamic__readme__contenttype, name="" + (name_prefix or "data") + ".dynamic.readme.content-type", definition={'type': 'string'}, rule='type')
                             data__dynamic__readme_any_of_count6 += 1
                         except JsonSchemaValueException: pass
                     if not data__dynamic__readme_any_of_count6:
-                        raise JsonSchemaValueException("data.dynamic.readme must be valid by one of anyOf definition", value=data__dynamic__readme, name="data.dynamic.readme", definition={'anyOf': [{'$ref': '#/definitions/file-directive'}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}, rule='anyOf')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic.readme cannot be validated by any definition", value=data__dynamic__readme, name="" + (name_prefix or "data") + ".dynamic.readme", definition={'anyOf': [{'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}, rule='anyOf')
                     data__dynamic__readme_is_dict = isinstance(data__dynamic__readme, dict)
                     if data__dynamic__readme_is_dict:
                         data__dynamic__readme_len = len(data__dynamic__readme)
                         if not all(prop in data__dynamic__readme for prop in ['file']):
-                            raise JsonSchemaValueException("data.dynamic.readme must contain ['file'] properties", value=data__dynamic__readme, name="data.dynamic.readme", definition={'anyOf': [{'$ref': '#/definitions/file-directive'}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}, rule='required')
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic.readme must contain ['file'] properties", value=data__dynamic__readme, name="" + (name_prefix or "data") + ".dynamic.readme", definition={'anyOf': [{'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}, rule='required')
                 if "license" in data__dynamic_keys:
                     data__dynamic_keys.remove("license")
                     data__dynamic__license = data__dynamic["license"]
                     if not isinstance(data__dynamic__license, (str)):
-                        raise JsonSchemaValueException("data.dynamic.license must be string", value=data__dynamic__license, name="data.dynamic.license", definition={'type': 'string', '$$description': ['PROVISIONAL: A string specifying the license of the package', '(might change with PEP 639)'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-expression``?'}, rule='type')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic.license must be string", value=data__dynamic__license, name="" + (name_prefix or "data") + ".dynamic.license", definition={'type': 'string', '$$description': ['PROVISIONAL: A string specifying the license of the package', '(might change with PEP 639)'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-expression``?'}, rule='type')
                 if "license-files" in data__dynamic_keys:
                     data__dynamic_keys.remove("license-files")
                     data__dynamic__licensefiles = data__dynamic["license-files"]
                     if not isinstance(data__dynamic__licensefiles, (list, tuple)):
-                        raise JsonSchemaValueException("data.dynamic.license-files must be array", value=data__dynamic__licensefiles, name="data.dynamic.license-files", definition={'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might change with PEP 639)'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-files.glob``?'}, rule='type')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic.license-files must be array", value=data__dynamic__licensefiles, name="" + (name_prefix or "data") + ".dynamic.license-files", definition={'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might change with PEP 639)'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-files.glob``?'}, rule='type')
                     data__dynamic__licensefiles_is_list = isinstance(data__dynamic__licensefiles, (list, tuple))
                     if data__dynamic__licensefiles_is_list:
                         data__dynamic__licensefiles_len = len(data__dynamic__licensefiles)
                         for data__dynamic__licensefiles_x, data__dynamic__licensefiles_item in enumerate(data__dynamic__licensefiles):
                             if not isinstance(data__dynamic__licensefiles_item, (str)):
-                                raise JsonSchemaValueException(""+"data.dynamic.license-files[{data__dynamic__licensefiles_x}]".format(**locals())+" must be string", value=data__dynamic__licensefiles_item, name=""+"data.dynamic.license-files[{data__dynamic__licensefiles_x}]".format(**locals())+"", definition={'type': 'string'}, rule='type')
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic.license-files[{data__dynamic__licensefiles_x}]".format(**locals()) + " must be string", value=data__dynamic__licensefiles_item, name="" + (name_prefix or "data") + ".dynamic.license-files[{data__dynamic__licensefiles_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
                 else: data__dynamic["license-files"] = ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*']
         if data_keys:
-            raise JsonSchemaValueException("data must not contain "+str(data_keys)+" properties", value=data, name="data", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$ref': '#/definitions/find-directive'}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'$ref': '#/definitions/attr-directive'}, {'$ref': '#/definitions/file-directive'}]}, 'classifiers': {'$ref': '#/definitions/file-directive'}, 'description': {'$ref': '#/definitions/file-directive'}, 'entry-points': {'$ref': '#/definitions/file-directive'}, 'readme': {'anyOf': [{'$ref': '#/definitions/file-directive'}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}, 'license': {'type': 'string', '$$description': ['PROVISIONAL: A string specifying the license of the package', '(might change with PEP 639)'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-expression``?'}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might change with PEP 639)'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-files.glob``?'}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}, rule='additionalProperties')
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must not contain "+str(data_keys)+" properties", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}]}, 'classifiers': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'description': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'entry-points': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'readme': {'anyOf': [{'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}, 'license': {'type': 'string', '$$description': ['PROVISIONAL: A string specifying the license of the package', '(might change with PEP 639)'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-expression``?'}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might change with PEP 639)'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-files.glob``?'}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}, rule='additionalProperties')
     return data
 
-def validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data, custom_formats={}):
+def validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data, custom_formats={}, name_prefix=None):
     if not isinstance(data, (dict)):
-        raise JsonSchemaValueException("data must be object", value=data, name="data", definition={'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, rule='type')
+        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, rule='type')
     data_is_dict = isinstance(data, dict)
     if data_is_dict:
         data_len = len(data)
         if not all(prop in data for prop in ['file']):
-            raise JsonSchemaValueException("data must contain ['file'] properties", value=data, name="data", definition={'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, rule='required')
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must contain ['file'] properties", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, rule='required')
         data_keys = set(data.keys())
         if "file" in data_keys:
             data_keys.remove("file")
@@ -505,48 +505,48 @@ def validate_https___setuptools_pypa_io_en_latest_references_keywords_html__defi
             if data__file_one_of_count7 < 2:
                 try:
                     if not isinstance(data__file, (str)):
-                        raise JsonSchemaValueException("data.file must be string", value=data__file, name="data.file", definition={'type': 'string'}, rule='type')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".file must be string", value=data__file, name="" + (name_prefix or "data") + ".file", definition={'type': 'string'}, rule='type')
                     data__file_one_of_count7 += 1
                 except JsonSchemaValueException: pass
             if data__file_one_of_count7 < 2:
                 try:
                     if not isinstance(data__file, (list, tuple)):
-                        raise JsonSchemaValueException("data.file must be array", value=data__file, name="data.file", definition={'type': 'array', 'items': {'type': 'string'}}, rule='type')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".file must be array", value=data__file, name="" + (name_prefix or "data") + ".file", definition={'type': 'array', 'items': {'type': 'string'}}, rule='type')
                     data__file_is_list = isinstance(data__file, (list, tuple))
                     if data__file_is_list:
                         data__file_len = len(data__file)
                         for data__file_x, data__file_item in enumerate(data__file):
                             if not isinstance(data__file_item, (str)):
-                                raise JsonSchemaValueException(""+"data.file[{data__file_x}]".format(**locals())+" must be string", value=data__file_item, name=""+"data.file[{data__file_x}]".format(**locals())+"", definition={'type': 'string'}, rule='type')
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".file[{data__file_x}]".format(**locals()) + " must be string", value=data__file_item, name="" + (name_prefix or "data") + ".file[{data__file_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
                     data__file_one_of_count7 += 1
                 except JsonSchemaValueException: pass
             if data__file_one_of_count7 != 1:
-                raise JsonSchemaValueException("data.file must be valid exactly by one of oneOf definition", value=data__file, name="data.file", definition={'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}, rule='oneOf')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".file must be valid exactly by one definition" + (" (" + str(data__file_one_of_count7) + " matches found)"), value=data__file, name="" + (name_prefix or "data") + ".file", definition={'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}, rule='oneOf')
         if data_keys:
-            raise JsonSchemaValueException("data must not contain "+str(data_keys)+" properties", value=data, name="data", definition={'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, rule='additionalProperties')
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must not contain "+str(data_keys)+" properties", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, rule='additionalProperties')
     return data
 
-def validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_attr_directive(data, custom_formats={}):
+def validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_attr_directive(data, custom_formats={}, name_prefix=None):
     if not isinstance(data, (dict)):
-        raise JsonSchemaValueException("data must be object", value=data, name="data", definition={'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, rule='type')
+        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, rule='type')
     data_is_dict = isinstance(data, dict)
     if data_is_dict:
         data_len = len(data)
         if not all(prop in data for prop in ['attr']):
-            raise JsonSchemaValueException("data must contain ['attr'] properties", value=data, name="data", definition={'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, rule='required')
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must contain ['attr'] properties", value=data, name="" + (name_prefix or "data") + "", definition={'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, rule='required')
         data_keys = set(data.keys())
         if "attr" in data_keys:
             data_keys.remove("attr")
             data__attr = data["attr"]
             if not isinstance(data__attr, (str)):
-                raise JsonSchemaValueException("data.attr must be string", value=data__attr, name="data.attr", definition={'type': 'string'}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".attr must be string", value=data__attr, name="" + (name_prefix or "data") + ".attr", definition={'type': 'string'}, rule='type')
         if data_keys:
-            raise JsonSchemaValueException("data must not contain "+str(data_keys)+" properties", value=data, name="data", definition={'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, rule='additionalProperties')
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must not contain "+str(data_keys)+" properties", value=data, name="" + (name_prefix or "data") + "", definition={'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, rule='additionalProperties')
     return data
 
-def validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_find_directive(data, custom_formats={}):
+def validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_find_directive(data, custom_formats={}, name_prefix=None):
     if not isinstance(data, (dict)):
-        raise JsonSchemaValueException("data must be object", value=data, name="data", definition={'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}, rule='type')
+        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}, rule='type')
     data_is_dict = isinstance(data, dict)
     if data_is_dict:
         data_keys = set(data.keys())
@@ -554,7 +554,7 @@ def validate_https___setuptools_pypa_io_en_latest_references_keywords_html__defi
             data_keys.remove("find")
             data__find = data["find"]
             if not isinstance(data__find, (dict)):
-                raise JsonSchemaValueException("data.find must be object", value=data__find, name="data.find", definition={'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".find must be object", value=data__find, name="" + (name_prefix or "data") + ".find", definition={'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}, rule='type')
             data__find_is_dict = isinstance(data__find, dict)
             if data__find_is_dict:
                 data__find_keys = set(data__find.keys())
@@ -562,49 +562,49 @@ def validate_https___setuptools_pypa_io_en_latest_references_keywords_html__defi
                     data__find_keys.remove("where")
                     data__find__where = data__find["where"]
                     if not isinstance(data__find__where, (list, tuple)):
-                        raise JsonSchemaValueException("data.find.where must be array", value=data__find__where, name="data.find.where", definition={'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, rule='type')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".find.where must be array", value=data__find__where, name="" + (name_prefix or "data") + ".find.where", definition={'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, rule='type')
                     data__find__where_is_list = isinstance(data__find__where, (list, tuple))
                     if data__find__where_is_list:
                         data__find__where_len = len(data__find__where)
                         for data__find__where_x, data__find__where_item in enumerate(data__find__where):
                             if not isinstance(data__find__where_item, (str)):
-                                raise JsonSchemaValueException(""+"data.find.where[{data__find__where_x}]".format(**locals())+" must be string", value=data__find__where_item, name=""+"data.find.where[{data__find__where_x}]".format(**locals())+"", definition={'type': 'string'}, rule='type')
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".find.where[{data__find__where_x}]".format(**locals()) + " must be string", value=data__find__where_item, name="" + (name_prefix or "data") + ".find.where[{data__find__where_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
                 if "exclude" in data__find_keys:
                     data__find_keys.remove("exclude")
                     data__find__exclude = data__find["exclude"]
                     if not isinstance(data__find__exclude, (list, tuple)):
-                        raise JsonSchemaValueException("data.find.exclude must be array", value=data__find__exclude, name="data.find.exclude", definition={'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, rule='type')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".find.exclude must be array", value=data__find__exclude, name="" + (name_prefix or "data") + ".find.exclude", definition={'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, rule='type')
                     data__find__exclude_is_list = isinstance(data__find__exclude, (list, tuple))
                     if data__find__exclude_is_list:
                         data__find__exclude_len = len(data__find__exclude)
                         for data__find__exclude_x, data__find__exclude_item in enumerate(data__find__exclude):
                             if not isinstance(data__find__exclude_item, (str)):
-                                raise JsonSchemaValueException(""+"data.find.exclude[{data__find__exclude_x}]".format(**locals())+" must be string", value=data__find__exclude_item, name=""+"data.find.exclude[{data__find__exclude_x}]".format(**locals())+"", definition={'type': 'string'}, rule='type')
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".find.exclude[{data__find__exclude_x}]".format(**locals()) + " must be string", value=data__find__exclude_item, name="" + (name_prefix or "data") + ".find.exclude[{data__find__exclude_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
                 if "include" in data__find_keys:
                     data__find_keys.remove("include")
                     data__find__include = data__find["include"]
                     if not isinstance(data__find__include, (list, tuple)):
-                        raise JsonSchemaValueException("data.find.include must be array", value=data__find__include, name="data.find.include", definition={'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, rule='type')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".find.include must be array", value=data__find__include, name="" + (name_prefix or "data") + ".find.include", definition={'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, rule='type')
                     data__find__include_is_list = isinstance(data__find__include, (list, tuple))
                     if data__find__include_is_list:
                         data__find__include_len = len(data__find__include)
                         for data__find__include_x, data__find__include_item in enumerate(data__find__include):
                             if not isinstance(data__find__include_item, (str)):
-                                raise JsonSchemaValueException(""+"data.find.include[{data__find__include_x}]".format(**locals())+" must be string", value=data__find__include_item, name=""+"data.find.include[{data__find__include_x}]".format(**locals())+"", definition={'type': 'string'}, rule='type')
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".find.include[{data__find__include_x}]".format(**locals()) + " must be string", value=data__find__include_item, name="" + (name_prefix or "data") + ".find.include[{data__find__include_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
                 if "namespaces" in data__find_keys:
                     data__find_keys.remove("namespaces")
                     data__find__namespaces = data__find["namespaces"]
                     if not isinstance(data__find__namespaces, (bool)):
-                        raise JsonSchemaValueException("data.find.namespaces must be boolean", value=data__find__namespaces, name="data.find.namespaces", definition={'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}, rule='type')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".find.namespaces must be boolean", value=data__find__namespaces, name="" + (name_prefix or "data") + ".find.namespaces", definition={'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}, rule='type')
                 if data__find_keys:
-                    raise JsonSchemaValueException("data.find must not contain "+str(data__find_keys)+" properties", value=data__find, name="data.find", definition={'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}, rule='additionalProperties')
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".find must not contain "+str(data__find_keys)+" properties", value=data__find, name="" + (name_prefix or "data") + ".find", definition={'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}, rule='additionalProperties')
         if data_keys:
-            raise JsonSchemaValueException("data must not contain "+str(data_keys)+" properties", value=data, name="data", definition={'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}, rule='additionalProperties')
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must not contain "+str(data_keys)+" properties", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}, rule='additionalProperties')
     return data
 
-def validate_https___docs_python_org_3_install(data, custom_formats={}):
+def validate_https___docs_python_org_3_install(data, custom_formats={}, name_prefix=None):
     if not isinstance(data, (dict)):
-        raise JsonSchemaValueException("data must be object", value=data, name="data", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://docs.python.org/3/install/', 'title': '``tool.distutils`` table', '$$description': ['Originally, ``distutils`` allowed developers to configure arguments for', '``setup.py`` scripts via `distutils configuration files', '`_.', '``tool.distutils`` subtables could be used with the same purpose', '(NOT CURRENTLY IMPLEMENTED).'], 'type': 'object', 'properties': {'global': {'type': 'object', 'description': 'Global options applied to all ``distutils`` commands'}}, 'patternProperties': {'.+': {'type': 'object'}}, '$comment': 'TODO: Is there a practical way of making this schema more specific?'}, rule='type')
+        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://docs.python.org/3/install/', 'title': '``tool.distutils`` table', '$$description': ['Originally, ``distutils`` allowed developers to configure arguments for', '``setup.py`` scripts via `distutils configuration files', '`_.', '``tool.distutils`` subtables could be used with the same purpose', '(NOT CURRENTLY IMPLEMENTED).'], 'type': 'object', 'properties': {'global': {'type': 'object', 'description': 'Global options applied to all ``distutils`` commands'}}, 'patternProperties': {'.+': {'type': 'object'}}, '$comment': 'TODO: Is there a practical way of making this schema more specific?'}, rule='type')
     data_is_dict = isinstance(data, dict)
     if data_is_dict:
         data_keys = set(data.keys())
@@ -612,45 +612,45 @@ def validate_https___docs_python_org_3_install(data, custom_formats={}):
             data_keys.remove("global")
             data__global = data["global"]
             if not isinstance(data__global, (dict)):
-                raise JsonSchemaValueException("data.global must be object", value=data__global, name="data.global", definition={'type': 'object', 'description': 'Global options applied to all ``distutils`` commands'}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".global must be object", value=data__global, name="" + (name_prefix or "data") + ".global", definition={'type': 'object', 'description': 'Global options applied to all ``distutils`` commands'}, rule='type')
         for data_key, data_val in data.items():
             if REGEX_PATTERNS['.+'].search(data_key):
                 if data_key in data_keys:
                     data_keys.remove(data_key)
                 if not isinstance(data_val, (dict)):
-                    raise JsonSchemaValueException(""+"data.{data_key}".format(**locals())+" must be object", value=data_val, name=""+"data.{data_key}".format(**locals())+"", definition={'type': 'object'}, rule='type')
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".{data_key}".format(**locals()) + " must be object", value=data_val, name="" + (name_prefix or "data") + ".{data_key}".format(**locals()) + "", definition={'type': 'object'}, rule='type')
     return data
 
-def validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata(data, custom_formats={}):
+def validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata(data, custom_formats={}, name_prefix=None):
     if not isinstance(data, (dict)):
-        raise JsonSchemaValueException("data must be object", value=data, name="data", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, rule='type')
+        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'gui-scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, rule='type')
     data_is_dict = isinstance(data, dict)
     if data_is_dict:
         data_len = len(data)
         if not all(prop in data for prop in ['name']):
-            raise JsonSchemaValueException("data must contain ['name'] properties", value=data, name="data", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, rule='required')
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must contain ['name'] properties", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'gui-scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, rule='required')
         data_keys = set(data.keys())
         if "name" in data_keys:
             data_keys.remove("name")
             data__name = data["name"]
             if not isinstance(data__name, (str)):
-                raise JsonSchemaValueException("data.name must be string", value=data__name, name="data.name", definition={'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".name must be string", value=data__name, name="" + (name_prefix or "data") + ".name", definition={'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, rule='type')
             if isinstance(data__name, str):
                 if not custom_formats["pep508-identifier"](data__name):
-                    raise JsonSchemaValueException("data.name must be pep508-identifier", value=data__name, name="data.name", definition={'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, rule='format')
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".name must be pep508-identifier", value=data__name, name="" + (name_prefix or "data") + ".name", definition={'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, rule='format')
         if "version" in data_keys:
             data_keys.remove("version")
             data__version = data["version"]
             if not isinstance(data__version, (str)):
-                raise JsonSchemaValueException("data.version must be string", value=data__version, name="data.version", definition={'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".version must be string", value=data__version, name="" + (name_prefix or "data") + ".version", definition={'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, rule='type')
             if isinstance(data__version, str):
                 if not custom_formats["pep440"](data__version):
-                    raise JsonSchemaValueException("data.version must be pep440", value=data__version, name="data.version", definition={'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, rule='format')
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".version must be pep440", value=data__version, name="" + (name_prefix or "data") + ".version", definition={'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, rule='format')
         if "description" in data_keys:
             data_keys.remove("description")
             data__description = data["description"]
             if not isinstance(data__description, (str)):
-                raise JsonSchemaValueException("data.description must be string", value=data__description, name="data.description", definition={'type': 'string', '$$description': ['The `summary description of the project', '`_']}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".description must be string", value=data__description, name="" + (name_prefix or "data") + ".description", definition={'type': 'string', '$$description': ['The `summary description of the project', '`_']}, rule='type')
         if "readme" in data_keys:
             data_keys.remove("readme")
             data__readme = data["readme"]
@@ -658,13 +658,13 @@ def validate_https___packaging_python_org_en_latest_specifications_declaring_pro
             if data__readme_one_of_count8 < 2:
                 try:
                     if not isinstance(data__readme, (str)):
-                        raise JsonSchemaValueException("data.readme must be string", value=data__readme, name="data.readme", definition={'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, rule='type')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme must be string", value=data__readme, name="" + (name_prefix or "data") + ".readme", definition={'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, rule='type')
                     data__readme_one_of_count8 += 1
                 except JsonSchemaValueException: pass
             if data__readme_one_of_count8 < 2:
                 try:
                     if not isinstance(data__readme, (dict)):
-                        raise JsonSchemaValueException("data.readme must be object", value=data__readme, name="data.readme", definition={'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}, rule='type')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme must be object", value=data__readme, name="" + (name_prefix or "data") + ".readme", definition={'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}, rule='type')
                     data__readme_any_of_count9 = 0
                     if not data__readme_any_of_count9:
                         try:
@@ -672,13 +672,13 @@ def validate_https___packaging_python_org_en_latest_specifications_declaring_pro
                             if data__readme_is_dict:
                                 data__readme_len = len(data__readme)
                                 if not all(prop in data__readme for prop in ['file']):
-                                    raise JsonSchemaValueException("data.readme must contain ['file'] properties", value=data__readme, name="data.readme", definition={'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, rule='required')
+                                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme must contain ['file'] properties", value=data__readme, name="" + (name_prefix or "data") + ".readme", definition={'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, rule='required')
                                 data__readme_keys = set(data__readme.keys())
                                 if "file" in data__readme_keys:
                                     data__readme_keys.remove("file")
                                     data__readme__file = data__readme["file"]
                                     if not isinstance(data__readme__file, (str)):
-                                        raise JsonSchemaValueException("data.readme.file must be string", value=data__readme__file, name="data.readme.file", definition={'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}, rule='type')
+                                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme.file must be string", value=data__readme__file, name="" + (name_prefix or "data") + ".readme.file", definition={'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}, rule='type')
                             data__readme_any_of_count9 += 1
                         except JsonSchemaValueException: pass
                     if not data__readme_any_of_count9:
@@ -687,40 +687,40 @@ def validate_https___packaging_python_org_en_latest_specifications_declaring_pro
                             if data__readme_is_dict:
                                 data__readme_len = len(data__readme)
                                 if not all(prop in data__readme for prop in ['text']):
-                                    raise JsonSchemaValueException("data.readme must contain ['text'] properties", value=data__readme, name="data.readme", definition={'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}, rule='required')
+                                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme must contain ['text'] properties", value=data__readme, name="" + (name_prefix or "data") + ".readme", definition={'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}, rule='required')
                                 data__readme_keys = set(data__readme.keys())
                                 if "text" in data__readme_keys:
                                     data__readme_keys.remove("text")
                                     data__readme__text = data__readme["text"]
                                     if not isinstance(data__readme__text, (str)):
-                                        raise JsonSchemaValueException("data.readme.text must be string", value=data__readme__text, name="data.readme.text", definition={'type': 'string', 'description': 'Full text describing the project.'}, rule='type')
+                                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme.text must be string", value=data__readme__text, name="" + (name_prefix or "data") + ".readme.text", definition={'type': 'string', 'description': 'Full text describing the project.'}, rule='type')
                             data__readme_any_of_count9 += 1
                         except JsonSchemaValueException: pass
                     if not data__readme_any_of_count9:
-                        raise JsonSchemaValueException("data.readme must be valid by one of anyOf definition", value=data__readme, name="data.readme", definition={'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, rule='anyOf')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme cannot be validated by any definition", value=data__readme, name="" + (name_prefix or "data") + ".readme", definition={'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, rule='anyOf')
                     data__readme_is_dict = isinstance(data__readme, dict)
                     if data__readme_is_dict:
                         data__readme_len = len(data__readme)
                         if not all(prop in data__readme for prop in ['content-type']):
-                            raise JsonSchemaValueException("data.readme must contain ['content-type'] properties", value=data__readme, name="data.readme", definition={'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}, rule='required')
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme must contain ['content-type'] properties", value=data__readme, name="" + (name_prefix or "data") + ".readme", definition={'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}, rule='required')
                         data__readme_keys = set(data__readme.keys())
                         if "content-type" in data__readme_keys:
                             data__readme_keys.remove("content-type")
                             data__readme__contenttype = data__readme["content-type"]
                             if not isinstance(data__readme__contenttype, (str)):
-                                raise JsonSchemaValueException("data.readme.content-type must be string", value=data__readme__contenttype, name="data.readme.content-type", definition={'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}, rule='type')
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme.content-type must be string", value=data__readme__contenttype, name="" + (name_prefix or "data") + ".readme.content-type", definition={'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}, rule='type')
                     data__readme_one_of_count8 += 1
                 except JsonSchemaValueException: pass
             if data__readme_one_of_count8 != 1:
-                raise JsonSchemaValueException("data.readme must be valid exactly by one of oneOf definition", value=data__readme, name="data.readme", definition={'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, rule='oneOf')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme must be valid exactly by one definition" + (" (" + str(data__readme_one_of_count8) + " matches found)"), value=data__readme, name="" + (name_prefix or "data") + ".readme", definition={'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, rule='oneOf')
         if "requires-python" in data_keys:
             data_keys.remove("requires-python")
             data__requirespython = data["requires-python"]
             if not isinstance(data__requirespython, (str)):
-                raise JsonSchemaValueException("data.requires-python must be string", value=data__requirespython, name="data.requires-python", definition={'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".requires-python must be string", value=data__requirespython, name="" + (name_prefix or "data") + ".requires-python", definition={'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, rule='type')
             if isinstance(data__requirespython, str):
                 if not custom_formats["pep508-versionspec"](data__requirespython):
-                    raise JsonSchemaValueException("data.requires-python must be pep508-versionspec", value=data__requirespython, name="data.requires-python", definition={'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, rule='format')
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".requires-python must be pep508-versionspec", value=data__requirespython, name="" + (name_prefix or "data") + ".requires-python", definition={'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, rule='format')
         if "license" in data_keys:
             data_keys.remove("license")
             data__license = data["license"]
@@ -731,13 +731,13 @@ def validate_https___packaging_python_org_en_latest_specifications_declaring_pro
                     if data__license_is_dict:
                         data__license_len = len(data__license)
                         if not all(prop in data__license for prop in ['file']):
-                            raise JsonSchemaValueException("data.license must contain ['file'] properties", value=data__license, name="data.license", definition={'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, rule='required')
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".license must contain ['file'] properties", value=data__license, name="" + (name_prefix or "data") + ".license", definition={'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, rule='required')
                         data__license_keys = set(data__license.keys())
                         if "file" in data__license_keys:
                             data__license_keys.remove("file")
                             data__license__file = data__license["file"]
                             if not isinstance(data__license__file, (str)):
-                                raise JsonSchemaValueException("data.license.file must be string", value=data__license__file, name="data.license.file", definition={'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}, rule='type')
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".license.file must be string", value=data__license__file, name="" + (name_prefix or "data") + ".license.file", definition={'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}, rule='type')
                     data__license_one_of_count10 += 1
                 except JsonSchemaValueException: pass
             if data__license_one_of_count10 < 2:
@@ -746,67 +746,67 @@ def validate_https___packaging_python_org_en_latest_specifications_declaring_pro
                     if data__license_is_dict:
                         data__license_len = len(data__license)
                         if not all(prop in data__license for prop in ['text']):
-                            raise JsonSchemaValueException("data.license must contain ['text'] properties", value=data__license, name="data.license", definition={'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}, rule='required')
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".license must contain ['text'] properties", value=data__license, name="" + (name_prefix or "data") + ".license", definition={'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}, rule='required')
                         data__license_keys = set(data__license.keys())
                         if "text" in data__license_keys:
                             data__license_keys.remove("text")
                             data__license__text = data__license["text"]
                             if not isinstance(data__license__text, (str)):
-                                raise JsonSchemaValueException("data.license.text must be string", value=data__license__text, name="data.license.text", definition={'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}, rule='type')
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".license.text must be string", value=data__license__text, name="" + (name_prefix or "data") + ".license.text", definition={'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}, rule='type')
                     data__license_one_of_count10 += 1
                 except JsonSchemaValueException: pass
             if data__license_one_of_count10 != 1:
-                raise JsonSchemaValueException("data.license must be valid exactly by one of oneOf definition", value=data__license, name="data.license", definition={'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, rule='oneOf')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".license must be valid exactly by one definition" + (" (" + str(data__license_one_of_count10) + " matches found)"), value=data__license, name="" + (name_prefix or "data") + ".license", definition={'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, rule='oneOf')
         if "authors" in data_keys:
             data_keys.remove("authors")
             data__authors = data["authors"]
             if not isinstance(data__authors, (list, tuple)):
-                raise JsonSchemaValueException("data.authors must be array", value=data__authors, name="data.authors", definition={'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".authors must be array", value=data__authors, name="" + (name_prefix or "data") + ".authors", definition={'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, rule='type')
             data__authors_is_list = isinstance(data__authors, (list, tuple))
             if data__authors_is_list:
                 data__authors_len = len(data__authors)
                 for data__authors_x, data__authors_item in enumerate(data__authors):
-                    validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_author(data__authors_item, custom_formats)
+                    validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_author(data__authors_item, custom_formats, (name_prefix or "data") + ".authors[{data__authors_x}]")
         if "maintainers" in data_keys:
             data_keys.remove("maintainers")
             data__maintainers = data["maintainers"]
             if not isinstance(data__maintainers, (list, tuple)):
-                raise JsonSchemaValueException("data.maintainers must be array", value=data__maintainers, name="data.maintainers", definition={'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".maintainers must be array", value=data__maintainers, name="" + (name_prefix or "data") + ".maintainers", definition={'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, rule='type')
             data__maintainers_is_list = isinstance(data__maintainers, (list, tuple))
             if data__maintainers_is_list:
                 data__maintainers_len = len(data__maintainers)
                 for data__maintainers_x, data__maintainers_item in enumerate(data__maintainers):
-                    validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_author(data__maintainers_item, custom_formats)
+                    validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_author(data__maintainers_item, custom_formats, (name_prefix or "data") + ".maintainers[{data__maintainers_x}]")
         if "keywords" in data_keys:
             data_keys.remove("keywords")
             data__keywords = data["keywords"]
             if not isinstance(data__keywords, (list, tuple)):
-                raise JsonSchemaValueException("data.keywords must be array", value=data__keywords, name="data.keywords", definition={'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".keywords must be array", value=data__keywords, name="" + (name_prefix or "data") + ".keywords", definition={'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, rule='type')
             data__keywords_is_list = isinstance(data__keywords, (list, tuple))
             if data__keywords_is_list:
                 data__keywords_len = len(data__keywords)
                 for data__keywords_x, data__keywords_item in enumerate(data__keywords):
                     if not isinstance(data__keywords_item, (str)):
-                        raise JsonSchemaValueException(""+"data.keywords[{data__keywords_x}]".format(**locals())+" must be string", value=data__keywords_item, name=""+"data.keywords[{data__keywords_x}]".format(**locals())+"", definition={'type': 'string'}, rule='type')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".keywords[{data__keywords_x}]".format(**locals()) + " must be string", value=data__keywords_item, name="" + (name_prefix or "data") + ".keywords[{data__keywords_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
         if "classifiers" in data_keys:
             data_keys.remove("classifiers")
             data__classifiers = data["classifiers"]
             if not isinstance(data__classifiers, (list, tuple)):
-                raise JsonSchemaValueException("data.classifiers must be array", value=data__classifiers, name="data.classifiers", definition={'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".classifiers must be array", value=data__classifiers, name="" + (name_prefix or "data") + ".classifiers", definition={'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, rule='type')
             data__classifiers_is_list = isinstance(data__classifiers, (list, tuple))
             if data__classifiers_is_list:
                 data__classifiers_len = len(data__classifiers)
                 for data__classifiers_x, data__classifiers_item in enumerate(data__classifiers):
                     if not isinstance(data__classifiers_item, (str)):
-                        raise JsonSchemaValueException(""+"data.classifiers[{data__classifiers_x}]".format(**locals())+" must be string", value=data__classifiers_item, name=""+"data.classifiers[{data__classifiers_x}]".format(**locals())+"", definition={'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, rule='type')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".classifiers[{data__classifiers_x}]".format(**locals()) + " must be string", value=data__classifiers_item, name="" + (name_prefix or "data") + ".classifiers[{data__classifiers_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, rule='type')
                     if isinstance(data__classifiers_item, str):
                         if not custom_formats["trove-classifier"](data__classifiers_item):
-                            raise JsonSchemaValueException(""+"data.classifiers[{data__classifiers_x}]".format(**locals())+" must be trove-classifier", value=data__classifiers_item, name=""+"data.classifiers[{data__classifiers_x}]".format(**locals())+"", definition={'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, rule='format')
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".classifiers[{data__classifiers_x}]".format(**locals()) + " must be trove-classifier", value=data__classifiers_item, name="" + (name_prefix or "data") + ".classifiers[{data__classifiers_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, rule='format')
         if "urls" in data_keys:
             data_keys.remove("urls")
             data__urls = data["urls"]
             if not isinstance(data__urls, (dict)):
-                raise JsonSchemaValueException("data.urls must be object", value=data__urls, name="data.urls", definition={'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".urls must be object", value=data__urls, name="" + (name_prefix or "data") + ".urls", definition={'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, rule='type')
             data__urls_is_dict = isinstance(data__urls, dict)
             if data__urls_is_dict:
                 data__urls_keys = set(data__urls.keys())
@@ -815,20 +815,20 @@ def validate_https___packaging_python_org_en_latest_specifications_declaring_pro
                         if data__urls_key in data__urls_keys:
                             data__urls_keys.remove(data__urls_key)
                         if not isinstance(data__urls_val, (str)):
-                            raise JsonSchemaValueException(""+"data.urls.{data__urls_key}".format(**locals())+" must be string", value=data__urls_val, name=""+"data.urls.{data__urls_key}".format(**locals())+"", definition={'type': 'string', 'format': 'url'}, rule='type')
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".urls.{data__urls_key}".format(**locals()) + " must be string", value=data__urls_val, name="" + (name_prefix or "data") + ".urls.{data__urls_key}".format(**locals()) + "", definition={'type': 'string', 'format': 'url'}, rule='type')
                         if isinstance(data__urls_val, str):
                             if not custom_formats["url"](data__urls_val):
-                                raise JsonSchemaValueException(""+"data.urls.{data__urls_key}".format(**locals())+" must be url", value=data__urls_val, name=""+"data.urls.{data__urls_key}".format(**locals())+"", definition={'type': 'string', 'format': 'url'}, rule='format')
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".urls.{data__urls_key}".format(**locals()) + " must be url", value=data__urls_val, name="" + (name_prefix or "data") + ".urls.{data__urls_key}".format(**locals()) + "", definition={'type': 'string', 'format': 'url'}, rule='format')
                 if data__urls_keys:
-                    raise JsonSchemaValueException("data.urls must not contain "+str(data__urls_keys)+" properties", value=data__urls, name="data.urls", definition={'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, rule='additionalProperties')
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".urls must not contain "+str(data__urls_keys)+" properties", value=data__urls, name="" + (name_prefix or "data") + ".urls", definition={'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, rule='additionalProperties')
         if "scripts" in data_keys:
             data_keys.remove("scripts")
             data__scripts = data["scripts"]
-            validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_entry_point_group(data__scripts, custom_formats)
+            validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_entry_point_group(data__scripts, custom_formats, (name_prefix or "data") + ".scripts")
         if "gui-scripts" in data_keys:
             data_keys.remove("gui-scripts")
             data__guiscripts = data["gui-scripts"]
-            validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_entry_point_group(data__guiscripts, custom_formats)
+            validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_entry_point_group(data__guiscripts, custom_formats, (name_prefix or "data") + ".gui-scripts")
         if "entry-points" in data_keys:
             data_keys.remove("entry-points")
             data__entrypoints = data["entry-points"]
@@ -839,9 +839,9 @@ def validate_https___packaging_python_org_en_latest_specifications_declaring_pro
                     if REGEX_PATTERNS['^.+$'].search(data__entrypoints_key):
                         if data__entrypoints_key in data__entrypoints_keys:
                             data__entrypoints_keys.remove(data__entrypoints_key)
-                        validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_entry_point_group(data__entrypoints_val, custom_formats)
+                        validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_entry_point_group(data__entrypoints_val, custom_formats, (name_prefix or "data") + ".entry-points.{data__entrypoints_key}")
                 if data__entrypoints_keys:
-                    raise JsonSchemaValueException("data.entry-points must not contain "+str(data__entrypoints_keys)+" properties", value=data__entrypoints, name="data.entry-points", definition={'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, rule='additionalProperties')
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".entry-points must not contain "+str(data__entrypoints_keys)+" properties", value=data__entrypoints, name="" + (name_prefix or "data") + ".entry-points", definition={'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}}}, rule='additionalProperties')
                 data__entrypoints_len = len(data__entrypoints)
                 if data__entrypoints_len != 0:
                     data__entrypoints_property_names = True
@@ -849,26 +849,26 @@ def validate_https___packaging_python_org_en_latest_specifications_declaring_pro
                         try:
                             if isinstance(data__entrypoints_key, str):
                                 if not custom_formats["python-entrypoint-group"](data__entrypoints_key):
-                                    raise JsonSchemaValueException("data.entry-points must be python-entrypoint-group", value=data__entrypoints_key, name="data.entry-points", definition={'format': 'python-entrypoint-group'}, rule='format')
+                                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".entry-points must be python-entrypoint-group", value=data__entrypoints_key, name="" + (name_prefix or "data") + ".entry-points", definition={'format': 'python-entrypoint-group'}, rule='format')
                         except JsonSchemaValueException:
                             data__entrypoints_property_names = False
                     if not data__entrypoints_property_names:
-                        raise JsonSchemaValueException("data.entry-points must be named by propertyName definition", value=data__entrypoints, name="data.entry-points", definition={'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, rule='propertyNames')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".entry-points must be named by propertyName definition", value=data__entrypoints, name="" + (name_prefix or "data") + ".entry-points", definition={'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}}}, rule='propertyNames')
         if "dependencies" in data_keys:
             data_keys.remove("dependencies")
             data__dependencies = data["dependencies"]
             if not isinstance(data__dependencies, (list, tuple)):
-                raise JsonSchemaValueException("data.dependencies must be array", value=data__dependencies, name="data.dependencies", definition={'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".dependencies must be array", value=data__dependencies, name="" + (name_prefix or "data") + ".dependencies", definition={'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}, rule='type')
             data__dependencies_is_list = isinstance(data__dependencies, (list, tuple))
             if data__dependencies_is_list:
                 data__dependencies_len = len(data__dependencies)
                 for data__dependencies_x, data__dependencies_item in enumerate(data__dependencies):
-                    validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_dependency(data__dependencies_item, custom_formats)
+                    validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_dependency(data__dependencies_item, custom_formats, (name_prefix or "data") + ".dependencies[{data__dependencies_x}]")
         if "optional-dependencies" in data_keys:
             data_keys.remove("optional-dependencies")
             data__optionaldependencies = data["optional-dependencies"]
             if not isinstance(data__optionaldependencies, (dict)):
-                raise JsonSchemaValueException("data.optional-dependencies must be object", value=data__optionaldependencies, name="data.optional-dependencies", definition={'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".optional-dependencies must be object", value=data__optionaldependencies, name="" + (name_prefix or "data") + ".optional-dependencies", definition={'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, rule='type')
             data__optionaldependencies_is_dict = isinstance(data__optionaldependencies, dict)
             if data__optionaldependencies_is_dict:
                 data__optionaldependencies_keys = set(data__optionaldependencies.keys())
@@ -877,14 +877,14 @@ def validate_https___packaging_python_org_en_latest_specifications_declaring_pro
                         if data__optionaldependencies_key in data__optionaldependencies_keys:
                             data__optionaldependencies_keys.remove(data__optionaldependencies_key)
                         if not isinstance(data__optionaldependencies_val, (list, tuple)):
-                            raise JsonSchemaValueException(""+"data.optional-dependencies.{data__optionaldependencies_key}".format(**locals())+" must be array", value=data__optionaldependencies_val, name=""+"data.optional-dependencies.{data__optionaldependencies_key}".format(**locals())+"", definition={'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}, rule='type')
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".optional-dependencies.{data__optionaldependencies_key}".format(**locals()) + " must be array", value=data__optionaldependencies_val, name="" + (name_prefix or "data") + ".optional-dependencies.{data__optionaldependencies_key}".format(**locals()) + "", definition={'type': 'array', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}, rule='type')
                         data__optionaldependencies_val_is_list = isinstance(data__optionaldependencies_val, (list, tuple))
                         if data__optionaldependencies_val_is_list:
                             data__optionaldependencies_val_len = len(data__optionaldependencies_val)
                             for data__optionaldependencies_val_x, data__optionaldependencies_val_item in enumerate(data__optionaldependencies_val):
-                                validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_dependency(data__optionaldependencies_val_item, custom_formats)
+                                validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_dependency(data__optionaldependencies_val_item, custom_formats, (name_prefix or "data") + ".optional-dependencies.{data__optionaldependencies_key}[{data__optionaldependencies_val_x}]")
                 if data__optionaldependencies_keys:
-                    raise JsonSchemaValueException("data.optional-dependencies must not contain "+str(data__optionaldependencies_keys)+" properties", value=data__optionaldependencies, name="data.optional-dependencies", definition={'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, rule='additionalProperties')
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".optional-dependencies must not contain "+str(data__optionaldependencies_keys)+" properties", value=data__optionaldependencies, name="" + (name_prefix or "data") + ".optional-dependencies", definition={'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, rule='additionalProperties')
                 data__optionaldependencies_len = len(data__optionaldependencies)
                 if data__optionaldependencies_len != 0:
                     data__optionaldependencies_property_names = True
@@ -892,34 +892,34 @@ def validate_https___packaging_python_org_en_latest_specifications_declaring_pro
                         try:
                             if isinstance(data__optionaldependencies_key, str):
                                 if not custom_formats["pep508-identifier"](data__optionaldependencies_key):
-                                    raise JsonSchemaValueException("data.optional-dependencies must be pep508-identifier", value=data__optionaldependencies_key, name="data.optional-dependencies", definition={'format': 'pep508-identifier'}, rule='format')
+                                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".optional-dependencies must be pep508-identifier", value=data__optionaldependencies_key, name="" + (name_prefix or "data") + ".optional-dependencies", definition={'format': 'pep508-identifier'}, rule='format')
                         except JsonSchemaValueException:
                             data__optionaldependencies_property_names = False
                     if not data__optionaldependencies_property_names:
-                        raise JsonSchemaValueException("data.optional-dependencies must be named by propertyName definition", value=data__optionaldependencies, name="data.optional-dependencies", definition={'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, rule='propertyNames')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".optional-dependencies must be named by propertyName definition", value=data__optionaldependencies, name="" + (name_prefix or "data") + ".optional-dependencies", definition={'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, rule='propertyNames')
         if "dynamic" in data_keys:
             data_keys.remove("dynamic")
             data__dynamic = data["dynamic"]
             if not isinstance(data__dynamic, (list, tuple)):
-                raise JsonSchemaValueException("data.dynamic must be array", value=data__dynamic, name="data.dynamic", definition={'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic must be array", value=data__dynamic, name="" + (name_prefix or "data") + ".dynamic", definition={'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}, rule='type')
             data__dynamic_is_list = isinstance(data__dynamic, (list, tuple))
             if data__dynamic_is_list:
                 data__dynamic_len = len(data__dynamic)
                 for data__dynamic_x, data__dynamic_item in enumerate(data__dynamic):
                     if data__dynamic_item not in ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']:
-                        raise JsonSchemaValueException(""+"data.dynamic[{data__dynamic_x}]".format(**locals())+" must be one of ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']", value=data__dynamic_item, name=""+"data.dynamic[{data__dynamic_x}]".format(**locals())+"", definition={'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}, rule='enum')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic[{data__dynamic_x}]".format(**locals()) + " must be one of ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']", value=data__dynamic_item, name="" + (name_prefix or "data") + ".dynamic[{data__dynamic_x}]".format(**locals()) + "", definition={'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}, rule='enum')
         if data_keys:
-            raise JsonSchemaValueException("data must not contain "+str(data_keys)+" properties", value=data, name="data", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, rule='additionalProperties')
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must not contain "+str(data_keys)+" properties", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'gui-scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, rule='additionalProperties')
     try:
         try:
             data_is_dict = isinstance(data, dict)
             if data_is_dict:
                 data_len = len(data)
                 if not all(prop in data for prop in ['version']):
-                    raise JsonSchemaValueException("data must contain ['version'] properties", value=data, name="data", definition={'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, rule='required')
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + " must contain ['version'] properties", value=data, name="" + (name_prefix or "data") + "", definition={'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, rule='required')
         except JsonSchemaValueException: pass
         else:
-            raise JsonSchemaValueException("data must not be valid by not definition", value=data, name="data", definition={'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, rule='not')
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must NOT match a disallowed definition", value=data, name="" + (name_prefix or "data") + "", definition={'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, rule='not')
     except JsonSchemaValueException:
         pass
     else:
@@ -935,25 +935,25 @@ def validate_https___packaging_python_org_en_latest_specifications_declaring_pro
                     for data__dynamic_key in data__dynamic:
                         try:
                             if data__dynamic_key != "version":
-                                raise JsonSchemaValueException("data.dynamic must be same as const definition: version", value=data__dynamic_key, name="data.dynamic", definition={'const': 'version'}, rule='const')
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic must be same as const definition: version", value=data__dynamic_key, name="" + (name_prefix or "data") + ".dynamic", definition={'const': 'version'}, rule='const')
                             data__dynamic_contains = True
                             break
                         except JsonSchemaValueException: pass
                     if not data__dynamic_contains:
-                        raise JsonSchemaValueException("data.dynamic must contain one of contains definition", value=data__dynamic, name="data.dynamic", definition={'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}, rule='contains')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic must contain one of contains definition", value=data__dynamic, name="" + (name_prefix or "data") + ".dynamic", definition={'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}, rule='contains')
     return data
 
-def validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_dependency(data, custom_formats={}):
+def validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_dependency(data, custom_formats={}, name_prefix=None):
     if not isinstance(data, (str)):
-        raise JsonSchemaValueException("data must be string", value=data, name="data", definition={'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}, rule='type')
+        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be string", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}, rule='type')
     if isinstance(data, str):
         if not custom_formats["pep508"](data):
-            raise JsonSchemaValueException("data must be pep508", value=data, name="data", definition={'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}, rule='format')
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must be pep508", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}, rule='format')
     return data
 
-def validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_entry_point_group(data, custom_formats={}):
+def validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_entry_point_group(data, custom_formats={}, name_prefix=None):
     if not isinstance(data, (dict)):
-        raise JsonSchemaValueException("data must be object", value=data, name="data", definition={'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, rule='type')
+        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, rule='type')
     data_is_dict = isinstance(data, dict)
     if data_is_dict:
         data_keys = set(data.keys())
@@ -962,12 +962,12 @@ def validate_https___packaging_python_org_en_latest_specifications_declaring_pro
                 if data_key in data_keys:
                     data_keys.remove(data_key)
                 if not isinstance(data_val, (str)):
-                    raise JsonSchemaValueException(""+"data.{data_key}".format(**locals())+" must be string", value=data_val, name=""+"data.{data_key}".format(**locals())+"", definition={'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}, rule='type')
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".{data_key}".format(**locals()) + " must be string", value=data_val, name="" + (name_prefix or "data") + ".{data_key}".format(**locals()) + "", definition={'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}, rule='type')
                 if isinstance(data_val, str):
                     if not custom_formats["python-entrypoint-reference"](data_val):
-                        raise JsonSchemaValueException(""+"data.{data_key}".format(**locals())+" must be python-entrypoint-reference", value=data_val, name=""+"data.{data_key}".format(**locals())+"", definition={'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}, rule='format')
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".{data_key}".format(**locals()) + " must be python-entrypoint-reference", value=data_val, name="" + (name_prefix or "data") + ".{data_key}".format(**locals()) + "", definition={'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}, rule='format')
         if data_keys:
-            raise JsonSchemaValueException("data must not contain "+str(data_keys)+" properties", value=data, name="data", definition={'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, rule='additionalProperties')
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must not contain "+str(data_keys)+" properties", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, rule='additionalProperties')
         data_len = len(data)
         if data_len != 0:
             data_property_names = True
@@ -975,16 +975,16 @@ def validate_https___packaging_python_org_en_latest_specifications_declaring_pro
                 try:
                     if isinstance(data_key, str):
                         if not custom_formats["python-entrypoint-name"](data_key):
-                            raise JsonSchemaValueException("data must be python-entrypoint-name", value=data_key, name="data", definition={'format': 'python-entrypoint-name'}, rule='format')
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + " must be python-entrypoint-name", value=data_key, name="" + (name_prefix or "data") + "", definition={'format': 'python-entrypoint-name'}, rule='format')
                 except JsonSchemaValueException:
                     data_property_names = False
             if not data_property_names:
-                raise JsonSchemaValueException("data must be named by propertyName definition", value=data, name="data", definition={'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, rule='propertyNames')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + " must be named by propertyName definition", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, rule='propertyNames')
     return data
 
-def validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_author(data, custom_formats={}):
+def validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_author(data, custom_formats={}, name_prefix=None):
     if not isinstance(data, (dict)):
-        raise JsonSchemaValueException("data must be object", value=data, name="data", definition={'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, rule='type')
+        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, rule='type')
     data_is_dict = isinstance(data, dict)
     if data_is_dict:
         data_keys = set(data.keys())
@@ -992,13 +992,13 @@ def validate_https___packaging_python_org_en_latest_specifications_declaring_pro
             data_keys.remove("name")
             data__name = data["name"]
             if not isinstance(data__name, (str)):
-                raise JsonSchemaValueException("data.name must be string", value=data__name, name="data.name", definition={'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".name must be string", value=data__name, name="" + (name_prefix or "data") + ".name", definition={'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, rule='type')
         if "email" in data_keys:
             data_keys.remove("email")
             data__email = data["email"]
             if not isinstance(data__email, (str)):
-                raise JsonSchemaValueException("data.email must be string", value=data__email, name="data.email", definition={'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".email must be string", value=data__email, name="" + (name_prefix or "data") + ".email", definition={'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}, rule='type')
             if isinstance(data__email, str):
                 if not REGEX_PATTERNS["idn-email_re_pattern"].match(data__email):
-                    raise JsonSchemaValueException("data.email must be idn-email", value=data__email, name="data.email", definition={'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}, rule='format')
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".email must be idn-email", value=data__email, name="" + (name_prefix or "data") + ".email", definition={'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}, rule='format')
     return data
\ No newline at end of file
diff --git a/setuptools/_vendor/_validate_pyproject/formats.py b/setuptools/_vendor/_validate_pyproject/formats.py
index 8ab8596c..af5fc90e 100644
--- a/setuptools/_vendor/_validate_pyproject/formats.py
+++ b/setuptools/_vendor/_validate_pyproject/formats.py
@@ -1,8 +1,9 @@
 import logging
+import os
 import re
 import string
-from itertools import chain
-from urllib.parse import urlparse
+import typing
+from itertools import chain as _chain
 
 _logger = logging.getLogger(__name__)
 
@@ -101,7 +102,7 @@ def pep508_versionspec(value: str) -> bool:
 
 def pep517_backend_reference(value: str) -> bool:
     module, _, obj = value.partition(":")
-    identifiers = (i.strip() for i in chain(module.split("."), obj.split(".")))
+    identifiers = (i.strip() for i in _chain(module.split("."), obj.split(".")))
     return all(python_identifier(i) for i in identifiers if i)
 
 
@@ -109,6 +110,60 @@ def pep517_backend_reference(value: str) -> bool:
 # Classifiers - PEP 301
 
 
+def _download_classifiers() -> str:
+    import cgi
+    from urllib.request import urlopen
+
+    url = "https://pypi.org/pypi?:action=list_classifiers"
+    with urlopen(url) as response:
+        content_type = response.getheader("content-type", "text/plain")
+        encoding = cgi.parse_header(content_type)[1].get("charset", "utf-8")
+        return response.read().decode(encoding)
+
+
+class _TroveClassifier:
+    """The ``trove_classifiers`` package is the official way of validating classifiers,
+    however this package might not be always available.
+    As a workaround we can still download a list from PyPI.
+    We also don't want to be over strict about it, so simply skipping silently is an
+    option (classifiers will be validated anyway during the upload to PyPI).
+    """
+
+    def __init__(self):
+        self.downloaded: typing.Union[None, False, typing.Set[str]] = None
+        # None => not cached yet
+        # False => cache not available
+        self.__name__ = "trove_classifier"  # Emulate a public function
+
+    def __call__(self, value: str) -> bool:
+        if self.downloaded is False:
+            return True
+
+        if os.getenv("NO_NETWORK"):
+            self.downloaded = False
+            msg = (
+                "Install ``trove-classifiers`` to ensure proper validation. "
+                "Skipping download of classifiers list from PyPI (NO_NETWORK)."
+            )
+            _logger.debug(msg)
+            return True
+
+        if self.downloaded is None:
+            msg = (
+                "Install ``trove-classifiers`` to ensure proper validation. "
+                "Meanwhile a list of classifiers will be downloaded from PyPI."
+            )
+            _logger.debug(msg)
+            try:
+                self.downloaded = set(_download_classifiers().splitlines())
+            except Exception:
+                self.downloaded = False
+                _logger.debug("Problem with download, skipping validation")
+                return True
+
+        return value in self.downloaded
+
+
 try:
     from trove_classifiers import classifiers as _trove_classifiers
 
@@ -116,18 +171,6 @@ try:
         return value in _trove_classifiers
 
 except ImportError:  # pragma: no cover
-
-    class _TroveClassifier:
-        def __init__(self):
-            self._warned = False
-            self.__name__ = "trove-classifier"
-
-        def __call__(self, value: str) -> bool:
-            if self._warned is False:
-                self._warned = True
-                _logger.warning("Install ``trove-classifiers`` to ensure validation.")
-            return True
-
     trove_classifier = _TroveClassifier()
 
 
@@ -136,10 +179,20 @@ except ImportError:  # pragma: no cover
 
 
 def url(value: str) -> bool:
+    from urllib.parse import urlparse
+
     try:
         parts = urlparse(value)
+        if not parts.scheme:
+            _logger.warning(
+                "For maximum compatibility please make sure to include a "
+                "`scheme` prefix in your URL (e.g. 'http://'). "
+                f"Given value: {value}"
+            )
+            if not (value.startswith("/") or value.startswith("\\") or "@" in value):
+                parts = urlparse(f"http://{value}")
+
         return bool(parts.scheme and parts.netloc)
-        # ^  TODO: should we enforce schema to be http(s)?
     except Exception:
         return False
 
@@ -182,8 +235,6 @@ def python_entrypoint_name(value: str) -> bool:
 
 
 def python_entrypoint_reference(value: str) -> bool:
-    if ":" not in value:
-        return False
     module, _, rest = value.partition(":")
     if "[" in rest:
         obj, _, extras_ = rest.partition("[")
@@ -196,5 +247,6 @@ def python_entrypoint_reference(value: str) -> bool:
     else:
         obj = rest
 
-    identifiers = chain(module.split("."), obj.split("."))
+    module_parts = module.split(".")
+    identifiers = _chain(module_parts, obj.split(".")) if rest else module_parts
     return all(python_identifier(i.strip()) for i in identifiers)
diff --git a/setuptools/_vendor/vendored.txt b/setuptools/_vendor/vendored.txt
index 1a71366d..2ef8c6c2 100644
--- a/setuptools/_vendor/vendored.txt
+++ b/setuptools/_vendor/vendored.txt
@@ -10,4 +10,4 @@ typing_extensions==4.0.1
 # required for importlib_resources and _metadata on older Pythons
 zipp==3.7.0
 tomli==2.0.1
-# validate-pyproject[all]==0.4  # Special handling in tools/vendored, don't uncomment or remove
+# validate-pyproject[all]==0.5.2  # Special handling in tools/vendored, don't uncomment or remove
-- 
cgit v1.2.1


From af187e8fc56617a5b97deeaff6173aaee3355016 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 3 Dec 2021 15:54:12 +0000
Subject: Implement read_configuration from pyproject.toml

This is the first step towards making setuptools understand
`pyproject.toml` as a configuration file.

The implementation deliberately allows splitting the act of loading the
configuration from a file in 2 stages: the reading of the file itself
and the expansion of directives (and other derived information).
---
 setuptools/config/pyprojecttoml.py            | 195 ++++++++++++++++++++++++++
 setuptools/tests/config/test_pyprojecttoml.py | 103 ++++++++++++++
 2 files changed, 298 insertions(+)
 create mode 100644 setuptools/config/pyprojecttoml.py
 create mode 100644 setuptools/tests/config/test_pyprojecttoml.py

diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
new file mode 100644
index 00000000..1228e324
--- /dev/null
+++ b/setuptools/config/pyprojecttoml.py
@@ -0,0 +1,195 @@
+"""Load setuptools configuration from ``pyproject.toml`` files"""
+import os
+import sys
+from contextlib import contextmanager
+from functools import partial
+from typing import Union
+import json
+
+from setuptools.errors import OptionError, FileError
+from distutils import log
+
+from . import expand as _expand
+
+_Path = Union[str, os.PathLike]
+
+
+def load_file(filepath: _Path):
+    try:
+        from setuptools.extern import tomli
+    except ImportError:  # Bootstrap problem (?) diagnosed by test_distutils_adoption
+        sys_path = sys.path.copy()
+        try:
+            from setuptools import _vendor
+            sys.path.append(_vendor.__path__[0])
+            import tomli
+        finally:
+            sys.path = sys_path
+
+    with open(filepath, "rb") as file:
+        return tomli.load(file)
+
+
+def validate(config: dict, filepath: _Path):
+    from setuptools.extern import _validate_pyproject
+    from setuptools.extern._validate_pyproject import fastjsonschema_exceptions
+
+    try:
+        return _validate_pyproject.validate(config)
+    except fastjsonschema_exceptions.JsonSchemaValueException as ex:
+        msg = [f"Schema: {ex}"]
+        if ex.value:
+            msg.append(f"Given value:\n{json.dumps(ex.value, indent=2)}")
+        if ex.rule:
+            msg.append(f"Offending rule: {json.dumps(ex.rule, indent=2)}")
+        if ex.definition:
+            msg.append(f"Definition:\n{json.dumps(ex.definition, indent=2)}")
+
+        log.error("\n\n".join(msg) + "\n")
+        raise
+
+
+def read_configuration(filepath, expand=True, ignore_option_errors=False):
+    """Read given configuration file and returns options from it as a dict.
+
+    :param str|unicode filepath: Path to configuration file in the ``pyproject.toml``
+        format.
+
+    :param bool expand: Whether to expand directives and other computed values
+        (i.e. post-process the given configuration)
+
+    :param bool ignore_option_errors: Whether to silently ignore
+        options, values of which could not be resolved (e.g. due to exceptions
+        in directives such as file:, attr:, etc.).
+        If False exceptions are propagated as expected.
+
+    :rtype: dict
+    """
+    filepath = os.path.abspath(filepath)
+
+    if not os.path.isfile(filepath):
+        raise FileError(f"Configuration file {filepath!r} does not exist.")
+
+    asdict = load_file(filepath) or {}
+    project_table = asdict.get("project")
+    tool_table = asdict.get("tool", {}).get("setuptools")
+    if not asdict or not(project_table or tool_table):
+        return {}  # User is not using pyproject to configure setuptools
+
+    with _ignore_errors(ignore_option_errors):
+        validate(asdict, filepath)
+
+    if expand:
+        root_dir = os.path.dirname(filepath)
+        return expand_configuration(asdict, root_dir, ignore_option_errors)
+
+    return asdict
+
+
+def expand_configuration(config, root_dir=None, ignore_option_errors=False):
+    """Given a configuration with unresolved fields (e.g. dynamic, cmdclass, ...)
+    find their final values.
+
+    :param dict config: Dict containing the configuration for the distribution
+    :param str root_dir: Top-level directory for the distribution/project
+        (the same directory where ``pyproject.toml`` is place)
+    :param bool ignore_option_errors: see :func:`read_configuration`
+
+    :rtype: dict
+    """
+    root_dir = root_dir or os.getcwd()
+    project_cfg = config.get("project", {})
+    setuptools_cfg = config.get("tool", {}).get("setuptools", {})
+    package_dir = setuptools_cfg.get("package-dir")
+
+    _expand_all_dynamic(project_cfg, setuptools_cfg, root_dir, ignore_option_errors)
+    _expand_packages(setuptools_cfg, root_dir, ignore_option_errors)
+    _canonic_package_data(setuptools_cfg)
+    _canonic_package_data(setuptools_cfg, "exclude-package-data")
+
+    process = partial(_process_field, ignore_option_errors=ignore_option_errors)
+    cmdclass = partial(_expand.cmdclass, package_dir=package_dir, root_dir=root_dir)
+    data_files = partial(_expand.canonic_data_files, root_dir=root_dir)
+    process(setuptools_cfg, "data-files", data_files)
+    process(setuptools_cfg, "cmdclass", cmdclass)
+
+    return config
+
+
+def _expand_all_dynamic(project_cfg, setuptools_cfg, root_dir, ignore_option_errors):
+    silent = ignore_option_errors
+    dynamic_cfg = setuptools_cfg.get("dynamic", {})
+    package_dir = setuptools_cfg.get("package-dir", None)
+    special = ("license", "readme", "version", "entry-points", "scripts", "gui-scripts")
+    # license-files are handled directly in the metadata, so no expansion
+    # readme, version and entry-points need special handling
+    dynamic = project_cfg.get("dynamic", [])
+    regular_dynamic = (x for x in dynamic if x not in special)
+
+    for field in regular_dynamic:
+        value = _expand_dynamic(dynamic_cfg, field, package_dir, root_dir, silent)
+        project_cfg[field] = value
+
+    if "version" in dynamic and "version" in dynamic_cfg:
+        version = _expand_dynamic(dynamic_cfg, "version", package_dir, root_dir, silent)
+        project_cfg["version"] = _expand.version(version)
+
+    if "readme" in dynamic:
+        project_cfg["readme"] = _expand_readme(dynamic_cfg, root_dir, silent)
+
+
+def _expand_dynamic(dynamic_cfg, field, package_dir, root_dir, ignore_option_errors):
+    if field in dynamic_cfg:
+        directive = dynamic_cfg[field]
+        if "file" in directive:
+            return _expand.read_files(directive["file"], root_dir)
+        if "attr" in directive:
+            return _expand.read_attr(directive["attr"], package_dir, root_dir)
+    elif not ignore_option_errors:
+        msg = f"Impossible to expand dynamic value of {field!r}. "
+        msg += f"No configuration found for `tool.setuptools.dynamic.{field}`"
+        raise OptionError(msg)
+    return None
+
+
+def _expand_readme(dynamic_cfg, root_dir, ignore_option_errors):
+    silent = ignore_option_errors
+    return {
+        "text": _expand_dynamic(dynamic_cfg, "readme", None, root_dir, silent),
+        "content-type": dynamic_cfg["readme"].get("content-type", "text/x-rst")
+    }
+
+
+def _expand_packages(setuptools_cfg, root_dir, ignore_option_errors=False):
+    packages = setuptools_cfg.get("packages")
+    if packages is None or isinstance(packages, (list, tuple)):
+        return
+
+    find = packages.get("find")
+    if isinstance(find, dict):
+        find["root_dir"] = root_dir
+        with _ignore_errors(ignore_option_errors):
+            setuptools_cfg["packages"] = _expand.find_packages(**find)
+
+
+def _process_field(container, field, fn, ignore_option_errors=False):
+    if field in container:
+        with _ignore_errors(ignore_option_errors):
+            container[field] = fn(container[field])
+
+
+def _canonic_package_data(setuptools_cfg, field="package-data"):
+    package_data = setuptools_cfg.get(field, {})
+    return _expand.canonic_package_data(package_data)
+
+
+@contextmanager
+def _ignore_errors(ignore_option_errors):
+    if not ignore_option_errors:
+        yield
+        return
+
+    try:
+        yield
+    except Exception as ex:
+        log.debug(f"Ignored error: {ex.__class__.__name__} - {ex}")
diff --git a/setuptools/tests/config/test_pyprojecttoml.py b/setuptools/tests/config/test_pyprojecttoml.py
new file mode 100644
index 00000000..7e0ee2b3
--- /dev/null
+++ b/setuptools/tests/config/test_pyprojecttoml.py
@@ -0,0 +1,103 @@
+import os
+
+from setuptools.config.pyprojecttoml import read_configuration, expand_configuration
+
+EXAMPLE = """
+[project]
+name = "myproj"
+keywords = ["some", "key", "words"]
+dynamic = ["version", "readme"]
+requires-python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+dependencies = [
+    'importlib-metadata>=0.12;python_version<"3.8"',
+    'importlib-resources>=1.0;python_version<"3.7"',
+    'pathlib2>=2.3.3,<3;python_version < "3.4" and sys.platform != "win32"',
+]
+
+[project.optional-dependencies]
+docs = [
+    "sphinx>=3",
+    "sphinx-argparse>=0.2.5",
+    "sphinx-rtd-theme>=0.4.3",
+]
+testing = [
+    "pytest>=1",
+    "coverage>=3,<5",
+]
+
+[project.scripts]
+exec = "pkg.__main__:exec"
+
+[build-system]
+requires = ["setuptools", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools]
+package-dir = {"" = "src"}
+zip-safe = true
+platforms = ["any"]
+
+[tool.setuptools.packages.find]
+where = ["src"]
+namespaces = true
+
+[tool.setuptools.cmdclass]
+sdist = "pkg.mod.CustomSdist"
+
+[tool.setuptools.dynamic.version]
+attr = "pkg.__version__.VERSION"
+
+[tool.setuptools.dynamic.readme]
+file = ["README.md"]
+content-type = "text/markdown"
+
+[tool.setuptools.package-data]
+"*" = ["*.txt"]
+
+[tool.setuptools.data-files]
+"data" = ["files/*.txt"]
+
+[tool.distutils.sdist]
+formats = "gztar"
+
+[tool.distutils.bdist_wheel]
+universal = true
+"""
+
+
+def test_read_configuration(tmp_path):
+    pyproject = tmp_path / "pyproject.toml"
+
+    files = [
+        "src/pkg/__init__.py",
+        "src/other/nested/__init__.py",
+        "files/file.txt"
+    ]
+    for file in files:
+        (tmp_path / file).parent.mkdir(exist_ok=True, parents=True)
+        (tmp_path / file).touch()
+
+    pyproject.write_text(EXAMPLE)
+    (tmp_path / "README.md").write_text("hello world")
+    (tmp_path / "src/pkg/mod.py").write_text("class CustomSdist: pass")
+    (tmp_path / "src/pkg/__version__.py").write_text("VERSION = (3, 10)")
+    (tmp_path / "src/pkg/__main__.py").write_text("def exec(): print('hello')")
+
+    config = read_configuration(pyproject, expand=False)
+    assert config["project"].get("version") is None
+    assert config["project"].get("readme") is None
+
+    expanded = expand_configuration(config, tmp_path)
+    assert read_configuration(pyproject, expand=True) == expanded
+    assert expanded["project"]["version"] == "3.10"
+    assert expanded["project"]["readme"]["text"] == "hello world"
+    assert set(expanded["tool"]["setuptools"]["packages"]) == {
+        "pkg",
+        "other",
+        "other.nested",
+    }
+    assert "" in expanded["tool"]["setuptools"]["package-data"]
+    assert "*" not in expanded["tool"]["setuptools"]["package-data"]
+    assert expanded["tool"]["setuptools"]["data-files"] == [
+        ("data", ["files/file.txt"])
+    ]
-- 
cgit v1.2.1


From 8826dc10a574fe9b7d61fd18be4e0b27d83eb033 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 9 Dec 2021 11:32:10 +0000
Subject: Expand dynamic entry_points from pyproject.toml

The user might specify dynamic `entry-points` via a `file:`
directive (a similar feature for `setup.cfg` is documented in
[declarative config]).

The changes introduced here add the ability to expand them
when reading the configuration from `pyproject.toml`.

[declarative config]: https://setuptools.pypa.io/en/latest/userguide/declarative_config.html
---
 setuptools/config/expand.py                   | 16 +++++++++++
 setuptools/config/pyprojecttoml.py            | 15 ++++++++++
 setuptools/tests/config/test_pyprojecttoml.py | 41 +++++++++++++++++++++++++--
 3 files changed, 69 insertions(+), 3 deletions(-)

diff --git a/setuptools/config/expand.py b/setuptools/config/expand.py
index feb55be1..4778ffb6 100644
--- a/setuptools/config/expand.py
+++ b/setuptools/config/expand.py
@@ -21,6 +21,7 @@ import io
 import os
 import sys
 from glob import iglob
+from configparser import ConfigParser
 from itertools import chain
 
 from distutils.errors import DistutilsOptionError
@@ -292,3 +293,18 @@ def canonic_data_files(data_files, root_dir=None):
         (dest, glob_relative(patterns, root_dir))
         for dest, patterns in data_files.items()
     ]
+
+
+def entry_points(text, text_source="entry-points"):
+    """Given the contents of entry-points file,
+    process it into a 2-level dictionary (``dict[str, dict[str, str]]``).
+    The first level keys are entry-point groups, the second level keys are
+    entry-point names, and the second level values are references to objects
+    (that correspond to the entry-point value).
+    """
+    parser = ConfigParser(default_section=None, delimiters=("=",))
+    parser.optionxform = str  # case sensitive
+    parser.read_string(text, text_source)
+    groups = {k: dict(v.items()) for k, v in parser.items()}
+    groups.pop(parser.default_section, None)
+    return groups
diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index 1228e324..4923d929 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -137,6 +137,11 @@ def _expand_all_dynamic(project_cfg, setuptools_cfg, root_dir, ignore_option_err
     if "readme" in dynamic:
         project_cfg["readme"] = _expand_readme(dynamic_cfg, root_dir, silent)
 
+    if "entry-points" in dynamic:
+        field = "entry-points"
+        value = _expand_dynamic(dynamic_cfg, field, package_dir, root_dir, silent)
+        project_cfg.update(_expand_entry_points(value, dynamic))
+
 
 def _expand_dynamic(dynamic_cfg, field, package_dir, root_dir, ignore_option_errors):
     if field in dynamic_cfg:
@@ -160,6 +165,16 @@ def _expand_readme(dynamic_cfg, root_dir, ignore_option_errors):
     }
 
 
+def _expand_entry_points(text, dynamic):
+    groups = _expand.entry_points(text)
+    expanded = {"entry-points": groups}
+    if "scripts" in dynamic and "console_scripts" in groups:
+        expanded["scripts"] = groups.pop("console_scripts")
+    if "gui-scripts" in dynamic and "gui_scripts" in groups:
+        expanded["gui-scripts"] = groups.pop("gui_scripts")
+    return expanded
+
+
 def _expand_packages(setuptools_cfg, root_dir, ignore_option_errors=False):
     packages = setuptools_cfg.get("packages")
     if packages is None or isinstance(packages, (list, tuple)):
diff --git a/setuptools/tests/config/test_pyprojecttoml.py b/setuptools/tests/config/test_pyprojecttoml.py
index 7e0ee2b3..fb0997da 100644
--- a/setuptools/tests/config/test_pyprojecttoml.py
+++ b/setuptools/tests/config/test_pyprojecttoml.py
@@ -1,4 +1,4 @@
-import os
+from configparser import ConfigParser
 
 from setuptools.config.pyprojecttoml import read_configuration, expand_configuration
 
@@ -88,9 +88,10 @@ def test_read_configuration(tmp_path):
     assert config["project"].get("readme") is None
 
     expanded = expand_configuration(config, tmp_path)
+    expanded_project = expanded["project"]
     assert read_configuration(pyproject, expand=True) == expanded
-    assert expanded["project"]["version"] == "3.10"
-    assert expanded["project"]["readme"]["text"] == "hello world"
+    assert expanded_project["version"] == "3.10"
+    assert expanded_project["readme"]["text"] == "hello world"
     assert set(expanded["tool"]["setuptools"]["packages"]) == {
         "pkg",
         "other",
@@ -101,3 +102,37 @@ def test_read_configuration(tmp_path):
     assert expanded["tool"]["setuptools"]["data-files"] == [
         ("data", ["files/file.txt"])
     ]
+
+
+ENTRY_POINTS = {
+    "console_scripts": {"a": "mod.a:func"},
+    "gui_scripts": {"b": "mod.b:func"},
+    "other": {"c": "mod.c:func [extra]"},
+}
+
+
+def test_expand_entry_point(tmp_path):
+    entry_points = ConfigParser()
+    entry_points.read_dict(ENTRY_POINTS)
+    with open(tmp_path / "entry-points.txt", "w") as f:
+        entry_points.write(f)
+
+    tool = {"setuptools": {"dynamic": {"entry-points": {"file": "entry-points.txt"}}}}
+    project = {"dynamic": ["scripts", "gui-scripts", "entry-points"]}
+    pyproject = {"project": project, "tool": tool}
+    expanded = expand_configuration(pyproject, tmp_path)
+    expanded_project = expanded["project"]
+    assert len(expanded_project["scripts"]) == 1
+    assert expanded_project["scripts"]["a"] == "mod.a:func"
+    assert len(expanded_project["gui-scripts"]) == 1
+    assert expanded_project["gui-scripts"]["b"] == "mod.b:func"
+    assert len(expanded_project["entry-points"]) == 1
+    assert expanded_project["entry-points"]["other"]["c"] == "mod.c:func [extra]"
+
+    project = {"dynamic": ["entry-points"]}
+    pyproject = {"project": project, "tool": tool}
+    expanded = expand_configuration(pyproject, tmp_path)
+    expanded_project = expanded["project"]
+    assert len(expanded_project["entry-points"]) == 3
+    assert "scripts" not in expanded_project
+    assert "gui-scripts" not in expanded_project
-- 
cgit v1.2.1


From a8112d962d3908196d352b5d8f0d03e45645037e Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 22 Dec 2021 18:52:06 +0000
Subject: Make include_package_data=True for `pyproject.toml` configs

There is frequent an opinion in the community that
`include_package_data=True` is a better default
(and a quality of life improvement).

Since we are migrating to a new configuration file, this change can
be implemented in a backward compatible way
(to avoid breaking existing packages):

- Config from `setup.cfg` defaults to `include_package_data=False`
- Config from `pyproject.toml` defaults to `include_package_data=True`

This also takes advantage that `ini2toml` (the provided library for
automatic conversion between `setup.cfg` and `pyproject.toml`) will
backfill `include_package_data=False` when the field is missing.
---
 setuptools/config/pyprojecttoml.py | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index 4923d929..8029847e 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -76,6 +76,12 @@ def read_configuration(filepath, expand=True, ignore_option_errors=False):
     if not asdict or not(project_table or tool_table):
         return {}  # User is not using pyproject to configure setuptools
 
+    # There is an overall sense in the community that making include_package_data=True
+    # the default would be an improvement.
+    # `ini2toml` backfills include_package_data=False when nothing is explicitly given,
+    # therefore setting a default here is backwards compatible.
+    tool_table.setdefault("include-package-data", True)
+
     with _ignore_errors(ignore_option_errors):
         validate(asdict, filepath)
 
-- 
cgit v1.2.1


From 9672a4883fdb0e24e913d076d01aa9d87bcc6ba1 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 23 Dec 2021 01:19:42 +0000
Subject: Add means of applying config read from pyproject.toml to dist

Since the Distrubition and DistributionMetadata classes are modeled
after (an old version of) core metadata, it is necessary to add a
translation layer between them and the configuration read from
pyproject.toml
---
 setuptools/config/_apply_pyprojecttoml.py | 236 ++++++++++++++++++++++++++++++
 setuptools/config/pyprojecttoml.py        |  20 ++-
 2 files changed, 252 insertions(+), 4 deletions(-)
 create mode 100644 setuptools/config/_apply_pyprojecttoml.py

diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py
new file mode 100644
index 00000000..4dddd09d
--- /dev/null
+++ b/setuptools/config/_apply_pyprojecttoml.py
@@ -0,0 +1,236 @@
+"""Translation layer between pyproject config and setuptools distribution and
+metadata objects.
+
+The distribution and metadata objects are modeled after (an old version of)
+core metadata, therefore configs in the format specified for ``pyproject.toml``
+need to be processed before being applied.
+"""
+import os
+from collections.abc import Mapping
+from email.headerregistry import Address
+from functools import partial
+from itertools import chain
+from types import MappingProxyType
+from typing import (TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Tuple,
+                    Type, Union)
+
+if TYPE_CHECKING:
+    from pkg_resources import EntryPoint  # noqa
+    from setuptools.dist import Distribution  # noqa
+
+EMPTY = MappingProxyType({})  # Immutable dict-like
+_Path = Union[os.PathLike, str]
+_DictOrStr = Union[dict, str]
+_CorrespFn = Callable[["Distribution", Any, _Path], None]
+_Correspondence = Union[str, _CorrespFn]
+
+
+def apply(dist: "Distribution", config: dict, filename: _Path) -> "Distribution":
+    """Apply configuration dict read with :func:`read_configuration`"""
+
+    root_dir = os.path.dirname(filename) or "."
+    tool_table = config.get("tool", {}).get("setuptools", {})
+    project_table = config.get("project", {}).copy()
+    _unify_entry_points(project_table)
+    _dynamic_license(project_table, tool_table)
+    for field, value in project_table.items():
+        norm_key = json_compatible_key(field)
+        corresp = PYPROJECT_CORRESPONDENCE.get(norm_key, norm_key)
+        if callable(corresp):
+            corresp(dist, value, root_dir)
+        else:
+            _set_config(dist, corresp, value)
+
+    for field, value in tool_table.items():
+        norm_key = json_compatible_key(field)
+        norm_key = TOOL_TABLE_RENAMES.get(norm_key, norm_key)
+        _set_config(dist, norm_key, value)
+
+    _copy_command_options(config, dist, filename)
+
+    current_directory = os.getcwd()
+    os.chdir(root_dir)
+    try:
+        dist._finalize_requires()
+        dist._finalize_license_files()
+    finally:
+        os.chdir(current_directory)
+
+    return dist
+
+
+def json_compatible_key(key: str) -> str:
+    """As defined in :pep:`566#json-compatible-metadata`"""
+    return key.lower().replace("-", "_")
+
+
+def _set_config(dist: "Distribution", field: str, value: Any):
+    setter = getattr(dist.metadata, f"set_{field}", None)
+    if setter:
+        setter(value)
+    elif hasattr(dist.metadata, field) or field in SETUPTOOLS_PATCHES:
+        setattr(dist.metadata, field, value)
+    else:
+        setattr(dist, field, value)
+
+
+def _long_description(dist: "Distribution", val: _DictOrStr, root_dir: _Path):
+    from setuptools.config import expand
+
+    if isinstance(val, str):
+        text = expand.read_files(val, root_dir)
+        ctype = "text/x-rst"
+    else:
+        text = val.get("text") or expand.read_files(val.get("file", []), root_dir)
+        ctype = val["content-type"]
+
+    _set_config(dist, "long_description", text)
+    _set_config(dist, "long_description_content_type", ctype)
+
+
+def _license(dist: "Distribution", val: Union[str, dict], _root_dir):
+    if isinstance(val, str):
+        _set_config(dist, "license", val)
+    elif "file" in val:
+        _set_config(dist, "license_files", [val["file"]])
+    else:
+        _set_config(dist, "license", val["text"])
+
+
+def _people(dist: "Distribution", val: List[dict], _root_dir: _Path, kind: str):
+    field = []
+    email_field = []
+    for person in val:
+        if "name" not in person:
+            email_field.append(person["email"])
+        elif "email" not in person:
+            field.append(person["name"])
+        else:
+            addr = Address(display_name=person["name"], addr_spec=person["email"])
+            email_field.append(str(addr))
+
+    if field:
+        _set_config(dist, kind, ", ".join(field))
+    if email_field:
+        _set_config(dist, f"{kind}_email", ", ".join(email_field))
+
+
+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)
+    _set_config(dist, "project_urls", val.copy())
+
+
+def _python_requires(dist: "Distribution", val: dict, _root_dir):
+    from setuptools.extern.packaging.specifiers import SpecifierSet
+
+    _set_config(dist, "python_requires", SpecifierSet(val))
+
+
+def _dynamic_license(project_table: dict, tool_table: dict):
+    # Dynamic license needs special handling (cannot be expanded in terms of PEP 621)
+    # due to the mutually exclusive `text` and `file`
+    dynamic_license = {"license", "license_files"}
+    dynamic = {json_compatible_key(k) for k in project_table.get("dynamic", [])}
+    dynamic_cfg = tool_table.get("dynamic", {})
+    dynamic_cfg.setdefault("license_files", DEFAULT_LICENSE_FILES)
+    keys = set(dynamic_cfg) & dynamic_license if "license" in dynamic else set()
+
+    for key in keys:
+        norm_key = json_compatible_key(key)
+        project_table[norm_key] = dynamic_cfg[key]
+
+
+def _unify_entry_points(project_table: dict):
+    project = project_table
+    entry_points = project.pop("entry-points", project.pop("entry_points", {}))
+    renaming = {"scripts": "console_scripts", "gui_scripts": "gui_scripts"}
+    for key, value in list(project.items()):  # eager to allow modifications
+        norm_key = json_compatible_key(key)
+        if norm_key in renaming and value:
+            entry_points[renaming[norm_key]] = project.pop(key)
+
+    if entry_points:
+        project["entry-points"] = {
+            name: [f"{k} = {v}" for k, v in group.items()]
+            for name, group in entry_points.items()
+        }
+
+
+def _copy_command_options(pyproject: dict, dist: "Distribution", filename: _Path):
+    from distutils import log
+
+    tool_table = pyproject.get("tool", {})
+    cmdclass = tool_table.get("setuptools", {}).get("cmdclass", {})
+    valid_options = _valid_command_options(cmdclass)
+
+    cmd_opts = dist.command_options
+    for cmd, config in pyproject.get("tool", {}).get("distutils", {}).items():
+        cmd = json_compatible_key(cmd)
+        valid = valid_options.get(cmd, set())
+        cmd_opts.setdefault(cmd, {})
+        for key, value in config.items():
+            key = json_compatible_key(key)
+            cmd_opts[cmd][key] = (str(filename), value)
+            if key not in valid:
+                # To avoid removing options that are specified dynamically we
+                # just log a warn...
+                log.warn(f"Command option {cmd}.{key} is not defined")
+
+
+def _valid_command_options(cmdclass: Mapping = EMPTY) -> Dict[str, Set[str]]:
+    from pkg_resources import iter_entry_points
+    from setuptools.dist import Distribution
+
+    valid_options = {"global": _normalise_cmd_options(Distribution.global_options)}
+
+    entry_points = (_load_ep(ep) for ep in iter_entry_points('distutils.commands'))
+    entry_points = (ep for ep in entry_points if ep)
+    for cmd, cmd_class in chain(entry_points, cmdclass.items()):
+        opts = valid_options.get(cmd, set())
+        opts = opts | _normalise_cmd_options(getattr(cmd_class, "user_options", []))
+        valid_options[cmd] = opts
+
+    return valid_options
+
+
+def _load_ep(ep: "EntryPoint") -> Optional[Tuple[str, Type]]:
+    # Ignore all the errors
+    try:
+        return (ep.name, ep.load())
+    except Exception as ex:
+        from distutils import log
+        msg = f"{ex.__class__.__name__} while trying to load entry-point {ep.name}"
+        log.warn(f"{msg}: {ex}")
+        return None
+
+
+def _normalise_cmd_option_key(name: str) -> str:
+    return json_compatible_key(name).strip("_=")
+
+
+def _normalise_cmd_options(desc: List[Tuple[str, Optional[str], str]]) -> Set[str]:
+    return {_normalise_cmd_option_key(fancy_option[0]) for fancy_option in desc}
+
+
+PYPROJECT_CORRESPONDENCE: Dict[str, _Correspondence] = {
+    "readme": _long_description,
+    "license": _license,
+    "authors": partial(_people, kind="author"),
+    "maintainers": partial(_people, kind="maintainer"),
+    "urls": _project_urls,
+    "dependencies": "install_requires",
+    "optional_dependencies": "extras_require",
+    "requires_python": _python_requires,
+}
+
+TOOL_TABLE_RENAMES = {"script_files": "scripts"}
+
+SETUPTOOLS_PATCHES = {"long_description_content_type", "project_urls",
+                      "provides_extras", "license_file", "license_files"}
+
+
+DEFAULT_LICENSE_FILES = ('LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*')
+# defaults from the `wheel` package and historically used by setuptools
diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index 8029847e..8ce69e21 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -1,15 +1,19 @@
 """Load setuptools configuration from ``pyproject.toml`` files"""
+import json
 import os
 import sys
 from contextlib import contextmanager
+from distutils import log
 from functools import partial
-from typing import Union
-import json
+from typing import TYPE_CHECKING, Union
 
-from setuptools.errors import OptionError, FileError
-from distutils import log
+from setuptools.errors import FileError, OptionError
 
 from . import expand as _expand
+from ._apply_pyprojecttoml import apply
+
+if TYPE_CHECKING:
+    from setuptools.dist import Distribution  # noqa
 
 _Path = Union[str, os.PathLike]
 
@@ -49,6 +53,14 @@ def validate(config: dict, filepath: _Path):
         raise
 
 
+def apply_configuration(dist: "Distribution", filepath: _Path) -> "Distribution":
+    """Apply the configuration from a ``pyproject.toml`` file into an existing
+    distribution object.
+    """
+    config = read_configuration(filepath)
+    return apply(dist, config, filepath)
+
+
 def read_configuration(filepath, expand=True, ignore_option_errors=False):
     """Read given configuration file and returns options from it as a dict.
 
-- 
cgit v1.2.1


From d7363d5458b34e313567c73a55a5ac514dd73241 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 23 Dec 2021 01:22:35 +0000
Subject: Add the apply_configuration API to setuptools.config.setupcfg

The apply_configuration is implemented in a way that it is consistent
for both pyproject.toml and setup.cfg
---
 setuptools/config/setupcfg.py | 36 +++++++++++++++++++++++++-----------
 1 file changed, 25 insertions(+), 11 deletions(-)

diff --git a/setuptools/config/setupcfg.py b/setuptools/config/setupcfg.py
index 80cf4541..e4855a76 100644
--- a/setuptools/config/setupcfg.py
+++ b/setuptools/config/setupcfg.py
@@ -10,7 +10,8 @@ from functools import wraps
 from distutils.errors import DistutilsOptionError, DistutilsFileError
 from setuptools.extern.packaging.version import Version, InvalidVersion
 from setuptools.extern.packaging.specifiers import SpecifierSet
-from setuptools.config import expand
+
+from . import expand
 
 
 def read_configuration(filepath, find_others=False, ignore_option_errors=False):
@@ -29,7 +30,26 @@ def read_configuration(filepath, find_others=False, ignore_option_errors=False):
 
     :rtype: dict
     """
-    from setuptools.dist import Distribution, _Distribution
+    from setuptools.dist import Distribution
+
+    dist = Distribution()
+    filenames = dist.find_config_files() if find_others else []
+    handlers = _apply(dist, filepath, filenames, ignore_option_errors)
+    return configuration_to_dict(handlers)
+
+
+def apply_configuration(dist, filepath):
+    """Apply the configuration from a ``setup.cfg`` file into an existing
+    distribution object.
+    """
+    _apply(dist, filepath)
+    dist._finalize_requires()
+    return dist
+
+
+def _apply(dist, filepath, other_files=(), ignore_option_errors=False):
+    """Read configuration from ``filepath`` and applies to the ``dist`` object."""
+    from setuptools.dist import _Distribution
 
     filepath = os.path.abspath(filepath)
 
@@ -38,24 +58,18 @@ def read_configuration(filepath, find_others=False, ignore_option_errors=False):
 
     current_directory = os.getcwd()
     os.chdir(os.path.dirname(filepath))
+    filenames = [*other_files, filepath]
 
     try:
-        dist = Distribution()
-
-        filenames = dist.find_config_files() if find_others else []
-        if filepath not in filenames:
-            filenames.append(filepath)
-
         _Distribution.parse_config_files(dist, filenames=filenames)
-
         handlers = parse_configuration(
             dist, dist.command_options, ignore_option_errors=ignore_option_errors
         )
-
+        dist._finalize_license_files()
     finally:
         os.chdir(current_directory)
 
-    return configuration_to_dict(handlers)
+    return handlers
 
 
 def _get_option(target_obj, key):
-- 
cgit v1.2.1


From 26a9264d3815f5acfeac802fb8855a97ec1d3174 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 23 Dec 2021 01:29:34 +0000
Subject: Test pyproject.toml config has the same effect as setup.cfg

---
 setup.cfg                                          |   2 +-
 setuptools/tests/config/downloads/.gitignore       |   2 +
 setuptools/tests/config/setupcfg_examples.txt      |  23 ++++
 .../tests/config/test_apply_pyprojecttoml.py       | 117 +++++++++++++++++++++
 4 files changed, 143 insertions(+), 1 deletion(-)
 create mode 100644 setuptools/tests/config/downloads/.gitignore
 create mode 100644 setuptools/tests/config/setupcfg_examples.txt
 create mode 100644 setuptools/tests/config/test_apply_pyprojecttoml.py

diff --git a/setup.cfg b/setup.cfg
index 6171f624..9612e891 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -67,6 +67,7 @@ testing =
 	build[virtualenv]
 	filelock>=3.4.0
 	pip_run>=8.8
+	ini2toml[lite]>=0.6.1
 
 testing-integration =
 	pytest
@@ -80,7 +81,6 @@ testing-integration =
 	build[virtualenv]
 	filelock>=3.4.0
 
-
 docs =
 	# upstream
 	sphinx
diff --git a/setuptools/tests/config/downloads/.gitignore b/setuptools/tests/config/downloads/.gitignore
new file mode 100644
index 00000000..d6b7ef32
--- /dev/null
+++ b/setuptools/tests/config/downloads/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/setuptools/tests/config/setupcfg_examples.txt b/setuptools/tests/config/setupcfg_examples.txt
new file mode 100644
index 00000000..5db35654
--- /dev/null
+++ b/setuptools/tests/config/setupcfg_examples.txt
@@ -0,0 +1,23 @@
+# ====================================================================
+# Some popular packages that use setup.cfg (and others not so popular)
+# Reference: https://hugovk.github.io/top-pypi-packages/
+# ====================================================================
+https://github.com/pypa/setuptools/raw/52c990172fec37766b3566679724aa8bf70ae06d/setup.cfg
+https://github.com/pypa/wheel/raw/0acd203cd896afec7f715aa2ff5980a403459a3b/setup.cfg
+https://github.com/python/importlib_metadata/raw/2f05392ca980952a6960d82b2f2d2ea10aa53239/setup.cfg
+https://github.com/jaraco/skeleton/raw/d9008b5c510cd6969127a6a2ab6f832edddef296/setup.cfg
+https://github.com/jaraco/zipp/raw/700d3a96390e970b6b962823bfea78b4f7e1c537/setup.cfg
+https://github.com/pallets/jinja/raw/7d72eb7fefb7dce065193967f31f805180508448/setup.cfg
+https://github.com/tkem/cachetools/raw/2fd87a94b8d3861d80e9e4236cd480bfdd21c90d/setup.cfg
+https://github.com/aio-libs/aiohttp/raw/5e0e6b7080f2408d5f1dd544c0e1cf88378b7b10/setup.cfg
+https://github.com/pallets/flask/raw/9486b6cf57bd6a8a261f67091aca8ca78eeec1e3/setup.cfg
+https://github.com/pallets/click/raw/6411f425fae545f42795665af4162006b36c5e4a/setup.cfg
+https://github.com/sqlalchemy/sqlalchemy/raw/533f5718904b620be8d63f2474229945d6f8ba5d/setup.cfg
+https://github.com/pytest-dev/pluggy/raw/461ef63291d13589c4e21aa182cd1529257e9a0a/setup.cfg
+https://github.com/pytest-dev/pytest/raw/c7be96dae487edbd2f55b561b31b68afac1dabe6/setup.cfg
+https://github.com/tqdm/tqdm/raw/fc69d5dcf578f7c7986fa76841a6b793f813df35/setup.cfg
+https://github.com/platformdirs/platformdirs/raw/7b7852128dd6f07511b618d6edea35046bd0c6ff/setup.cfg
+https://github.com/pandas-dev/pandas/raw/bc17343f934a33dc231c8c74be95d8365537c376/setup.cfg
+https://github.com/django/django/raw/4e249d11a6e56ca8feb4b055b681cec457ef3a3d/setup.cfg
+https://github.com/pyscaffold/pyscaffold/raw/de7aa5dc059fbd04307419c667cc4961bc9df4b8/setup.cfg
+https://github.com/pypa/virtualenv/raw/f92eda6e3da26a4d28c2663ffb85c4960bdb990c/setup.cfg
diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py
new file mode 100644
index 00000000..f93d1db9
--- /dev/null
+++ b/setuptools/tests/config/test_apply_pyprojecttoml.py
@@ -0,0 +1,117 @@
+"""Make sure that applying the configuration from pyproject.toml is equivalent to
+applying a similar configuration from setup.cfg
+"""
+import io
+import re
+from pathlib import Path
+from urllib.request import urlopen
+from unittest.mock import Mock
+
+import pytest
+from ini2toml.api import Translator
+
+import setuptools  # noqa ensure monkey patch to metadata
+from setuptools.dist import Distribution
+from setuptools.config import setupcfg, pyprojecttoml
+from setuptools.config import expand
+
+
+EXAMPLES = (Path(__file__).parent / "setupcfg_examples.txt").read_text()
+EXAMPLE_URLS = [x for x in EXAMPLES.splitlines() if not x.startswith("#")]
+DOWNLOAD_DIR = Path(__file__).parent / "downloads"
+
+
+@pytest.mark.parametrize("url", EXAMPLE_URLS)
+@pytest.mark.filterwarnings("ignore")
+def test_apply_pyproject_equivalent_to_setupcfg(url, monkeypatch, tmp_path):
+    monkeypatch.setattr(expand, "read_attr", Mock(return_value="0.0.1"))
+    setupcfg_example = retrieve_file(url, DOWNLOAD_DIR)
+    pyproject_example = Path(tmp_path, "pyproject.toml")
+    toml_config = Translator().translate(setupcfg_example.read_text(), "setup.cfg")
+    pyproject_example.write_text(toml_config)
+
+    dist_toml = pyprojecttoml.apply_configuration(Distribution(), pyproject_example)
+    dist_cfg = setupcfg.apply_configuration(Distribution(), setupcfg_example)
+
+    pkg_info_toml = core_metadata(dist_toml)
+    pkg_info_cfg = core_metadata(dist_cfg)
+    assert pkg_info_toml == pkg_info_cfg
+
+    if any(getattr(d, "license_files", None) for d in (dist_toml, dist_cfg)):
+        assert set(dist_toml.license_files) == set(dist_cfg.license_files)
+
+    if any(getattr(d, "entry_points", None) for d in (dist_toml, dist_cfg)):
+        print(dist_cfg.entry_points)
+        ep_toml = {(k, *sorted(i.replace(" ", "") for i in v))
+                   for k, v in dist_toml.entry_points.items()}
+        ep_cfg = {(k, *sorted(i.replace(" ", "") for i in v))
+                  for k, v in dist_cfg.entry_points.items()}
+        assert ep_toml == ep_cfg
+
+    if any(getattr(d, "package_data", None) for d in (dist_toml, dist_cfg)):
+        pkg_data_toml = {(k, *sorted(v)) for k, v in dist_toml.package_data.items()}
+        pkg_data_cfg = {(k, *sorted(v)) for k, v in dist_cfg.package_data.items()}
+        assert pkg_data_toml == pkg_data_cfg
+
+    if any(getattr(d, "data_files", None) for d in (dist_toml, dist_cfg)):
+        data_files_toml = {(k, *sorted(v)) for k, v in dist_toml.data_files}
+        data_files_cfg = {(k, *sorted(v)) for k, v in dist_cfg.data_files}
+        assert data_files_toml == data_files_cfg
+
+    assert set(dist_toml.install_requires) == set(dist_cfg.install_requires)
+    if any(getattr(d, "extras_require", None) for d in (dist_toml, dist_cfg)):
+        if (
+            "testing" in dist_toml.extras_require
+            and "testing" not in dist_cfg.extras_require
+        ):
+            # ini2toml can automatically convert `tests_require` to `testing` extra
+            dist_toml.extras_require.pop("testing")
+        extra_req_toml = {(k, *sorted(v)) for k, v in dist_toml.extras_require.items()}
+        extra_req_cfg = {(k, *sorted(v)) for k, v in dist_cfg.extras_require.items()}
+        assert extra_req_toml == extra_req_cfg
+
+
+NAME_REMOVE = ("http://", "https://", "github.com/", "/raw/")
+
+
+def retrieve_file(url, download_dir):
+    file_name = url.strip()
+    for part in NAME_REMOVE:
+        file_name = file_name.replace(part, '').strip().strip('/:').strip()
+    file_name = re.sub(r"[^\-_\.\w\d]+", "_", file_name)
+    path = Path(download_dir, file_name)
+    if not path.exists():
+        download_dir.mkdir(exist_ok=True, parents=True)
+        download(url, path)
+    return path
+
+
+def download(url, dest):
+    with urlopen(url) as f:
+        data = f.read()
+
+    with open(dest, "wb") as f:
+        f.write(data)
+
+    assert Path(dest).exists()
+
+
+def core_metadata(dist) -> str:
+    buffer = io.StringIO()
+    dist.metadata.write_pkg_file(buffer)
+    value = "\n".join(buffer.getvalue().strip().splitlines())
+
+    # ---- 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)
+    # May be redundant with Home-page
+    value = re.sub(r"^Project-URL: Homepage,.*$", "", value, flags=re.M)
+    # May be missing in original (relying on default) but backfilled in the TOML
+    value = re.sub(r"^Description-Content-Type:.*$", "", value, flags=re.M)
+    # ini2toml can automatically convert `tests_require` to `testing` extra
+    value = value.replace("Provides-Extra: testing\n", "")
+    # Remove empty lines
+    value = re.sub(r"^\s*$", "", value, flags=re.M)
+    value = re.sub(r"^\n", "", value, flags=re.M)
+
+    return value
-- 
cgit v1.2.1


From 051b825eeef3b4a4efe07e2b714f8c12d321dcb6 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 4 Feb 2022 11:29:02 +0000
Subject: Fix pyproject config when tool table is not present

Co-authored-by: Henry Schreiner 
---
 setuptools/config/pyprojecttoml.py                 |  2 +-
 .../tests/config/test_apply_pyprojecttoml.py       | 72 ++++++++++++++++++++++
 2 files changed, 73 insertions(+), 1 deletion(-)

diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index 8ce69e21..d86cd1cb 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -84,7 +84,7 @@ def read_configuration(filepath, expand=True, ignore_option_errors=False):
 
     asdict = load_file(filepath) or {}
     project_table = asdict.get("project")
-    tool_table = asdict.get("tool", {}).get("setuptools")
+    tool_table = asdict.get("tool", {}).get("setuptools", {})
     if not asdict or not(project_table or tool_table):
         return {}  # User is not using pyproject to configure setuptools
 
diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py
index f93d1db9..bfdbd843 100644
--- a/setuptools/tests/config/test_apply_pyprojecttoml.py
+++ b/setuptools/tests/config/test_apply_pyprojecttoml.py
@@ -71,6 +71,78 @@ def test_apply_pyproject_equivalent_to_setupcfg(url, monkeypatch, tmp_path):
         assert extra_req_toml == extra_req_cfg
 
 
+PEP621_EXAMPLE = """\
+[project]
+name = "spam"
+version = "2020.0.0"
+description = "Lovely Spam! Wonderful Spam!"
+readme = "README.rst"
+requires-python = ">=3.8"
+license = {file = "LICENSE.txt"}
+keywords = ["egg", "bacon", "sausage", "tomatoes", "Lobster Thermidor"]
+authors = [
+  {email = "hi@pradyunsg.me"},
+  {name = "Tzu-Ping Chung"}
+]
+maintainers = [
+  {name = "Brett Cannon", email = "brett@python.org"}
+]
+classifiers = [
+  "Development Status :: 4 - Beta",
+  "Programming Language :: Python"
+]
+
+dependencies = [
+  "httpx",
+  "gidgethub[httpx]>4.0.0",
+  "django>2.1; os_name != 'nt'",
+  "django>2.0; os_name == 'nt'"
+]
+
+[project.optional-dependencies]
+test = [
+  "pytest < 5.0.0",
+  "pytest-cov[all]"
+]
+
+[project.urls]
+homepage = "http://example.com"
+documentation = "http://readthedocs.org"
+repository = "http://github.com"
+changelog = "http://github.com/me/spam/blob/master/CHANGELOG.md"
+
+[project.scripts]
+spam-cli = "spam:main_cli"
+
+[project.gui-scripts]
+spam-gui = "spam:main_gui"
+
+[project.entry-points."spam.magical"]
+tomatoes = "spam:main_tomatoes"
+"""
+
+PEP621_EXAMPLE_SCRIPT = """
+def main_cli(): pass
+def main_gui(): pass
+def main_tomatoes(): pass
+"""
+
+
+def test_pep621_example(tmp_path):
+    """Make sure the example in PEP 621 works"""
+    pyproject = tmp_path / "pyproject.toml"
+    pyproject.write_text(PEP621_EXAMPLE)
+    (tmp_path / "README.rst").write_text("hello world")
+    (tmp_path / "LICENSE.txt").write_text("--- LICENSE stub ---")
+    (tmp_path / "spam.py").write_text(PEP621_EXAMPLE_SCRIPT)
+
+    dist = pyprojecttoml.apply_configuration(Distribution(), pyproject)
+    assert set(dist.metadata.license_files) == {"LICENSE.txt"}
+
+
+# --- Auxiliary Functions ---
+
+
 NAME_REMOVE = ("http://", "https://", "github.com/", "/raw/")
 
 
-- 
cgit v1.2.1


From c9272278b2b15dbf64c2eeb7e9d8a90802d0d572 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 4 Feb 2022 11:27:32 +0000
Subject: Remove no longer needed tomli import workaround

---
 setuptools/config/pyprojecttoml.py | 14 ++------------
 1 file changed, 2 insertions(+), 12 deletions(-)

diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index d86cd1cb..127fb102 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -1,7 +1,6 @@
 """Load setuptools configuration from ``pyproject.toml`` files"""
 import json
 import os
-import sys
 from contextlib import contextmanager
 from distutils import log
 from functools import partial
@@ -18,17 +17,8 @@ if TYPE_CHECKING:
 _Path = Union[str, os.PathLike]
 
 
-def load_file(filepath: _Path):
-    try:
-        from setuptools.extern import tomli
-    except ImportError:  # Bootstrap problem (?) diagnosed by test_distutils_adoption
-        sys_path = sys.path.copy()
-        try:
-            from setuptools import _vendor
-            sys.path.append(_vendor.__path__[0])
-            import tomli
-        finally:
-            sys.path = sys_path
+def load_file(filepath: _Path) -> dict:
+    from setuptools.extern import tomli  # type: ignore
 
     with open(filepath, "rb") as file:
         return tomli.load(file)
-- 
cgit v1.2.1


From 905eed7cde46908d7e6ab646cdf202d904a619c6 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 7 Feb 2022 19:23:21 +0000
Subject: Update version of test dependency 'ini2toml'

---
 setup.cfg | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setup.cfg b/setup.cfg
index 9612e891..7a8278b9 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -67,7 +67,7 @@ testing =
 	build[virtualenv]
 	filelock>=3.4.0
 	pip_run>=8.8
-	ini2toml[lite]>=0.6.1
+	ini2toml[lite]>=0.7
 
 testing-integration =
 	pytest
-- 
cgit v1.2.1


From b426b2b9219d656357275318eb03a5b1f503887f Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 9 Feb 2022 17:20:10 +0000
Subject: Prevent resource warnings in test_apply_pyprojecttoml

Co-authored-by: Sviatoslav Sydorenko 
---
 setuptools/tests/config/test_apply_pyprojecttoml.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py
index bfdbd843..7e9dafea 100644
--- a/setuptools/tests/config/test_apply_pyprojecttoml.py
+++ b/setuptools/tests/config/test_apply_pyprojecttoml.py
@@ -169,9 +169,9 @@ def download(url, dest):
 
 
 def core_metadata(dist) -> str:
-    buffer = io.StringIO()
-    dist.metadata.write_pkg_file(buffer)
-    value = "\n".join(buffer.getvalue().strip().splitlines())
+    with io.StringIO() as buffer:
+        dist.metadata.write_pkg_file(buffer)
+        value = "\n".join(buffer.getvalue().strip().splitlines())
 
     # ---- DIFF NORMALISATION ----
     # PEP 621 is very particular about author/maintainer metadata conversion, so skip
-- 
cgit v1.2.1


From e91969a6bd63fb526ead83f97830bbc4bff139e3 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 9 Feb 2022 17:46:59 +0000
Subject: Add a 'uses_network' marker to tests that require connectivity

---
 conftest.py                                            | 1 +
 setuptools/tests/config/test_apply_pyprojecttoml.py    | 1 +
 setuptools/tests/integration/test_pip_install_sdist.py | 1 +
 3 files changed, 3 insertions(+)

diff --git a/conftest.py b/conftest.py
index 43f33ba4..723e5b43 100644
--- a/conftest.py
+++ b/conftest.py
@@ -19,6 +19,7 @@ def pytest_addoption(parser):
 
 def pytest_configure(config):
     config.addinivalue_line("markers", "integration: integration tests")
+    config.addinivalue_line("markers", "uses_network: tests may try to download files")
 
 
 collect_ignore = [
diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py
index 7e9dafea..4d9c8c5f 100644
--- a/setuptools/tests/config/test_apply_pyprojecttoml.py
+++ b/setuptools/tests/config/test_apply_pyprojecttoml.py
@@ -23,6 +23,7 @@ DOWNLOAD_DIR = Path(__file__).parent / "downloads"
 
 @pytest.mark.parametrize("url", EXAMPLE_URLS)
 @pytest.mark.filterwarnings("ignore")
+@pytest.mark.uses_network
 def test_apply_pyproject_equivalent_to_setupcfg(url, monkeypatch, tmp_path):
     monkeypatch.setattr(expand, "read_attr", Mock(return_value="0.0.1"))
     setupcfg_example = retrieve_file(url, DOWNLOAD_DIR)
diff --git a/setuptools/tests/integration/test_pip_install_sdist.py b/setuptools/tests/integration/test_pip_install_sdist.py
index 86cc4235..0177c22d 100644
--- a/setuptools/tests/integration/test_pip_install_sdist.py
+++ b/setuptools/tests/integration/test_pip_install_sdist.py
@@ -112,6 +112,7 @@ ALREADY_LOADED = ("pytest", "mypy")  # loaded by pytest/pytest-enabler
 
 
 @pytest.mark.parametrize('package, version', EXAMPLES)
+@pytest.mark.uses_network
 def test_install_sdist(package, version, tmp_path, venv_python, setuptools_wheel):
     venv_pip = (venv_python, "-m", "pip")
     sdist = retrieve_sdist(package, version, tmp_path)
-- 
cgit v1.2.1


From 9ee2697b3d42cd0c0c67037d2eddff2f45e865a6 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 10 Feb 2022 20:05:01 +0000
Subject: Update test dependency ini2toml to 0.8

---
 setup.cfg | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setup.cfg b/setup.cfg
index 7a8278b9..c0670fbb 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -67,7 +67,7 @@ testing =
 	build[virtualenv]
 	filelock>=3.4.0
 	pip_run>=8.8
-	ini2toml[lite]>=0.7
+	ini2toml[lite]>=0.8
 
 testing-integration =
 	pytest
-- 
cgit v1.2.1


From e5c551906084d2cb737229774ea0be108861acdd Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 18 Feb 2022 19:12:51 +0000
Subject: Avoid failing due to 3rd party config in pyproject.toml

---
 setuptools/config/pyprojecttoml.py            |  6 ++++--
 setuptools/tests/config/test_pyprojecttoml.py | 18 ++++++++++++++++++
 2 files changed, 22 insertions(+), 2 deletions(-)

diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index 127fb102..9075c791 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -73,7 +73,7 @@ def read_configuration(filepath, expand=True, ignore_option_errors=False):
         raise FileError(f"Configuration file {filepath!r} does not exist.")
 
     asdict = load_file(filepath) or {}
-    project_table = asdict.get("project")
+    project_table = asdict.get("project", {})
     tool_table = asdict.get("tool", {}).get("setuptools", {})
     if not asdict or not(project_table or tool_table):
         return {}  # User is not using pyproject to configure setuptools
@@ -85,7 +85,9 @@ def read_configuration(filepath, expand=True, ignore_option_errors=False):
     tool_table.setdefault("include-package-data", True)
 
     with _ignore_errors(ignore_option_errors):
-        validate(asdict, filepath)
+        # Don't complain about unrelated errors (e.g. tools not using the "tool" table)
+        subset = {"project": project_table, "tool": {"setuptools": tool_table}}
+        validate(subset, filepath)
 
     if expand:
         root_dir = os.path.dirname(filepath)
diff --git a/setuptools/tests/config/test_pyprojecttoml.py b/setuptools/tests/config/test_pyprojecttoml.py
index fb0997da..dd1a898d 100644
--- a/setuptools/tests/config/test_pyprojecttoml.py
+++ b/setuptools/tests/config/test_pyprojecttoml.py
@@ -136,3 +136,21 @@ def test_expand_entry_point(tmp_path):
     assert len(expanded_project["entry-points"]) == 3
     assert "scripts" not in expanded_project
     assert "gui-scripts" not in expanded_project
+
+
+EXAMPLE_INVALID_3RD_PARTY_CONFIG = """
+[project]
+name = "myproj"
+version = "1.2"
+
+[my-tool.that-disrespect.pep518]
+value = 42
+"""
+
+
+def test_ignore_unrelated_config(tmp_path):
+    pyproject = tmp_path / "pyproject.toml"
+    pyproject.write_text(EXAMPLE_INVALID_3RD_PARTY_CONFIG)
+
+    # Make sure no error is raised due to 3rd party configs in pyproject.toml
+    assert read_configuration(pyproject) is not None
-- 
cgit v1.2.1


From 5d4457ecc0f4f09f48132a92d1322787bd76a44d Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 18 Feb 2022 19:19:11 +0000
Subject: Add tests against "empty" pyproject.toml

---
 setuptools/tests/config/test_pyprojecttoml.py | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/setuptools/tests/config/test_pyprojecttoml.py b/setuptools/tests/config/test_pyprojecttoml.py
index dd1a898d..759f0454 100644
--- a/setuptools/tests/config/test_pyprojecttoml.py
+++ b/setuptools/tests/config/test_pyprojecttoml.py
@@ -1,5 +1,7 @@
 from configparser import ConfigParser
 
+import pytest
+
 from setuptools.config.pyprojecttoml import read_configuration, expand_configuration
 
 EXAMPLE = """
@@ -154,3 +156,12 @@ def test_ignore_unrelated_config(tmp_path):
 
     # Make sure no error is raised due to 3rd party configs in pyproject.toml
     assert read_configuration(pyproject) is not None
+
+
+@pytest.mark.parametrize("config", ("", "[tool.something]\nvalue = 42"))
+def test_empty(tmp_path, config):
+    pyproject = tmp_path / "pyproject.toml"
+    pyproject.write_text(config)
+
+    # Make sure no error is raised
+    assert read_configuration(pyproject) == {}
-- 
cgit v1.2.1


From cf32acbcc180938cf665ba1dfa65243bb8e2277f Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 18 Feb 2022 22:09:55 +0000
Subject: Avoid using pkg_resources for entry points

---
 setuptools/config/_apply_pyprojecttoml.py | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py
index 4dddd09d..0d2ead88 100644
--- a/setuptools/config/_apply_pyprojecttoml.py
+++ b/setuptools/config/_apply_pyprojecttoml.py
@@ -15,7 +15,7 @@ from typing import (TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Tup
                     Type, Union)
 
 if TYPE_CHECKING:
-    from pkg_resources import EntryPoint  # noqa
+    from setuptools._importlib import metadata  # noqa
     from setuptools.dist import Distribution  # noqa
 
 EMPTY = MappingProxyType({})  # Immutable dict-like
@@ -181,13 +181,14 @@ def _copy_command_options(pyproject: dict, dist: "Distribution", filename: _Path
 
 
 def _valid_command_options(cmdclass: Mapping = EMPTY) -> Dict[str, Set[str]]:
-    from pkg_resources import iter_entry_points
+    from .._importlib import metadata
     from setuptools.dist import Distribution
 
     valid_options = {"global": _normalise_cmd_options(Distribution.global_options)}
 
-    entry_points = (_load_ep(ep) for ep in iter_entry_points('distutils.commands'))
-    entry_points = (ep for ep in entry_points if ep)
+    unloaded_entry_points = metadata.entry_points(group='distutils.commands')
+    loaded_entry_points = (_load_ep(ep) for ep in unloaded_entry_points)
+    entry_points = (ep for ep in loaded_entry_points if ep)
     for cmd, cmd_class in chain(entry_points, cmdclass.items()):
         opts = valid_options.get(cmd, set())
         opts = opts | _normalise_cmd_options(getattr(cmd_class, "user_options", []))
@@ -196,7 +197,7 @@ def _valid_command_options(cmdclass: Mapping = EMPTY) -> Dict[str, Set[str]]:
     return valid_options
 
 
-def _load_ep(ep: "EntryPoint") -> Optional[Tuple[str, Type]]:
+def _load_ep(ep: "metadata.EntryPoint") -> Optional[Tuple[str, Type]]:
     # Ignore all the errors
     try:
         return (ep.name, ep.load())
-- 
cgit v1.2.1


From a4b474ecb7ca027ba06b351b254ee57725184ee3 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 23 Feb 2022 02:57:49 +0000
Subject: Back-fill Description-Content-Type according to readme suffix

According to PEP 621, the backend should fill-in the content-type if the
`readme` field is passed as a string. The value is derived from the
extension of the file (an error should be raised when that is not
possible).

Previously all READMEs were wrongly assumed rst.
This error was reported in:

https://discuss.python.org/t/help-testing-experimental-features-in-setuptools/13821/4
---
 setuptools/config/_apply_pyprojecttoml.py          | 25 +++++++++++--
 .../tests/config/test_apply_pyprojecttoml.py       | 41 ++++++++++++++++++++--
 2 files changed, 61 insertions(+), 5 deletions(-)

diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py
index 0d2ead88..f711c8a2 100644
--- a/setuptools/config/_apply_pyprojecttoml.py
+++ b/setuptools/config/_apply_pyprojecttoml.py
@@ -74,18 +74,39 @@ def _set_config(dist: "Distribution", field: str, value: Any):
         setattr(dist, field, value)
 
 
+_CONTENT_TYPES = {
+    ".md": "text/markdown",
+    ".rst": "text/x-rst",
+    ".txt": "text/plain",
+}
+
+
+def _guess_content_type(file: str) -> Optional[str]:
+    _, ext = os.path.splitext(file.lower())
+    if not ext:
+        return None
+
+    if ext in _CONTENT_TYPES:
+        return _CONTENT_TYPES[ext]
+
+    valid = ", ".join(f"{k} ({v})" for k, v in _CONTENT_TYPES.items())
+    msg = f"only the following file extensions are recognized: {valid}."
+    raise ValueError(f"Undefined content type for {file}, {msg}")
+
+
 def _long_description(dist: "Distribution", val: _DictOrStr, root_dir: _Path):
     from setuptools.config import expand
 
     if isinstance(val, str):
         text = expand.read_files(val, root_dir)
-        ctype = "text/x-rst"
+        ctype = _guess_content_type(val)
     else:
         text = val.get("text") or expand.read_files(val.get("file", []), root_dir)
         ctype = val["content-type"]
 
     _set_config(dist, "long_description", text)
-    _set_config(dist, "long_description_content_type", ctype)
+    if ctype:
+        _set_config(dist, "long_description_content_type", ctype)
 
 
 def _license(dist: "Distribution", val: Union[str, dict], _root_dir):
diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py
index 4d9c8c5f..5b5a8dfa 100644
--- a/setuptools/tests/config/test_apply_pyprojecttoml.py
+++ b/setuptools/tests/config/test_apply_pyprojecttoml.py
@@ -129,18 +129,53 @@ def main_tomatoes(): pass
 """
 
 
-def test_pep621_example(tmp_path):
-    """Make sure the example in PEP 621 works"""
+def _pep621_example_project(tmp_path, readme="README.rst"):
     pyproject = tmp_path / "pyproject.toml"
-    pyproject.write_text(PEP621_EXAMPLE)
+    text = PEP621_EXAMPLE
+    replacements = {'readme = "README.rst"': f'readme = "{readme}"'}
+    for orig, subst in replacements.items():
+        text = text.replace(orig, subst)
+    pyproject.write_text(text)
+
     (tmp_path / "README.rst").write_text("hello world")
     (tmp_path / "LICENSE.txt").write_text("--- LICENSE stub ---")
     (tmp_path / "spam.py").write_text(PEP621_EXAMPLE_SCRIPT)
+    return pyproject
+
 
+def test_pep621_example(tmp_path):
+    """Make sure the example in PEP 621 works"""
+    pyproject = _pep621_example_project(tmp_path)
     dist = pyprojecttoml.apply_configuration(Distribution(), pyproject)
     assert set(dist.metadata.license_files) == {"LICENSE.txt"}
 
 
+@pytest.mark.parametrize(
+    "readme, ctype",
+    [
+        ("Readme.txt", "text/plain"),
+        ("readme.md", "text/markdown"),
+        ("text.rst", "text/x-rst"),
+    ]
+)
+def test_readme_content_type(tmp_path, readme, ctype):
+    pyproject = _pep621_example_project(tmp_path, readme)
+    dist = pyprojecttoml.apply_configuration(Distribution(), pyproject)
+    assert dist.metadata.long_description_content_type == ctype
+
+
+def test_undefined_content_type(tmp_path):
+    pyproject = _pep621_example_project(tmp_path, "README.tex")
+    with pytest.raises(ValueError, match="Undefined content type for README.tex"):
+        pyprojecttoml.apply_configuration(Distribution(), pyproject)
+
+
+def test_no_explicit_content_type_for_missing_extension(tmp_path):
+    pyproject = _pep621_example_project(tmp_path, "README")
+    dist = pyprojecttoml.apply_configuration(Distribution(), pyproject)
+    assert dist.metadata.long_description_content_type is None
+
+
 # --- Auxiliary Functions ---
 
 
-- 
cgit v1.2.1


From 0497954f685c73a18449c28a5f9cdf9e5cfc31f9 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 5 Mar 2022 10:28:59 +0000
Subject: Update test dependency ini2toml to 0.9

---
 setup.cfg | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setup.cfg b/setup.cfg
index c0670fbb..7e428850 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -67,7 +67,7 @@ testing =
 	build[virtualenv]
 	filelock>=3.4.0
 	pip_run>=8.8
-	ini2toml[lite]>=0.8
+	ini2toml[lite]>=0.9
 
 testing-integration =
 	pytest
-- 
cgit v1.2.1


From d3853304ea2e5ef35adb4a4e73ca3afc2193c174 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 24 Dec 2021 16:42:37 +0000
Subject: Add pyproject.toml to dist.parse_config_files

---
 setuptools/dist.py | 18 ++++++++++++++----
 1 file changed, 14 insertions(+), 4 deletions(-)

diff --git a/setuptools/dist.py b/setuptools/dist.py
index e825785e..c0e8e1b3 100644
--- a/setuptools/dist.py
+++ b/setuptools/dist.py
@@ -19,6 +19,7 @@ from glob import iglob
 import itertools
 import textwrap
 from typing import List, Optional, TYPE_CHECKING
+from pathlib import Path
 
 from collections import defaultdict
 from email import message_from_file
@@ -28,7 +29,7 @@ from distutils.util import rfc822_escape
 
 from setuptools.extern import packaging
 from setuptools.extern import ordered_set
-from setuptools.extern.more_itertools import unique_everseen, always_iterable
+from setuptools.extern.more_itertools import unique_everseen, always_iterable, partition
 
 from ._importlib import metadata
 
@@ -38,7 +39,7 @@ import setuptools
 import setuptools.command
 from setuptools import windows_support
 from setuptools.monkey import get_unpatched
-from setuptools.config import parse_configuration
+from setuptools.config import setupcfg, pyprojecttoml
 import pkg_resources
 from setuptools.extern.packaging import version, requirements
 from . import _reqs
@@ -811,13 +812,22 @@ class Distribution(_Distribution):
     def parse_config_files(self, filenames=None, ignore_option_errors=False):
         """Parses configuration files from various levels
         and loads configuration.
-
         """
+        tomlfiles = []
+        if filenames is not None:
+            tomlfiles, other = partition(lambda f: Path(f).suffix == ".toml", filenames)
+            filenames = other
+        elif os.path.exists("pyproject.toml"):
+            tomlfiles = ["pyproject.toml"]
+
         self._parse_config_files(filenames=filenames)
 
-        parse_configuration(
+        setupcfg.parse_configuration(
             self, self.command_options, ignore_option_errors=ignore_option_errors
         )
+        for filename in tomlfiles:
+            pyprojecttoml.apply_configuration(self, filename)
+
         self._finalize_requires()
         self._finalize_license_files()
 
-- 
cgit v1.2.1


From dea4be5af3ee798ff0a944d944da56cf9dac899d Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 25 Dec 2021 14:17:34 +0000
Subject: Add deprecation notice for config.{read,parse}_configuration

Since now setuptools supports 2 types of files for configuration
(`setup.cfg` and `pyproject.toml`), it is very trick to provide a single
`read_configuration` function that will provide compatible outputs for
both formats.

Instead the `config.{setupcfg,pyprojecttoml}` modules have their own
`read_configuration` functions that differ between themselves in terms
of arguments and format of the return value.

Therefore the users should be importing directly the specific submodule
and calling the read function from there.

The `config.setupcfg` submodule is advertised as "provisional" in the
deprecation note because the main proposal debated in the setuptools
issue tracker reached some level of consensus around deprecating
`setup.cfg` files.
---
 setuptools/config/__init__.py | 46 ++++++++++++++++++++++++++++++++-----------
 1 file changed, 35 insertions(+), 11 deletions(-)

diff --git a/setuptools/config/__init__.py b/setuptools/config/__init__.py
index fa48907a..35458d8e 100644
--- a/setuptools/config/__init__.py
+++ b/setuptools/config/__init__.py
@@ -1,11 +1,35 @@
-# For backward compatibility, the following classes/functions are exposed
-# from `config.setupcfg`
-from setuptools.config.setupcfg import (
-    parse_configuration,
-    read_configuration,
-)
-
-__all__ = (
-    'parse_configuration',
-    'read_configuration'
-)
+"""For backward compatibility, expose main functions from
+``setuptools.config.setupcfg``
+"""
+import warnings
+from functools import wraps
+from textwrap import dedent
+from typing import Callable, TypeVar, cast
+
+from .._deprecation_warning import SetuptoolsDeprecationWarning
+from . import setupcfg
+
+Fn = TypeVar("Fn", bound=Callable)
+
+__all__ = ('parse_configuration', 'read_configuration')
+
+
+def _deprecation_notice(fn: Fn) -> Fn:
+    @wraps(fn)
+    def _wrapper(*args, **kwargs):
+        msg = f"""\
+        As setuptools moves its configuration towards `pyproject.toml`,
+        `{__name__}.{fn.__name__}` became deprecated.
+
+        For the time being, you can use the `{setupcfg.__name__}` module
+        to access a backward compatible API, but this module is provisional
+        and might be removed in the future.
+        """
+        warnings.warn(dedent(msg), SetuptoolsDeprecationWarning)
+        return fn(*args, **kwargs)
+
+    return cast(Fn, _wrapper)
+
+
+read_configuration = _deprecation_notice(setupcfg.read_configuration)
+parse_configuration = _deprecation_notice(setupcfg.parse_configuration)
-- 
cgit v1.2.1


From 2b333e983514a69b0ba04c2668debf5ba99e07d2 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 23 Dec 2021 16:20:56 +0000
Subject: Add backend test with pyproject.toml-based configs

---
 setuptools/tests/test_build_meta.py | 152 +++++++++++++++++++++++++++++++++++-
 1 file changed, 148 insertions(+), 4 deletions(-)

diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py
index eb43fe9b..bbe56379 100644
--- a/setuptools/tests/test_build_meta.py
+++ b/setuptools/tests/test_build_meta.py
@@ -7,12 +7,15 @@ import importlib
 import contextlib
 from concurrent import futures
 import re
+from zipfile import ZipFile
 
 import pytest
 from jaraco import path
 
 from .textwrap import DALS
 
+SETUP_SCRIPT_STUB = "__import__('setuptools').setup()"
+
 
 TIMEOUT = int(os.getenv("TIMEOUT_BACKEND_TEST", "180"))  # in seconds
 IS_PYPY = '__pypy__' in sys.builtin_module_names
@@ -82,7 +85,7 @@ class BuildBackendCaller(BuildBackendBase):
 
 
 defns = [
-    {
+    {  # simple setup.py script
         'setup.py': DALS("""
             __import__('setuptools').setup(
                 name='foo',
@@ -96,7 +99,7 @@ defns = [
                 print('hello')
             """),
     },
-    {
+    {  # setup.py that relies on __name__
         'setup.py': DALS("""
             assert __name__ == '__main__'
             __import__('setuptools').setup(
@@ -111,7 +114,7 @@ defns = [
                 print('hello')
             """),
     },
-    {
+    {  # setup.py script that runs arbitrary code
         'setup.py': DALS("""
             variable = True
             def function():
@@ -129,7 +132,22 @@ defns = [
                 print('hello')
             """),
     },
-    {
+    {  # setup.cfg only
+        'setup.cfg': DALS("""
+        [metadata]
+        name = foo
+        version = 0.0.0
+
+        [options]
+        py_modules=hello
+        setup_requires=six
+        """),
+        'hello.py': DALS("""
+        def run():
+            print('hello')
+        """)
+    },
+    {  # setup.cfg and setup.py
         'setup.cfg': DALS("""
         [metadata]
         name = foo
@@ -139,6 +157,7 @@ defns = [
         py_modules=hello
         setup_requires=six
         """),
+        'setup.py': "__import__('setuptools').setup()",
         'hello.py': DALS("""
         def run():
             print('hello')
@@ -223,6 +242,131 @@ class TestBuildMetaBackend:
         assert third_result == second_result
         assert os.path.getsize(os.path.join(dist_dir, third_result)) > 0
 
+    @pytest.mark.parametrize("setup_script", [None, SETUP_SCRIPT_STUB])
+    def test_build_with_pyproject_config(self, tmpdir, setup_script):
+        files = {
+            'pyproject.toml': DALS("""
+                [build-system]
+                requires = ["setuptools", "wheel"]
+                build-backend = "setuptools.build_meta"
+
+                [project]
+                name = "foo"
+                description = "This is a Python package"
+                dynamic = ["version", "license", "readme"]
+                classifiers = [
+                    "Development Status :: 5 - Production/Stable",
+                    "Intended Audience :: Developers"
+                ]
+                urls = {Homepage = "http://github.com"}
+                dependencies = [
+                    "appdirs",
+                ]
+
+                [project.optional-dependencies]
+                all = [
+                    "tomli>=1",
+                    "pyscaffold>=4,<5",
+                    'importlib; python_version == "2.6"',
+                ]
+
+                [project.scripts]
+                foo = "foo.cli:main"
+
+                [tool.setuptools]
+                package-dir = {"" = "src"}
+                packages = {find = {where = ["src"]}}
+
+                [tool.setuptools.dynamic]
+                version = {attr = "foo.__version__"}
+                license = "MIT"
+                license_files = ["LICENSE*"]
+                readme = {file = "README.rst"}
+
+                [tool.distutils.sdist]
+                formats = "gztar"
+
+                [tool.distutils.bdist_wheel]
+                universal = true
+                """),
+            "MANIFEST.in": DALS("""
+                global-include *.py *.txt
+                global-exclude *.py[cod]
+                """),
+            "README.rst": "This is a ``README``",
+            "LICENSE.txt": "---- placeholder MIT license ----",
+            "src": {
+                "foo": {
+                    "__init__.py": "__version__ = '0.1'",
+                    "cli.py": "def main(): print('hello world')",
+                    "data.txt": "def main(): print('hello world')",
+                }
+            }
+        }
+        if setup_script:
+            files["setup.py"] = setup_script
+
+        build_backend = self.get_build_backend()
+        with tmpdir.as_cwd():
+            path.build(files)
+            sdist_path = build_backend.build_sdist("temp")
+            wheel_file = build_backend.build_wheel("temp")
+
+        with tarfile.open(os.path.join(tmpdir, "temp", sdist_path)) as tar:
+            sdist_contents = set(tar.getnames())
+
+        with ZipFile(os.path.join(tmpdir, "temp", wheel_file)) as zipfile:
+            wheel_contents = set(zipfile.namelist())
+            metadata = str(zipfile.read("foo-0.1.dist-info/METADATA"), "utf-8")
+            license = str(zipfile.read("foo-0.1.dist-info/LICENSE.txt"), "utf-8")
+            epoints = str(zipfile.read("foo-0.1.dist-info/entry_points.txt"), "utf-8")
+
+        assert sdist_contents - {"foo-0.1/setup.py"} == {
+            'foo-0.1',
+            'foo-0.1/LICENSE.txt',
+            'foo-0.1/MANIFEST.in',
+            'foo-0.1/PKG-INFO',
+            'foo-0.1/README.rst',
+            'foo-0.1/pyproject.toml',
+            'foo-0.1/setup.cfg',
+            'foo-0.1/src',
+            'foo-0.1/src/foo',
+            'foo-0.1/src/foo/__init__.py',
+            'foo-0.1/src/foo/cli.py',
+            'foo-0.1/src/foo/data.txt',
+            'foo-0.1/src/foo.egg-info',
+            'foo-0.1/src/foo.egg-info/PKG-INFO',
+            'foo-0.1/src/foo.egg-info/SOURCES.txt',
+            'foo-0.1/src/foo.egg-info/dependency_links.txt',
+            'foo-0.1/src/foo.egg-info/entry_points.txt',
+            'foo-0.1/src/foo.egg-info/requires.txt',
+            'foo-0.1/src/foo.egg-info/top_level.txt',
+        }
+        assert wheel_contents == {
+            "foo/__init__.py",
+            "foo/cli.py",
+            "foo/data.txt",  # include_package_data defaults to True
+            "foo-0.1.dist-info/LICENSE.txt",
+            "foo-0.1.dist-info/METADATA",
+            "foo-0.1.dist-info/WHEEL",
+            "foo-0.1.dist-info/entry_points.txt",
+            "foo-0.1.dist-info/top_level.txt",
+            "foo-0.1.dist-info/RECORD",
+        }
+        assert license == "---- placeholder MIT license ----"
+        for line in (
+            "Summary: This is a Python package",
+            "License: MIT",
+            "Classifier: Intended Audience :: Developers",
+            "Requires-Dist: appdirs",
+            "Requires-Dist: tomli (>=1) ; extra == 'all'",
+            "Requires-Dist: importlib ; (python_version == \"2.6\") and extra == 'all'"
+        ):
+            assert line in metadata
+
+        assert metadata.strip().endswith("This is a ``README``")
+        assert epoints.strip() == "[console_scripts]\nfoo = foo.cli:main"
+
     def test_build_sdist(self, build_backend):
         dist_dir = os.path.abspath('pip-sdist')
         os.makedirs(dist_dir)
-- 
cgit v1.2.1


From 09f784fd39256eeda666e223a0b583c77da19a0c Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 24 Dec 2021 17:55:27 +0000
Subject: Test editable installs with pyproject.toml metadata

---
 setuptools/tests/test_editable_install.py | 109 ++++++++++++++++++++++++++++++
 1 file changed, 109 insertions(+)
 create mode 100644 setuptools/tests/test_editable_install.py

diff --git a/setuptools/tests/test_editable_install.py b/setuptools/tests/test_editable_install.py
new file mode 100644
index 00000000..2957cba0
--- /dev/null
+++ b/setuptools/tests/test_editable_install.py
@@ -0,0 +1,109 @@
+import subprocess
+from textwrap import dedent
+
+import pytest
+import jaraco.envs
+import path
+
+
+@pytest.fixture
+def venv(tmp_path, setuptools_wheel):
+    env = jaraco.envs.VirtualEnv()
+    vars(env).update(
+        root=path.Path(tmp_path),  # workaround for error on windows
+        name=".venv",
+        create_opts=["--no-setuptools"],
+        req=str(setuptools_wheel),
+    )
+    return env.create()
+
+
+SETUP_SCRIPT_STUB = "__import__('setuptools').setup()"
+
+EXAMPLE = {
+    'pyproject.toml': dedent("""\
+        [build-system]
+        requires = ["setuptools", "wheel"]
+        build-backend = "setuptools.build_meta"
+
+        [project]
+        name = "mypkg"
+        version = "3.14159"
+        description = "This is a Python package"
+        dynamic = ["license", "readme"]
+        classifiers = [
+            "Development Status :: 5 - Production/Stable",
+            "Intended Audience :: Developers"
+        ]
+        urls = {Homepage = "http://github.com"}
+        dependencies = ['importlib-metadata; python_version<"3.8"']
+
+        [tool.setuptools]
+        package-dir = {"" = "src"}
+        packages = {find = {where = ["src"]}}
+
+        [tool.setuptools.dynamic]
+        license = "MIT"
+        license_files = ["LICENSE*"]
+        readme = {file = "README.rst"}
+
+        [tool.distutils.egg_info]
+        tag-build = ".post0"
+        """),
+    "MANIFEST.in": dedent("""\
+        global-include *.py *.txt
+        global-exclude *.py[cod]
+        """).strip(),
+    "README.rst": "This is a ``README``",
+    "LICENSE.txt": "---- placeholder MIT license ----",
+    "src": {
+        "mypkg": {
+            "__init__.py": dedent("""\
+                import sys
+
+                if sys.version_info[:2] >= (3, 8):
+                    from importlib.metadata import PackageNotFoundError, version
+                else:
+                    from importlib_metadata import PackageNotFoundError, version
+
+                try:
+                    __version__ = version(__name__)
+                except PackageNotFoundError:
+                    __version__ = "unknown"
+                """),
+            "__main__.py": dedent("""\
+                from importlib.resources import read_text
+                from . import __version__, __name__ as parent
+                from .mod import x
+
+                data = read_text(parent, "data.txt")
+                print(__version__, data, x)
+                """),
+            "mod.py": "x = ''",
+            "data.txt": "Hello World",
+        }
+    }
+}
+
+
+@pytest.mark.parametrize("setup_script", [SETUP_SCRIPT_STUB, None])
+def test_editable_with_pyproject(tmp_path, venv, setup_script):
+    if setup_script is None:
+        pytest.skip("Editable install currently only supported with `setup.py` stub")
+
+    project = tmp_path / "mypkg"
+    files = {**EXAMPLE, "setup.py": setup_script}
+    project.mkdir()
+    jaraco.path.build(files, prefix=project)
+
+    cmd = [venv.exe(), "-m", "pip", "install",
+           "--no-build-isolation",  # required to force current version of setuptools
+           "-e", str(project)]
+    print(str(subprocess.check_output(cmd), "utf-8"))
+
+    cmd = [venv.exe(), "-m", "mypkg"]
+    assert subprocess.check_output(cmd).strip() == b"3.14159.post0 Hello World"
+
+    (project / "src/mypkg/data.txt").write_text("foobar")
+    (project / "src/mypkg/mod.py").write_text("x = 42")
+    assert subprocess.check_output(cmd).strip() == b"3.14159.post0 foobar 42"
-- 
cgit v1.2.1


From 9e8e3d3693953e8f75539506b3f97b0df30ce77c Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 13 Jan 2022 10:15:00 +0000
Subject: Replace skip in editable install test with xfail
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

… as suggested in code review
---
 setuptools/tests/test_editable_install.py | 16 ++++++++++------
 1 file changed, 10 insertions(+), 6 deletions(-)

diff --git a/setuptools/tests/test_editable_install.py b/setuptools/tests/test_editable_install.py
index 2957cba0..ca8288d5 100644
--- a/setuptools/tests/test_editable_install.py
+++ b/setuptools/tests/test_editable_install.py
@@ -18,8 +18,6 @@ def venv(tmp_path, setuptools_wheel):
     return env.create()
 
 
-SETUP_SCRIPT_STUB = "__import__('setuptools').setup()"
-
 EXAMPLE = {
     'pyproject.toml': dedent("""\
         [build-system]
@@ -86,11 +84,17 @@ EXAMPLE = {
 }
 
 
-@pytest.mark.parametrize("setup_script", [SETUP_SCRIPT_STUB, None])
-def test_editable_with_pyproject(tmp_path, venv, setup_script):
-    if setup_script is None:
-        pytest.skip("Editable install currently only supported with `setup.py` stub")
+SETUP_SCRIPT_STUB = "__import__('setuptools').setup()"
+MISSING_SETUP_SCRIPT = pytest.param(
+    None,
+    marks=pytest.mark.xfail(
+        reason="Editable install is currently only supported with `setup.py`"
+    )
+)
+
 
+@pytest.mark.parametrize("setup_script", [SETUP_SCRIPT_STUB, MISSING_SETUP_SCRIPT])
+def test_editable_with_pyproject(tmp_path, venv, setup_script):
     project = tmp_path / "mypkg"
     files = {**EXAMPLE, "setup.py": setup_script}
     project.mkdir()
-- 
cgit v1.2.1


From aab5899b4cd7e262a71635cf669ebf63f9b1e7ff Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 1 Feb 2022 12:44:26 +0000
Subject: Add news fragment

---
 changelog.d/3068.change.rst      | 13 +++++++++++++
 changelog.d/3068.deprecation.rst |  8 ++++++++
 docs/conf.py                     |  8 +++++++-
 3 files changed, 28 insertions(+), 1 deletion(-)
 create mode 100644 changelog.d/3068.change.rst
 create mode 100644 changelog.d/3068.deprecation.rst

diff --git a/changelog.d/3068.change.rst b/changelog.d/3068.change.rst
new file mode 100644
index 00000000..ca71972b
--- /dev/null
+++ b/changelog.d/3068.change.rst
@@ -0,0 +1,13 @@
+Added **experimental** support for ``pyproject.toml`` configuration
+(as introduced by :pep:`621`). Configuration parameters not covered by
+standards are handled in the ``[tool.setuptools]`` sub-table.
+
+In the future, existing ``setup.cfg`` configuration
+may be automatically converted into the ``pyproject.toml`` equivalent before taking effect
+(as proposed in :issue:`1688`). Meanwhile users can use automated tools like
+:pypi:`ini2toml` to help in the transition.
+
+Please note that the legacy backend is not guaranteed to work with
+``pyproject.toml`` configuration.
+
+-- by :user:`abravalheri`
diff --git a/changelog.d/3068.deprecation.rst b/changelog.d/3068.deprecation.rst
new file mode 100644
index 00000000..3bae915c
--- /dev/null
+++ b/changelog.d/3068.deprecation.rst
@@ -0,0 +1,8 @@
+Deprecated ``setuptools.config.read_configuration``,
+``setuptools.config.parse_configuration`` and other functions or classes
+from ``setuptools.config``.
+
+Users that still need to parse and process configuration from ``setup.cfg`` can
+import a direct replacement from ``setuptools.config.setupcfg``, however this
+module is transitional and might be removed in the future
+(the ``setup.cfg`` configuration format itself is likely to be deprecated in the future).
diff --git a/docs/conf.py b/docs/conf.py
index da4d9f33..7f66483a 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -93,10 +93,16 @@ intersphinx_mapping.update({
 
 # Add support for linking usernames
 github_url = 'https://github.com'
+github_repo_org = 'pypa'
+github_repo_name = 'setuptools'
+github_repo_slug = f'{github_repo_org}/{github_repo_name}'
+github_repo_url = f'{github_url}/{github_repo_slug}'
 github_sponsors_url = f'{github_url}/sponsors'
 extlinks = {
+    'issue': (f'{github_repo_url}/issues/%s', 'issue #%s'),  # noqa: WPS323
+    'pr': (f'{github_repo_url}/pull/%s', 'PR #%s'),  # noqa: WPS323
     'user': (f'{github_sponsors_url}/%s', '@'),  # noqa: WPS323
-    'pypi': ('https://pypi.org/project/%s', '%s'),
+    'pypi': ('https://pypi.org/project/%s', '%s'),  # noqa: WPS323
 }
 extensions += ['sphinx.ext.extlinks']
 
-- 
cgit v1.2.1


From c9cf0dabd7deabc2bc9539e983681de43d4c9e61 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 11 Feb 2022 17:47:51 +0000
Subject: Ensure build_meta don't have problems with instructions after setup()

This is a regression test for a problem identified in:
https://github.com/pypa/setuptools/pull/2970#issuecomment-1036078047
---
 setuptools/tests/test_build_meta.py | 38 ++++++++++++++++++++++++++++++++++++-
 1 file changed, 37 insertions(+), 1 deletion(-)

diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py
index bbe56379..1f416e6a 100644
--- a/setuptools/tests/test_build_meta.py
+++ b/setuptools/tests/test_build_meta.py
@@ -132,6 +132,29 @@ defns = [
                 print('hello')
             """),
     },
+    {  # setup.py script that constructs temp files to be included in the distribution
+        'setup.py': DALS("""
+            # Some packages construct files on the fly, include them in the package,
+            # and immediately remove them after `setup()` (e.g. pybind11==2.9.1).
+            # Therefore, we cannot use `distutils.core.run_setup(..., stop_after=...)`
+            # to obtain a distribution object first, and then run the distutils
+            # commands later, because these files will be removed in the meantime.
+
+            with open('world.py', 'w') as f:
+                f.write('x = 42')
+
+            try:
+                __import__('setuptools').setup(
+                    name='foo',
+                    version='0.0.0',
+                    py_modules=['world'],
+                    setup_requires=['six'],
+                )
+            finally:
+                # Some packages will clean temporary files
+                __import__('os').unlink('world.py')
+            """),
+    },
     {  # setup.cfg only
         'setup.cfg': DALS("""
         [metadata]
@@ -193,7 +216,20 @@ class TestBuildMetaBackend:
         os.makedirs(dist_dir)
         wheel_name = build_backend.build_wheel(dist_dir)
 
-        assert os.path.isfile(os.path.join(dist_dir, wheel_name))
+        wheel_file = os.path.join(dist_dir, wheel_name)
+        assert os.path.isfile(wheel_file)
+
+        # Temporary files should be removed
+        assert not os.path.isfile('world.py')
+
+        with ZipFile(wheel_file) as zipfile:
+            wheel_contents = set(zipfile.namelist())
+
+        # Each one of the examples have a single module
+        # that should be included in the distribution
+        python_scripts = (f for f in wheel_contents if f.endswith('.py'))
+        modules = [f for f in python_scripts if not f.endswith('setup.py')]
+        assert len(modules) == 1
 
     @pytest.mark.parametrize('build_type', ('wheel', 'sdist'))
     def test_build_with_existing_file_present(self, build_type, tmpdir_cwd):
-- 
cgit v1.2.1


From 98c8edbe25d8fcb532816837faf67e3cb963c940 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 11 Feb 2022 23:08:44 +0000
Subject: Test if not-zip-safe file is being generated with project metadata

---
 setuptools/tests/test_build_meta.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py
index 1f416e6a..ed929473 100644
--- a/setuptools/tests/test_build_meta.py
+++ b/setuptools/tests/test_build_meta.py
@@ -310,6 +310,7 @@ class TestBuildMetaBackend:
                 foo = "foo.cli:main"
 
                 [tool.setuptools]
+                zip-safe = false
                 package-dir = {"" = "src"}
                 packages = {find = {where = ["src"]}}
 
@@ -377,6 +378,7 @@ class TestBuildMetaBackend:
             'foo-0.1/src/foo.egg-info/entry_points.txt',
             'foo-0.1/src/foo.egg-info/requires.txt',
             'foo-0.1/src/foo.egg-info/top_level.txt',
+            'foo-0.1/src/foo.egg-info/not-zip-safe',
         }
         assert wheel_contents == {
             "foo/__init__.py",
-- 
cgit v1.2.1


From 86e6a10f5cd456c3b5a4833045bbde8fa3f8097a Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 12 Feb 2022 13:49:58 +0000
Subject: Test static metadata in pyproject.toml is not overwritten by setup.py

These tests were initially motivated by a discussion in:
https://github.com/pybind/pybind11/pull/3711#issuecomment-1036641321
---
 setuptools/tests/test_build_meta.py | 57 +++++++++++++++++++++++++++++++++++++
 1 file changed, 57 insertions(+)

diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py
index ed929473..323a41a4 100644
--- a/setuptools/tests/test_build_meta.py
+++ b/setuptools/tests/test_build_meta.py
@@ -405,6 +405,63 @@ class TestBuildMetaBackend:
         assert metadata.strip().endswith("This is a ``README``")
         assert epoints.strip() == "[console_scripts]\nfoo = foo.cli:main"
 
+    def test_static_metadata_in_pyproject_config(self, tmpdir):
+        # Make sure static metadata in pyproject.toml is not overwritten by setup.py
+        # as required by PEP 621
+        files = {
+            'pyproject.toml': DALS("""
+                [build-system]
+                requires = ["setuptools", "wheel"]
+                build-backend = "setuptools.build_meta"
+
+                [project]
+                name = "foo"
+                description = "This is a Python package"
+                version = "42"
+                dependencies = ["six"]
+                """),
+            'hello.py': DALS("""
+                def run():
+                    print('hello')
+                """),
+            'setup.py': DALS("""
+                __import__('setuptools').setup(
+                    name='bar',
+                    version='13',
+                )
+                """),
+        }
+        build_backend = self.get_build_backend()
+        with tmpdir.as_cwd():
+            path.build(files)
+            sdist_path = build_backend.build_sdist("temp")
+            wheel_file = build_backend.build_wheel("temp")
+
+        assert (tmpdir / "temp/foo-42.tar.gz").exists()
+        assert (tmpdir / "temp/foo-42-py3-none-any.whl").exists()
+        assert not (tmpdir / "temp/bar-13.tar.gz").exists()
+        assert not (tmpdir / "temp/bar-42.tar.gz").exists()
+        assert not (tmpdir / "temp/foo-13.tar.gz").exists()
+        assert not (tmpdir / "temp/bar-13-py3-none-any.whl").exists()
+        assert not (tmpdir / "temp/bar-42-py3-none-any.whl").exists()
+        assert not (tmpdir / "temp/foo-13-py3-none-any.whl").exists()
+
+        with tarfile.open(os.path.join(tmpdir, "temp", sdist_path)) as tar:
+            pkg_info = str(tar.extractfile('foo-42/PKG-INFO').read(), "utf-8")
+            members = tar.getnames()
+            assert "bar-13/PKG-INFO" not in members
+
+        with ZipFile(os.path.join(tmpdir, "temp", wheel_file)) as zipfile:
+            metadata = str(zipfile.read("foo-42.dist-info/METADATA"), "utf-8")
+            members = zipfile.namelist()
+            assert "bar-13.dist-info/METADATA" not in members
+
+        for file in pkg_info, metadata:
+            for line in ("Name: foo", "Version: 42"):
+                assert line in file
+            for line in ("Name: bar", "Version: 13"):
+                assert line not in file
+
     def test_build_sdist(self, build_backend):
         dist_dir = os.path.abspath('pip-sdist')
         os.makedirs(dist_dir)
-- 
cgit v1.2.1


From 854969d916552153888958bd8605a65b52c77b70 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 18 Feb 2022 20:47:27 +0000
Subject: Explicitly inform users that pyproject.toml config is experimental

---
 pytest.ini                         |  2 ++
 setuptools/config/pyprojecttoml.py | 12 ++++++++++++
 2 files changed, 14 insertions(+)

diff --git a/pytest.ini b/pytest.ini
index 4a5ad50d..14c7e94c 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -58,3 +58,5 @@ filterwarnings=
 	# https://github.com/pytest-dev/pytest/discussions/9296
 	ignore:Distutils was imported before setuptools
 	ignore:Setuptools is replacing distutils
+
+	ignore:Support for project metadata in .pyproject.toml. is still experimental
diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index 9075c791..93bfa7f1 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -1,6 +1,7 @@
 """Load setuptools configuration from ``pyproject.toml`` files"""
 import json
 import os
+import warnings
 from contextlib import contextmanager
 from distutils import log
 from functools import partial
@@ -78,6 +79,13 @@ def read_configuration(filepath, expand=True, ignore_option_errors=False):
     if not asdict or not(project_table or tool_table):
         return {}  # User is not using pyproject to configure setuptools
 
+    # TODO: Remove once the future stabilizes
+    msg = (
+        "Support for project metadata in `pyproject.toml` is still experimental "
+        "and may be removed (or change) in future releases."
+    )
+    warnings.warn(msg, _ExperimentalProjectMetadata)
+
     # There is an overall sense in the community that making include_package_data=True
     # the default would be an improvement.
     # `ini2toml` backfills include_package_data=False when nothing is explicitly given,
@@ -218,3 +226,7 @@ def _ignore_errors(ignore_option_errors):
         yield
     except Exception as ex:
         log.debug(f"Ignored error: {ex.__class__.__name__} - {ex}")
+
+
+class _ExperimentalProjectMetadata(UserWarning):
+    """Explicitly inform users that `pyproject.toml` configuration is experimental"""
-- 
cgit v1.2.1


From e9c1a3234eae3d4ae166ee88b667e3834bbe9dbf Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 5 Mar 2022 10:37:25 +0000
Subject: Rely on validate-pyproject default errors

---
 setuptools/config/pyprojecttoml.py | 16 +---------------
 1 file changed, 1 insertion(+), 15 deletions(-)

diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index 93bfa7f1..95138948 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -1,5 +1,4 @@
 """Load setuptools configuration from ``pyproject.toml`` files"""
-import json
 import os
 import warnings
 from contextlib import contextmanager
@@ -27,21 +26,8 @@ def load_file(filepath: _Path) -> dict:
 
 def validate(config: dict, filepath: _Path):
     from setuptools.extern import _validate_pyproject
-    from setuptools.extern._validate_pyproject import fastjsonschema_exceptions
 
-    try:
-        return _validate_pyproject.validate(config)
-    except fastjsonschema_exceptions.JsonSchemaValueException as ex:
-        msg = [f"Schema: {ex}"]
-        if ex.value:
-            msg.append(f"Given value:\n{json.dumps(ex.value, indent=2)}")
-        if ex.rule:
-            msg.append(f"Offending rule: {json.dumps(ex.rule, indent=2)}")
-        if ex.definition:
-            msg.append(f"Definition:\n{json.dumps(ex.definition, indent=2)}")
-
-        log.error("\n\n".join(msg) + "\n")
-        raise
+    return _validate_pyproject.validate(config)
 
 
 def apply_configuration(dist: "Distribution", filepath: _Path) -> "Distribution":
-- 
cgit v1.2.1


From 0cc747816126a7d2ba4a9ce8b1b9054ab7201537 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 5 Mar 2022 13:01:59 +0000
Subject: Show significant error messages to user and avoid traceback pollution

---
 setuptools/config/pyprojecttoml.py            | 23 +++++++---
 setuptools/tests/config/test_pyprojecttoml.py | 62 ++++++++++++++++++++++-----
 2 files changed, 69 insertions(+), 16 deletions(-)

diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index 95138948..421311e5 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -1,10 +1,10 @@
 """Load setuptools configuration from ``pyproject.toml`` files"""
 import os
 import warnings
+import logging
 from contextlib import contextmanager
-from distutils import log
 from functools import partial
-from typing import TYPE_CHECKING, Union
+from typing import TYPE_CHECKING, Union, cast
 
 from setuptools.errors import FileError, OptionError
 
@@ -15,6 +15,7 @@ if TYPE_CHECKING:
     from setuptools.dist import Distribution  # noqa
 
 _Path = Union[str, os.PathLike]
+_logger = logging.getLogger(__name__)
 
 
 def load_file(filepath: _Path) -> dict:
@@ -25,9 +26,21 @@ def load_file(filepath: _Path) -> dict:
 
 
 def validate(config: dict, filepath: _Path):
-    from setuptools.extern import _validate_pyproject
+    from setuptools.extern._validate_pyproject import validate as _validate
 
-    return _validate_pyproject.validate(config)
+    try:
+        return _validate(config)
+    except Exception as ex:
+        if ex.__class__.__name__ != "ValidationError":
+            # Workaround for the fact that `extern` can duplicate imports
+            ex_cls = ex.__class__.__name
+            error = ValueError(f"invalid pyproject.toml config: {ex_cls} - {ex}")
+            raise error from None
+
+        _logger.error(f"configuration error: {ex.summary}")  # type: ignore
+        _logger.debug(ex.details)  # type: ignore
+        error = ValueError(f"invalid pyproject.toml config: {ex.name}")  # type: ignore
+        raise error from None
 
 
 def apply_configuration(dist: "Distribution", filepath: _Path) -> "Distribution":
@@ -211,7 +224,7 @@ def _ignore_errors(ignore_option_errors):
     try:
         yield
     except Exception as ex:
-        log.debug(f"Ignored error: {ex.__class__.__name__} - {ex}")
+        _logger.debug(f"ignored error: {ex.__class__.__name__} - {ex}")
 
 
 class _ExperimentalProjectMetadata(UserWarning):
diff --git a/setuptools/tests/config/test_pyprojecttoml.py b/setuptools/tests/config/test_pyprojecttoml.py
index 759f0454..2132197d 100644
--- a/setuptools/tests/config/test_pyprojecttoml.py
+++ b/setuptools/tests/config/test_pyprojecttoml.py
@@ -1,4 +1,6 @@
+import logging
 from configparser import ConfigParser
+from inspect import cleandoc
 
 import pytest
 
@@ -140,22 +142,60 @@ def test_expand_entry_point(tmp_path):
     assert "gui-scripts" not in expanded_project
 
 
-EXAMPLE_INVALID_3RD_PARTY_CONFIG = """
-[project]
-name = "myproj"
-version = "1.2"
+@pytest.mark.parametrize(
+    "example",
+    (
+        """
+        [project]
+        name = "myproj"
+        version = "1.2"
+
+        [my-tool.that-disrespect.pep518]
+        value = 42
+        """,
+    )
+)
+def test_ignore_unrelated_config(tmp_path, example):
+    pyproject = tmp_path / "pyproject.toml"
+    pyproject.write_text(cleandoc(example))
 
-[my-tool.that-disrespect.pep518]
-value = 42
-"""
+    # Make sure no error is raised due to 3rd party configs in pyproject.toml
+    assert read_configuration(pyproject) is not None
 
 
-def test_ignore_unrelated_config(tmp_path):
+@pytest.mark.parametrize(
+    "example, error_msg, value_shown_in_debug",
+    [
+        (
+            """
+            [project]
+            name = "myproj"
+            version = "1.2"
+            requires = ['pywin32; platform_system=="Windows"' ]
+            """,
+            "configuration error: `project` must not contain {'requires'} properties",
+            '"requires": ["pywin32; platform_system==\\"Windows\\""]'
+        ),
+    ]
+)
+def test_invalid_example(tmp_path, caplog, example, error_msg, value_shown_in_debug):
+    caplog.set_level(logging.DEBUG)
     pyproject = tmp_path / "pyproject.toml"
-    pyproject.write_text(EXAMPLE_INVALID_3RD_PARTY_CONFIG)
+    pyproject.write_text(cleandoc(example))
 
-    # Make sure no error is raised due to 3rd party configs in pyproject.toml
-    assert read_configuration(pyproject) is not None
+    caplog.clear()
+    with pytest.raises(ValueError, match="invalid pyproject.toml"):
+        read_configuration(pyproject)
+
+    # Make sure the logs give guidance to the user
+    error_log = caplog.record_tuples[0]
+    assert error_log[1] == logging.ERROR
+    assert error_msg in error_log[2]
+
+    debug_log = caplog.record_tuples[1]
+    assert debug_log[1] == logging.DEBUG
+    debug_msg = "".join(line.strip() for line in debug_log[2].splitlines())
+    assert value_shown_in_debug in debug_msg
 
 
 @pytest.mark.parametrize("config", ("", "[tool.something]\nvalue = 42"))
-- 
cgit v1.2.1


From 298e74565bb8c3cbd6b27235638e366bb636e17a Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 5 Mar 2022 13:06:26 +0000
Subject: Removed unused import

---
 setuptools/config/pyprojecttoml.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index 421311e5..cdce2331 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -4,7 +4,7 @@ import warnings
 import logging
 from contextlib import contextmanager
 from functools import partial
-from typing import TYPE_CHECKING, Union, cast
+from typing import TYPE_CHECKING, Union
 
 from setuptools.errors import FileError, OptionError
 
-- 
cgit v1.2.1


From 96adc4fee56c7e50ea07fbf576c3fc3b2ecec0d2 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 5 Mar 2022 14:53:33 +0000
Subject: Fix variable name error

---
 setuptools/config/pyprojecttoml.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index cdce2331..4e7e08c7 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -33,7 +33,7 @@ def validate(config: dict, filepath: _Path):
     except Exception as ex:
         if ex.__class__.__name__ != "ValidationError":
             # Workaround for the fact that `extern` can duplicate imports
-            ex_cls = ex.__class__.__name
+            ex_cls = ex.__class__.__name__
             error = ValueError(f"invalid pyproject.toml config: {ex_cls} - {ex}")
             raise error from None
 
-- 
cgit v1.2.1


From 1bb00212e5ebe2e415c36f5f0fe754a62d8b44f5 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 25 Dec 2021 14:53:00 +0000
Subject: Add some type hints to config.setupcfg

---
 setuptools/config/setupcfg.py            | 77 +++++++++++++++++++++++---------
 setuptools/tests/config/test_setupcfg.py |  1 +
 2 files changed, 57 insertions(+), 21 deletions(-)

diff --git a/setuptools/config/setupcfg.py b/setuptools/config/setupcfg.py
index e4855a76..76feb6cd 100644
--- a/setuptools/config/setupcfg.py
+++ b/setuptools/config/setupcfg.py
@@ -6,6 +6,8 @@ import functools
 from collections import defaultdict
 from functools import partial
 from functools import wraps
+from typing import (TYPE_CHECKING, Callable, Any, Dict, Generic, Iterable, List,
+                    Optional, Tuple, TypeVar, Union)
 
 from distutils.errors import DistutilsOptionError, DistutilsFileError
 from setuptools.extern.packaging.version import Version, InvalidVersion
@@ -13,8 +15,26 @@ from setuptools.extern.packaging.specifiers import SpecifierSet
 
 from . import expand
 
-
-def read_configuration(filepath, find_others=False, ignore_option_errors=False):
+if TYPE_CHECKING:
+    from setuptools.dist import Distribution  # noqa
+    from distutils.dist import DistributionMetadata  # noqa
+
+_Path = Union[str, os.PathLike]
+SingleCommandOptions = Dict["str", Tuple["str", Any]]
+"""Dict that associate the name of the options of a particular command to a
+tuple. The first element of the tuple indicates the origin of the option value
+(e.g. the name of the configuration file where it was read from),
+while the second element of the tuple is the option value itself
+"""
+AllCommandOptions = Dict["str", SingleCommandOptions]  # cmd name => its options
+Target = TypeVar("Target", bound=Union["Distribution", "DistributionMetadata"])
+
+
+def read_configuration(
+    filepath: _Path,
+    find_others=False,
+    ignore_option_errors=False
+) -> dict:
     """Read given configuration file and returns options from it as a dict.
 
     :param str|unicode filepath: Path to configuration file
@@ -38,7 +58,7 @@ def read_configuration(filepath, find_others=False, ignore_option_errors=False):
     return configuration_to_dict(handlers)
 
 
-def apply_configuration(dist, filepath):
+def apply_configuration(dist: "Distribution", filepath: _Path) -> "Distribution":
     """Apply the configuration from a ``setup.cfg`` file into an existing
     distribution object.
     """
@@ -47,7 +67,11 @@ def apply_configuration(dist, filepath):
     return dist
 
 
-def _apply(dist, filepath, other_files=(), ignore_option_errors=False):
+def _apply(
+    dist: "Distribution", filepath: _Path,
+    other_files: Iterable[_Path] = (),
+    ignore_option_errors: bool = False
+) -> Tuple["ConfigHandler", ...]:
     """Read configuration from ``filepath`` and applies to the ``dist`` object."""
     from setuptools.dist import _Distribution
 
@@ -72,7 +96,7 @@ def _apply(dist, filepath, other_files=(), ignore_option_errors=False):
     return handlers
 
 
-def _get_option(target_obj, key):
+def _get_option(target_obj: Target, key: str):
     """
     Given a target object and option key, get that option from
     the target object, either through a get_{key} method or
@@ -84,7 +108,7 @@ def _get_option(target_obj, key):
     return getter()
 
 
-def configuration_to_dict(handlers):
+def configuration_to_dict(handlers: Tuple["ConfigHandler", ...]) -> dict:
     """Returns configuration data gathered by given handlers as a dict.
 
     :param list[ConfigHandler] handlers: Handlers list,
@@ -92,7 +116,7 @@ def configuration_to_dict(handlers):
 
     :rtype: dict
     """
-    config_dict = defaultdict(dict)
+    config_dict: dict = defaultdict(dict)
 
     for handler in handlers:
         for option in handler.set_options:
@@ -102,7 +126,11 @@ def configuration_to_dict(handlers):
     return config_dict
 
 
-def parse_configuration(distribution, command_options, ignore_option_errors=False):
+def parse_configuration(
+    distribution: "Distribution",
+    command_options: AllCommandOptions,
+    ignore_option_errors=False
+) -> Tuple["ConfigMetadataHandler", "ConfigOptionsHandler"]:
     """Performs additional parsing of configuration options
     for a distribution.
 
@@ -130,24 +158,29 @@ def parse_configuration(distribution, command_options, ignore_option_errors=Fals
     return meta, options
 
 
-class ConfigHandler:
+class ConfigHandler(Generic[Target]):
     """Handles metadata supplied in configuration files."""
 
-    section_prefix = None
+    section_prefix: str
     """Prefix for config sections handled by this handler.
     Must be provided by class heirs.
 
     """
 
-    aliases = {}
+    aliases: Dict[str, str] = {}
     """Options aliases.
     For compatibility with various packages. E.g.: d2to1 and pbr.
     Note: `-` in keys is replaced with `_` by config parser.
 
     """
 
-    def __init__(self, target_obj, options, ignore_option_errors=False):
-        sections = {}
+    def __init__(
+        self,
+        target_obj: Target,
+        options: AllCommandOptions,
+        ignore_option_errors=False
+    ):
+        sections: AllCommandOptions = {}
 
         section_prefix = self.section_prefix
         for section_name, section_options in options.items():
@@ -160,7 +193,7 @@ class ConfigHandler:
         self.ignore_option_errors = ignore_option_errors
         self.target_obj = target_obj
         self.sections = sections
-        self.set_options = []
+        self.set_options: List[str] = []
 
     @property
     def parsers(self):
@@ -382,7 +415,7 @@ class ConfigHandler:
             if section_name:  # [section.option] variant
                 method_postfix = '_%s' % section_name
 
-            section_parser_method = getattr(
+            section_parser_method: Optional[Callable] = getattr(
                 self,
                 # Dots in section names are translated into dunderscores.
                 ('parse_section%s' % method_postfix).replace('.', '__'),
@@ -413,7 +446,7 @@ class ConfigHandler:
         return config_handler
 
 
-class ConfigMetadataHandler(ConfigHandler):
+class ConfigMetadataHandler(ConfigHandler["DistributionMetadata"]):
 
     section_prefix = 'metadata'
 
@@ -431,11 +464,13 @@ class ConfigMetadataHandler(ConfigHandler):
     """
 
     def __init__(
-        self, target_obj, options, ignore_option_errors=False, package_dir=None
+        self,
+        target_obj: "DistributionMetadata",
+        options: AllCommandOptions,
+        ignore_option_errors=False,
+        package_dir: Optional[dict] = None
     ):
-        super(ConfigMetadataHandler, self).__init__(
-            target_obj, options, ignore_option_errors
-        )
+        super().__init__(target_obj, options, ignore_option_errors)
         self.package_dir = package_dir
 
     @property
@@ -499,7 +534,7 @@ class ConfigMetadataHandler(ConfigHandler):
         return expand.version(self._parse_attr(value, self.package_dir))
 
 
-class ConfigOptionsHandler(ConfigHandler):
+class ConfigOptionsHandler(ConfigHandler["Distribution"]):
 
     section_prefix = 'options'
 
diff --git a/setuptools/tests/config/test_setupcfg.py b/setuptools/tests/config/test_setupcfg.py
index 268cf91d..5bfefac0 100644
--- a/setuptools/tests/config/test_setupcfg.py
+++ b/setuptools/tests/config/test_setupcfg.py
@@ -14,6 +14,7 @@ from ..textwrap import DALS
 
 class ErrConfigHandler(ConfigHandler):
     """Erroneous handler. Fails to implement required methods."""
+    section_prefix = "**err**"
 
 
 def make_package_dir(name, base_dir, ns=False):
-- 
cgit v1.2.1


From 441a1fa000bf66bc4ee6812bc7f3a039b85f2902 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 25 Dec 2021 15:12:57 +0000
Subject: Add some type hints to config.pyprojecttoml

---
 setuptools/config/_apply_pyprojecttoml.py |  2 +-
 setuptools/config/pyprojecttoml.py        | 30 ++++++++++++++++++++----------
 2 files changed, 21 insertions(+), 11 deletions(-)

diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py
index f711c8a2..3ce74512 100644
--- a/setuptools/config/_apply_pyprojecttoml.py
+++ b/setuptools/config/_apply_pyprojecttoml.py
@@ -18,7 +18,7 @@ if TYPE_CHECKING:
     from setuptools._importlib import metadata  # noqa
     from setuptools.dist import Distribution  # noqa
 
-EMPTY = MappingProxyType({})  # Immutable dict-like
+EMPTY: Mapping = MappingProxyType({})  # Immutable dict-like
 _Path = Union[os.PathLike, str]
 _DictOrStr = Union[dict, str]
 _CorrespFn = Callable[["Distribution", Any, _Path], None]
diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index 4e7e08c7..1ebdd08d 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -4,7 +4,7 @@ import warnings
 import logging
 from contextlib import contextmanager
 from functools import partial
-from typing import TYPE_CHECKING, Union
+from typing import TYPE_CHECKING, Callable, Optional, Union
 
 from setuptools.errors import FileError, OptionError
 
@@ -51,7 +51,7 @@ def apply_configuration(dist: "Distribution", filepath: _Path) -> "Distribution"
     return apply(dist, config, filepath)
 
 
-def read_configuration(filepath, expand=True, ignore_option_errors=False):
+def read_configuration(filepath: _Path, expand=True, ignore_option_errors=False):
     """Read given configuration file and returns options from it as a dict.
 
     :param str|unicode filepath: Path to configuration file in the ``pyproject.toml``
@@ -103,7 +103,9 @@ def read_configuration(filepath, expand=True, ignore_option_errors=False):
     return asdict
 
 
-def expand_configuration(config, root_dir=None, ignore_option_errors=False):
+def expand_configuration(
+    config: dict, root_dir: Optional[_Path] = None, ignore_option_errors=False
+) -> dict:
     """Given a configuration with unresolved fields (e.g. dynamic, cmdclass, ...)
     find their final values.
 
@@ -133,7 +135,9 @@ def expand_configuration(config, root_dir=None, ignore_option_errors=False):
     return config
 
 
-def _expand_all_dynamic(project_cfg, setuptools_cfg, root_dir, ignore_option_errors):
+def _expand_all_dynamic(
+    project_cfg: dict, setuptools_cfg: dict, root_dir: _Path, ignore_option_errors: bool
+):
     silent = ignore_option_errors
     dynamic_cfg = setuptools_cfg.get("dynamic", {})
     package_dir = setuptools_cfg.get("package-dir", None)
@@ -160,7 +164,10 @@ def _expand_all_dynamic(project_cfg, setuptools_cfg, root_dir, ignore_option_err
         project_cfg.update(_expand_entry_points(value, dynamic))
 
 
-def _expand_dynamic(dynamic_cfg, field, package_dir, root_dir, ignore_option_errors):
+def _expand_dynamic(
+    dynamic_cfg: dict, field: str, package_dir: Optional[dict],
+    root_dir: _Path, ignore_option_errors: bool
+):
     if field in dynamic_cfg:
         directive = dynamic_cfg[field]
         if "file" in directive:
@@ -174,7 +181,7 @@ def _expand_dynamic(dynamic_cfg, field, package_dir, root_dir, ignore_option_err
     return None
 
 
-def _expand_readme(dynamic_cfg, root_dir, ignore_option_errors):
+def _expand_readme(dynamic_cfg: dict, root_dir: _Path, ignore_option_errors: bool):
     silent = ignore_option_errors
     return {
         "text": _expand_dynamic(dynamic_cfg, "readme", None, root_dir, silent),
@@ -182,7 +189,7 @@ def _expand_readme(dynamic_cfg, root_dir, ignore_option_errors):
     }
 
 
-def _expand_entry_points(text, dynamic):
+def _expand_entry_points(text: str, dynamic: set):
     groups = _expand.entry_points(text)
     expanded = {"entry-points": groups}
     if "scripts" in dynamic and "console_scripts" in groups:
@@ -192,7 +199,7 @@ def _expand_entry_points(text, dynamic):
     return expanded
 
 
-def _expand_packages(setuptools_cfg, root_dir, ignore_option_errors=False):
+def _expand_packages(setuptools_cfg: dict, root_dir: _Path, ignore_option_errors=False):
     packages = setuptools_cfg.get("packages")
     if packages is None or isinstance(packages, (list, tuple)):
         return
@@ -204,7 +211,10 @@ def _expand_packages(setuptools_cfg, root_dir, ignore_option_errors=False):
             setuptools_cfg["packages"] = _expand.find_packages(**find)
 
 
-def _process_field(container, field, fn, ignore_option_errors=False):
+def _process_field(
+    container: dict, field: str,
+    fn: Callable, ignore_option_errors=False
+):
     if field in container:
         with _ignore_errors(ignore_option_errors):
             container[field] = fn(container[field])
@@ -216,7 +226,7 @@ def _canonic_package_data(setuptools_cfg, field="package-data"):
 
 
 @contextmanager
-def _ignore_errors(ignore_option_errors):
+def _ignore_errors(ignore_option_errors: bool):
     if not ignore_option_errors:
         yield
         return
-- 
cgit v1.2.1


From d3e62b109e0d6ec57dcf14207c7dd91610138666 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 25 Dec 2021 15:35:16 +0000
Subject: Add some type hints to config.expand

---
 setuptools/config/expand.py | 78 ++++++++++++++++++++++++++++++---------------
 1 file changed, 52 insertions(+), 26 deletions(-)

diff --git a/setuptools/config/expand.py b/setuptools/config/expand.py
index 4778ffb6..cf034d69 100644
--- a/setuptools/config/expand.py
+++ b/setuptools/config/expand.py
@@ -22,18 +22,22 @@ import os
 import sys
 from glob import iglob
 from configparser import ConfigParser
+from importlib.machinery import ModuleSpec
 from itertools import chain
+from typing import Callable, Dict, Iterable, List, Optional, Tuple, Union, cast
+from types import ModuleType
 
 from distutils.errors import DistutilsOptionError
 
 chain_iter = chain.from_iterable
+_Path = Union[str, os.PathLike]
 
 
 class StaticModule:
     """Proxy to a module object that avoids executing arbitrary code."""
 
-    def __init__(self, name, spec):
-        with open(spec.origin) as strm:
+    def __init__(self, name: str, spec: ModuleSpec):
+        with open(spec.origin) as strm:  # type: ignore
             src = strm.read()
         module = ast.parse(src)
         vars(self).update(locals())
@@ -62,7 +66,9 @@ class StaticModule:
             raise AttributeError(f"{self.name} has no attribute {attr}") from e
 
 
-def glob_relative(patterns, root_dir=None):
+def glob_relative(
+    patterns: Iterable[str], root_dir: Optional[_Path] = None
+) -> List[str]:
     """Expand the list of glob patterns, but preserving relative paths.
 
     :param list[str] patterns: List of glob patterns
@@ -91,7 +97,7 @@ def glob_relative(patterns, root_dir=None):
     return expanded_values
 
 
-def read_files(filepaths, root_dir=None):
+def read_files(filepaths: Union[str, bytes, Iterable[_Path]], root_dir=None) -> str:
     """Return the content of the files concatenated using ``\n`` as str
 
     This function is sandboxed and won't reach anything outside ``root_dir``
@@ -99,7 +105,7 @@ def read_files(filepaths, root_dir=None):
     (By default ``root_dir`` is the current directory).
     """
     if isinstance(filepaths, (str, bytes)):
-        filepaths = [filepaths]
+        filepaths = [filepaths]  # type: ignore
 
     root_dir = os.path.abspath(root_dir or os.getcwd())
     _filepaths = (os.path.join(root_dir, path) for path in filepaths)
@@ -110,12 +116,12 @@ def read_files(filepaths, root_dir=None):
     )
 
 
-def _read_file(filepath):
+def _read_file(filepath: Union[bytes, _Path]) -> str:
     with io.open(filepath, encoding='utf-8') as f:
         return f.read()
 
 
-def _assert_local(filepath, root_dir):
+def _assert_local(filepath: _Path, root_dir: str):
     if not os.path.abspath(filepath).startswith(root_dir):
         msg = f"Cannot access {filepath!r} (or anything outside {root_dir!r})"
         raise DistutilsOptionError(msg)
@@ -123,7 +129,11 @@ def _assert_local(filepath, root_dir):
     return True
 
 
-def read_attr(attr_desc, package_dir=None, root_dir=None):
+def read_attr(
+    attr_desc: str,
+    package_dir: Optional[dict] = None,
+    root_dir: Optional[_Path] = None
+):
     """Reads the value of an attribute from a module.
 
     This function will try to read the attributed statically first
@@ -146,8 +156,8 @@ def read_attr(attr_desc, package_dir=None, root_dir=None):
     attr_name = attrs_path.pop()
     module_name = '.'.join(attrs_path)
     module_name = module_name or '__init__'
-    parent_path, path, module_name = _find_module(module_name, package_dir, root_dir)
-    spec = _find_spec(module_name, path, parent_path)
+    _parent_path, path, module_name = _find_module(module_name, package_dir, root_dir)
+    spec = _find_spec(module_name, path)
 
     try:
         return getattr(StaticModule(module_name, spec), attr_name)
@@ -157,7 +167,7 @@ def read_attr(attr_desc, package_dir=None, root_dir=None):
         return getattr(module, attr_name)
 
 
-def _find_spec(module_name, module_path, parent_path):
+def _find_spec(module_name: str, module_path: Optional[_Path]) -> ModuleSpec:
     spec = importlib.util.spec_from_file_location(module_name, module_path)
     spec = spec or importlib.util.find_spec(module_name)
 
@@ -167,17 +177,19 @@ def _find_spec(module_name, module_path, parent_path):
     return spec
 
 
-def _load_spec(spec, module_name):
+def _load_spec(spec: ModuleSpec, module_name: str) -> ModuleType:
     name = getattr(spec, "__name__", module_name)
     if name in sys.modules:
         return sys.modules[name]
     module = importlib.util.module_from_spec(spec)
     sys.modules[name] = module  # cache (it also ensures `==` works on loaded items)
-    spec.loader.exec_module(module)
+    spec.loader.exec_module(module)  # type: ignore
     return module
 
 
-def _find_module(module_name, package_dir, root_dir):
+def _find_module(
+    module_name: str, package_dir: Optional[dict], root_dir: _Path
+) -> Tuple[_Path, Optional[str], str]:
     """Given a module (that could normally be imported by ``module_name``
     after the build is complete), find the path to the parent directory where
     it is contained and the canonical name that could be used to import it
@@ -209,26 +221,36 @@ def _find_module(module_name, package_dir, root_dir):
     return parent_path, module_path, module_name
 
 
-def resolve_class(qualified_class_name, package_dir=None, root_dir=None):
+def resolve_class(
+    qualified_class_name: str,
+    package_dir: Optional[dict] = None,
+    root_dir: Optional[_Path] = None
+) -> Callable:
     """Given a qualified class name, return the associated class object"""
     root_dir = root_dir or os.getcwd()
     idx = qualified_class_name.rfind('.')
     class_name = qualified_class_name[idx + 1 :]
     pkg_name = qualified_class_name[:idx]
 
-    parent_path, path, module_name = _find_module(pkg_name, package_dir, root_dir)
-    module = _load_spec(_find_spec(module_name, path, parent_path), module_name)
+    _parent_path, path, module_name = _find_module(pkg_name, package_dir, root_dir)
+    module = _load_spec(_find_spec(module_name, path), module_name)
     return getattr(module, class_name)
 
 
-def cmdclass(values, package_dir=None, root_dir=None):
+def cmdclass(
+    values: Dict[str, str],
+    package_dir: Optional[dict] = None,
+    root_dir: Optional[_Path] = None
+) -> Dict[str, Callable]:
     """Given a dictionary mapping command names to strings for qualified class
     names, apply :func:`resolve_class` to the dict values.
     """
     return {k: resolve_class(v, package_dir, root_dir) for k, v in values.items()}
 
 
-def find_packages(*, namespaces=False, root_dir=None, **kwargs):
+def find_packages(
+    *, namespaces=False, root_dir: Optional[_Path] = None, **kwargs
+) -> List[str]:
     """Works similarly to :func:`setuptools.find_packages`, but with all
     arguments given as keyword arguments. Moreover, ``where`` can be given
     as a list (the results will be simply concatenated).
@@ -243,7 +265,7 @@ def find_packages(*, namespaces=False, root_dir=None, **kwargs):
     if namespaces:
         from setuptools import PEP420PackageFinder as PackageFinder
     else:
-        from setuptools import PackageFinder
+        from setuptools import PackageFinder  # type: ignore
 
     root_dir = root_dir or "."
     where = kwargs.pop('where', ['.'])
@@ -253,18 +275,20 @@ def find_packages(*, namespaces=False, root_dir=None, **kwargs):
     return list(chain_iter(PackageFinder.find(x, **kwargs) for x in target))
 
 
-def _nest_path(parent, path):
+def _nest_path(parent: _Path, path: _Path) -> str:
     path = parent if path == "." else os.path.join(parent, path)
     return os.path.normpath(path)
 
 
-def version(value):
+def version(value: Union[Callable, Iterable[Union[str, int]], str]) -> str:
     """When getting the version directly from an attribute,
     it should be normalised to string.
     """
     if callable(value):
         value = value()
 
+    value = cast(Iterable[Union[str, int]], value)
+
     if not isinstance(value, str):
         if hasattr(value, '__iter__'):
             value = '.'.join(map(str, value))
@@ -274,13 +298,15 @@ def version(value):
     return value
 
 
-def canonic_package_data(package_data):
+def canonic_package_data(package_data: dict) -> dict:
     if "*" in package_data:
         package_data[""] = package_data.pop("*")
     return package_data
 
 
-def canonic_data_files(data_files, root_dir=None):
+def canonic_data_files(
+    data_files: Union[list, dict], root_dir: Optional[_Path] = None
+) -> List[Tuple[str, List[str]]]:
     """For compatibility with ``setup.py``, ``data_files`` should be a list
     of pairs instead of a dict.
 
@@ -295,14 +321,14 @@ def canonic_data_files(data_files, root_dir=None):
     ]
 
 
-def entry_points(text, text_source="entry-points"):
+def entry_points(text: str, text_source="entry-points") -> Dict[str, dict]:
     """Given the contents of entry-points file,
     process it into a 2-level dictionary (``dict[str, dict[str, str]]``).
     The first level keys are entry-point groups, the second level keys are
     entry-point names, and the second level values are references to objects
     (that correspond to the entry-point value).
     """
-    parser = ConfigParser(default_section=None, delimiters=("=",))
+    parser = ConfigParser(default_section=None, delimiters=("=",))  # type: ignore
     parser.optionxform = str  # case sensitive
     parser.read_string(text, text_source)
     groups = {k: dict(v.items()) for k, v in parser.items()}
-- 
cgit v1.2.1


From 54f61809ed9da2a1ab3d14f7a8f06c3d74ad799c Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 19 Feb 2022 20:30:10 +0000
Subject: Find namespaces by default when using config in 'pyproject.toml'

---
 setuptools/config/expand.py                   | 2 +-
 setuptools/config/setupcfg.py                 | 5 +----
 setuptools/tests/config/test_expand.py        | 5 +++--
 setuptools/tests/config/test_pyprojecttoml.py | 3 +--
 4 files changed, 6 insertions(+), 9 deletions(-)

diff --git a/setuptools/config/expand.py b/setuptools/config/expand.py
index cf034d69..9d51a0a8 100644
--- a/setuptools/config/expand.py
+++ b/setuptools/config/expand.py
@@ -249,7 +249,7 @@ def cmdclass(
 
 
 def find_packages(
-    *, namespaces=False, root_dir: Optional[_Path] = None, **kwargs
+    *, namespaces=True, root_dir: Optional[_Path] = None, **kwargs
 ) -> List[str]:
     """Works similarly to :func:`setuptools.find_packages`, but with all
     arguments given as keyword arguments. Moreover, ``where`` can be given
diff --git a/setuptools/config/setupcfg.py b/setuptools/config/setupcfg.py
index 76feb6cd..5a449655 100644
--- a/setuptools/config/setupcfg.py
+++ b/setuptools/config/setupcfg.py
@@ -580,15 +580,12 @@ class ConfigOptionsHandler(ConfigHandler["Distribution"]):
         if trimmed_value not in find_directives:
             return self._parse_list(value)
 
-        findns = trimmed_value == find_directives[1]
-
         # Read function arguments from a dedicated section.
         find_kwargs = self.parse_section_packages__find(
             self.sections.get('packages.find', {})
         )
 
-        if findns:
-            find_kwargs["namespaces"] = True
+        find_kwargs["namespaces"] = (trimmed_value == find_directives[1])
 
         return expand.find_packages(**find_kwargs)
 
diff --git a/setuptools/tests/config/test_expand.py b/setuptools/tests/config/test_expand.py
index 1898792b..2461347d 100644
--- a/setuptools/tests/config/test_expand.py
+++ b/setuptools/tests/config/test_expand.py
@@ -88,9 +88,10 @@ def test_resolve_class():
 @pytest.mark.parametrize(
     'args, pkgs',
     [
-        ({"where": ["."]}, {"pkg", "other"}),
-        ({"where": [".", "dir1"]}, {"pkg", "other", "dir2"}),
+        ({"where": ["."], "namespaces": False}, {"pkg", "other"}),
+        ({"where": [".", "dir1"], "namespaces": False}, {"pkg", "other", "dir2"}),
         ({"namespaces": True}, {"pkg", "other", "dir1", "dir1.dir2"}),
+        ({}, {"pkg", "other", "dir1", "dir1.dir2"}),  # default value for `namespaces`
     ]
 )
 def test_find_packages(tmp_path, monkeypatch, args, pkgs):
diff --git a/setuptools/tests/config/test_pyprojecttoml.py b/setuptools/tests/config/test_pyprojecttoml.py
index 2132197d..395824bf 100644
--- a/setuptools/tests/config/test_pyprojecttoml.py
+++ b/setuptools/tests/config/test_pyprojecttoml.py
@@ -43,7 +43,6 @@ platforms = ["any"]
 
 [tool.setuptools.packages.find]
 where = ["src"]
-namespaces = true
 
 [tool.setuptools.cmdclass]
 sdist = "pkg.mod.CustomSdist"
@@ -74,7 +73,7 @@ def test_read_configuration(tmp_path):
 
     files = [
         "src/pkg/__init__.py",
-        "src/other/nested/__init__.py",
+        "src/other/nested/__init__.py",  # ensure namespaces are discovered by default
         "files/file.txt"
     ]
     for file in files:
-- 
cgit v1.2.1


From 5c334b319454d2c0c1b4eb3e2377c73e570d875e Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 19 Feb 2022 21:13:03 +0000
Subject: Add news fragment

---
 changelog.d/3125.change.rst | 10 ++++++++++
 1 file changed, 10 insertions(+)
 create mode 100644 changelog.d/3125.change.rst

diff --git a/changelog.d/3125.change.rst b/changelog.d/3125.change.rst
new file mode 100644
index 00000000..716e95c0
--- /dev/null
+++ b/changelog.d/3125.change.rst
@@ -0,0 +1,10 @@
+Implicit namespaces (as introduced in :pep:`420`) are now considered by default
+during :doc:`package discovery `, when
+``setuptools`` configuration and project metadata are added to the
+``pyproject.toml`` file.
+
+To disable this behaviour, use ``namespaces = False`` when explicitly setting
+the ``[tool.setuptools.packages.find]`` section in ``pyproject.toml``.
+
+This change is backwards compatible and does not affect the behaviour of
+configuration done in ``setup.cfg`` or ``setup.py``.
-- 
cgit v1.2.1


From 6376ad10547315c15dfec719ff3f384e7a94dfc2 Mon Sep 17 00:00:00 2001
From: Andrey Bienkowski 
Date: Sun, 6 Mar 2022 00:43:07 +0300
Subject: XXX: Debugging

---
 setuptools/command/easy_install.py | 37 +++++++++++++++++++++++++++++++++++++
 1 file changed, 37 insertions(+)

diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py
index 107850a9..318eac31 100644
--- a/setuptools/command/easy_install.py
+++ b/setuptools/command/easy_install.py
@@ -221,6 +221,42 @@ class easy_install(Command):
         raise SystemExit()
 
     def finalize_options(self):  # noqa: C901  # is too complex (25)  # FIXME
+        print(sysconfig._INSTALL_SCHEMES)
+        print(f'{os.name}_user')
+
+        def global_trace(frame, event, arg):
+            pass
+
+        import sys
+
+        sys.settrace(global_trace)
+
+        XXX = [set()]
+
+        def trace(frame, event, arg):
+            config_vars = getattr(self, 'config_vars', {})
+            o = {
+                ('USER_BASE', site.USER_BASE),
+                ('USER_SITE',  site.USER_SITE),
+                ('install_dir', self.install_dir),
+                ('install_userbase', self.install_userbase),
+                ('install_usersite', self.install_usersite),
+                ('install_purelib', self.install_purelib),
+                ('install_scripts', self.install_scripts),
+                ('userbase', config_vars.get('userbase')),
+                ('usersite', config_vars.get('usersite')),
+            }
+            if XXX[0] - o:
+                print('-', XXX[0] - o)
+            if o - XXX[0]:
+                print('+', o - XXX[0])
+            XXX[0] = o
+            lines, start = inspect.getsourcelines(frame)
+            print(frame.f_lineno, lines[frame.f_lineno - start])
+
+        import inspect
+        inspect.currentframe().f_trace = trace
+
         self.version and self._render_version()
 
         py_version = sys.version.split()[0]
@@ -459,6 +495,7 @@ class easy_install(Command):
         instdir = normalize_path(self.install_dir)
         pth_file = os.path.join(instdir, 'easy-install.pth')
 
+        print('XXX', instdir, os.path.exists(instdir))
         if not os.path.exists(instdir):
             try:
                 os.makedirs(instdir)
-- 
cgit v1.2.1


From 2e7ba454d431945237125b951c5482a452b67e4e Mon Sep 17 00:00:00 2001
From: Andrey Bienkowski 
Date: Sat, 5 Mar 2022 18:22:01 +0300
Subject: Test: editable install \w --user&build isolation

Add a new test and confirm that it
fails in the expected manner
---
 setuptools/tests/test_easy_install.py | 42 +++++++++++++++++++++++++++++++++++
 1 file changed, 42 insertions(+)

diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py
index 5831b267..09c4e075 100644
--- a/setuptools/tests/test_easy_install.py
+++ b/setuptools/tests/test_easy_install.py
@@ -40,6 +40,8 @@ import pkg_resources
 from . import contexts
 from .textwrap import DALS
 
+import py
+
 
 @pytest.fixture(autouse=True)
 def pip_disable_index(monkeypatch):
@@ -1109,3 +1111,43 @@ def test_use_correct_python_version_string(tmpdir, tmpdir_cwd, monkeypatch):
     assert cmd.config_vars['py_version'] == '3.10.1'
     assert cmd.config_vars['py_version_short'] == '3.10'
     assert cmd.config_vars['py_version_nodot'] == '310'
+
+
+def test_editable_user_and_build_isolation(setup_context, monkeypatch, tmpdir):
+    ''' `setup.py develop` should honor `--user` even under build isolation'''
+
+    # == Arrange ==
+    # Pretend that build isolation was enabled
+    # e.g pip sets the environment varible PYTHONNOUSERSITE=1
+    monkeypatch.setattr('site.ENABLE_USER_SITE', False)
+
+    # Patching $HOME for 2 reasons:
+    # 1. setuptools/command/easy_install.py:create_home_path
+    #    tries creating directories in $HOME
+    # given `self.config_vars['DESTDIRS'] = "/home/user/.pyenv/versions/3.9.10 /home/user/.pyenv/versions/3.9.10/lib /home/user/.pyenv/versions/3.9.10/lib/python3.9 /home/user/.pyenv/versions/3.9.10/lib/python3.9/lib-dynload"``  # noqa: E501
+    # it will `makedirs("/home/user/.pyenv/versions/3.9.10 /home/user/.pyenv/versions/3.9.10/lib /home/user/.pyenv/versions/3.9.10/lib/python3.9 /home/user/.pyenv/versions/3.9.10/lib/python3.9/lib-dynload")``  # noqa: E501
+    # 2. We are going to force `site` to update site.USER_BASE and site.USER_SITE
+    #    To point inside our new home
+    monkeypatch.setenv('HOME', str(tmpdir / 'home'))
+    monkeypatch.setattr('site.USER_BASE', None)
+    monkeypatch.setattr('site.USER_SITE', None)
+    user_site = py.path.local(site.getusersitepackages())
+    user_site.ensure_dir()
+
+    sys_prefix = (tmpdir / 'sys_prefix').ensure_dir()
+    monkeypatch.setattr('sys.prefix', str(sys_prefix))
+
+    # == Sanity check ==
+    assert sys_prefix.listdir() == []
+    assert user_site.listdir() == []
+
+    # == Act ==
+    run_setup('setup.py', ['develop', '--user'])
+
+    # == Assert ==
+    # Should not install to sys.prefix
+    with pytest.raises(AssertionError):
+        assert sys_prefix.listdir() == []
+    # Should install to user site
+    with pytest.raises(AssertionError):
+        assert {f.basename for f in user_site.listdir()} == {'UNKNOWN.egg-link'}
-- 
cgit v1.2.1


From 45340d00688ba29fc3492c52c88c47d14ce918e6 Mon Sep 17 00:00:00 2001
From: Andrey Bienkowski 
Date: Sun, 6 Mar 2022 07:58:24 +0300
Subject: Revert "XXX: Debugging"

This reverts commit 6376ad10547315c15dfec719ff3f384e7a94dfc2.
---
 setuptools/command/easy_install.py | 37 -------------------------------------
 1 file changed, 37 deletions(-)

diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py
index 318eac31..107850a9 100644
--- a/setuptools/command/easy_install.py
+++ b/setuptools/command/easy_install.py
@@ -221,42 +221,6 @@ class easy_install(Command):
         raise SystemExit()
 
     def finalize_options(self):  # noqa: C901  # is too complex (25)  # FIXME
-        print(sysconfig._INSTALL_SCHEMES)
-        print(f'{os.name}_user')
-
-        def global_trace(frame, event, arg):
-            pass
-
-        import sys
-
-        sys.settrace(global_trace)
-
-        XXX = [set()]
-
-        def trace(frame, event, arg):
-            config_vars = getattr(self, 'config_vars', {})
-            o = {
-                ('USER_BASE', site.USER_BASE),
-                ('USER_SITE',  site.USER_SITE),
-                ('install_dir', self.install_dir),
-                ('install_userbase', self.install_userbase),
-                ('install_usersite', self.install_usersite),
-                ('install_purelib', self.install_purelib),
-                ('install_scripts', self.install_scripts),
-                ('userbase', config_vars.get('userbase')),
-                ('usersite', config_vars.get('usersite')),
-            }
-            if XXX[0] - o:
-                print('-', XXX[0] - o)
-            if o - XXX[0]:
-                print('+', o - XXX[0])
-            XXX[0] = o
-            lines, start = inspect.getsourcelines(frame)
-            print(frame.f_lineno, lines[frame.f_lineno - start])
-
-        import inspect
-        inspect.currentframe().f_trace = trace
-
         self.version and self._render_version()
 
         py_version = sys.version.split()[0]
@@ -495,7 +459,6 @@ class easy_install(Command):
         instdir = normalize_path(self.install_dir)
         pth_file = os.path.join(instdir, 'easy-install.pth')
 
-        print('XXX', instdir, os.path.exists(instdir))
         if not os.path.exists(instdir):
             try:
                 os.makedirs(instdir)
-- 
cgit v1.2.1


From b828c32cd49f2281156644fce55d3c40663081dd Mon Sep 17 00:00:00 2001
From: Andrey Bienkowski 
Date: Sat, 5 Mar 2022 15:20:42 +0300
Subject: Fix editable --user installs with build isolation

https://github.com/pypa/setuptools/issues/3019
---
 changelog.d/3151.breaking.rst         |  1 +
 setuptools/command/easy_install.py    | 18 ++++++------------
 setuptools/tests/test_easy_install.py |  6 ++----
 3 files changed, 9 insertions(+), 16 deletions(-)
 create mode 100644 changelog.d/3151.breaking.rst

diff --git a/changelog.d/3151.breaking.rst b/changelog.d/3151.breaking.rst
new file mode 100644
index 00000000..73f7c1a8
--- /dev/null
+++ b/changelog.d/3151.breaking.rst
@@ -0,0 +1 @@
+Made ``setup.py develop --user`` install to the user site packages directory even if it is disabled in the current interpreter.
diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py
index 107850a9..940c916f 100644
--- a/setuptools/command/easy_install.py
+++ b/setuptools/command/easy_install.py
@@ -169,12 +169,8 @@ class easy_install(Command):
         self.install_data = None
         self.install_base = None
         self.install_platbase = None
-        if site.ENABLE_USER_SITE:
-            self.install_userbase = site.USER_BASE
-            self.install_usersite = site.USER_SITE
-        else:
-            self.install_userbase = None
-            self.install_usersite = None
+        self.install_userbase = site.USER_BASE
+        self.install_usersite = site.USER_SITE
         self.no_find_links = None
 
         # Options not specifiable via command line
@@ -253,11 +249,9 @@ class easy_install(Command):
             getattr(sys, 'windir', '').replace('.', ''),
         )
 
-        if site.ENABLE_USER_SITE:
-            self.config_vars['userbase'] = self.install_userbase
-            self.config_vars['usersite'] = self.install_usersite
-
-        elif self.user:
+        self.config_vars['userbase'] = self.install_userbase
+        self.config_vars['usersite'] = self.install_usersite
+        if self.user and not site.ENABLE_USER_SITE:
             log.warn("WARNING: The user site-packages directory is disabled.")
 
         self._fix_install_dir_for_user_site()
@@ -373,7 +367,7 @@ class easy_install(Command):
         """
         Fix the install_dir if "--user" was used.
         """
-        if not self.user or not site.ENABLE_USER_SITE:
+        if not self.user:
             return
 
         self.create_home_path()
diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py
index 09c4e075..7a8b64a6 100644
--- a/setuptools/tests/test_easy_install.py
+++ b/setuptools/tests/test_easy_install.py
@@ -1146,8 +1146,6 @@ def test_editable_user_and_build_isolation(setup_context, monkeypatch, tmpdir):
 
     # == Assert ==
     # Should not install to sys.prefix
-    with pytest.raises(AssertionError):
-        assert sys_prefix.listdir() == []
+    assert sys_prefix.listdir() == []
     # Should install to user site
-    with pytest.raises(AssertionError):
-        assert {f.basename for f in user_site.listdir()} == {'UNKNOWN.egg-link'}
+    assert {f.basename for f in user_site.listdir()} == {'UNKNOWN.egg-link'}
-- 
cgit v1.2.1

-- 
cgit v1.2.1


From a28da5fad3dd78e9234e16f07601c8979f9e116b Mon Sep 17 00:00:00 2001
From: Andrey Bienkowski 
Date: Sun, 6 Mar 2022 09:10:48 +0300
Subject: Fix test_editable_user_and_build_isolation

This test broke on my machine for some reason
---
 setuptools/tests/test_easy_install.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py
index 7a8b64a6..fd4a83ee 100644
--- a/setuptools/tests/test_easy_install.py
+++ b/setuptools/tests/test_easy_install.py
@@ -1148,4 +1148,7 @@ def test_editable_user_and_build_isolation(setup_context, monkeypatch, tmpdir):
     # Should not install to sys.prefix
     assert sys_prefix.listdir() == []
     # Should install to user site
-    assert {f.basename for f in user_site.listdir()} == {'UNKNOWN.egg-link'}
+    installed = {f.basename for f in user_site.listdir()}
+    # sometimes easy-install.pth is created and sometimes not
+    installed = installed - {"easy-install.pth"}
+    assert installed == {'UNKNOWN.egg-link'}
-- 
cgit v1.2.1


From 9413b864b83197c09dc6efd0f18021a41a220d2d Mon Sep 17 00:00:00 2001
From: Xing Han Lu 
Date: Sun, 6 Mar 2022 17:47:42 -0500
Subject: Add news fragment

---
 changelog.d/3144.doc.rst | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/3144.doc.rst

diff --git a/changelog.d/3144.doc.rst b/changelog.d/3144.doc.rst
new file mode 100644
index 00000000..36cc6521
--- /dev/null
+++ b/changelog.d/3144.doc.rst
@@ -0,0 +1 @@
+Added documentation on using console_scripts from setup.py, which was previously only shown in setup.cfg  -- by :user:`xhlulu`
\ No newline at end of file
-- 
cgit v1.2.1


From 0e46f782d08d1ee5afc610eddc0f028fe5922439 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 7 Mar 2022 18:19:10 +0000
Subject: [CI] Allow pre-built distribution to be used in tests with
 virtualenvs

---
 changelog.d/3147.misc.rst    | 4 ++++
 setuptools/tests/fixtures.py | 8 ++++++++
 tox.ini                      | 2 ++
 3 files changed, 14 insertions(+)
 create mode 100644 changelog.d/3147.misc.rst

diff --git a/changelog.d/3147.misc.rst b/changelog.d/3147.misc.rst
new file mode 100644
index 00000000..1b0b8685
--- /dev/null
+++ b/changelog.d/3147.misc.rst
@@ -0,0 +1,4 @@
+Added options to provide a pre-build ``setuptools`` wheel or sdist for being
+used during tests with virtual environments.
+Paths for these pre-built distribution files can now be set via the environment
+variables: ``PRE_BUILT_SETUPTOOLS_SDIST`` and ``PRE_BUILT_SETUPTOOLS_WHEEL``.
diff --git a/setuptools/tests/fixtures.py b/setuptools/tests/fixtures.py
index e912399d..25ab49fd 100644
--- a/setuptools/tests/fixtures.py
+++ b/setuptools/tests/fixtures.py
@@ -1,6 +1,8 @@
+import os
 import contextlib
 import sys
 import subprocess
+from pathlib import Path
 
 import pytest
 import path
@@ -64,6 +66,9 @@ def sample_project(tmp_path):
 
 @pytest.fixture(scope="session")
 def setuptools_sdist(tmp_path_factory, request):
+    if os.getenv("PRE_BUILT_SETUPTOOLS_SDIST"):
+        return Path(os.getenv("PRE_BUILT_SETUPTOOLS_SDIST")).resolve()
+
     with contexts.session_locked_tmp_dir(
             request, tmp_path_factory, "sdist_build") as tmp:
         dist = next(tmp.glob("*.tar.gz"), None)
@@ -79,6 +84,9 @@ def setuptools_sdist(tmp_path_factory, request):
 
 @pytest.fixture(scope="session")
 def setuptools_wheel(tmp_path_factory, request):
+    if os.getenv("PRE_BUILT_SETUPTOOLS_WHEEL"):
+        return Path(os.getenv("PRE_BUILT_SETUPTOOLS_WHEEL")).resolve()
+
     with contexts.session_locked_tmp_dir(
             request, tmp_path_factory, "wheel_build") as tmp:
         dist = next(tmp.glob("*.whl"), None)
diff --git a/tox.ini b/tox.ini
index 6b587e28..a56ea24b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -14,6 +14,8 @@ usedevelop = True
 extras = testing
 passenv =
 	SETUPTOOLS_USE_DISTUTILS
+	PRE_BUILT_SETUPTOOLS_WHEEL
+	PRE_BUILT_SETUPTOOLS_SDIST
 	TIMEOUT_BACKEND_TEST  # timeout (in seconds) for test_build_meta
 	windir  # required for test_pkg_resources
 	# honor git config in pytest-perf
-- 
cgit v1.2.1


From 418b58e24823803690b39d20bdb599c9bc74bd62 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 7 Mar 2022 18:38:11 +0000
Subject: Add venv to the default exclude list

---
 setuptools/discovery.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/setuptools/discovery.py b/setuptools/discovery.py
index 9073f660..2eb1f5ea 100644
--- a/setuptools/discovery.py
+++ b/setuptools/discovery.py
@@ -188,6 +188,7 @@ class FlatLayoutPackageFinder(PEP420PackageFinder):
         "tools",
         "build",
         "dist",
+        "venv",
         # ---- Task runners / Build tools ----
         "tasks",  # invoke
         "fabfile",  # fabric
-- 
cgit v1.2.1


From e628030fde4138b7bfb713d56fd1150dcbc8ca12 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 7 Mar 2022 23:40:38 +0000
Subject: [CI] Disable test_pip_upgrade_from_source when network if off

As discussed in #3149, builds with setuptools will always try to
download `wheel`, therefore if the network is not available there is
little sense in testing those builds (they will fail).
---
 setuptools/tests/test_virtualenv.py | 56 +++++++++++++------------------------
 1 file changed, 20 insertions(+), 36 deletions(-)

diff --git a/setuptools/tests/test_virtualenv.py b/setuptools/tests/test_virtualenv.py
index 0ba89643..65358543 100644
--- a/setuptools/tests/test_virtualenv.py
+++ b/setuptools/tests/test_virtualenv.py
@@ -1,7 +1,8 @@
 import os
 import sys
-import itertools
 import subprocess
+from urllib.request import urlopen
+from urllib.error import URLError
 
 import pathlib
 
@@ -31,56 +32,39 @@ def test_clean_env_install(venv_without_setuptools, setuptools_wheel):
     venv_without_setuptools.run(cmd)
 
 
-def _get_pip_versions():
-    # This fixture will attempt to detect if tests are being run without
-    # network connectivity and if so skip some tests
-
-    network = True
+def access_pypi():
+    # Detect if tests are being run without connectivity
     if not os.environ.get('NETWORK_REQUIRED', False):  # pragma: nocover
-        try:
-            from urllib.request import urlopen
-            from urllib.error import URLError
-        except ImportError:
-            from urllib2 import urlopen, URLError  # Python 2.7 compat
-
         try:
             urlopen('https://pypi.org', timeout=1)
         except URLError:
             # No network, disable most of these tests
-            network = False
+            return False
 
-    def mark(param, *marks):
-        if not isinstance(param, type(pytest.param(''))):
-            param = pytest.param(param)
-        return param._replace(marks=param.marks + marks)
+    return True
 
-    def skip_network(param):
-        return param if network else mark(param, pytest.mark.skip(reason="no network"))
 
-    network_versions = [
-        mark('pip<20', pytest.mark.xfail(reason='pypa/pip#6599')),
+@pytest.mark.skipif(
+    'platform.python_implementation() == "PyPy"',
+    reason="https://github.com/pypa/setuptools/pull/2865#issuecomment-965834995",
+)
+@pytest.mark.skipif(not access_pypi(), reason="no network")
+# ^-- Even when it is not necessary to install a different version of `pip`
+#     the build process will still try to download `wheel`, see #3147 and #2986.
+@pytest.mark.parametrize(
+    'pip_version',
+    [
+        None,
+        pytest.param('pip<20', marks=pytest.mark.xfail(reason='pypa/pip#6599')),
         'pip<20.1',
         'pip<21',
         'pip<22',
-        mark(
+        pytest.param(
             'https://github.com/pypa/pip/archive/main.zip',
-            pytest.mark.xfail(reason='#2975'),
+            marks=pytest.mark.xfail(reason='#2975'),
         ),
     ]
-
-    versions = itertools.chain(
-        [None],
-        map(skip_network, network_versions)
-    )
-
-    return list(versions)
-
-
-@pytest.mark.skipif(
-    'platform.python_implementation() == "PyPy"',
-    reason="https://github.com/pypa/setuptools/pull/2865#issuecomment-965834995",
 )
-@pytest.mark.parametrize('pip_version', _get_pip_versions())
 def test_pip_upgrade_from_source(pip_version, venv_without_setuptools,
                                  setuptools_wheel, setuptools_sdist):
     """
-- 
cgit v1.2.1


From d0d83d95c9be99576591988e932949f16dba0a6f Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 8 Mar 2022 10:35:37 +0000
Subject: Update changelog.d/3147.misc.rst

---
 changelog.d/3147.misc.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/changelog.d/3147.misc.rst b/changelog.d/3147.misc.rst
index 1b0b8685..89556edd 100644
--- a/changelog.d/3147.misc.rst
+++ b/changelog.d/3147.misc.rst
@@ -1,4 +1,4 @@
-Added options to provide a pre-build ``setuptools`` wheel or sdist for being
+Added options to provide a pre-built ``setuptools`` wheel or sdist for being
 used during tests with virtual environments.
 Paths for these pre-built distribution files can now be set via the environment
 variables: ``PRE_BUILT_SETUPTOOLS_SDIST`` and ``PRE_BUILT_SETUPTOOLS_WHEEL``.
-- 
cgit v1.2.1


From c2f4907fcdec3b8a68e595ee9b9fc57103992ce2 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 8 Mar 2022 11:22:55 +0000
Subject: Replace direct usage of the `py` library

According to https://pypi.org/project/py/, this library is in
maintenance mode and should not be used in new code.
---
 setuptools/tests/test_easy_install.py | 22 +++++++++++-----------
 1 file changed, 11 insertions(+), 11 deletions(-)

diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py
index fd4a83ee..878eb7c3 100644
--- a/setuptools/tests/test_easy_install.py
+++ b/setuptools/tests/test_easy_install.py
@@ -19,6 +19,7 @@ import subprocess
 import pathlib
 import warnings
 from collections import namedtuple
+from pathlib import Path
 
 import pytest
 from jaraco import path
@@ -40,8 +41,6 @@ import pkg_resources
 from . import contexts
 from .textwrap import DALS
 
-import py
-
 
 @pytest.fixture(autouse=True)
 def pip_disable_index(monkeypatch):
@@ -1113,7 +1112,7 @@ def test_use_correct_python_version_string(tmpdir, tmpdir_cwd, monkeypatch):
     assert cmd.config_vars['py_version_nodot'] == '310'
 
 
-def test_editable_user_and_build_isolation(setup_context, monkeypatch, tmpdir):
+def test_editable_user_and_build_isolation(setup_context, monkeypatch, tmp_path):
     ''' `setup.py develop` should honor `--user` even under build isolation'''
 
     # == Arrange ==
@@ -1128,27 +1127,28 @@ def test_editable_user_and_build_isolation(setup_context, monkeypatch, tmpdir):
     # it will `makedirs("/home/user/.pyenv/versions/3.9.10 /home/user/.pyenv/versions/3.9.10/lib /home/user/.pyenv/versions/3.9.10/lib/python3.9 /home/user/.pyenv/versions/3.9.10/lib/python3.9/lib-dynload")``  # noqa: E501
     # 2. We are going to force `site` to update site.USER_BASE and site.USER_SITE
     #    To point inside our new home
-    monkeypatch.setenv('HOME', str(tmpdir / 'home'))
+    monkeypatch.setenv('HOME', str(tmp_path / 'home'))
     monkeypatch.setattr('site.USER_BASE', None)
     monkeypatch.setattr('site.USER_SITE', None)
-    user_site = py.path.local(site.getusersitepackages())
-    user_site.ensure_dir()
+    user_site = Path(site.getusersitepackages())
+    user_site.mkdir(parents=True, exist_ok=True)
 
-    sys_prefix = (tmpdir / 'sys_prefix').ensure_dir()
+    sys_prefix = (tmp_path / 'sys_prefix')
+    sys_prefix.mkdir(parents=True, exist_ok=True)
     monkeypatch.setattr('sys.prefix', str(sys_prefix))
 
     # == Sanity check ==
-    assert sys_prefix.listdir() == []
-    assert user_site.listdir() == []
+    assert list(sys_prefix.glob("*")) == []
+    assert list(user_site.glob("*")) == []
 
     # == Act ==
     run_setup('setup.py', ['develop', '--user'])
 
     # == Assert ==
     # Should not install to sys.prefix
-    assert sys_prefix.listdir() == []
+    assert list(sys_prefix.glob("*")) == []
     # Should install to user site
-    installed = {f.basename for f in user_site.listdir()}
+    installed = {f.name for f in user_site.glob("*")}
     # sometimes easy-install.pth is created and sometimes not
     installed = installed - {"easy-install.pth"}
     assert installed == {'UNKNOWN.egg-link'}
-- 
cgit v1.2.1


From a283eb486afc5bf335af4e40d305a8a5779aa843 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 5 Mar 2022 16:10:48 +0000
Subject: Fix wrong order when partitioning TOML config files

---
 setuptools/dist.py | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/setuptools/dist.py b/setuptools/dist.py
index 4743eeed..8c995aca 100644
--- a/setuptools/dist.py
+++ b/setuptools/dist.py
@@ -818,11 +818,13 @@ class Distribution(_Distribution):
         and loads configuration.
         """
         tomlfiles = []
+        standard_project_metadata = Path(self.src_root or os.curdir, "pyproject.toml")
         if filenames is not None:
-            tomlfiles, other = partition(lambda f: Path(f).suffix == ".toml", filenames)
-            filenames = other
-        elif os.path.exists("pyproject.toml"):
-            tomlfiles = ["pyproject.toml"]
+            parts = partition(lambda f: Path(f).suffix == ".toml", filenames)
+            filenames = list(parts[0])  # 1st element => predicate is False
+            tomlfiles = list(parts[1])  # 2nd element => predicate is True
+        elif standard_project_metadata.exists():
+            tomlfiles = [standard_project_metadata]
 
         self._parse_config_files(filenames=filenames)
 
-- 
cgit v1.2.1


From 604f7af913e89c7d4d744c477c76d64e32828624 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 5 Mar 2022 16:44:04 +0000
Subject: Import package finders directly from discovery module

---
 setuptools/config/expand.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/setuptools/config/expand.py b/setuptools/config/expand.py
index 9d51a0a8..db8f19d0 100644
--- a/setuptools/config/expand.py
+++ b/setuptools/config/expand.py
@@ -263,9 +263,9 @@ def find_packages(
     """
 
     if namespaces:
-        from setuptools import PEP420PackageFinder as PackageFinder
+        from setuptools.discovery import PEP420PackageFinder as PackageFinder
     else:
-        from setuptools import PackageFinder  # type: ignore
+        from setuptools.discovery import PackageFinder  # type: ignore
 
     root_dir = root_dir or "."
     where = kwargs.pop('where', ['.'])
-- 
cgit v1.2.1


From fccbdde4179247dd0070386a6651228149d5b294 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 5 Mar 2022 16:47:31 +0000
Subject: Add test capturing the expectation of package_dir being
 autodiscovered

---
 setuptools/tests/test_config_discovery.py | 50 +++++++++++++++++++++----------
 1 file changed, 35 insertions(+), 15 deletions(-)

diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py
index 2215cddb..406e7fc3 100644
--- a/setuptools/tests/test_config_discovery.py
+++ b/setuptools/tests/test_config_discovery.py
@@ -7,6 +7,7 @@ from setuptools.command.sdist import sdist
 from setuptools.dist import Distribution
 
 import pytest
+from path import Path as _Path
 
 from .contexts import quiet
 from .integration.helpers import get_sdist_members, get_wheel_members, run
@@ -59,21 +60,7 @@ class TestDiscoverPackagesAndPyModules:
         files, options = self._get_info(circumstance)
         _populate_project_dir(tmp_path, files, options)
 
-        here = os.getcwd()
-        root = "/".join(os.path.split(tmp_path))  # POSIX-style
-        dist = Distribution({**options, "src_root": root})
-        dist.script_name = 'setup.py'
-        dist.set_defaults()
-        cmd = sdist(dist)
-        cmd.ensure_finalized()
-        assert cmd.distribution.packages or cmd.distribution.py_modules
-
-        with quiet():
-            try:
-                os.chdir(tmp_path)
-                cmd.run()
-            finally:
-                os.chdir(here)
+        _, cmd = _run_sdist_programatically(tmp_path, options)
 
         manifest = [f.replace(os.sep, "/") for f in cmd.filelist.files]
         for file in files:
@@ -183,6 +170,20 @@ class TestNoConfig:
         assert dist_file.is_file()
 
 
+def test_autodiscovered_packagedir_with_attr_directive_in_config(tmp_path):
+    _populate_project_dir(tmp_path, ["src/pkg/__init__.py"], {})
+    (tmp_path / "src/pkg/__init__.py").write_text("version = 42")
+    (tmp_path / "setup.cfg").write_text("[metadata]\nversion = attr: pkg.version")
+
+    dist, _ = _run_sdist_programatically(tmp_path, {})
+    assert dist.get_name() == "pkg"
+    assert dist.get_version() == "42"
+
+    _run_build(tmp_path, "--sdist")
+    dist_file = tmp_path / "dist/pkg-42.tar.gz"
+    assert dist_file.is_file()
+
+
 def _populate_project_dir(root, files, options):
     # NOTE: Currently pypa/build will refuse to build the project if no
     # `pyproject.toml` or `setup.py` is found. So it is impossible to do
@@ -220,3 +221,22 @@ def _write_setupcfg(root, options):
 def _run_build(path, *flags):
     cmd = [sys.executable, "-m", "build", "--no-isolation", *flags, str(path)]
     return run(cmd, env={'DISTUTILS_DEBUG': '1'})
+
+
+def _run_sdist_programatically(dist_path, options):
+    root = "/".join(os.path.split(dist_path))  # POSIX-style
+    dist = Distribution({**options, "src_root": root})
+    dist.script_name = 'setup.py'
+
+    if (dist_path / "setup.cfg").exists():
+        dist.parse_config_files([dist_path / "setup.cfg"])
+
+    dist.set_defaults()
+    cmd = sdist(dist)
+    cmd.ensure_finalized()
+    assert cmd.distribution.packages or cmd.distribution.py_modules
+
+    with quiet(), _Path(dist_path):
+        cmd.run()
+
+    return dist, cmd
-- 
cgit v1.2.1


From 12832c1cd421f614eeab6f46ae198610a2684bca Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 5 Mar 2022 16:48:11 +0000
Subject: Allow package_dir autodiscovery for setup.cfg

---
 setuptools/config/setupcfg.py | 42 ++++++++++++++++++++++++++++++------------
 setuptools/discovery.py       |  7 ++++---
 2 files changed, 34 insertions(+), 15 deletions(-)

diff --git a/setuptools/config/setupcfg.py b/setuptools/config/setupcfg.py
index 5a449655..5a315c54 100644
--- a/setuptools/config/setupcfg.py
+++ b/setuptools/config/setupcfg.py
@@ -147,14 +147,20 @@ def parse_configuration(
     options = ConfigOptionsHandler(distribution, command_options, ignore_option_errors)
     options.parse()
 
+    # Make sure package_dir is populated correctly, so `attr:` directives can work
+    distribution.set_defaults(name=False)  # Skip name since it is defined in metadata
+
     meta = ConfigMetadataHandler(
         distribution.metadata,
         command_options,
         ignore_option_errors,
         distribution.package_dir,
+        distribution.src_root,
     )
     meta.parse()
 
+    distribution.set_defaults.analyse_name()  # Now we can set a default name
+
     return meta, options
 
 
@@ -313,7 +319,7 @@ class ConfigHandler(Generic[Target]):
         return parser
 
     @classmethod
-    def _parse_file(cls, value):
+    def _parse_file(cls, value, root_dir: _Path):
         """Represents value as a string, allowing including text
         from nearest files using `file:` directive.
 
@@ -336,10 +342,10 @@ class ConfigHandler(Generic[Target]):
 
         spec = value[len(include_directive) :]
         filepaths = (path.strip() for path in spec.split(','))
-        return expand.read_files(filepaths)
+        return expand.read_files(filepaths, root_dir)
 
     @classmethod
-    def _parse_attr(cls, value, package_dir=None):
+    def _parse_attr(cls, value, package_dir, root_dir: _Path):
         """Represents value as a module attribute.
 
         Examples:
@@ -354,7 +360,7 @@ class ConfigHandler(Generic[Target]):
             return value
 
         attr_desc = value.replace(attr_directive, '')
-        return expand.read_attr(attr_desc, package_dir)
+        return expand.read_attr(attr_desc, package_dir, root_dir)
 
     @classmethod
     def _get_parser_compound(cls, *parse_methods):
@@ -468,16 +474,18 @@ class ConfigMetadataHandler(ConfigHandler["DistributionMetadata"]):
         target_obj: "DistributionMetadata",
         options: AllCommandOptions,
         ignore_option_errors=False,
-        package_dir: Optional[dict] = None
+        package_dir: Optional[dict] = None,
+        root_dir: _Path = os.curdir
     ):
         super().__init__(target_obj, options, ignore_option_errors)
         self.package_dir = package_dir
+        self.root_dir = root_dir
 
     @property
     def parsers(self):
         """Metadata item name to parser function mapping."""
         parse_list = self._parse_list
-        parse_file = self._parse_file
+        parse_file = partial(self._parse_file, root_dir=self.root_dir)
         parse_dict = self._parse_dict
         exclude_files_parser = self._exclude_files_parser
 
@@ -514,7 +522,7 @@ class ConfigMetadataHandler(ConfigHandler["DistributionMetadata"]):
         :rtype: str
 
         """
-        version = self._parse_file(value)
+        version = self._parse_file(value, self.root_dir)
 
         if version != value:
             version = version.strip()
@@ -531,13 +539,22 @@ class ConfigMetadataHandler(ConfigHandler["DistributionMetadata"]):
 
             return version
 
-        return expand.version(self._parse_attr(value, self.package_dir))
+        return expand.version(self._parse_attr(value, self.package_dir, self.root_dir))
 
 
 class ConfigOptionsHandler(ConfigHandler["Distribution"]):
 
     section_prefix = 'options'
 
+    def __init__(
+        self,
+        target_obj: "Distribution",
+        options: AllCommandOptions,
+        ignore_option_errors=False
+    ):
+        super().__init__(target_obj, options, ignore_option_errors)
+        self.root_dir = target_obj.src_root
+
     @property
     def parsers(self):
         """Metadata item name to parser function mapping."""
@@ -546,6 +563,7 @@ class ConfigOptionsHandler(ConfigHandler["Distribution"]):
         parse_bool = self._parse_bool
         parse_dict = self._parse_dict
         parse_cmdclass = self._parse_cmdclass
+        parse_file = partial(self._parse_file, root_dir=self.root_dir)
 
         return {
             'zip_safe': parse_bool,
@@ -559,14 +577,14 @@ class ConfigOptionsHandler(ConfigHandler["Distribution"]):
             'setup_requires': parse_list_semicolon,
             'tests_require': parse_list_semicolon,
             'packages': self._parse_packages,
-            'entry_points': self._parse_file,
+            'entry_points': parse_file,
             'py_modules': parse_list,
             'python_requires': SpecifierSet,
             'cmdclass': parse_cmdclass,
         }
 
     def _parse_cmdclass(self, value):
-        return expand.cmdclass(self._parse_dict(value))
+        return expand.cmdclass(self._parse_dict(value), self.root_dir)
 
     def _parse_packages(self, value):
         """Parses `packages` option value.
@@ -587,7 +605,7 @@ class ConfigOptionsHandler(ConfigHandler["Distribution"]):
 
         find_kwargs["namespaces"] = (trimmed_value == find_directives[1])
 
-        return expand.find_packages(**find_kwargs)
+        return expand.find_packages(**find_kwargs, root_dir=self.root_dir)
 
     def parse_section_packages__find(self, section_options):
         """Parses `packages.find` configuration file section.
@@ -652,4 +670,4 @@ class ConfigOptionsHandler(ConfigHandler["Distribution"]):
         :param dict section_options:
         """
         parsed = self._parse_section_to_dict(section_options, self._parse_list)
-        self['data_files'] = expand.canonic_data_files(parsed)
+        self['data_files'] = expand.canonic_data_files(parsed, self.root_dir)
diff --git a/setuptools/discovery.py b/setuptools/discovery.py
index 2eb1f5ea..ca61afe8 100644
--- a/setuptools/discovery.py
+++ b/setuptools/discovery.py
@@ -243,7 +243,7 @@ class ConfigDiscovery:
         self._called = False
         self._root_dir = None  # delay so `src_root` can be set in dist
 
-    def __call__(self, force=False):
+    def __call__(self, force=False, name=True):
         """Automatically discover missing configuration fields
         and modifies the given ``distribution`` object in-place.
 
@@ -261,7 +261,8 @@ class ConfigDiscovery:
         self._root_dir = self.dist.src_root or os.curdir
 
         self._analyse_package_layout()
-        self._analyse_name()  # depends on ``packages`` and ``py_modules``
+        if name:
+            self.analyse_name()  # depends on ``packages`` and ``py_modules``
 
         self._called = True
 
@@ -329,7 +330,7 @@ class ConfigDiscovery:
         log.debug(f"`flat-layout` detected -- analysing {self._root_dir}")
         return True
 
-    def _analyse_name(self):
+    def analyse_name(self):
         """The packages/modules are the essential contribution of the author.
         Therefore the name of the distribution can be derived from them.
         """
-- 
cgit v1.2.1


From 78294b8ff1c2ec841681a5769baa5741e86eefec Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 5 Mar 2022 17:25:51 +0000
Subject: Externalize find_parent_package from discovery class

---
 setuptools/discovery.py | 77 ++++++++++++++++++++++++++++++-------------------
 1 file changed, 47 insertions(+), 30 deletions(-)

diff --git a/setuptools/discovery.py b/setuptools/discovery.py
index ca61afe8..15d25947 100644
--- a/setuptools/discovery.py
+++ b/setuptools/discovery.py
@@ -47,6 +47,9 @@ import _distutils_hack.override  # noqa: F401
 from distutils import log
 from distutils.util import convert_path
 
+from typing import Dict, List, Optional, Union
+_Path = Union[str, os.PathLike]
+
 chain_iter = itertools.chain.from_iterable
 
 
@@ -364,37 +367,51 @@ class ConfigDiscovery:
             return None
 
         packages = sorted(self.dist.packages, key=len)
-        common_ancestors = []
-        for i, name in enumerate(packages):
-            if not all(n.startswith(name) for n in packages[i+1:]):
-                # Since packages are sorted by length, this condition is able
-                # to find a list of all common ancestors.
-                # When there is divergence (e.g. multiple root packages)
-                # the list will be empty
-                break
-            common_ancestors.append(name)
-
-        for name in common_ancestors:
-            init = os.path.join(self._find_package_path(name), "__init__.py")
-            if os.path.isfile(init):
-                log.debug(f"Common parent package detected, name: {name}")
-                return name
+        package_dir = self.dist.package_dir or {}
+
+        parent_pkg = find_parent_package(packages, package_dir, self._root_dir)
+        if parent_pkg:
+            log.debug(f"Common parent package detected, name: {parent_pkg}")
+            return parent_pkg
 
         log.warn("No parent package detected, impossible to derive `name`")
         return None
 
-    def _find_package_path(self, name):
-        """Given a package name, return the path where it should be found on
-        disk, considering the ``package_dir`` option.
-        """
-        package_dir = self.dist.package_dir or {}
-        parts = name.split(".")
-        for i in range(len(parts), 0, -1):
-            # Look backwards, the most specific package_dir first
-            partial_name = ".".join(parts[:i])
-            if partial_name in package_dir:
-                parent = package_dir[partial_name]
-                return os.path.join(self._root_dir, parent, *parts[i:])
-
-        parent = (package_dir.get("") or "").split("/")
-        return os.path.join(self._root_dir, *parent, *parts)
+
+def find_parent_package(
+    packages: List[str], package_dir: Dict[str, str], root_dir: _Path
+) -> Optional[str]:
+    packages = sorted(packages, key=len)
+    common_ancestors = []
+    for i, name in enumerate(packages):
+        if not all(n.startswith(name) for n in packages[i+1:]):
+            # Since packages are sorted by length, this condition is able
+            # to find a list of all common ancestors.
+            # When there is divergence (e.g. multiple root packages)
+            # the list will be empty
+            break
+        common_ancestors.append(name)
+
+    for name in common_ancestors:
+        pkg_path = _find_package_path(name, package_dir, root_dir)
+        init = os.path.join(pkg_path, "__init__.py")
+        if os.path.isfile(init):
+            return name
+
+    return None
+
+
+def _find_package_path(name: str, package_dir: Dict[str, str], root_dir: _Path) -> str:
+    """Given a package name, return the path where it should be found on
+    disk, considering the ``package_dir`` option.
+    """
+    parts = name.split(".")
+    for i in range(len(parts), 0, -1):
+        # Look backwards, the most specific package_dir first
+        partial_name = ".".join(parts[:i])
+        if partial_name in package_dir:
+            parent = package_dir[partial_name]
+            return os.path.join(root_dir, parent, *parts[i:])
+
+    parent = package_dir.get("") or ""
+    return os.path.join(root_dir, *parent.split("/"), *parts)
-- 
cgit v1.2.1


From 83d11a1402a800d6b9617c2dbb514fbf8de38591 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 5 Mar 2022 18:21:37 +0000
Subject: Allow expand.find_packges to fill package_dir

---
 setuptools/config/expand.py            | 46 +++++++++++++++++++++++++++++++---
 setuptools/discovery.py                |  4 +--
 setuptools/tests/config/test_expand.py | 11 +++++---
 3 files changed, 52 insertions(+), 9 deletions(-)

diff --git a/setuptools/config/expand.py b/setuptools/config/expand.py
index db8f19d0..f9cc5962 100644
--- a/setuptools/config/expand.py
+++ b/setuptools/config/expand.py
@@ -249,7 +249,11 @@ def cmdclass(
 
 
 def find_packages(
-    *, namespaces=True, root_dir: Optional[_Path] = None, **kwargs
+    *,
+    namespaces=True,
+    fill_package_dir: Optional[Dict[str, str]] = None,
+    root_dir: Optional[_Path] = None,
+    **kwargs
 ) -> List[str]:
     """Works similarly to :func:`setuptools.find_packages`, but with all
     arguments given as keyword arguments. Moreover, ``where`` can be given
@@ -259,6 +263,13 @@ def find_packages(
     behave like :func:`setuptools.find_namespace_packages`` (i.e. include
     implicit namespaces as per :pep:`420`).
 
+    The ``where`` argument will be considered relative to ``root_dir`` (or the current
+    working directory when ``root_dir`` is not given).
+
+    If the ``fill_package_dir`` argument is passed, this function will consider it as a
+    similar data structure to the ``package_dir`` configuration parameter add fill-in
+    any missing package location.
+
     :rtype: list
     """
 
@@ -267,12 +278,39 @@ def find_packages(
     else:
         from setuptools.discovery import PackageFinder  # type: ignore
 
-    root_dir = root_dir or "."
+    root_dir = root_dir or os.curdir
     where = kwargs.pop('where', ['.'])
     if isinstance(where, str):
         where = [where]
-    target = [_nest_path(root_dir, path) for path in where]
-    return list(chain_iter(PackageFinder.find(x, **kwargs) for x in target))
+
+    packages = []
+    fill_package_dir = {} if fill_package_dir is None else fill_package_dir
+    for path in where:
+        pkgs = PackageFinder.find(_nest_path(root_dir, path), **kwargs)
+        packages.extend(pkgs)
+        if fill_package_dir.get("") != path:
+            parent_pkgs = _parent_packages(pkgs)
+            parent = {pkg: "/".join([path, *pkg.split(".")]) for pkg in parent_pkgs}
+            fill_package_dir.update(parent)
+
+    return packages
+
+
+def _parent_packages(packages: List[str]) -> List[str]:
+    """Remove children packages from the list
+    >>> _parent_packages(["a", "a.b1", "a.b2", "a.b1.c1"])
+    ['a']
+    >>> _parent_packages(["a", "b", "c.d", "c.d.e.f", "g.h", "a.a1"])
+    ['a', 'b', 'c.d', 'g.h']
+    """
+    pkgs = sorted(packages, key=len)
+    top_level = pkgs[:]
+    size = len(pkgs)
+    for i, name in enumerate(reversed(pkgs)):
+        if any(name.startswith(f"{other}.") for other in top_level):
+            top_level.pop(size - i - 1)
+
+    return top_level
 
 
 def _nest_path(parent: _Path, path: _Path) -> str:
diff --git a/setuptools/discovery.py b/setuptools/discovery.py
index 15d25947..80e2a23b 100644
--- a/setuptools/discovery.py
+++ b/setuptools/discovery.py
@@ -393,7 +393,7 @@ def find_parent_package(
         common_ancestors.append(name)
 
     for name in common_ancestors:
-        pkg_path = _find_package_path(name, package_dir, root_dir)
+        pkg_path = find_package_path(name, package_dir, root_dir)
         init = os.path.join(pkg_path, "__init__.py")
         if os.path.isfile(init):
             return name
@@ -401,7 +401,7 @@ def find_parent_package(
     return None
 
 
-def _find_package_path(name: str, package_dir: Dict[str, str], root_dir: _Path) -> str:
+def find_package_path(name: str, package_dir: Dict[str, str], root_dir: _Path) -> str:
     """Given a package name, return the path where it should be found on
     disk, considering the ``package_dir`` option.
     """
diff --git a/setuptools/tests/config/test_expand.py b/setuptools/tests/config/test_expand.py
index 2461347d..a7b0c21d 100644
--- a/setuptools/tests/config/test_expand.py
+++ b/setuptools/tests/config/test_expand.py
@@ -5,6 +5,7 @@ import pytest
 from distutils.errors import DistutilsOptionError
 from setuptools.command.sdist import sdist
 from setuptools.config import expand
+from setuptools.discovery import find_package_path
 
 
 def write_files(files, root_dir):
@@ -102,9 +103,13 @@ def test_find_packages(tmp_path, monkeypatch, args, pkgs):
     }
     write_files({k: "" for k in files}, tmp_path)
 
-    with monkeypatch.context() as m:
-        m.chdir(tmp_path)
-        assert set(expand.find_packages(**args)) == pkgs
+    package_dir = {}
+    kwargs = {"root_dir": tmp_path, "fill_package_dir": package_dir, **args}
+    where = kwargs.get("where", ["."])
+    assert set(expand.find_packages(**kwargs)) == pkgs
+    for pkg in pkgs:
+        pkg_path = find_package_path(pkg, package_dir, tmp_path)
+        assert os.path.exists(pkg_path)
 
     # Make sure the same APIs work outside cwd
     where = [
-- 
cgit v1.2.1


From 755e053ee6c3b82024a687082a164e1c4a88cfbb Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 5 Mar 2022 20:38:11 +0000
Subject: Make sure package_dir is populated before processing cmdclass and
 'attr:' in setup.cfg

---
 setuptools/config/expand.py               | 46 ++++++++++++++++++++-
 setuptools/config/setupcfg.py             | 67 +++++++++++++++++++------------
 setuptools/tests/config/test_setupcfg.py  | 37 +++++++++--------
 setuptools/tests/test_config_discovery.py | 32 +++++++++++----
 4 files changed, 132 insertions(+), 50 deletions(-)

diff --git a/setuptools/config/expand.py b/setuptools/config/expand.py
index f9cc5962..b12b263d 100644
--- a/setuptools/config/expand.py
+++ b/setuptools/config/expand.py
@@ -24,11 +24,26 @@ from glob import iglob
 from configparser import ConfigParser
 from importlib.machinery import ModuleSpec
 from itertools import chain
-from typing import Callable, Dict, Iterable, List, Optional, Tuple, Union, cast
+from typing import (
+    TYPE_CHECKING,
+    Callable,
+    Dict,
+    Iterable,
+    List,
+    Optional,
+    Tuple,
+    Union,
+    cast
+)
 from types import ModuleType
 
 from distutils.errors import DistutilsOptionError
 
+if TYPE_CHECKING:
+    from setuptools.dist import Distribution  # noqa
+    from setuptools.discovery import ConfigDiscovery  # noqa
+    from distutils.dist import DistributionMetadata  # noqa
+
 chain_iter = chain.from_iterable
 _Path = Union[str, os.PathLike]
 
@@ -372,3 +387,32 @@ def entry_points(text: str, text_source="entry-points") -> Dict[str, dict]:
     groups = {k: dict(v.items()) for k, v in parser.items()}
     groups.pop(parser.default_section, None)
     return groups
+
+
+class EnsurePackagesDiscovered:
+    """Some expand functions require all the packages to already be discovered before
+    they run, e.g. :func:`read_attr`, :func:`resolve_class`, :func:`cmdclass`.
+
+    Therefore in some cases we will need to run autodiscovery during the parsing of the
+    configuration. However, it is better to postpone calling package discovery as much
+    as possible.
+
+    We should only run the discovery if absolutely necessary, otherwise we can miss
+    files that define important configuration (like ``package_dir``) are processed.
+    """
+
+    def __init__(self, distribution: "Distribution"):
+        self._dist = distribution
+        self._called = False
+
+    def __call__(self):
+        self._called = True
+        self._dist.set_defaults(name=False)  # Skip name since we are parsing metadata
+        return self._dist.package_dir
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, _exc_type, _exc_value, _traceback):
+        if self._called:
+            self._dist.set_defaults.analyse_name()  # Now we can set a default name
diff --git a/setuptools/config/setupcfg.py b/setuptools/config/setupcfg.py
index 5a315c54..36460d95 100644
--- a/setuptools/config/setupcfg.py
+++ b/setuptools/config/setupcfg.py
@@ -144,22 +144,27 @@ def parse_configuration(
         If False exceptions are propagated as expected.
     :rtype: list
     """
-    options = ConfigOptionsHandler(distribution, command_options, ignore_option_errors)
-    options.parse()
-
-    # Make sure package_dir is populated correctly, so `attr:` directives can work
-    distribution.set_defaults(name=False)  # Skip name since it is defined in metadata
-
-    meta = ConfigMetadataHandler(
-        distribution.metadata,
-        command_options,
-        ignore_option_errors,
-        distribution.package_dir,
-        distribution.src_root,
-    )
-    meta.parse()
+    with expand.EnsurePackagesDiscovered(distribution) as ensure_discovered:
+        options = ConfigOptionsHandler(
+            distribution,
+            command_options,
+            ignore_option_errors,
+            ensure_discovered,
+        )
 
-    distribution.set_defaults.analyse_name()  # Now we can set a default name
+        options.parse()
+        if not distribution.package_dir:
+            distribution.package_dir = options.package_dir  # Filled by `find_packages`
+
+        meta = ConfigMetadataHandler(
+            distribution.metadata,
+            command_options,
+            ignore_option_errors,
+            ensure_discovered,
+            distribution.package_dir,
+            distribution.src_root,
+        )
+        meta.parse()
 
     return meta, options
 
@@ -184,7 +189,8 @@ class ConfigHandler(Generic[Target]):
         self,
         target_obj: Target,
         options: AllCommandOptions,
-        ignore_option_errors=False
+        ignore_option_errors,
+        ensure_discovered: expand.EnsurePackagesDiscovered,
     ):
         sections: AllCommandOptions = {}
 
@@ -200,6 +206,7 @@ class ConfigHandler(Generic[Target]):
         self.target_obj = target_obj
         self.sections = sections
         self.set_options: List[str] = []
+        self.ensure_discovered = ensure_discovered
 
     @property
     def parsers(self):
@@ -344,8 +351,7 @@ class ConfigHandler(Generic[Target]):
         filepaths = (path.strip() for path in spec.split(','))
         return expand.read_files(filepaths, root_dir)
 
-    @classmethod
-    def _parse_attr(cls, value, package_dir, root_dir: _Path):
+    def _parse_attr(self, value, package_dir, root_dir: _Path):
         """Represents value as a module attribute.
 
         Examples:
@@ -360,6 +366,9 @@ class ConfigHandler(Generic[Target]):
             return value
 
         attr_desc = value.replace(attr_directive, '')
+
+        # Make sure package_dir is populated correctly, so `attr:` directives can work
+        package_dir.update(self.ensure_discovered())
         return expand.read_attr(attr_desc, package_dir, root_dir)
 
     @classmethod
@@ -473,11 +482,12 @@ class ConfigMetadataHandler(ConfigHandler["DistributionMetadata"]):
         self,
         target_obj: "DistributionMetadata",
         options: AllCommandOptions,
-        ignore_option_errors=False,
+        ignore_option_errors: bool,
+        ensure_discovered: expand.EnsurePackagesDiscovered,
         package_dir: Optional[dict] = None,
         root_dir: _Path = os.curdir
     ):
-        super().__init__(target_obj, options, ignore_option_errors)
+        super().__init__(target_obj, options, ignore_option_errors, ensure_discovered)
         self.package_dir = package_dir
         self.root_dir = root_dir
 
@@ -550,10 +560,12 @@ class ConfigOptionsHandler(ConfigHandler["Distribution"]):
         self,
         target_obj: "Distribution",
         options: AllCommandOptions,
-        ignore_option_errors=False
+        ignore_option_errors: bool,
+        ensure_discovered: expand.EnsurePackagesDiscovered,
     ):
-        super().__init__(target_obj, options, ignore_option_errors)
+        super().__init__(target_obj, options, ignore_option_errors, ensure_discovered)
         self.root_dir = target_obj.src_root
+        self.package_dir: Dict[str, str] = {}  # To be filled by `find_packages`
 
     @property
     def parsers(self):
@@ -584,7 +596,8 @@ class ConfigOptionsHandler(ConfigHandler["Distribution"]):
         }
 
     def _parse_cmdclass(self, value):
-        return expand.cmdclass(self._parse_dict(value), self.root_dir)
+        package_dir = self.ensure_discovered()
+        return expand.cmdclass(self._parse_dict(value), package_dir, self.root_dir)
 
     def _parse_packages(self, value):
         """Parses `packages` option value.
@@ -603,9 +616,13 @@ class ConfigOptionsHandler(ConfigHandler["Distribution"]):
             self.sections.get('packages.find', {})
         )
 
-        find_kwargs["namespaces"] = (trimmed_value == find_directives[1])
+        find_kwargs.update(
+            namespaces=(trimmed_value == find_directives[1]),
+            root_dir=self.root_dir,
+            fill_package_dir=self.package_dir,
+        )
 
-        return expand.find_packages(**find_kwargs, root_dir=self.root_dir)
+        return expand.find_packages(**find_kwargs)
 
     def parse_section_packages__find(self, section_options):
         """Parses `packages.find` configuration file section.
diff --git a/setuptools/tests/config/test_setupcfg.py b/setuptools/tests/config/test_setupcfg.py
index 5bfefac0..8cd3ae7f 100644
--- a/setuptools/tests/config/test_setupcfg.py
+++ b/setuptools/tests/config/test_setupcfg.py
@@ -1,8 +1,8 @@
 import configparser
 import contextlib
-import importlib
-import os
-from unittest.mock import patch
+import inspect
+from pathlib import Path
+from unittest.mock import Mock, patch
 
 import pytest
 
@@ -69,7 +69,7 @@ def get_dist(tmpdir, kwargs_initial=None, parse=True):
 def test_parsers_implemented():
 
     with pytest.raises(NotImplementedError):
-        handler = ErrConfigHandler(None, {})
+        handler = ErrConfigHandler(None, {}, False, Mock())
         handler.parsers
 
 
@@ -857,23 +857,26 @@ class TestOptions:
             with get_dist(tmpdir) as dist:
                 dist.parse_config_files()
 
-    def test_cmdclass(self, tmpdir, monkeypatch):
-        module_path = os.path.join(tmpdir, "custom_build.py")
-        with open(module_path, "w") as f:
-            f.write("from distutils.core import Command\n")
-            f.write("class CustomCmd(Command): pass\n")
-
-        fake_env(
-            tmpdir,
-            '[options]\n' 'cmdclass =\n' '    customcmd = custom_build.CustomCmd\n',
+    def test_cmdclass(self, tmpdir):
+        module_path = Path(tmpdir, "src/custom_build.py")  # auto discovery for src
+        module_path.parent.mkdir(parents=True, exist_ok=True)
+        module_path.write_text(
+            "from distutils.core import Command\n"
+            "class CustomCmd(Command): pass\n"
         )
 
-        with monkeypatch.context() as m:
-            m.syspath_prepend(tmpdir)
-            custom_build = importlib.import_module("custom_build")
+        setup_cfg = """
+            [options]
+            cmdclass =
+                customcmd = custom_build.CustomCmd
+        """
+        fake_env(tmpdir, inspect.cleandoc(setup_cfg))
 
         with get_dist(tmpdir) as dist:
-            assert dist.cmdclass == {'customcmd': custom_build.CustomCmd}
+            cmdclass = dist.cmdclass['customcmd']
+            assert cmdclass.__name__ == "CustomCmd"
+            assert cmdclass.__module__ == "custom_build"
+            assert module_path.samefile(inspect.getfile(cmdclass))
 
 
 saved_dist_init = _Distribution.__init__
diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py
index 406e7fc3..4456ad0f 100644
--- a/setuptools/tests/test_config_discovery.py
+++ b/setuptools/tests/test_config_discovery.py
@@ -5,6 +5,7 @@ from itertools import product
 
 from setuptools.command.sdist import sdist
 from setuptools.dist import Distribution
+from setuptools.discovery import find_package_path
 
 import pytest
 from path import Path as _Path
@@ -170,14 +171,28 @@ class TestNoConfig:
         assert dist_file.is_file()
 
 
-def test_autodiscovered_packagedir_with_attr_directive_in_config(tmp_path):
-    _populate_project_dir(tmp_path, ["src/pkg/__init__.py"], {})
-    (tmp_path / "src/pkg/__init__.py").write_text("version = 42")
-    (tmp_path / "setup.cfg").write_text("[metadata]\nversion = attr: pkg.version")
+@pytest.mark.parametrize(
+    "folder, opts",
+    [
+        ("src", {}),
+        ("lib", {"packages": "find:", "packages.find": {"where": "lib"}}),
+    ]
+)
+def test_discovered_packagedir_with_attr_directive_in_config(tmp_path, folder, opts):
+    _populate_project_dir(tmp_path, [f"{folder}/pkg/__init__.py", "setup.cfg"], opts)
+    (tmp_path / folder / "pkg/__init__.py").write_text("version = 42")
+    (tmp_path / "setup.cfg").write_text(
+        "[metadata]\nversion = attr: pkg.version\n"
+        + (tmp_path / "setup.cfg").read_text()
+    )
 
     dist, _ = _run_sdist_programatically(tmp_path, {})
     assert dist.get_name() == "pkg"
     assert dist.get_version() == "42"
+    assert dist.package_dir
+    package_path = find_package_path("pkg", dist.package_dir, tmp_path)
+    assert os.path.exists(package_path)
+    assert folder in _Path(package_path).parts()
 
     _run_build(tmp_path, "--sdist")
     dist_file = tmp_path / "dist/pkg-42.tar.gz"
@@ -205,7 +220,10 @@ def _write_setupcfg(root, options):
     setupcfg = ConfigParser()
     setupcfg.add_section("options")
     for key, value in options.items():
-        if isinstance(value, list):
+        if key == "packages.find":
+            setupcfg.add_section(f"options.{key}")
+            setupcfg[f"options.{key}"].update(value)
+        elif isinstance(value, list):
             setupcfg["options"][key] = ", ".join(value)
         elif isinstance(value, dict):
             str_value = "\n".join(f"\t{k} = {v}" for k, v in value.items())
@@ -223,9 +241,9 @@ def _run_build(path, *flags):
     return run(cmd, env={'DISTUTILS_DEBUG': '1'})
 
 
-def _run_sdist_programatically(dist_path, options):
+def _run_sdist_programatically(dist_path, attrs):
     root = "/".join(os.path.split(dist_path))  # POSIX-style
-    dist = Distribution({**options, "src_root": root})
+    dist = Distribution({**attrs, "src_root": root})
     dist.script_name = 'setup.py'
 
     if (dist_path / "setup.cfg").exists():
-- 
cgit v1.2.1


From ce5a84e8c279e4f767cca61e170feb17269ae570 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 5 Mar 2022 21:53:31 +0000
Subject: Postpone expanding dynamic config in pyproject.toml

---
 setuptools/config/pyprojecttoml.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index 1ebdd08d..c0b0d755 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -121,7 +121,6 @@ def expand_configuration(
     setuptools_cfg = config.get("tool", {}).get("setuptools", {})
     package_dir = setuptools_cfg.get("package-dir")
 
-    _expand_all_dynamic(project_cfg, setuptools_cfg, root_dir, ignore_option_errors)
     _expand_packages(setuptools_cfg, root_dir, ignore_option_errors)
     _canonic_package_data(setuptools_cfg)
     _canonic_package_data(setuptools_cfg, "exclude-package-data")
@@ -129,8 +128,10 @@ def expand_configuration(
     process = partial(_process_field, ignore_option_errors=ignore_option_errors)
     cmdclass = partial(_expand.cmdclass, package_dir=package_dir, root_dir=root_dir)
     data_files = partial(_expand.canonic_data_files, root_dir=root_dir)
+
     process(setuptools_cfg, "data-files", data_files)
     process(setuptools_cfg, "cmdclass", cmdclass)
+    _expand_all_dynamic(project_cfg, setuptools_cfg, root_dir, ignore_option_errors)
 
     return config
 
-- 
cgit v1.2.1


From 786bdcfaf9de8d1d6af01701c47b3e4a4076a5a3 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 5 Mar 2022 23:36:51 +0000
Subject: Capture expectations about discovery and attr/cmdclass
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

… `pyproject.toml` configs
---
 setup.cfg                                     |  1 +
 setuptools/tests/config/test_pyprojecttoml.py | 70 ++++++++++++++++++++-------
 setuptools/tests/test_config_discovery.py     |  2 +-
 3 files changed, 55 insertions(+), 18 deletions(-)

diff --git a/setup.cfg b/setup.cfg
index 7e428850..b54bb5d3 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -68,6 +68,7 @@ testing =
 	filelock>=3.4.0
 	pip_run>=8.8
 	ini2toml[lite]>=0.9
+	tomli-w>=1.0.0
 
 testing-integration =
 	pytest
diff --git a/setuptools/tests/config/test_pyprojecttoml.py b/setuptools/tests/config/test_pyprojecttoml.py
index 395824bf..235876f0 100644
--- a/setuptools/tests/config/test_pyprojecttoml.py
+++ b/setuptools/tests/config/test_pyprojecttoml.py
@@ -3,6 +3,7 @@ from configparser import ConfigParser
 from inspect import cleandoc
 
 import pytest
+import tomli_w
 
 from setuptools.config.pyprojecttoml import read_configuration, expand_configuration
 
@@ -58,7 +59,7 @@ content-type = "text/markdown"
 "*" = ["*.txt"]
 
 [tool.setuptools.data-files]
-"data" = ["files/*.txt"]
+"data" = ["_files/*.txt"]
 
 [tool.distutils.sdist]
 formats = "gztar"
@@ -68,33 +69,34 @@ universal = true
 """
 
 
-def test_read_configuration(tmp_path):
-    pyproject = tmp_path / "pyproject.toml"
+def create_example(path, pkg_root):
+    pyproject = path / "pyproject.toml"
 
     files = [
-        "src/pkg/__init__.py",
-        "src/other/nested/__init__.py",  # ensure namespaces are discovered by default
-        "files/file.txt"
+        f"{pkg_root}/pkg/__init__.py",
+        f"{pkg_root}/other/nested/__init__.py",  # ensure namespaces are discovered
+        "_files/file.txt"
     ]
     for file in files:
-        (tmp_path / file).parent.mkdir(exist_ok=True, parents=True)
-        (tmp_path / file).touch()
+        (path / file).parent.mkdir(exist_ok=True, parents=True)
+        (path / file).touch()
 
     pyproject.write_text(EXAMPLE)
-    (tmp_path / "README.md").write_text("hello world")
-    (tmp_path / "src/pkg/mod.py").write_text("class CustomSdist: pass")
-    (tmp_path / "src/pkg/__version__.py").write_text("VERSION = (3, 10)")
-    (tmp_path / "src/pkg/__main__.py").write_text("def exec(): print('hello')")
+    (path / "README.md").write_text("hello world")
+    (path / f"{pkg_root}/pkg/mod.py").write_text("class CustomSdist: pass")
+    (path / f"{pkg_root}/pkg/__version__.py").write_text("VERSION = (3, 10)")
+    (path / f"{pkg_root}/pkg/__main__.py").write_text("def exec(): print('hello')")
 
-    config = read_configuration(pyproject, expand=False)
-    assert config["project"].get("version") is None
-    assert config["project"].get("readme") is None
 
-    expanded = expand_configuration(config, tmp_path)
+def verify_example(config, path):
+    pyproject = path / "pyproject.toml"
+    pyproject.write_text(tomli_w.dumps(config), encoding="utf-8")
+    expanded = expand_configuration(config, path)
     expanded_project = expanded["project"]
     assert read_configuration(pyproject, expand=True) == expanded
     assert expanded_project["version"] == "3.10"
     assert expanded_project["readme"]["text"] == "hello world"
+    assert "packages" in expanded["tool"]["setuptools"]
     assert set(expanded["tool"]["setuptools"]["packages"]) == {
         "pkg",
         "other",
@@ -103,10 +105,44 @@ def test_read_configuration(tmp_path):
     assert "" in expanded["tool"]["setuptools"]["package-data"]
     assert "*" not in expanded["tool"]["setuptools"]["package-data"]
     assert expanded["tool"]["setuptools"]["data-files"] == [
-        ("data", ["files/file.txt"])
+        ("data", ["_files/file.txt"])
     ]
 
 
+def test_read_configuration(tmp_path):
+    create_example(tmp_path, "src")
+    pyproject = tmp_path / "pyproject.toml"
+
+    config = read_configuration(pyproject, expand=False)
+    assert config["project"].get("version") is None
+    assert config["project"].get("readme") is None
+
+    verify_example(config, tmp_path)
+
+
+@pytest.mark.parametrize(
+    "pkg_root, opts",
+    [
+        (".", {}),
+        ("src", {}),
+        ("lib", {"packages": {"find": {"where": ["lib"]}}}),
+    ]
+)
+def test_discovered_package_dir_with_attr_directive_in_config(tmp_path, pkg_root, opts):
+    create_example(tmp_path, pkg_root)
+
+    pyproject = tmp_path / "pyproject.toml"
+
+    config = read_configuration(pyproject, expand=False)
+    assert config["project"].get("version") is None
+    assert config["project"].get("readme") is None
+    config["tool"]["setuptools"].pop("packages", None)
+    config["tool"]["setuptools"].pop("package-dir", None)
+
+    config["tool"]["setuptools"].update(opts)
+    verify_example(config, tmp_path)
+
+
 ENTRY_POINTS = {
     "console_scripts": {"a": "mod.a:func"},
     "gui_scripts": {"b": "mod.b:func"},
diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py
index 4456ad0f..d60513e3 100644
--- a/setuptools/tests/test_config_discovery.py
+++ b/setuptools/tests/test_config_discovery.py
@@ -178,7 +178,7 @@ class TestNoConfig:
         ("lib", {"packages": "find:", "packages.find": {"where": "lib"}}),
     ]
 )
-def test_discovered_packagedir_with_attr_directive_in_config(tmp_path, folder, opts):
+def test_discovered_package_dir_with_attr_directive_in_config(tmp_path, folder, opts):
     _populate_project_dir(tmp_path, [f"{folder}/pkg/__init__.py", "setup.cfg"], opts)
     (tmp_path / folder / "pkg/__init__.py").write_text("version = 42")
     (tmp_path / "setup.cfg").write_text(
-- 
cgit v1.2.1


From e495568f12265a6fbc1a68e66331173dbf11d871 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 5 Mar 2022 23:39:55 +0000
Subject: Take discovery into account when expanding pyproject.toml

---
 setuptools/config/pyprojecttoml.py | 116 ++++++++++++++++++++++++++++++-------
 1 file changed, 96 insertions(+), 20 deletions(-)

diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index c0b0d755..1d1ae603 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -1,10 +1,10 @@
 """Load setuptools configuration from ``pyproject.toml`` files"""
+import logging
 import os
 import warnings
-import logging
 from contextlib import contextmanager
 from functools import partial
-from typing import TYPE_CHECKING, Callable, Optional, Union
+from typing import TYPE_CHECKING, Callable, Optional, Tuple, Union
 
 from setuptools.errors import FileError, OptionError
 
@@ -47,11 +47,16 @@ def apply_configuration(dist: "Distribution", filepath: _Path) -> "Distribution"
     """Apply the configuration from a ``pyproject.toml`` file into an existing
     distribution object.
     """
-    config = read_configuration(filepath)
+    config = read_configuration(filepath, dist=dist)
     return apply(dist, config, filepath)
 
 
-def read_configuration(filepath: _Path, expand=True, ignore_option_errors=False):
+def read_configuration(
+    filepath: _Path,
+    expand=True,
+    ignore_option_errors=False,
+    dist: Optional["Distribution"] = None,
+):
     """Read given configuration file and returns options from it as a dict.
 
     :param str|unicode filepath: Path to configuration file in the ``pyproject.toml``
@@ -65,6 +70,12 @@ def read_configuration(filepath: _Path, expand=True, ignore_option_errors=False)
         in directives such as file:, attr:, etc.).
         If False exceptions are propagated as expected.
 
+    :param Distribution|None: Distribution object to which the configuration refers.
+        If not given a dummy object will be created and discarded after the
+        configuration is read. This is used for auto-discovery of packages in the case
+        a dynamic configuration (e.g. ``attr`` or ``cmdclass``) is expanded.
+        When ``expand=False`` this object is simply ignored.
+
     :rtype: dict
     """
     filepath = os.path.abspath(filepath)
@@ -75,7 +86,7 @@ def read_configuration(filepath: _Path, expand=True, ignore_option_errors=False)
     asdict = load_file(filepath) or {}
     project_table = asdict.get("project", {})
     tool_table = asdict.get("tool", {}).get("setuptools", {})
-    if not asdict or not(project_table or tool_table):
+    if not asdict or not (project_table or tool_table):
         return {}  # User is not using pyproject to configure setuptools
 
     # TODO: Remove once the future stabilizes
@@ -98,13 +109,16 @@ def read_configuration(filepath: _Path, expand=True, ignore_option_errors=False)
 
     if expand:
         root_dir = os.path.dirname(filepath)
-        return expand_configuration(asdict, root_dir, ignore_option_errors)
+        return expand_configuration(asdict, root_dir, ignore_option_errors, dist)
 
     return asdict
 
 
 def expand_configuration(
-    config: dict, root_dir: Optional[_Path] = None, ignore_option_errors=False
+    config: dict,
+    root_dir: Optional[_Path] = None,
+    ignore_option_errors=False,
+    dist: Optional["Distribution"] = None,
 ) -> dict:
     """Given a configuration with unresolved fields (e.g. dynamic, cmdclass, ...)
     find their final values.
@@ -113,35 +127,94 @@ def expand_configuration(
     :param str root_dir: Top-level directory for the distribution/project
         (the same directory where ``pyproject.toml`` is place)
     :param bool ignore_option_errors: see :func:`read_configuration`
+    :param Distribution|None: Distribution object to which the configuration refers.
+        If not given a dummy object will be created and discarded after the
+        configuration is read. Used in the case a dynamic configuration
+        (e.g. ``attr`` or ``cmdclass``).
 
     :rtype: dict
     """
     root_dir = root_dir or os.getcwd()
     project_cfg = config.get("project", {})
     setuptools_cfg = config.get("tool", {}).get("setuptools", {})
-    package_dir = setuptools_cfg.get("package-dir")
+
+    # A distribution object is required for discovering the correct package_dir
+    dist, setuptools_cfg = _ensure_dist_and_package_dir(
+        dist, project_cfg, setuptools_cfg, root_dir
+    )
 
     _expand_packages(setuptools_cfg, root_dir, ignore_option_errors)
     _canonic_package_data(setuptools_cfg)
     _canonic_package_data(setuptools_cfg, "exclude-package-data")
 
-    process = partial(_process_field, ignore_option_errors=ignore_option_errors)
-    cmdclass = partial(_expand.cmdclass, package_dir=package_dir, root_dir=root_dir)
-    data_files = partial(_expand.canonic_data_files, root_dir=root_dir)
+    with _expand.EnsurePackagesDiscovered(dist) as ensure_discovered:
+        _fill_discovered_attrs(dist, setuptools_cfg, ensure_discovered)
+        package_dir = setuptools_cfg["package-dir"]
+
+        process = partial(_process_field, ignore_option_errors=ignore_option_errors)
+        cmdclass = partial(_expand.cmdclass, package_dir=package_dir, root_dir=root_dir)
+        data_files = partial(_expand.canonic_data_files, root_dir=root_dir)
 
-    process(setuptools_cfg, "data-files", data_files)
-    process(setuptools_cfg, "cmdclass", cmdclass)
-    _expand_all_dynamic(project_cfg, setuptools_cfg, root_dir, ignore_option_errors)
+        process(setuptools_cfg, "data-files", data_files)
+        process(setuptools_cfg, "cmdclass", cmdclass)
+        _expand_all_dynamic(project_cfg, setuptools_cfg, root_dir, ignore_option_errors)
 
     return config
 
 
+def _ensure_dist_and_package_dir(
+    dist: Optional["Distribution"],
+    project_cfg: dict,
+    setuptools_cfg: dict,
+    root_dir: _Path,
+) -> Tuple["Distribution", dict]:
+    from setuptools.dist import Distribution
+
+    attrs = {"src_root": root_dir, "name": project_cfg.get("name", None)}
+    dist = dist or Distribution(attrs)
+
+    # dist and setuptools_cfg should use the same package_dir
+    if dist.package_dir is None:
+        dist.package_dir = setuptools_cfg.get("package-dir", {})
+    if setuptools_cfg.get("package-dir") is None:
+        setuptools_cfg["package-dir"] = dist.package_dir
+
+    return dist, setuptools_cfg
+
+
+def _fill_discovered_attrs(
+    dist: "Distribution",
+    setuptools_cfg: dict,
+    ensure_discovered: _expand.EnsurePackagesDiscovered,
+):
+    """When entering the context, the values of ``packages``, ``py_modules`` and
+    ``package_dir`` that are missing in ``dist`` are copied from ``setuptools_cfg``.
+    When existing the context, if these values are missing in ``setuptools_cfg``, they
+    will be copied from ``dist``.
+    """
+    package_dir = setuptools_cfg["package-dir"]
+    dist.package_dir = package_dir  # need to be the same object
+
+    # Set `py_modules` and `packages` in dist to short-circuit auto-discovery,
+    # but avoid overwriting empty lists purposefully set by users.
+    if isinstance(setuptools_cfg.get("py_modules"), list) and dist.py_modules is None:
+        dist.py_modules = setuptools_cfg["py-modules"]
+    if isinstance(setuptools_cfg.get("packages"), list) and dist.packages is None:
+        dist.packages = setuptools_cfg["packages"]
+
+    package_dir.update(ensure_discovered())
+
+    # If anything was discovered set them back, so they count in the final config.
+    setuptools_cfg.setdefault("packages", dist.packages)
+    setuptools_cfg.setdefault("py-modules", dist.py_modules)
+
+
 def _expand_all_dynamic(
     project_cfg: dict, setuptools_cfg: dict, root_dir: _Path, ignore_option_errors: bool
 ):
     silent = ignore_option_errors
     dynamic_cfg = setuptools_cfg.get("dynamic", {})
-    package_dir = setuptools_cfg.get("package-dir", None)
+    package_dir = setuptools_cfg["package-dir"]
     special = ("license", "readme", "version", "entry-points", "scripts", "gui-scripts")
     # license-files are handled directly in the metadata, so no expansion
     # readme, version and entry-points need special handling
@@ -166,8 +239,11 @@ def _expand_all_dynamic(
 
 
 def _expand_dynamic(
-    dynamic_cfg: dict, field: str, package_dir: Optional[dict],
-    root_dir: _Path, ignore_option_errors: bool
+    dynamic_cfg: dict,
+    field: str,
+    package_dir: dict,
+    root_dir: _Path,
+    ignore_option_errors: bool,
 ):
     if field in dynamic_cfg:
         directive = dynamic_cfg[field]
@@ -186,7 +262,7 @@ def _expand_readme(dynamic_cfg: dict, root_dir: _Path, ignore_option_errors: boo
     silent = ignore_option_errors
     return {
         "text": _expand_dynamic(dynamic_cfg, "readme", None, root_dir, silent),
-        "content-type": dynamic_cfg["readme"].get("content-type", "text/x-rst")
+        "content-type": dynamic_cfg["readme"].get("content-type", "text/x-rst"),
     }
 
 
@@ -208,13 +284,13 @@ def _expand_packages(setuptools_cfg: dict, root_dir: _Path, ignore_option_errors
     find = packages.get("find")
     if isinstance(find, dict):
         find["root_dir"] = root_dir
+        find["fill_package_dir"] = setuptools_cfg["package-dir"]
         with _ignore_errors(ignore_option_errors):
             setuptools_cfg["packages"] = _expand.find_packages(**find)
 
 
 def _process_field(
-    container: dict, field: str,
-    fn: Callable, ignore_option_errors=False
+    container: dict, field: str, fn: Callable, ignore_option_errors=False
 ):
     if field in container:
         with _ignore_errors(ignore_option_errors):
-- 
cgit v1.2.1


From 2dab062b06422c271d69283dbc5a713c2a2a35c5 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 6 Mar 2022 00:53:14 +0000
Subject: Mark features related auto-discovery and pyproject metadata as
 experimental

---
 changelog.d/2887.change.1.rst        | 2 +-
 changelog.d/2887.change.2.rst        | 2 +-
 changelog.d/3068.change.rst          | 2 +-
 docs/userguide/package_discovery.rst | 6 ++++++
 4 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/changelog.d/2887.change.1.rst b/changelog.d/2887.change.1.rst
index 11d7a716..66832176 100644
--- a/changelog.d/2887.change.1.rst
+++ b/changelog.d/2887.change.1.rst
@@ -1,4 +1,4 @@
-Added automatic discovery for ``py_modules`` and ``packages``
+**[EXPERIMENTAL]** Added automatic discovery for ``py_modules`` and ``packages``
 -- by :user:`abravalheri`.
 
 Setuptools will try to find these values assuming that the package uses either
diff --git a/changelog.d/2887.change.2.rst b/changelog.d/2887.change.2.rst
index a6aa041a..1e3cc182 100644
--- a/changelog.d/2887.change.2.rst
+++ b/changelog.d/2887.change.2.rst
@@ -1,4 +1,4 @@
-Added automatic configuration for the ``name`` metadata
+**[EXPERIMENTAL]** Added automatic configuration for the ``name`` metadata
 -- by :user:`abravalheri`.
 
 Setuptools will adopt the name of the top-level package (or module in the case
diff --git a/changelog.d/3068.change.rst b/changelog.d/3068.change.rst
index ca71972b..26ec747b 100644
--- a/changelog.d/3068.change.rst
+++ b/changelog.d/3068.change.rst
@@ -1,4 +1,4 @@
-Added **experimental** support for ``pyproject.toml`` configuration
+**[EXPERIMENTAL]** Add support for ``pyproject.toml`` configuration
 (as introduced by :pep:`621`). Configuration parameters not covered by
 standards are handled in the ``[tool.setuptools]`` sub-table.
 
diff --git a/docs/userguide/package_discovery.rst b/docs/userguide/package_discovery.rst
index 99e45bed..16661a3f 100644
--- a/docs/userguide/package_discovery.rst
+++ b/docs/userguide/package_discovery.rst
@@ -46,6 +46,10 @@ the following sections.
 Automatic discovery
 ===================
 
+.. warning:: Automatic discovery is an **experimental** future and might change
+   (or be completely removed) in the future.
+   See :ref:`custom-discovery` for a stable way of configuring ``setuptools``.
+
 By default setuptools will consider 2 popular project layouts, each one with
 its own set of advantages and disadvantages [#layout1]_ [#layout2]_.
 
@@ -166,6 +170,8 @@ Also note that you can customise your project layout by explicitly setting
    place.
 
 
+.. custom-discovery::
+
 Custom discovery
 ================
 
-- 
cgit v1.2.1


From 0bdb1b7315583638a16e6488b9df32983c0c2f85 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 6 Mar 2022 00:56:54 +0000
Subject: Add news fragment

---
 changelog.d/3152.change.rst | 4 ++++
 1 file changed, 4 insertions(+)
 create mode 100644 changelog.d/3152.change.rst

diff --git a/changelog.d/3152.change.rst b/changelog.d/3152.change.rst
new file mode 100644
index 00000000..802a39ca
--- /dev/null
+++ b/changelog.d/3152.change.rst
@@ -0,0 +1,4 @@
+**[EXPERIMENTAL]** Added support for ``attr:`` and ``cmdclass`` configurations
+in ``setup.cfg`` and ``pyproject.toml`` when ``package_dir`` is implicitly
+found via auto-discovery.
+
-- 
cgit v1.2.1


From 74f7724eda7c18d732b55022f49221bea46f9f85 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 6 Mar 2022 01:04:11 +0000
Subject: Fix small errors in docs

---
 docs/userguide/package_discovery.rst | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/docs/userguide/package_discovery.rst b/docs/userguide/package_discovery.rst
index 16661a3f..762c440e 100644
--- a/docs/userguide/package_discovery.rst
+++ b/docs/userguide/package_discovery.rst
@@ -46,7 +46,7 @@ the following sections.
 Automatic discovery
 ===================
 
-.. warning:: Automatic discovery is an **experimental** future and might change
+.. warning:: Automatic discovery is an **experimental** feature and might change
    (or be completely removed) in the future.
    See :ref:`custom-discovery` for a stable way of configuring ``setuptools``.
 
@@ -170,7 +170,7 @@ Also note that you can customise your project layout by explicitly setting
    place.
 
 
-.. custom-discovery::
+.. _custom-discovery:
 
 Custom discovery
 ================
-- 
cgit v1.2.1


From 13336595c118ad030f41c70f61e8b28534bb0e6e Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 9 Mar 2022 15:53:26 +0000
Subject: Test the behavior of license and license-files in pyproject.toml

---
 .../tests/config/test_apply_pyprojecttoml.py       | 24 ++++++++++++++++++++++
 1 file changed, 24 insertions(+)

diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py
index 5b5a8dfa..3788ff58 100644
--- a/setuptools/tests/config/test_apply_pyprojecttoml.py
+++ b/setuptools/tests/config/test_apply_pyprojecttoml.py
@@ -176,6 +176,30 @@ def test_no_explicit_content_type_for_missing_extension(tmp_path):
     assert dist.metadata.long_description_content_type is None
 
 
+# TODO: After PEP 639 is accepted, we have to move the license-files
+#       to the `project` table instead of `tool.setuptools`
+def test_license(tmp_path):
+    pyproject = _pep621_example_project(tmp_path, "README")
+    text = pyproject.read_text(encoding="utf-8")
+
+    # Sanity-check
+    assert 'license = {file = "LICENSE.txt"}' in text
+    assert "[tool.setuptools]" not in text
+
+    text += '\n[tool.setuptools]\nlicense-files = ["_FILE*"]\n'
+    pyproject.write_text(text, encoding="utf-8")
+    (tmp_path / "_FILE.txt").touch()
+    (tmp_path / "_FILE.rst").touch()
+
+    # Would normally match the `license_files` glob patterns, but we want to exclude it
+    # by being explicit. On the other hand, its contents should be added to `license`
+    (tmp_path / "LICENSE.txt").write_text("Super License\n", encoding="utf-8")
+
+    dist = pyprojecttoml.apply_configuration(Distribution(), pyproject)
+    assert set(dist.metadata.license_files) == {"_FILE.rst", "_FILE.txt"}
+    assert dist.metadata.license == "Super License\n"
+
+
 # --- Auxiliary Functions ---
 
 
-- 
cgit v1.2.1


From e24b26dc3382c56a22ca8d6751ff82ee3348ee42 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 9 Mar 2022 23:40:07 +0000
Subject: Update vendored dependency validate-pyproject to 0.6

---
 .../fastjsonschema_validations.py                  | 43 ++++++++++------------
 setuptools/_vendor/_validate_pyproject/formats.py  |  4 +-
 setuptools/_vendor/vendored.txt                    |  2 +-
 3 files changed, 23 insertions(+), 26 deletions(-)

diff --git a/setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py b/setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py
index 556e6fed..3feda6a8 100644
--- a/setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py
+++ b/setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py
@@ -30,7 +30,7 @@ def validate(data, custom_formats={}, name_prefix=None):
 
 def validate_https___packaging_python_org_en_latest_specifications_declaring_build_dependencies(data, custom_formats={}, name_prefix=None):
     if not isinstance(data, (dict)):
-        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-build-dependencies/', 'title': 'Data structure for ``pyproject.toml`` files', '$$description': ['File format containing build-time configurations for the Python ecosystem. ', ':pep:`517` initially defined a build-system independent format for source trees', 'which was complemented by :pep:`518` to provide a way of specifying dependencies ', 'for building Python projects.', 'Please notice the ``project`` table (as initially defined in  :pep:`621`) is not included', 'in this schema and should be considered separately.'], 'type': 'object', 'additionalProperties': False, 'properties': {'build-system': {'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, 'tool': {'type': 'object', 'properties': {'distutils': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://docs.python.org/3/install/', 'title': '``tool.distutils`` table', '$$description': ['Originally, ``distutils`` allowed developers to configure arguments for', '``setup.py`` scripts via `distutils configuration files', '`_.', '``tool.distutils`` subtables could be used with the same purpose', '(NOT CURRENTLY IMPLEMENTED).'], 'type': 'object', 'properties': {'global': {'type': 'object', 'description': 'Global options applied to all ``distutils`` commands'}}, 'patternProperties': {'.+': {'type': 'object'}}, '$comment': 'TODO: Is there a practical way of making this schema more specific?'}, 'setuptools': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$ref': '#/definitions/find-directive'}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'$ref': '#/definitions/attr-directive'}, {'$ref': '#/definitions/file-directive'}]}, 'classifiers': {'$ref': '#/definitions/file-directive'}, 'description': {'$ref': '#/definitions/file-directive'}, 'entry-points': {'$ref': '#/definitions/file-directive'}, 'readme': {'anyOf': [{'$ref': '#/definitions/file-directive'}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}, 'license': {'type': 'string', '$$description': ['PROVISIONAL: A string specifying the license of the package', '(might change with PEP 639)'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-expression``?'}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might change with PEP 639)'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-files.glob``?'}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}}}}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, rule='type')
+        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-build-dependencies/', 'title': 'Data structure for ``pyproject.toml`` files', '$$description': ['File format containing build-time configurations for the Python ecosystem. ', ':pep:`517` initially defined a build-system independent format for source trees', 'which was complemented by :pep:`518` to provide a way of specifying dependencies ', 'for building Python projects.', 'Please notice the ``project`` table (as initially defined in  :pep:`621`) is not included', 'in this schema and should be considered separately.'], 'type': 'object', 'additionalProperties': False, 'properties': {'build-system': {'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, 'tool': {'type': 'object', 'properties': {'distutils': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://docs.python.org/3/install/', 'title': '``tool.distutils`` table', '$$description': ['Originally, ``distutils`` allowed developers to configure arguments for', '``setup.py`` scripts via `distutils configuration files', '`_.', '``tool.distutils`` subtables could be used with the same purpose', '(NOT CURRENTLY IMPLEMENTED).'], 'type': 'object', 'properties': {'global': {'type': 'object', 'description': 'Global options applied to all ``distutils`` commands'}}, 'patternProperties': {'.+': {'type': 'object'}}, '$comment': 'TODO: Is there a practical way of making this schema more specific?'}, 'setuptools': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$ref': '#/definitions/find-directive'}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might become standard with PEP 639).'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Probably ``project.license-files``?'}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'additionalProperties': False, 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'$ref': '#/definitions/attr-directive'}, {'$ref': '#/definitions/file-directive'}]}, 'classifiers': {'$ref': '#/definitions/file-directive'}, 'description': {'$ref': '#/definitions/file-directive'}, 'entry-points': {'$ref': '#/definitions/file-directive'}, 'readme': {'anyOf': [{'$ref': '#/definitions/file-directive'}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}}}}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, rule='type')
     data_is_dict = isinstance(data, dict)
     if data_is_dict:
         data_keys = set(data.keys())
@@ -85,7 +85,7 @@ def validate_https___packaging_python_org_en_latest_specifications_declaring_bui
             data_keys.remove("tool")
             data__tool = data["tool"]
             if not isinstance(data__tool, (dict)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".tool must be object", value=data__tool, name="" + (name_prefix or "data") + ".tool", definition={'type': 'object', 'properties': {'distutils': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://docs.python.org/3/install/', 'title': '``tool.distutils`` table', '$$description': ['Originally, ``distutils`` allowed developers to configure arguments for', '``setup.py`` scripts via `distutils configuration files', '`_.', '``tool.distutils`` subtables could be used with the same purpose', '(NOT CURRENTLY IMPLEMENTED).'], 'type': 'object', 'properties': {'global': {'type': 'object', 'description': 'Global options applied to all ``distutils`` commands'}}, 'patternProperties': {'.+': {'type': 'object'}}, '$comment': 'TODO: Is there a practical way of making this schema more specific?'}, 'setuptools': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$ref': '#/definitions/find-directive'}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'$ref': '#/definitions/attr-directive'}, {'$ref': '#/definitions/file-directive'}]}, 'classifiers': {'$ref': '#/definitions/file-directive'}, 'description': {'$ref': '#/definitions/file-directive'}, 'entry-points': {'$ref': '#/definitions/file-directive'}, 'readme': {'anyOf': [{'$ref': '#/definitions/file-directive'}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}, 'license': {'type': 'string', '$$description': ['PROVISIONAL: A string specifying the license of the package', '(might change with PEP 639)'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-expression``?'}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might change with PEP 639)'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-files.glob``?'}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}}}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".tool must be object", value=data__tool, name="" + (name_prefix or "data") + ".tool", definition={'type': 'object', 'properties': {'distutils': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://docs.python.org/3/install/', 'title': '``tool.distutils`` table', '$$description': ['Originally, ``distutils`` allowed developers to configure arguments for', '``setup.py`` scripts via `distutils configuration files', '`_.', '``tool.distutils`` subtables could be used with the same purpose', '(NOT CURRENTLY IMPLEMENTED).'], 'type': 'object', 'properties': {'global': {'type': 'object', 'description': 'Global options applied to all ``distutils`` commands'}}, 'patternProperties': {'.+': {'type': 'object'}}, '$comment': 'TODO: Is there a practical way of making this schema more specific?'}, 'setuptools': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$ref': '#/definitions/find-directive'}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might become standard with PEP 639).'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Probably ``project.license-files``?'}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'additionalProperties': False, 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'$ref': '#/definitions/attr-directive'}, {'$ref': '#/definitions/file-directive'}]}, 'classifiers': {'$ref': '#/definitions/file-directive'}, 'description': {'$ref': '#/definitions/file-directive'}, 'entry-points': {'$ref': '#/definitions/file-directive'}, 'readme': {'anyOf': [{'$ref': '#/definitions/file-directive'}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}}}, rule='type')
             data__tool_is_dict = isinstance(data__tool, dict)
             if data__tool_is_dict:
                 data__tool_keys = set(data__tool.keys())
@@ -98,12 +98,12 @@ def validate_https___packaging_python_org_en_latest_specifications_declaring_bui
                     data__tool__setuptools = data__tool["setuptools"]
                     validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data__tool__setuptools, custom_formats, (name_prefix or "data") + ".tool.setuptools")
         if data_keys:
-            raise JsonSchemaValueException("" + (name_prefix or "data") + " must not contain "+str(data_keys)+" properties", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-build-dependencies/', 'title': 'Data structure for ``pyproject.toml`` files', '$$description': ['File format containing build-time configurations for the Python ecosystem. ', ':pep:`517` initially defined a build-system independent format for source trees', 'which was complemented by :pep:`518` to provide a way of specifying dependencies ', 'for building Python projects.', 'Please notice the ``project`` table (as initially defined in  :pep:`621`) is not included', 'in this schema and should be considered separately.'], 'type': 'object', 'additionalProperties': False, 'properties': {'build-system': {'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, 'tool': {'type': 'object', 'properties': {'distutils': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://docs.python.org/3/install/', 'title': '``tool.distutils`` table', '$$description': ['Originally, ``distutils`` allowed developers to configure arguments for', '``setup.py`` scripts via `distutils configuration files', '`_.', '``tool.distutils`` subtables could be used with the same purpose', '(NOT CURRENTLY IMPLEMENTED).'], 'type': 'object', 'properties': {'global': {'type': 'object', 'description': 'Global options applied to all ``distutils`` commands'}}, 'patternProperties': {'.+': {'type': 'object'}}, '$comment': 'TODO: Is there a practical way of making this schema more specific?'}, 'setuptools': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$ref': '#/definitions/find-directive'}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'$ref': '#/definitions/attr-directive'}, {'$ref': '#/definitions/file-directive'}]}, 'classifiers': {'$ref': '#/definitions/file-directive'}, 'description': {'$ref': '#/definitions/file-directive'}, 'entry-points': {'$ref': '#/definitions/file-directive'}, 'readme': {'anyOf': [{'$ref': '#/definitions/file-directive'}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}, 'license': {'type': 'string', '$$description': ['PROVISIONAL: A string specifying the license of the package', '(might change with PEP 639)'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-expression``?'}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might change with PEP 639)'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-files.glob``?'}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}}}}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, rule='additionalProperties')
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must not contain "+str(data_keys)+" properties", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-build-dependencies/', 'title': 'Data structure for ``pyproject.toml`` files', '$$description': ['File format containing build-time configurations for the Python ecosystem. ', ':pep:`517` initially defined a build-system independent format for source trees', 'which was complemented by :pep:`518` to provide a way of specifying dependencies ', 'for building Python projects.', 'Please notice the ``project`` table (as initially defined in  :pep:`621`) is not included', 'in this schema and should be considered separately.'], 'type': 'object', 'additionalProperties': False, 'properties': {'build-system': {'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, 'tool': {'type': 'object', 'properties': {'distutils': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://docs.python.org/3/install/', 'title': '``tool.distutils`` table', '$$description': ['Originally, ``distutils`` allowed developers to configure arguments for', '``setup.py`` scripts via `distutils configuration files', '`_.', '``tool.distutils`` subtables could be used with the same purpose', '(NOT CURRENTLY IMPLEMENTED).'], 'type': 'object', 'properties': {'global': {'type': 'object', 'description': 'Global options applied to all ``distutils`` commands'}}, 'patternProperties': {'.+': {'type': 'object'}}, '$comment': 'TODO: Is there a practical way of making this schema more specific?'}, 'setuptools': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$ref': '#/definitions/find-directive'}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might become standard with PEP 639).'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Probably ``project.license-files``?'}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'additionalProperties': False, 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'$ref': '#/definitions/attr-directive'}, {'$ref': '#/definitions/file-directive'}]}, 'classifiers': {'$ref': '#/definitions/file-directive'}, 'description': {'$ref': '#/definitions/file-directive'}, 'entry-points': {'$ref': '#/definitions/file-directive'}, 'readme': {'anyOf': [{'$ref': '#/definitions/file-directive'}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}}}}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, rule='additionalProperties')
     return data
 
 def validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data, custom_formats={}, name_prefix=None):
     if not isinstance(data, (dict)):
-        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}]}, 'classifiers': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'description': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'entry-points': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'readme': {'anyOf': [{'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}, 'license': {'type': 'string', '$$description': ['PROVISIONAL: A string specifying the license of the package', '(might change with PEP 639)'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-expression``?'}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might change with PEP 639)'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-files.glob``?'}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}, rule='type')
+        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might become standard with PEP 639).'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Probably ``project.license-files``?'}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'additionalProperties': False, 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}]}, 'classifiers': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'description': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'entry-points': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'readme': {'anyOf': [{'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}, rule='type')
     data_is_dict = isinstance(data, dict)
     if data_is_dict:
         data_keys = set(data.keys())
@@ -404,11 +404,23 @@ def validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data,
                         if isinstance(data__cmdclass_val, str):
                             if not custom_formats["python-qualified-identifier"](data__cmdclass_val):
                                 raise JsonSchemaValueException("" + (name_prefix or "data") + ".cmdclass.{data__cmdclass_key}".format(**locals()) + " must be python-qualified-identifier", value=data__cmdclass_val, name="" + (name_prefix or "data") + ".cmdclass.{data__cmdclass_key}".format(**locals()) + "", definition={'type': 'string', 'format': 'python-qualified-identifier'}, rule='format')
+        if "license-files" in data_keys:
+            data_keys.remove("license-files")
+            data__licensefiles = data["license-files"]
+            if not isinstance(data__licensefiles, (list, tuple)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".license-files must be array", value=data__licensefiles, name="" + (name_prefix or "data") + ".license-files", definition={'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might become standard with PEP 639).'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Probably ``project.license-files``?'}, rule='type')
+            data__licensefiles_is_list = isinstance(data__licensefiles, (list, tuple))
+            if data__licensefiles_is_list:
+                data__licensefiles_len = len(data__licensefiles)
+                for data__licensefiles_x, data__licensefiles_item in enumerate(data__licensefiles):
+                    if not isinstance(data__licensefiles_item, (str)):
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".license-files[{data__licensefiles_x}]".format(**locals()) + " must be string", value=data__licensefiles_item, name="" + (name_prefix or "data") + ".license-files[{data__licensefiles_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
+        else: data["license-files"] = ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*']
         if "dynamic" in data_keys:
             data_keys.remove("dynamic")
             data__dynamic = data["dynamic"]
             if not isinstance(data__dynamic, (dict)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic must be object", value=data__dynamic, name="" + (name_prefix or "data") + ".dynamic", definition={'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}]}, 'classifiers': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'description': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'entry-points': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'readme': {'anyOf': [{'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}, 'license': {'type': 'string', '$$description': ['PROVISIONAL: A string specifying the license of the package', '(might change with PEP 639)'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-expression``?'}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might change with PEP 639)'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-files.glob``?'}}}, rule='type')
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic must be object", value=data__dynamic, name="" + (name_prefix or "data") + ".dynamic", definition={'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'additionalProperties': False, 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}]}, 'classifiers': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'description': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'entry-points': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'readme': {'anyOf': [{'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}}}, rule='type')
             data__dynamic_is_dict = isinstance(data__dynamic, dict)
             if data__dynamic_is_dict:
                 data__dynamic_keys = set(data__dynamic.keys())
@@ -468,25 +480,10 @@ def validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data,
                         data__dynamic__readme_len = len(data__dynamic__readme)
                         if not all(prop in data__dynamic__readme for prop in ['file']):
                             raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic.readme must contain ['file'] properties", value=data__dynamic__readme, name="" + (name_prefix or "data") + ".dynamic.readme", definition={'anyOf': [{'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}, rule='required')
-                if "license" in data__dynamic_keys:
-                    data__dynamic_keys.remove("license")
-                    data__dynamic__license = data__dynamic["license"]
-                    if not isinstance(data__dynamic__license, (str)):
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic.license must be string", value=data__dynamic__license, name="" + (name_prefix or "data") + ".dynamic.license", definition={'type': 'string', '$$description': ['PROVISIONAL: A string specifying the license of the package', '(might change with PEP 639)'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-expression``?'}, rule='type')
-                if "license-files" in data__dynamic_keys:
-                    data__dynamic_keys.remove("license-files")
-                    data__dynamic__licensefiles = data__dynamic["license-files"]
-                    if not isinstance(data__dynamic__licensefiles, (list, tuple)):
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic.license-files must be array", value=data__dynamic__licensefiles, name="" + (name_prefix or "data") + ".dynamic.license-files", definition={'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might change with PEP 639)'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-files.glob``?'}, rule='type')
-                    data__dynamic__licensefiles_is_list = isinstance(data__dynamic__licensefiles, (list, tuple))
-                    if data__dynamic__licensefiles_is_list:
-                        data__dynamic__licensefiles_len = len(data__dynamic__licensefiles)
-                        for data__dynamic__licensefiles_x, data__dynamic__licensefiles_item in enumerate(data__dynamic__licensefiles):
-                            if not isinstance(data__dynamic__licensefiles_item, (str)):
-                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic.license-files[{data__dynamic__licensefiles_x}]".format(**locals()) + " must be string", value=data__dynamic__licensefiles_item, name="" + (name_prefix or "data") + ".dynamic.license-files[{data__dynamic__licensefiles_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
-                else: data__dynamic["license-files"] = ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*']
+                if data__dynamic_keys:
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic must not contain "+str(data__dynamic_keys)+" properties", value=data__dynamic, name="" + (name_prefix or "data") + ".dynamic", definition={'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'additionalProperties': False, 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}]}, 'classifiers': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'description': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'entry-points': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'readme': {'anyOf': [{'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}}}, rule='additionalProperties')
         if data_keys:
-            raise JsonSchemaValueException("" + (name_prefix or "data") + " must not contain "+str(data_keys)+" properties", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}]}, 'classifiers': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'description': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'entry-points': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'readme': {'anyOf': [{'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}, 'license': {'type': 'string', '$$description': ['PROVISIONAL: A string specifying the license of the package', '(might change with PEP 639)'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-expression``?'}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might change with PEP 639)'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Maybe ``license-files.glob``?'}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}, rule='additionalProperties')
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must not contain "+str(data_keys)+" properties", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might become standard with PEP 639).'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Probably ``project.license-files``?'}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'additionalProperties': False, 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}]}, 'classifiers': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'description': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'entry-points': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'readme': {'anyOf': [{'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}, rule='additionalProperties')
     return data
 
 def validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data, custom_formats={}, name_prefix=None):
diff --git a/setuptools/_vendor/_validate_pyproject/formats.py b/setuptools/_vendor/_validate_pyproject/formats.py
index af5fc90e..a288eb5f 100644
--- a/setuptools/_vendor/_validate_pyproject/formats.py
+++ b/setuptools/_vendor/_validate_pyproject/formats.py
@@ -161,14 +161,14 @@ class _TroveClassifier:
                 _logger.debug("Problem with download, skipping validation")
                 return True
 
-        return value in self.downloaded
+        return value in self.downloaded or value.lower().startswith("private ::")
 
 
 try:
     from trove_classifiers import classifiers as _trove_classifiers
 
     def trove_classifier(value: str) -> bool:
-        return value in _trove_classifiers
+        return value in _trove_classifiers or value.lower().startswith("private ::")
 
 except ImportError:  # pragma: no cover
     trove_classifier = _TroveClassifier()
diff --git a/setuptools/_vendor/vendored.txt b/setuptools/_vendor/vendored.txt
index 2ef8c6c2..21054883 100644
--- a/setuptools/_vendor/vendored.txt
+++ b/setuptools/_vendor/vendored.txt
@@ -10,4 +10,4 @@ typing_extensions==4.0.1
 # required for importlib_resources and _metadata on older Pythons
 zipp==3.7.0
 tomli==2.0.1
-# validate-pyproject[all]==0.5.2  # Special handling in tools/vendored, don't uncomment or remove
+# validate-pyproject[all]==0.6  # Special handling in tools/vendored, don't uncomment or remove
-- 
cgit v1.2.1


From 85528e935f3ab59275b417c59735ad13d2f28553 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 9 Mar 2022 23:41:34 +0000
Subject: Change pyproject.toml tests to not use dynamic for
 license/license-files

---
 setuptools/tests/config/test_apply_pyprojecttoml.py | 7 ++++---
 setuptools/tests/test_build_meta.py                 | 6 +++---
 setuptools/tests/test_editable_install.py           | 6 +++---
 3 files changed, 10 insertions(+), 9 deletions(-)

diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py
index 3788ff58..181be475 100644
--- a/setuptools/tests/config/test_apply_pyprojecttoml.py
+++ b/setuptools/tests/config/test_apply_pyprojecttoml.py
@@ -147,6 +147,7 @@ def test_pep621_example(tmp_path):
     """Make sure the example in PEP 621 works"""
     pyproject = _pep621_example_project(tmp_path)
     dist = pyprojecttoml.apply_configuration(Distribution(), pyproject)
+    assert dist.metadata.license == "--- LICENSE stub ---"
     assert set(dist.metadata.license_files) == {"LICENSE.txt"}
 
 
@@ -178,7 +179,7 @@ def test_no_explicit_content_type_for_missing_extension(tmp_path):
 
 # TODO: After PEP 639 is accepted, we have to move the license-files
 #       to the `project` table instead of `tool.setuptools`
-def test_license(tmp_path):
+def test_license_and_license_files(tmp_path):
     pyproject = _pep621_example_project(tmp_path, "README")
     text = pyproject.read_text(encoding="utf-8")
 
@@ -193,11 +194,11 @@ def test_license(tmp_path):
 
     # Would normally match the `license_files` glob patterns, but we want to exclude it
     # by being explicit. On the other hand, its contents should be added to `license`
-    (tmp_path / "LICENSE.txt").write_text("Super License\n", encoding="utf-8")
+    (tmp_path / "LICENSE.txt").write_text("LicenseRef-Proprietary\n", encoding="utf-8")
 
     dist = pyprojecttoml.apply_configuration(Distribution(), pyproject)
     assert set(dist.metadata.license_files) == {"_FILE.rst", "_FILE.txt"}
-    assert dist.metadata.license == "Super License\n"
+    assert dist.metadata.license == "LicenseRef-Proprietary\n"
 
 
 # --- Auxiliary Functions ---
diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py
index 323a41a4..dfbe8379 100644
--- a/setuptools/tests/test_build_meta.py
+++ b/setuptools/tests/test_build_meta.py
@@ -288,8 +288,9 @@ class TestBuildMetaBackend:
 
                 [project]
                 name = "foo"
+                license = {text = "MIT"}
                 description = "This is a Python package"
-                dynamic = ["version", "license", "readme"]
+                dynamic = ["version", "readme"]
                 classifiers = [
                     "Development Status :: 5 - Production/Stable",
                     "Intended Audience :: Developers"
@@ -313,11 +314,10 @@ class TestBuildMetaBackend:
                 zip-safe = false
                 package-dir = {"" = "src"}
                 packages = {find = {where = ["src"]}}
+                license-files = ["LICENSE*"]
 
                 [tool.setuptools.dynamic]
                 version = {attr = "foo.__version__"}
-                license = "MIT"
-                license_files = ["LICENSE*"]
                 readme = {file = "README.rst"}
 
                 [tool.distutils.sdist]
diff --git a/setuptools/tests/test_editable_install.py b/setuptools/tests/test_editable_install.py
index ca8288d5..aac4f5ee 100644
--- a/setuptools/tests/test_editable_install.py
+++ b/setuptools/tests/test_editable_install.py
@@ -27,8 +27,9 @@ EXAMPLE = {
         [project]
         name = "mypkg"
         version = "3.14159"
+        license = {text = "MIT"}
         description = "This is a Python package"
-        dynamic = ["license", "readme"]
+        dynamic = ["readme"]
         classifiers = [
             "Development Status :: 5 - Production/Stable",
             "Intended Audience :: Developers"
@@ -39,10 +40,9 @@ EXAMPLE = {
         [tool.setuptools]
         package-dir = {"" = "src"}
         packages = {find = {where = ["src"]}}
+        license-files = ["LICENSE*"]
 
         [tool.setuptools.dynamic]
-        license = "MIT"
-        license_files = ["LICENSE*"]
         readme = {file = "README.rst"}
 
         [tool.distutils.egg_info]
-- 
cgit v1.2.1


From 7bc15259ed34a6bd6dc25f4916412e329a1accf5 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 9 Mar 2022 23:42:14 +0000
Subject: Change pyproject.toml processing to not use dynamic for
 license/license-files

---
 setuptools/config/_apply_pyprojecttoml.py | 29 +++++------------------------
 setuptools/config/pyprojecttoml.py        |  3 +--
 2 files changed, 6 insertions(+), 26 deletions(-)

diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py
index 3ce74512..ce638c62 100644
--- a/setuptools/config/_apply_pyprojecttoml.py
+++ b/setuptools/config/_apply_pyprojecttoml.py
@@ -32,7 +32,6 @@ def apply(dist: "Distribution", config: dict, filename: _Path) -> "Distribution"
     tool_table = config.get("tool", {}).get("setuptools", {})
     project_table = config.get("project", {}).copy()
     _unify_entry_points(project_table)
-    _dynamic_license(project_table, tool_table)
     for field, value in project_table.items():
         norm_key = json_compatible_key(field)
         corresp = PYPROJECT_CORRESPONDENCE.get(norm_key, norm_key)
@@ -109,11 +108,11 @@ def _long_description(dist: "Distribution", val: _DictOrStr, root_dir: _Path):
         _set_config(dist, "long_description_content_type", ctype)
 
 
-def _license(dist: "Distribution", val: Union[str, dict], _root_dir):
-    if isinstance(val, str):
-        _set_config(dist, "license", val)
-    elif "file" in val:
-        _set_config(dist, "license_files", [val["file"]])
+def _license(dist: "Distribution", val: dict, root_dir: _Path):
+    from setuptools.config import expand
+
+    if "file" in val:
+        _set_config(dist, "license", expand.read_files([val["file"]], root_dir))
     else:
         _set_config(dist, "license", val["text"])
 
@@ -150,20 +149,6 @@ def _python_requires(dist: "Distribution", val: dict, _root_dir):
     _set_config(dist, "python_requires", SpecifierSet(val))
 
 
-def _dynamic_license(project_table: dict, tool_table: dict):
-    # Dynamic license needs special handling (cannot be expanded in terms of PEP 621)
-    # due to the mutually exclusive `text` and `file`
-    dynamic_license = {"license", "license_files"}
-    dynamic = {json_compatible_key(k) for k in project_table.get("dynamic", [])}
-    dynamic_cfg = tool_table.get("dynamic", {})
-    dynamic_cfg.setdefault("license_files", DEFAULT_LICENSE_FILES)
-    keys = set(dynamic_cfg) & dynamic_license if "license" in dynamic else set()
-
-    for key in keys:
-        norm_key = json_compatible_key(key)
-        project_table[norm_key] = dynamic_cfg[key]
-
-
 def _unify_entry_points(project_table: dict):
     project = project_table
     entry_points = project.pop("entry-points", project.pop("entry_points", {}))
@@ -252,7 +237,3 @@ TOOL_TABLE_RENAMES = {"script_files": "scripts"}
 
 SETUPTOOLS_PATCHES = {"long_description_content_type", "project_urls",
                       "provides_extras", "license_file", "license_files"}
-
-
-DEFAULT_LICENSE_FILES = ('LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*')
-# defaults from the `wheel` package and historically used by setuptools
diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index 1d1ae603..a4a54061 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -215,8 +215,7 @@ def _expand_all_dynamic(
     silent = ignore_option_errors
     dynamic_cfg = setuptools_cfg.get("dynamic", {})
     package_dir = setuptools_cfg["package-dir"]
-    special = ("license", "readme", "version", "entry-points", "scripts", "gui-scripts")
-    # license-files are handled directly in the metadata, so no expansion
+    special = ("readme", "version", "entry-points", "scripts", "gui-scripts")
     # readme, version and entry-points need special handling
     dynamic = project_cfg.get("dynamic", [])
     regular_dynamic = (x for x in dynamic if x not in special)
-- 
cgit v1.2.1


From 558c163cb00c10725b2e20674e80a0c60b09ca7a Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 12 Mar 2022 13:39:39 -0500
Subject: Refactor UnixCCompiler.link, extracting two functions and reducing
 mccabe complexity from 15 to 11.

---
 distutils/unixccompiler.py | 66 ++++++++++++++++++++++++++++++----------------
 1 file changed, 43 insertions(+), 23 deletions(-)

diff --git a/distutils/unixccompiler.py b/distutils/unixccompiler.py
index a07e5988..a5064fe8 100644
--- a/distutils/unixccompiler.py
+++ b/distutils/unixccompiler.py
@@ -42,6 +42,38 @@ if sys.platform == 'darwin':
 #     options and carry on.
 
 
+def _split_env(cmd):
+    """
+    For macOS, split command into 'env' portion (if any)
+    and the rest of the linker command.
+
+    >>> _split_env(['a', 'b', 'c'])
+    ([], ['a', 'b', 'c'])
+    >>> _split_env(['/usr/bin/env', 'A=3', 'gcc'])
+    (['/usr/bin/env', 'A=3'], ['gcc'])
+    """
+    pivot = 0
+    if os.path.basename(cmd[0]) == "env":
+        pivot = 1
+        while '=' in cmd[pivot]:
+            pivot += 1
+    return cmd[:pivot], cmd[pivot:]
+
+
+def _split_aix(cmd):
+    """
+    AIX platforms prefix the compiler with the ld_so_aix
+    script, so split that from the linker command.
+
+    >>> _split_aix(['a', 'b', 'c'])
+    ([], ['a', 'b', 'c'])
+    >>> _split_aix(['/bin/foo/ld_so_aix', 'gcc'])
+    (['/bin/foo/ld_so_aix'], ['gcc'])
+    """
+    pivot = os.path.basename(cmd[0]) == 'ld_so_aix'
+    return cmd[:pivot], cmd[pivot:]
+
+
 class UnixCCompiler(CCompiler):
 
     compiler_type = 'unix'
@@ -173,30 +205,18 @@ class UnixCCompiler(CCompiler):
                 ld_args.extend(extra_postargs)
             self.mkpath(os.path.dirname(output_filename))
             try:
-                if target_desc == CCompiler.EXECUTABLE:
-                    linker = self.linker_exe[:]
-                else:
-                    linker = self.linker_so[:]
+                linker = (
+                    self.linker_exe
+                    if target_desc == CCompiler.EXECUTABLE else
+                    self.linker_so
+                )[:]
                 if target_lang == "c++" and self.compiler_cxx:
-                    # skip over environment variable settings if /usr/bin/env
-                    # is used to set up the linker's environment.
-                    # This is needed on OSX. Note: this assumes that the
-                    # normal and C++ compiler have the same environment
-                    # settings.
-                    i = 0
-                    if os.path.basename(linker[0]) == "env":
-                        i = 1
-                        while '=' in linker[i]:
-                            i += 1
-
-                    if os.path.basename(linker[i]) == 'ld_so_aix':
-                        # AIX platforms prefix the compiler with the ld_so_aix
-                        # script, so we need to adjust our linker index
-                        offset = 1
-                    else:
-                        offset = 0
-
-                    linker[i+offset] = self.compiler_cxx[i]
+                    env, linker_ne = _split_env(linker)
+                    aix, linker_na = _split_aix(linker_ne)
+                    _, compiler_cxx_ne = _split_env(self.compiler_cxx)
+
+                    linker_na[0] = compiler_cxx_ne[0]
+                    linker = env + aix + linker_na
 
                 if sys.platform == 'darwin':
                     linker = _osx_support.compiler_fixup(linker, ld_args)
-- 
cgit v1.2.1


From b7e7582d7ef0c9e13141bbbb3a78adc362369047 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 12 Mar 2022 13:56:30 -0500
Subject: Extract darwin detection logic to macos_compat module and do it once.

---
 distutils/_macos_compat.py | 12 ++++++++++++
 distutils/unixccompiler.py | 13 ++++---------
 2 files changed, 16 insertions(+), 9 deletions(-)
 create mode 100644 distutils/_macos_compat.py

diff --git a/distutils/_macos_compat.py b/distutils/_macos_compat.py
new file mode 100644
index 00000000..17769e91
--- /dev/null
+++ b/distutils/_macos_compat.py
@@ -0,0 +1,12 @@
+import sys
+import importlib
+
+
+def bypass_compiler_fixup(cmd, args):
+    return cmd
+
+
+if sys.platform == 'darwin':
+    compiler_fixup = importlib.import_module('_osx_support').compiler_fixup
+else:
+    compiler_fixup = bypass_compiler_fixup
diff --git a/distutils/unixccompiler.py b/distutils/unixccompiler.py
index a5064fe8..5ee65cf6 100644
--- a/distutils/unixccompiler.py
+++ b/distutils/unixccompiler.py
@@ -22,9 +22,7 @@ from distutils.ccompiler import \
 from distutils.errors import \
      DistutilsExecError, CompileError, LibError, LinkError
 from distutils import log
-
-if sys.platform == 'darwin':
-    import _osx_support
+from ._macos_compat import compiler_fixup
 
 # XXX Things not currently handled:
 #   * optimization/debug/warning flags; we just use whatever's in Python's
@@ -141,10 +139,8 @@ class UnixCCompiler(CCompiler):
                 raise CompileError(msg)
 
     def _compile(self, obj, src, ext, cc_args, extra_postargs, pp_opts):
-        compiler_so = self.compiler_so
-        if sys.platform == 'darwin':
-            compiler_so = _osx_support.compiler_fixup(compiler_so,
-                                                    cc_args + extra_postargs)
+        compiler_so = compiler_fixup(
+            self.compiler_so, cc_args + extra_postargs)
         try:
             self.spawn(compiler_so + cc_args + [src, '-o', obj] +
                        extra_postargs)
@@ -218,8 +214,7 @@ class UnixCCompiler(CCompiler):
                     linker_na[0] = compiler_cxx_ne[0]
                     linker = env + aix + linker_na
 
-                if sys.platform == 'darwin':
-                    linker = _osx_support.compiler_fixup(linker, ld_args)
+                linker = compiler_fixup(linker, ld_args)
 
                 self.spawn(linker + ld_args)
             except DistutilsExecError as msg:
-- 
cgit v1.2.1


From dc69b3614135a6881e014d9f4fa2410aea06d7bf Mon Sep 17 00:00:00 2001
From: Isuru Fernando 
Date: Sat, 12 Mar 2022 12:58:58 -0600
Subject: Update the test description

---
 distutils/tests/test_unixccompiler.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/distutils/tests/test_unixccompiler.py b/distutils/tests/test_unixccompiler.py
index cd282fbe..d9891189 100644
--- a/distutils/tests/test_unixccompiler.py
+++ b/distutils/tests/test_unixccompiler.py
@@ -217,8 +217,9 @@ class UnixCCompilerTestCase(support.TempdirManager, unittest.TestCase):
 
     @unittest.skipIf(sys.platform == 'win32', "can't test on Windows")
     def test_cc_overrides_ldshared_for_cxx_correctly(self):
-        # Issue #18080:
+        # Issur https://github.com/pypa/distutils/issues/126
         # ensure that setting CC env variable also changes default linker
+        # correctly when C++ extensions are built
         def gcv(v):
             if v == 'LDSHARED':
                 return 'gcc-4.2 -bundle -undefined dynamic_lookup '
-- 
cgit v1.2.1


From c2e0fa8f14533a9fc3c5505d384672fe34f8b943 Mon Sep 17 00:00:00 2001
From: Isuru Fernando 
Date: Sat, 12 Mar 2022 13:10:34 -0600
Subject: Add some comments

---
 distutils/unixccompiler.py | 24 +++++++++++++++++++++++-
 1 file changed, 23 insertions(+), 1 deletion(-)

diff --git a/distutils/unixccompiler.py b/distutils/unixccompiler.py
index 467aa662..4a260654 100644
--- a/distutils/unixccompiler.py
+++ b/distutils/unixccompiler.py
@@ -205,6 +205,10 @@ class UnixCCompiler(CCompiler):
                 ld_args.extend(extra_postargs)
             self.mkpath(os.path.dirname(output_filename))
             try:
+                # If we are building an executable, use the C compiler
+                # given by linker_exe as the linker command,
+                # else use the C compiler + shared options given by
+                # linker_so.
                 linker = (
                     self.linker_exe
                     if target_desc == CCompiler.EXECUTABLE else
@@ -216,10 +220,28 @@ class UnixCCompiler(CCompiler):
                     _, compiler_cxx_ne = _split_env(self.compiler_cxx)
                     _, linker_exe_ne = _split_env(self.linker_exe)
 
+                    # Linker command given by linker_na usually starts with
+                    # with the C compiler given by linker_exe_ne and then
+                    # some options for shared library building if we are
+                    # building a shared library.
+                    # This may not always be true because the user can use
+                    # LDSHARED env variable to override the linker command.
+                    # When building C++ extensions, we need to replace all of
+                    # the C compiler which can be multiple words with the
+                    # C++ compiler.
+                    # To ensure that we are replacing the C compiler, we first
+                    # check that the linker command starts with the C compiler
+                    # and replace that part with the C++ compiler.
                     if len(linker_na) >= len(linker_exe_ne) and \
                             linker_na[:len(linker_exe_ne)] == linker_exe_ne:
-                        linker_na = self.compiler_cxx + \
+                        linker_na = compiler_cxx_ne + \
                             linker_na[len(linker_exe_ne):]
+                    else:
+                        # This occurs if the user has set LDSHARED env variable
+                        # and we do not know how to plug in the C++ compiler
+                        # in this case. Therefore we fallback to the previous
+                        # potentially buggy functionality.
+                        linker_na[0] = compiler_cxx_ne[0]
 
                     linker = env + aix + linker_na
 
-- 
cgit v1.2.1


From 143ba8c746ff5f719a2f8067f5afd04b110eef9c Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 13 Mar 2022 10:11:27 +0000
Subject: Use windows-2019 for tests in GitHub Actions

For the time being let's just use the older version for the GHA host, so
we can keep testing on Windows, instead of skipping it at all.
---
 .github/workflows/main.yml | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index d2979efd..5be824c1 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -24,8 +24,7 @@ jobs:
         platform:
         - ubuntu-latest
         - macos-latest
-        # disable tests on Windows due to pypa/distutils#118
-        # - windows-latest
+        - windows-2019
         include:
         - platform: ubuntu-latest
           python: "3.10"
-- 
cgit v1.2.1


From 4d3b445d5a53bda3424aac273e80e51e92a08b2c Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 13 Mar 2022 14:15:58 +0000
Subject: Fix problem with path objects for Windows

---
 setuptools/discovery.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setuptools/discovery.py b/setuptools/discovery.py
index 80e2a23b..1d1b3814 100644
--- a/setuptools/discovery.py
+++ b/setuptools/discovery.py
@@ -88,7 +88,7 @@ class _Finder:
         exclude = exclude or cls.DEFAULT_EXCLUDE
         return list(
             cls._find_iter(
-                convert_path(where),
+                convert_path(str(where)),
                 cls._build_filter(*cls.ALWAYS_EXCLUDE, *exclude),
                 cls._build_filter(*include),
             )
-- 
cgit v1.2.1


From 04f6a194c4acf810d4f3ca3c901d6cc1cc955db1 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sun, 13 Mar 2022 13:04:04 -0400
Subject: Vendor nspektr. Utilize it in Distribution._install_dependencies.

---
 .../_vendor/nspektr-0.3.0.dist-info/INSTALLER      |   1 +
 setuptools/_vendor/nspektr-0.3.0.dist-info/LICENSE |  19 +++
 .../_vendor/nspektr-0.3.0.dist-info/METADATA       |  57 ++++++++
 setuptools/_vendor/nspektr-0.3.0.dist-info/RECORD  |  11 ++
 .../_vendor/nspektr-0.3.0.dist-info/REQUESTED      |   0
 setuptools/_vendor/nspektr-0.3.0.dist-info/WHEEL   |   5 +
 .../_vendor/nspektr-0.3.0.dist-info/top_level.txt  |   1 +
 setuptools/_vendor/nspektr/__init__.py             | 145 +++++++++++++++++++++
 setuptools/_vendor/nspektr/_compat.py              |  21 +++
 setuptools/_vendor/vendored.txt                    |   1 +
 setuptools/dist.py                                 |  20 +--
 setuptools/extern/__init__.py                      |   2 +-
 tools/vendored.py                                  |  11 ++
 13 files changed, 276 insertions(+), 18 deletions(-)
 create mode 100644 setuptools/_vendor/nspektr-0.3.0.dist-info/INSTALLER
 create mode 100644 setuptools/_vendor/nspektr-0.3.0.dist-info/LICENSE
 create mode 100644 setuptools/_vendor/nspektr-0.3.0.dist-info/METADATA
 create mode 100644 setuptools/_vendor/nspektr-0.3.0.dist-info/RECORD
 create mode 100644 setuptools/_vendor/nspektr-0.3.0.dist-info/REQUESTED
 create mode 100644 setuptools/_vendor/nspektr-0.3.0.dist-info/WHEEL
 create mode 100644 setuptools/_vendor/nspektr-0.3.0.dist-info/top_level.txt
 create mode 100644 setuptools/_vendor/nspektr/__init__.py
 create mode 100644 setuptools/_vendor/nspektr/_compat.py

diff --git a/setuptools/_vendor/nspektr-0.3.0.dist-info/INSTALLER b/setuptools/_vendor/nspektr-0.3.0.dist-info/INSTALLER
new file mode 100644
index 00000000..a1b589e3
--- /dev/null
+++ b/setuptools/_vendor/nspektr-0.3.0.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/setuptools/_vendor/nspektr-0.3.0.dist-info/LICENSE b/setuptools/_vendor/nspektr-0.3.0.dist-info/LICENSE
new file mode 100644
index 00000000..353924be
--- /dev/null
+++ b/setuptools/_vendor/nspektr-0.3.0.dist-info/LICENSE
@@ -0,0 +1,19 @@
+Copyright Jason R. Coombs
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
diff --git a/setuptools/_vendor/nspektr-0.3.0.dist-info/METADATA b/setuptools/_vendor/nspektr-0.3.0.dist-info/METADATA
new file mode 100644
index 00000000..aadc3749
--- /dev/null
+++ b/setuptools/_vendor/nspektr-0.3.0.dist-info/METADATA
@@ -0,0 +1,57 @@
+Metadata-Version: 2.1
+Name: nspektr
+Version: 0.3.0
+Summary: package inspector
+Home-page: https://github.com/jaraco/nspektr
+Author: Jason R. Coombs
+Author-email: jaraco@jaraco.com
+License: UNKNOWN
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3 :: Only
+Requires-Python: >=3.7
+License-File: LICENSE
+Requires-Dist: jaraco.context
+Requires-Dist: jaraco.functools
+Requires-Dist: more-itertools
+Requires-Dist: packaging
+Requires-Dist: importlib-metadata (>=3.6) ; python_version < "3.10"
+Provides-Extra: docs
+Requires-Dist: sphinx ; extra == 'docs'
+Requires-Dist: jaraco.packaging (>=9) ; extra == 'docs'
+Requires-Dist: rst.linker (>=1.9) ; extra == 'docs'
+Provides-Extra: testing
+Requires-Dist: pytest (>=6) ; extra == 'testing'
+Requires-Dist: pytest-checkdocs (>=2.4) ; extra == 'testing'
+Requires-Dist: pytest-flake8 ; extra == 'testing'
+Requires-Dist: pytest-cov ; extra == 'testing'
+Requires-Dist: pytest-enabler (>=1.0.1) ; extra == 'testing'
+Requires-Dist: pytest-black (>=0.3.7) ; (platform_python_implementation != "PyPy") and extra == 'testing'
+Requires-Dist: pytest-mypy (>=0.9.1) ; (platform_python_implementation != "PyPy") and extra == 'testing'
+
+.. image:: https://img.shields.io/pypi/v/nspektr.svg
+   :target: `PyPI link`_
+
+.. image:: https://img.shields.io/pypi/pyversions/nspektr.svg
+   :target: `PyPI link`_
+
+.. _PyPI link: https://pypi.org/project/nspektr
+
+.. image:: https://github.com/jaraco/nspektr/workflows/tests/badge.svg
+   :target: https://github.com/jaraco/nspektr/actions?query=workflow%3A%22tests%22
+   :alt: tests
+
+.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
+   :target: https://github.com/psf/black
+   :alt: Code style: Black
+
+.. .. image:: https://readthedocs.org/projects/skeleton/badge/?version=latest
+..    :target: https://skeleton.readthedocs.io/en/latest/?badge=latest
+
+.. image:: https://img.shields.io/badge/skeleton-2022-informational
+   :target: https://blog.jaraco.com/skeleton
+
+
diff --git a/setuptools/_vendor/nspektr-0.3.0.dist-info/RECORD b/setuptools/_vendor/nspektr-0.3.0.dist-info/RECORD
new file mode 100644
index 00000000..5e5de5eb
--- /dev/null
+++ b/setuptools/_vendor/nspektr-0.3.0.dist-info/RECORD
@@ -0,0 +1,11 @@
+nspektr-0.3.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+nspektr-0.3.0.dist-info/LICENSE,sha256=2z8CRrH5J48VhFuZ_sR4uLUG63ZIeZNyL4xuJUKF-vg,1050
+nspektr-0.3.0.dist-info/METADATA,sha256=X0stV4vwFBDBxvzhBl4kAHVdGWPIjEitqAuTJItcQH0,2162
+nspektr-0.3.0.dist-info/RECORD,,
+nspektr-0.3.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+nspektr-0.3.0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
+nspektr-0.3.0.dist-info/top_level.txt,sha256=uEA20Ixo04XS3wOIt5-Jk5ZuMkBrtlleFipRr8Y1SjQ,8
+nspektr/__init__.py,sha256=d6-d-ZlGAQQP-MEi_NZMiyn2vLbq8Hw3HxICgm3X0Q8,3949
+nspektr/__pycache__/__init__.cpython-310.pyc,,
+nspektr/__pycache__/_compat.cpython-310.pyc,,
+nspektr/_compat.py,sha256=2QoozYhuhgow_NMUATmhoM-yppBV3jiZYQgdiP-ww0s,582
diff --git a/setuptools/_vendor/nspektr-0.3.0.dist-info/REQUESTED b/setuptools/_vendor/nspektr-0.3.0.dist-info/REQUESTED
new file mode 100644
index 00000000..e69de29b
diff --git a/setuptools/_vendor/nspektr-0.3.0.dist-info/WHEEL b/setuptools/_vendor/nspektr-0.3.0.dist-info/WHEEL
new file mode 100644
index 00000000..becc9a66
--- /dev/null
+++ b/setuptools/_vendor/nspektr-0.3.0.dist-info/WHEEL
@@ -0,0 +1,5 @@
+Wheel-Version: 1.0
+Generator: bdist_wheel (0.37.1)
+Root-Is-Purelib: true
+Tag: py3-none-any
+
diff --git a/setuptools/_vendor/nspektr-0.3.0.dist-info/top_level.txt b/setuptools/_vendor/nspektr-0.3.0.dist-info/top_level.txt
new file mode 100644
index 00000000..b10ef50a
--- /dev/null
+++ b/setuptools/_vendor/nspektr-0.3.0.dist-info/top_level.txt
@@ -0,0 +1 @@
+nspektr
diff --git a/setuptools/_vendor/nspektr/__init__.py b/setuptools/_vendor/nspektr/__init__.py
new file mode 100644
index 00000000..938bbdb9
--- /dev/null
+++ b/setuptools/_vendor/nspektr/__init__.py
@@ -0,0 +1,145 @@
+import itertools
+import functools
+import contextlib
+
+from setuptools.extern.packaging.requirements import Requirement
+from setuptools.extern.packaging.version import Version
+from setuptools.extern.more_itertools import always_iterable
+from setuptools.extern.jaraco.context import suppress
+from setuptools.extern.jaraco.functools import apply
+
+from ._compat import metadata, repair_extras
+
+
+def resolve(req: Requirement) -> metadata.Distribution:
+    """
+    Resolve the requirement to its distribution.
+
+    Ignore exception detail for Python 3.9 compatibility.
+
+    >>> resolve(Requirement('pytest<3'))  # doctest: +IGNORE_EXCEPTION_DETAIL
+    Traceback (most recent call last):
+    ...
+    importlib.metadata.PackageNotFoundError: No package metadata was found for pytest<3
+    """
+    dist = metadata.distribution(req.name)
+    if not req.specifier.contains(Version(dist.version), prereleases=True):
+        raise metadata.PackageNotFoundError(str(req))
+    dist.extras = req.extras  # type: ignore
+    return dist
+
+
+@apply(bool)
+@suppress(metadata.PackageNotFoundError)
+def is_satisfied(req: Requirement):
+    return resolve(req)
+
+
+unsatisfied = functools.partial(itertools.filterfalse, is_satisfied)
+
+
+class NullMarker:
+    @classmethod
+    def wrap(cls, req: Requirement):
+        return req.marker or cls()
+
+    def evaluate(self, *args, **kwargs):
+        return True
+
+
+def find_direct_dependencies(dist, extras=None):
+    """
+    Find direct, declared dependencies for dist.
+    """
+    simple = (
+        req
+        for req in map(Requirement, always_iterable(dist.requires))
+        if NullMarker.wrap(req).evaluate(dict(extra=None))
+    )
+    extra_deps = (
+        req
+        for req in map(Requirement, always_iterable(dist.requires))
+        for extra in always_iterable(getattr(dist, 'extras', extras))
+        if NullMarker.wrap(req).evaluate(dict(extra=extra))
+    )
+    return itertools.chain(simple, extra_deps)
+
+
+def traverse(items, visit):
+    """
+    Given an iterable of items, traverse the items.
+
+    For each item, visit is called to return any additional items
+    to include in the traversal.
+    """
+    while True:
+        try:
+            item = next(items)
+        except StopIteration:
+            return
+        yield item
+        items = itertools.chain(items, visit(item))
+
+
+def find_req_dependencies(req):
+    with contextlib.suppress(metadata.PackageNotFoundError):
+        dist = resolve(req)
+        yield from find_direct_dependencies(dist)
+
+
+def find_dependencies(dist, extras=None):
+    """
+    Find all reachable dependencies for dist.
+
+    dist is an importlib.metadata.Distribution (or similar).
+    TODO: create a suitable protocol for type hint.
+
+    >>> deps = find_dependencies(resolve(Requirement('nspektr')))
+    >>> all(isinstance(dep, Requirement) for dep in deps)
+    True
+    >>> not any('pytest' in str(dep) for dep in deps)
+    True
+    >>> test_deps = find_dependencies(resolve(Requirement('nspektr[testing]')))
+    >>> any('pytest' in str(dep) for dep in test_deps)
+    True
+    """
+
+    def visit(req, seen=set()):
+        if req in seen:
+            return ()
+        seen.add(req)
+        return find_req_dependencies(req)
+
+    return traverse(find_direct_dependencies(dist, extras), visit)
+
+
+class Unresolved(Exception):
+    def __iter__(self):
+        return iter(self.args[0])
+
+
+def missing(ep):
+    """
+    Generate the unresolved dependencies (if any) of ep.
+    """
+    return unsatisfied(find_dependencies(ep.dist, repair_extras(ep.extras)))
+
+
+def check(ep):
+    """
+    >>> ep, = metadata.entry_points(group='console_scripts', name='pip')
+    >>> check(ep)
+    >>> dist = metadata.distribution('nspektr')
+
+    Since 'docs' extras are not installed, requesting them should fail.
+
+    >>> ep = metadata.EntryPoint(
+    ...     group=None, name=None, value='nspektr [docs]')._for(dist)
+    >>> check(ep)
+    Traceback (most recent call last):
+    ...
+    nspektr.Unresolved: [...]
+    """
+    missed = list(missing(ep))
+    if missed:
+        raise Unresolved(missed)
diff --git a/setuptools/_vendor/nspektr/_compat.py b/setuptools/_vendor/nspektr/_compat.py
new file mode 100644
index 00000000..3278379a
--- /dev/null
+++ b/setuptools/_vendor/nspektr/_compat.py
@@ -0,0 +1,21 @@
+import contextlib
+import sys
+
+
+if sys.version_info >= (3, 10):
+    import importlib.metadata as metadata
+else:
+    import setuptools.extern.importlib_metadata as metadata  # type: ignore # noqa: F401
+
+
+def repair_extras(extras):
+    """
+    Repair extras that appear as match objects.
+
+    python/importlib_metadata#369 revealed a flaw in the EntryPoint
+    implementation. This function wraps the extras to ensure
+    they are proper strings even on older implementations.
+    """
+    with contextlib.suppress(AttributeError):
+        return list(item.group(0) for item in extras)
+    return extras
diff --git a/setuptools/_vendor/vendored.txt b/setuptools/_vendor/vendored.txt
index db24b402..4320b352 100644
--- a/setuptools/_vendor/vendored.txt
+++ b/setuptools/_vendor/vendored.txt
@@ -5,6 +5,7 @@ more_itertools==8.8.0
 jaraco.text==3.7.0
 importlib_resources==5.4.0
 importlib_metadata==4.11.1
+nspektr==0.3.0
 # required for importlib_metadata on older Pythons
 typing_extensions==4.0.1
 # required for importlib_resources and _metadata on older Pythons
diff --git a/setuptools/dist.py b/setuptools/dist.py
index e825785e..fcce7560 100644
--- a/setuptools/dist.py
+++ b/setuptools/dist.py
@@ -28,7 +28,8 @@ from distutils.util import rfc822_escape
 
 from setuptools.extern import packaging
 from setuptools.extern import ordered_set
-from setuptools.extern.more_itertools import unique_everseen, always_iterable
+from setuptools.extern.more_itertools import unique_everseen
+from setuptools.extern import nspektr
 
 from ._importlib import metadata
 
@@ -876,25 +877,10 @@ class Distribution(_Distribution):
         Given an entry point, ensure that any declared extras for
         its distribution are installed.
         """
-        reqs = {
-            req
-            for req in map(requirements.Requirement, always_iterable(ep.dist.requires))
-            for extra in ep.extras
-            if extra in req.extras
-        }
-        missing = itertools.filterfalse(self._is_installed, reqs)
-        for req in missing:
+        for req in nspektr.missing(ep):
             # fetch_build_egg expects pkg_resources.Requirement
             self.fetch_build_egg(pkg_resources.Requirement(str(req)))
 
-    def _is_installed(self, req):
-        try:
-            dist = metadata.distribution(req.name)
-        except metadata.PackageNotFoundError:
-            return False
-        found_ver = packaging.version.Version(dist.version())
-        return found_ver in req.specifier
-
     def get_egg_cache_dir(self):
         egg_cache_dir = os.path.join(os.curdir, '.eggs')
         if not os.path.exists(egg_cache_dir):
diff --git a/setuptools/extern/__init__.py b/setuptools/extern/__init__.py
index 98235a4b..7907fafc 100644
--- a/setuptools/extern/__init__.py
+++ b/setuptools/extern/__init__.py
@@ -71,6 +71,6 @@ class VendorImporter:
 
 names = (
     'packaging', 'pyparsing', 'ordered_set', 'more_itertools', 'importlib_metadata',
-    'zipp', 'importlib_resources', 'jaraco', 'typing_extensions',
+    'zipp', 'importlib_resources', 'jaraco', 'typing_extensions', 'nspektr',
 )
 VendorImporter(__name__, names, 'setuptools._vendor').install()
diff --git a/tools/vendored.py b/tools/vendored.py
index 8a122ad7..cd15adbf 100644
--- a/tools/vendored.py
+++ b/tools/vendored.py
@@ -89,6 +89,16 @@ def rewrite_more_itertools(pkg_files: Path):
     more_file.write_text(text)
 
 
+def rewrite_nspektr(pkg_files: Path, new_root):
+    for file in pkg_files.glob('*.py'):
+        text = file.read_text()
+        text = re.sub(r' (more_itertools)', rf' {new_root}.\1', text)
+        text = re.sub(r' (jaraco\.\w+)', rf' {new_root}.\1', text)
+        text = re.sub(r' (packaging)', rf' {new_root}.\1', text)
+        text = re.sub(r' (importlib_metadata)', rf' {new_root}.\1', text)
+        file.write_text(text)
+
+
 def clean(vendor):
     """
     Remove all files out of the vendor directory except the meta
@@ -133,6 +143,7 @@ def update_setuptools():
     rewrite_importlib_resources(vendor / 'importlib_resources', 'setuptools.extern')
     rewrite_importlib_metadata(vendor / 'importlib_metadata', 'setuptools.extern')
     rewrite_more_itertools(vendor / "more_itertools")
+    rewrite_nspektr(vendor / "nspektr", 'setuptools.extern')
 
 
 __name__ == '__main__' and update_vendored()
-- 
cgit v1.2.1


From 542304c1c9accea1b8c04d3afc18b24bddbeb151 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sun, 13 Mar 2022 13:14:46 -0400
Subject: Update changelog.

---
 changelog.d/3170.change.rst | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/3170.change.rst

diff --git a/changelog.d/3170.change.rst b/changelog.d/3170.change.rst
new file mode 100644
index 00000000..8e356ca3
--- /dev/null
+++ b/changelog.d/3170.change.rst
@@ -0,0 +1 @@
+Adopt nspektr (vendored) to implement Distribution._install_dependencies.
-- 
cgit v1.2.1


From e7b99faf0add4e9bbc1970a975c371657a125e70 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sun, 13 Mar 2022 13:15:38 -0400
Subject: =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins=20(delint).?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 setuptools/dist.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setuptools/dist.py b/setuptools/dist.py
index fcce7560..b55996f0 100644
--- a/setuptools/dist.py
+++ b/setuptools/dist.py
@@ -41,7 +41,7 @@ from setuptools import windows_support
 from setuptools.monkey import get_unpatched
 from setuptools.config import parse_configuration
 import pkg_resources
-from setuptools.extern.packaging import version, requirements
+from setuptools.extern.packaging import version
 from . import _reqs
 from . import _entry_points
 
-- 
cgit v1.2.1


From a0f9662e537fdcc074a7801b11495747f8c22601 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 11 Mar 2022 23:58:47 +0000
Subject: Attempt to re-enable Windows tests

According to a comment in pypa/distutils#118 this problem might be
solved by allowing tox to pass some environment variables.
---
 .github/workflows/main.yml | 3 +--
 tox.ini                    | 4 ++++
 2 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index d2979efd..c680fb36 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -24,8 +24,7 @@ jobs:
         platform:
         - ubuntu-latest
         - macos-latest
-        # disable tests on Windows due to pypa/distutils#118
-        # - windows-latest
+        - windows-latest
         include:
         - platform: ubuntu-latest
           python: "3.10"
diff --git a/tox.ini b/tox.ini
index a56ea24b..5fb7cb5b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -20,6 +20,8 @@ passenv =
 	windir  # required for test_pkg_resources
 	# honor git config in pytest-perf
 	HOME
+	PROGRAMFILES
+	PROGRAMFILES(x86)
 
 [testenv:integration]
 deps = {[testenv]deps}
@@ -27,6 +29,8 @@ extras = testing-integration
 passenv =
 	{[testenv]passenv}
 	DOWNLOAD_PATH
+	PROGRAMFILES
+	PROGRAMFILES(x86)
 setenv =
     PROJECT_ROOT = {toxinidir}
 commands =
-- 
cgit v1.2.1


From 8f3cdf705cbf5ba29238c9e5e900727d488bf463 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 13 Mar 2022 10:11:27 +0000
Subject: Use windows-2019 for tests in GitHub Actions

For the time being let's just use the older version for the GHA host, so
we can keep testing on Windows, instead of skipping it at all.
---
 .github/workflows/main.yml | 2 +-
 tox.ini                    | 4 ----
 2 files changed, 1 insertion(+), 5 deletions(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index c680fb36..5be824c1 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -24,7 +24,7 @@ jobs:
         platform:
         - ubuntu-latest
         - macos-latest
-        - windows-latest
+        - windows-2019
         include:
         - platform: ubuntu-latest
           python: "3.10"
diff --git a/tox.ini b/tox.ini
index 5fb7cb5b..a56ea24b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -20,8 +20,6 @@ passenv =
 	windir  # required for test_pkg_resources
 	# honor git config in pytest-perf
 	HOME
-	PROGRAMFILES
-	PROGRAMFILES(x86)
 
 [testenv:integration]
 deps = {[testenv]deps}
@@ -29,8 +27,6 @@ extras = testing-integration
 passenv =
 	{[testenv]passenv}
 	DOWNLOAD_PATH
-	PROGRAMFILES
-	PROGRAMFILES(x86)
 setenv =
     PROJECT_ROOT = {toxinidir}
 commands =
-- 
cgit v1.2.1


From 722e1fd0e50ad69fbdd4d0373fc5bd4d75a1d845 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 13 Mar 2022 20:55:27 +0000
Subject: [Docs] Improve documentation about migration from distutils

---
 docs/deprecated/distutils-legacy.rst | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/docs/deprecated/distutils-legacy.rst b/docs/deprecated/distutils-legacy.rst
index 148dc259..cdc4e39b 100644
--- a/docs/deprecated/distutils-legacy.rst
+++ b/docs/deprecated/distutils-legacy.rst
@@ -3,11 +3,10 @@ Porting from Distutils
 
 Setuptools and the PyPA have a `stated goal `_ to make Setuptools the reference API for distutils.
 
-Since the 49.1.2 release, Setuptools includes a local, vendored copy of distutils (from late copies of CPython) that is disabled by default. To enable the use of this copy of distutils when invoking setuptools, set the enviroment variable:
+Since the 49.1.2 release, Setuptools includes a local, vendored copy of distutils (from late copies of CPython) that is enabled by default. To disable the use of this copy of distutils when invoking setuptools, set the enviroment variable:
 
-    SETUPTOOLS_USE_DISTUTILS=local
+    SETUPTOOLS_USE_DISTUTILS=stdlib
 
-This behavior is planned to become the default.
 
 Prefer Setuptools
 -----------------
@@ -20,12 +19,15 @@ As Distutils is deprecated, any usage of functions or objects from distutils is
 
 ``distutils.command.{build_clib,build_ext,build_py,sdist}`` → ``setuptools.command.*``
 
-``distutils.log`` → (no replacement yet)
+``distutils.log`` → :mod:`logging` (standard library)
 
 ``distutils.version.*`` → ``packaging.version.*``
 
 ``distutils.errors.*`` → ``setuptools.errors.*`` [#errors]_
 
+
+Migration is also provided by :pep:`632#migration-advice`.
+
 If a project relies on uses of ``distutils`` that do not have a suitable replacement above, please search the `Setuptools issue tracker `_ and file a request, describing the use-case so that Setuptools' maintainers can investigate. Please provide enough detail to help the maintainers understand how distutils is used, what value it provides, and why that behavior should be supported.
 
 
-- 
cgit v1.2.1


From c522737c9488472d30ced2a11739e607a6f8ddff Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 13 Mar 2022 21:00:22 +0000
Subject: Fix version of setuptools for default local distutils

---
 docs/deprecated/distutils-legacy.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/deprecated/distutils-legacy.rst b/docs/deprecated/distutils-legacy.rst
index cdc4e39b..f600c52b 100644
--- a/docs/deprecated/distutils-legacy.rst
+++ b/docs/deprecated/distutils-legacy.rst
@@ -3,7 +3,7 @@ Porting from Distutils
 
 Setuptools and the PyPA have a `stated goal `_ to make Setuptools the reference API for distutils.
 
-Since the 49.1.2 release, Setuptools includes a local, vendored copy of distutils (from late copies of CPython) that is enabled by default. To disable the use of this copy of distutils when invoking setuptools, set the enviroment variable:
+Since the 60.0.0 release, Setuptools includes a local, vendored copy of distutils (from late copies of CPython) that is enabled by default. To disable the use of this copy of distutils when invoking setuptools, set the enviroment variable:
 
     SETUPTOOLS_USE_DISTUTILS=stdlib
 
-- 
cgit v1.2.1


From 82141a2e22141cefe93f8b686ee7489ad7461a71 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 13 Mar 2022 21:04:12 +0000
Subject: Fix PEP 632 link display

---
 docs/deprecated/distutils-legacy.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/deprecated/distutils-legacy.rst b/docs/deprecated/distutils-legacy.rst
index f600c52b..9987013a 100644
--- a/docs/deprecated/distutils-legacy.rst
+++ b/docs/deprecated/distutils-legacy.rst
@@ -26,7 +26,7 @@ As Distutils is deprecated, any usage of functions or objects from distutils is
 ``distutils.errors.*`` → ``setuptools.errors.*`` [#errors]_
 
 
-Migration is also provided by :pep:`632#migration-advice`.
+Migration is also provided by :pep:`PEP 632 <632#migration-advice>`.
 
 If a project relies on uses of ``distutils`` that do not have a suitable replacement above, please search the `Setuptools issue tracker `_ and file a request, describing the use-case so that Setuptools' maintainers can investigate. Please provide enough detail to help the maintainers understand how distutils is used, what value it provides, and why that behavior should be supported.
 
-- 
cgit v1.2.1


From 19609c020dc01146658d80a9ba13ce4369f6c483 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 13 Mar 2022 21:15:00 +0000
Subject: Link packaging

---
 docs/conf.py                         | 1 +
 docs/deprecated/distutils-legacy.rst | 4 ++--
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/docs/conf.py b/docs/conf.py
index da4d9f33..4c00d46f 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -200,6 +200,7 @@ favicons = [
 
 intersphinx_mapping['pip'] = 'https://pip.pypa.io/en/latest', None
 intersphinx_mapping['PyPUG'] = ('https://packaging.python.org/en/latest/', None)
+intersphinx_mapping['packaging'] = ('https://packaging.pypa.io/en/latest/', None)
 intersphinx_mapping['importlib-resources'] = (
     'https://importlib-resources.readthedocs.io/en/latest', None
 )
diff --git a/docs/deprecated/distutils-legacy.rst b/docs/deprecated/distutils-legacy.rst
index 9987013a..e73cdff5 100644
--- a/docs/deprecated/distutils-legacy.rst
+++ b/docs/deprecated/distutils-legacy.rst
@@ -21,12 +21,12 @@ As Distutils is deprecated, any usage of functions or objects from distutils is
 
 ``distutils.log`` → :mod:`logging` (standard library)
 
-``distutils.version.*`` → ``packaging.version.*``
+``distutils.version.*`` → :doc:`packaging.version.* `
 
 ``distutils.errors.*`` → ``setuptools.errors.*`` [#errors]_
 
 
-Migration is also provided by :pep:`PEP 632 <632#migration-advice>`.
+Migration advice is also provided by :pep:`PEP 632 <632#migration-advice>`.
 
 If a project relies on uses of ``distutils`` that do not have a suitable replacement above, please search the `Setuptools issue tracker `_ and file a request, describing the use-case so that Setuptools' maintainers can investigate. Please provide enough detail to help the maintainers understand how distutils is used, what value it provides, and why that behavior should be supported.
 
-- 
cgit v1.2.1


From fb258ed194228a95a3043c19abe215bd8c23ddab Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 14 Mar 2022 01:12:06 +0000
Subject: Exclude PyPy+Windows from test matrix

---
 .github/workflows/main.yml | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 5be824c1..bc5b1e4d 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -29,6 +29,11 @@ jobs:
         - platform: ubuntu-latest
           python: "3.10"
           distutils: stdlib
+        exclude:
+        # The combination of PyPy+Windows+pytest-xdist+ProcessPoolExecutor is flaky/problematic
+        - platform: windows-2019
+          python: pypy-3.7
+          distutils: local
     runs-on: ${{ matrix.platform }}
     env:
       SETUPTOOLS_USE_DISTUTILS: ${{ matrix.distutils }}
-- 
cgit v1.2.1


From 8afae7f683766e7e716a226a43ccc95d69675851 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 14 Mar 2022 01:18:43 +0000
Subject: Just skip the most problematic test for PyPy on Windows

---
 .github/workflows/main.yml          | 5 -----
 setuptools/tests/test_build_meta.py | 7 +++++++
 2 files changed, 7 insertions(+), 5 deletions(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index bc5b1e4d..5be824c1 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -29,11 +29,6 @@ jobs:
         - platform: ubuntu-latest
           python: "3.10"
           distutils: stdlib
-        exclude:
-        # The combination of PyPy+Windows+pytest-xdist+ProcessPoolExecutor is flaky/problematic
-        - platform: windows-2019
-          python: pypy-3.7
-          distutils: local
     runs-on: ${{ matrix.platform }}
     env:
       SETUPTOOLS_USE_DISTUTILS: ${{ matrix.distutils }}
diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py
index eb43fe9b..c4cdda03 100644
--- a/setuptools/tests/test_build_meta.py
+++ b/setuptools/tests/test_build_meta.py
@@ -18,6 +18,13 @@ TIMEOUT = int(os.getenv("TIMEOUT_BACKEND_TEST", "180"))  # in seconds
 IS_PYPY = '__pypy__' in sys.builtin_module_names
 
 
+pytestmark = pytest.mark.skipif(
+    sys.platform == "win32" and IS_PYPY,
+    reason="The combination of PyPy + Windows + pytest-xdist + ProcessPoolExecutor "
+    "is flaky and problematic"
+)
+
+
 class BuildBackendBase:
     def __init__(self, cwd='.', env={}, backend_name='setuptools.build_meta'):
         self.cwd = cwd
-- 
cgit v1.2.1


From 8122993a053444e9a68cc007ea58a417b5ae44a9 Mon Sep 17 00:00:00 2001
From: Josip Delic 
Date: Fri, 11 Mar 2022 12:07:15 +0100
Subject: Add test for zipefile mode

---
 setuptools/tests/test_wheel.py | 84 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 84 insertions(+)

diff --git a/setuptools/tests/test_wheel.py b/setuptools/tests/test_wheel.py
index a15c3a46..c83abb62 100644
--- a/setuptools/tests/test_wheel.py
+++ b/setuptools/tests/test_wheel.py
@@ -6,6 +6,8 @@
 from distutils.sysconfig import get_config_var
 from distutils.util import get_platform
 import contextlib
+import pathlib
+import stat
 import glob
 import inspect
 import os
@@ -614,3 +616,85 @@ def test_wheel_is_compatible(monkeypatch):
     monkeypatch.setattr('setuptools.wheel.sys_tags', sys_tags)
     assert Wheel(
         'onnxruntime-0.1.2-cp36-cp36m-manylinux1_x86_64.whl').is_compatible()
+
+
+def test_wheel_mode():
+    @contextlib.contextmanager
+    def build_wheel(extra_file_defs=None, **kwargs):
+        file_defs = {
+            'setup.py': (DALS(
+                '''
+                # -*- coding: utf-8 -*-
+                from setuptools import setup
+                import setuptools
+                setup(**%r)
+                '''
+            ) % kwargs).encode('utf-8'),
+        }
+        if extra_file_defs:
+            file_defs.update(extra_file_defs)
+        with tempdir() as source_dir:
+            path.build(file_defs, source_dir)
+            runsh = pathlib.Path(source_dir) / "script.sh"
+            os.chmod(runsh, 0o777)
+            subprocess.check_call((sys.executable, 'setup.py',
+                                   '-q', 'bdist_wheel'), cwd=source_dir)
+            yield glob.glob(os.path.join(source_dir, 'dist', '*.whl'))[0]
+
+    params = dict(
+        id='script',
+        file_defs={
+            'script.py': DALS(
+                '''
+                #/usr/bin/python
+                print('hello world!')
+                '''
+            ),
+            'script.sh': DALS(
+                '''
+                #/bin/sh
+                echo 'hello world!'
+                '''
+            ),
+        },
+        setup_kwargs=dict(
+            scripts=['script.py', 'script.sh'],
+        ),
+        install_tree=flatten_tree({
+            'foo-1.0-py{py_version}.egg': {
+                'EGG-INFO': [
+                    'PKG-INFO',
+                    'RECORD',
+                    'WHEEL',
+                    'top_level.txt',
+                    {'scripts': [
+                        'script.py',
+                        'script.sh'
+                    ]}
+
+                ]
+            }
+        })
+    )
+
+    project_name = params.get('name', 'foo')
+    version = params.get('version', '1.0')
+    install_tree = params.get('install_tree')
+    file_defs = params.get('file_defs', {})
+    setup_kwargs = params.get('setup_kwargs', {})
+
+    with build_wheel(
+        name=project_name,
+        version=version,
+        install_requires=[],
+        extras_require={},
+        extra_file_defs=file_defs,
+        **setup_kwargs
+    ) as filename, tempdir() as install_dir:
+        _check_wheel_install(filename, install_dir,
+                             install_tree, project_name,
+                             version, None)
+        w = Wheel(filename)
+        script_sh = pathlib.Path(install_dir) / w.egg_name() / "EGG-INFO" / "scripts" / "script.sh"
+        assert script_sh.exists()
+        assert oct(stat.S_IMODE(script_sh.stat().st_mode)) == "0o777"
-- 
cgit v1.2.1


From 8aa366d568b67339f04ca538c6fb11aad6ad1c91 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 14 Mar 2022 10:46:17 +0000
Subject: Update setuptools/tests/test_wheel.py

Attempt to fix flake8
---
 setuptools/tests/test_wheel.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/setuptools/tests/test_wheel.py b/setuptools/tests/test_wheel.py
index c83abb62..293e8262 100644
--- a/setuptools/tests/test_wheel.py
+++ b/setuptools/tests/test_wheel.py
@@ -695,6 +695,7 @@ def test_wheel_mode():
                              install_tree, project_name,
                              version, None)
         w = Wheel(filename)
-        script_sh = pathlib.Path(install_dir) / w.egg_name() / "EGG-INFO" / "scripts" / "script.sh"
+        base = pathlib.Path(install_dir) / w.egg_name()
+        script_sh = base / "EGG-INFO" / "scripts" / "script.sh"
         assert script_sh.exists()
         assert oct(stat.S_IMODE(script_sh.stat().st_mode)) == "0o777"
-- 
cgit v1.2.1


From d8fd1c29384bdf0cfab2c030910cee1cf19fc0af Mon Sep 17 00:00:00 2001
From: Josip Delic 
Date: Thu, 10 Mar 2022 18:05:05 +0100
Subject: Fix ZipFile mode not set

---
 setuptools/wheel.py | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/setuptools/wheel.py b/setuptools/wheel.py
index 9819e8b9..6e8cfa98 100644
--- a/setuptools/wheel.py
+++ b/setuptools/wheel.py
@@ -27,6 +27,20 @@ NAMESPACE_PACKAGE_INIT = \
     "__import__('pkg_resources').declare_namespace(__name__)\n"
 
 
+class ZipFilePreserveMode(zipfile.ZipFile):
+    """ Extended ZipFile class to preserve file mode """
+    def _extract_member(self, member, targetpath, pwd):
+        if not isinstance(member, zipfile.ZipInfo):
+            member = self.getinfo(member)
+
+        targetpath = super()._extract_member(member, targetpath, pwd)
+
+        attr = member.external_attr >> 16
+        if attr != 0:
+            os.chmod(targetpath, attr)
+        return targetpath
+
+
 def unpack(src_dir, dst_dir):
     '''Move everything under `src_dir` to `dst_dir`, and delete the former.'''
     for dirpath, dirnames, filenames in os.walk(src_dir):
@@ -91,7 +105,7 @@ class Wheel:
 
     def install_as_egg(self, destination_eggdir):
         '''Install wheel as an egg directory.'''
-        with zipfile.ZipFile(self.filename) as zf:
+        with ZipFilePreserveMode(self.filename) as zf:
             self._install_as_egg(destination_eggdir, zf)
 
     def _install_as_egg(self, destination_eggdir, zf):
-- 
cgit v1.2.1


From 069735fa1e2cfc8161474f3b23bf19dacf6c51ca Mon Sep 17 00:00:00 2001
From: Josip Delic 
Date: Mon, 14 Mar 2022 17:37:45 +0100
Subject: Deactivate tests on windows

---
 setuptools/tests/test_wheel.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/setuptools/tests/test_wheel.py b/setuptools/tests/test_wheel.py
index 293e8262..183c2e30 100644
--- a/setuptools/tests/test_wheel.py
+++ b/setuptools/tests/test_wheel.py
@@ -618,6 +618,7 @@ def test_wheel_is_compatible(monkeypatch):
         'onnxruntime-0.1.2-cp36-cp36m-manylinux1_x86_64.whl').is_compatible()
 
 
+@pytest.mark.skipif(sys.platform == 'win32', reason='non-Windows only')
 def test_wheel_mode():
     @contextlib.contextmanager
     def build_wheel(extra_file_defs=None, **kwargs):
-- 
cgit v1.2.1


From 4882320b58c36a46532559894a9bd943b00adf0f Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 14 Mar 2022 17:13:37 +0000
Subject: Extract reusable _unpack_zipfile_obj from archive_utils

---
 setuptools/archive_util.py | 50 +++++++++++++++++++++++++++-------------------
 1 file changed, 29 insertions(+), 21 deletions(-)

diff --git a/setuptools/archive_util.py b/setuptools/archive_util.py
index 73b2db75..d8e10c13 100644
--- a/setuptools/archive_util.py
+++ b/setuptools/archive_util.py
@@ -100,29 +100,37 @@ def unpack_zipfile(filename, extract_dir, progress_filter=default_filter):
         raise UnrecognizedFormat("%s is not a zip file" % (filename,))
 
     with zipfile.ZipFile(filename) as z:
-        for info in z.infolist():
-            name = info.filename
+        _unpack_zipfile_obj(z, extract_dir, progress_filter)
 
-            # don't extract absolute paths or ones with .. in them
-            if name.startswith('/') or '..' in name.split('/'):
-                continue
 
-            target = os.path.join(extract_dir, *name.split('/'))
-            target = progress_filter(name, target)
-            if not target:
-                continue
-            if name.endswith('/'):
-                # directory
-                ensure_directory(target)
-            else:
-                # file
-                ensure_directory(target)
-                data = z.read(info.filename)
-                with open(target, 'wb') as f:
-                    f.write(data)
-            unix_attributes = info.external_attr >> 16
-            if unix_attributes:
-                os.chmod(target, unix_attributes)
+def _unpack_zipfile_obj(zipfile_obj, extract_dir, progress_filter=default_filter):
+    """Internal/private API used by other parts of setuptools.
+    Similar to ``unpack_zipfile``, but receives an already opened :obj:`zipfile.ZipFile`
+    object instead of a filename.
+    """
+    for info in zipfile_obj.infolist():
+        name = info.filename
+
+        # don't extract absolute paths or ones with .. in them
+        if name.startswith('/') or '..' in name.split('/'):
+            continue
+
+        target = os.path.join(extract_dir, *name.split('/'))
+        target = progress_filter(name, target)
+        if not target:
+            continue
+        if name.endswith('/'):
+            # directory
+            ensure_directory(target)
+        else:
+            # file
+            ensure_directory(target)
+            data = zipfile_obj.read(info.filename)
+            with open(target, 'wb') as f:
+                f.write(data)
+        unix_attributes = info.external_attr >> 16
+        if unix_attributes:
+            os.chmod(target, unix_attributes)
 
 
 def _resolve_tar_file_or_dir(tar_obj, tar_member_obj):
-- 
cgit v1.2.1


From 35e034013a39d5bcaea45cb4442cb5c2ffb145d7 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 14 Mar 2022 17:14:13 +0000
Subject: Use function from archive_util instead of overwritting ZipFile

---
 setuptools/wheel.py | 20 +++-----------------
 1 file changed, 3 insertions(+), 17 deletions(-)

diff --git a/setuptools/wheel.py b/setuptools/wheel.py
index 6e8cfa98..0ced0ff2 100644
--- a/setuptools/wheel.py
+++ b/setuptools/wheel.py
@@ -15,6 +15,7 @@ from pkg_resources import parse_version
 from setuptools.extern.packaging.tags import sys_tags
 from setuptools.extern.packaging.utils import canonicalize_name
 from setuptools.command.egg_info import write_requirements
+from setuptools.archive_util import _unpack_zipfile_obj
 
 
 WHEEL_NAME = re.compile(
@@ -27,20 +28,6 @@ NAMESPACE_PACKAGE_INIT = \
     "__import__('pkg_resources').declare_namespace(__name__)\n"
 
 
-class ZipFilePreserveMode(zipfile.ZipFile):
-    """ Extended ZipFile class to preserve file mode """
-    def _extract_member(self, member, targetpath, pwd):
-        if not isinstance(member, zipfile.ZipInfo):
-            member = self.getinfo(member)
-
-        targetpath = super()._extract_member(member, targetpath, pwd)
-
-        attr = member.external_attr >> 16
-        if attr != 0:
-            os.chmod(targetpath, attr)
-        return targetpath
-
-
 def unpack(src_dir, dst_dir):
     '''Move everything under `src_dir` to `dst_dir`, and delete the former.'''
     for dirpath, dirnames, filenames in os.walk(src_dir):
@@ -105,7 +92,7 @@ class Wheel:
 
     def install_as_egg(self, destination_eggdir):
         '''Install wheel as an egg directory.'''
-        with ZipFilePreserveMode(self.filename) as zf:
+        with zipfile.ZipFile(self.filename) as zf:
             self._install_as_egg(destination_eggdir, zf)
 
     def _install_as_egg(self, destination_eggdir, zf):
@@ -135,8 +122,7 @@ class Wheel:
             raise ValueError(
                 'unsupported wheel format version: %s' % wheel_version)
         # Extract to target directory.
-        os.mkdir(destination_eggdir)
-        zf.extractall(destination_eggdir)
+        _unpack_zipfile_obj(zf, destination_eggdir)
         # Convert metadata.
         dist_info = os.path.join(destination_eggdir, dist_info)
         dist = pkg_resources.Distribution.from_location(
-- 
cgit v1.2.1


From c9d369c2dbb9cc3036d33244a0e0064677454fa1 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 14 Mar 2022 17:16:48 +0000
Subject: Run the test on Windows, but don't check the file mode

---
 setuptools/tests/test_wheel.py | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/setuptools/tests/test_wheel.py b/setuptools/tests/test_wheel.py
index 183c2e30..89d65d0b 100644
--- a/setuptools/tests/test_wheel.py
+++ b/setuptools/tests/test_wheel.py
@@ -618,7 +618,6 @@ def test_wheel_is_compatible(monkeypatch):
         'onnxruntime-0.1.2-cp36-cp36m-manylinux1_x86_64.whl').is_compatible()
 
 
-@pytest.mark.skipif(sys.platform == 'win32', reason='non-Windows only')
 def test_wheel_mode():
     @contextlib.contextmanager
     def build_wheel(extra_file_defs=None, **kwargs):
@@ -699,4 +698,6 @@ def test_wheel_mode():
         base = pathlib.Path(install_dir) / w.egg_name()
         script_sh = base / "EGG-INFO" / "scripts" / "script.sh"
         assert script_sh.exists()
-        assert oct(stat.S_IMODE(script_sh.stat().st_mode)) == "0o777"
+        if sys.platform != 'win32':
+            # Editable file mode has no effect on Windows
+            assert oct(stat.S_IMODE(script_sh.stat().st_mode)) == "0o777"
-- 
cgit v1.2.1


From 44fe94c5480d3271f2add326d3c443f757b8521e Mon Sep 17 00:00:00 2001
From: Josip Delic 
Date: Tue, 15 Mar 2022 21:42:59 +0100
Subject: Add in changelog.d a note

---
 changelog.d/3167.change.rst | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/3167.change.rst

diff --git a/changelog.d/3167.change.rst b/changelog.d/3167.change.rst
new file mode 100644
index 00000000..5f44bec4
--- /dev/null
+++ b/changelog.d/3167.change.rst
@@ -0,0 +1 @@
+Honor unix file mode in ZipFile when installing wheel via ``install_as_egg`` -- by :user:`delijati`
-- 
cgit v1.2.1


From 5a0fbfb860b8c380c9a84c0bd977dbba2ed1825c Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 16 Mar 2022 14:55:58 +0000
Subject: Fix towncrier command in tools/finalize

---
 tools/finalize.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/tools/finalize.py b/tools/finalize.py
index e4f65543..5a4df5df 100644
--- a/tools/finalize.py
+++ b/tools/finalize.py
@@ -42,6 +42,7 @@ def update_changelog():
     cmd = [
         sys.executable, '-m',
         'towncrier',
+        'build',
         '--version', get_version(),
         '--yes',
     ]
-- 
cgit v1.2.1


From 02f3821b9af91feadae2326b78a814ac2fbbe520 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 16 Mar 2022 14:56:08 +0000
Subject: =?UTF-8?q?Bump=20version:=2060.9.3=20=E2=86=92=2060.10.0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .bumpversion.cfg            |  2 +-
 CHANGES.rst                 | 36 ++++++++++++++++++++++++++++++++++++
 changelog.d/2971.change.rst |  1 -
 changelog.d/3120.misc.rst   |  4 ----
 changelog.d/3124.misc.rst   |  2 --
 changelog.d/3133.misc.rst   |  1 -
 changelog.d/3137.change.rst |  1 -
 changelog.d/3144.doc.rst    |  1 -
 changelog.d/3147.misc.rst   |  4 ----
 changelog.d/3148.doc.1.rst  |  3 ---
 changelog.d/3148.doc.2.rst  |  4 ----
 changelog.d/3170.change.rst |  1 -
 setup.cfg                   |  2 +-
 13 files changed, 38 insertions(+), 24 deletions(-)
 delete mode 100644 changelog.d/2971.change.rst
 delete mode 100644 changelog.d/3120.misc.rst
 delete mode 100644 changelog.d/3124.misc.rst
 delete mode 100644 changelog.d/3133.misc.rst
 delete mode 100644 changelog.d/3137.change.rst
 delete mode 100644 changelog.d/3144.doc.rst
 delete mode 100644 changelog.d/3147.misc.rst
 delete mode 100644 changelog.d/3148.doc.1.rst
 delete mode 100644 changelog.d/3148.doc.2.rst
 delete mode 100644 changelog.d/3170.change.rst

diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 79260da6..fd32042d 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 60.9.3
+current_version = 60.10.0
 commit = True
 tag = True
 
diff --git a/CHANGES.rst b/CHANGES.rst
index a24cd2ad..3c724e47 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,3 +1,39 @@
+v60.10.0
+--------
+
+
+Changes
+^^^^^^^
+* #2971: Deprecated upload_docs command, to be removed in the future.
+* #3137: Use samefile from stdlib, supported on Windows since Python 3.2.
+* #3170: Adopt nspektr (vendored) to implement Distribution._install_dependencies.
+
+Documentation changes
+^^^^^^^^^^^^^^^^^^^^^
+* #3144: Added documentation on using console_scripts from setup.py, which was previously only shown in setup.cfg  -- by :user:`xhlulu`
+* #3148: Added clarifications about ``MANIFEST.in``, that include links to PyPUG docs
+  and more prominent mentions to using a revision control system plugin as an
+  alternative.
+* #3148: Removed mention to ``pkg_resources`` as the recommended way of accessing data
+  files, in favour of :doc:`importlib.resources`.
+  Additionally more emphasis was put on the fact that *package data files* reside
+  **inside** the *package directory* (and therefore should be *read-only*).
+
+Misc
+^^^^
+* #3120: Added workaround for intermittent failures of backend tests on PyPy.
+  These tests now are marked with `XFAIL
+  `_, instead of erroring
+  out directly.
+* #3124: Improved configuration for :pypi:`rst-linker` (extension used to build the
+  changelog).
+* #3133: Enhanced isolation of tests using virtual environments - PYTHONPATH is not leaking to spawned subprocesses  -- by :user:`befeleme`
+* #3147: Added options to provide a pre-built ``setuptools`` wheel or sdist for being
+  used during tests with virtual environments.
+  Paths for these pre-built distribution files can now be set via the environment
+  variables: ``PRE_BUILT_SETUPTOOLS_SDIST`` and ``PRE_BUILT_SETUPTOOLS_WHEEL``.
+
+
 v60.9.3
 -------
 
diff --git a/changelog.d/2971.change.rst b/changelog.d/2971.change.rst
deleted file mode 100644
index b9a093b4..00000000
--- a/changelog.d/2971.change.rst
+++ /dev/null
@@ -1 +0,0 @@
-Deprecated upload_docs command, to be removed in the future.
diff --git a/changelog.d/3120.misc.rst b/changelog.d/3120.misc.rst
deleted file mode 100644
index 3531a0ab..00000000
--- a/changelog.d/3120.misc.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-Added workaround for intermittent failures of backend tests on PyPy.
-These tests now are marked with `XFAIL
-`_, instead of erroring
-out directly.
diff --git a/changelog.d/3124.misc.rst b/changelog.d/3124.misc.rst
deleted file mode 100644
index aba19b80..00000000
--- a/changelog.d/3124.misc.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Improved configuration for :pypi:`rst-linker` (extension used to build the
-changelog).
diff --git a/changelog.d/3133.misc.rst b/changelog.d/3133.misc.rst
deleted file mode 100644
index 3377e061..00000000
--- a/changelog.d/3133.misc.rst
+++ /dev/null
@@ -1 +0,0 @@
-Enhanced isolation of tests using virtual environments - PYTHONPATH is not leaking to spawned subprocesses  -- by :user:`befeleme`
diff --git a/changelog.d/3137.change.rst b/changelog.d/3137.change.rst
deleted file mode 100644
index e4186054..00000000
--- a/changelog.d/3137.change.rst
+++ /dev/null
@@ -1 +0,0 @@
-Use samefile from stdlib, supported on Windows since Python 3.2.
diff --git a/changelog.d/3144.doc.rst b/changelog.d/3144.doc.rst
deleted file mode 100644
index 36cc6521..00000000
--- a/changelog.d/3144.doc.rst
+++ /dev/null
@@ -1 +0,0 @@
-Added documentation on using console_scripts from setup.py, which was previously only shown in setup.cfg  -- by :user:`xhlulu`
\ No newline at end of file
diff --git a/changelog.d/3147.misc.rst b/changelog.d/3147.misc.rst
deleted file mode 100644
index 89556edd..00000000
--- a/changelog.d/3147.misc.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-Added options to provide a pre-built ``setuptools`` wheel or sdist for being
-used during tests with virtual environments.
-Paths for these pre-built distribution files can now be set via the environment
-variables: ``PRE_BUILT_SETUPTOOLS_SDIST`` and ``PRE_BUILT_SETUPTOOLS_WHEEL``.
diff --git a/changelog.d/3148.doc.1.rst b/changelog.d/3148.doc.1.rst
deleted file mode 100644
index af89bde2..00000000
--- a/changelog.d/3148.doc.1.rst
+++ /dev/null
@@ -1,3 +0,0 @@
-Added clarifications about ``MANIFEST.in``, that include links to PyPUG docs
-and more prominent mentions to using a revision control system plugin as an
-alternative.
diff --git a/changelog.d/3148.doc.2.rst b/changelog.d/3148.doc.2.rst
deleted file mode 100644
index f46fb248..00000000
--- a/changelog.d/3148.doc.2.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-Removed mention to ``pkg_resources`` as the recommended way of accessing data
-files, in favour of :doc:`importlib.resources`.
-Additionally more emphasis was put on the fact that *package data files* reside
-**inside** the *package directory* (and therefore should be *read-only*).
diff --git a/changelog.d/3170.change.rst b/changelog.d/3170.change.rst
deleted file mode 100644
index 8e356ca3..00000000
--- a/changelog.d/3170.change.rst
+++ /dev/null
@@ -1 +0,0 @@
-Adopt nspektr (vendored) to implement Distribution._install_dependencies.
diff --git a/setup.cfg b/setup.cfg
index 6171f624..58300194 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = setuptools
-version = 60.9.3
+version = 60.10.0
 author = Python Packaging Authority
 author_email = distutils-sig@python.org
 description = Easily download, build, install, upgrade, and uninstall Python packages
-- 
cgit v1.2.1


From d8b40086b932bd8a511b73dc9858f16b2432b307 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 13 Mar 2022 21:43:38 +0000
Subject: Improve package discovery docs

---
 docs/userguide/package_discovery.rst | 106 ++++++++++++++++++-----------------
 1 file changed, 55 insertions(+), 51 deletions(-)

diff --git a/docs/userguide/package_discovery.rst b/docs/userguide/package_discovery.rst
index 762c440e..42bba92c 100644
--- a/docs/userguide/package_discovery.rst
+++ b/docs/userguide/package_discovery.rst
@@ -16,8 +16,9 @@ Package Discovery and Namespace Package
     place to start.
 
 ``Setuptools`` provide powerful tools to handle package discovery, including
-support for namespace package. Normally, you would specify the package to be
-included manually in the following manner:
+support for namespace package.
+
+Normally, you would specify the package to be included manually in the following manner:
 
 .. tab:: setup.cfg
 
@@ -38,6 +39,50 @@ included manually in the following manner:
             packages=['mypkg1', 'mypkg2']
         )
 
+If your packages are not in the root of the repository you also need to
+configure ``package_dir``:
+
+.. tab:: setup.cfg
+
+    .. code-block:: ini
+
+        [options]
+        # ...
+        package_dir =
+            = src
+            # directory containing all the packages (e.g.  src/mypkg1, src/mypkg2)
+        # OR
+        package_dir =
+            mypkg1 = lib1
+            # mypkg1.mod corresponds to lib1/mod.py
+            # mypkg1.subpkg.mod corresponds to lib1/subpkg/mod.py
+            mypkg2 = lib2
+            # mypkg2.mod corresponds to lib2/mod.py
+            mypkg2.subpkg = lib3
+            # pkg2.subpkg.mod corresponds to lib3/mod.py
+
+.. tab:: setup.py
+
+    .. code-block:: python
+
+        setup(
+            # ...
+            package_dir = {"": "src"}
+            # directory containing all the packages (e.g.  src/mypkg1, src/mypkg2)
+        )
+
+        # OR
+
+        setup(
+            # ...
+            package_dir = {
+                "mypkg1": "lib1",  # mypkg1.mod corresponds to lib1/mod.py
+                                 # mypkg1.subpkg.mod corresponds to lib1/subpkg/mod.py
+                "mypkg2": "lib2",   # mypkg2.mod corresponds to lib2/mod.py
+                "mypkg2.subpkg": "lib3"  # mypkg2.subpkg.mod corresponds to lib3/mod.py
+                # ...
+        )
+
 This can get tiresome really quickly. To speed things up, you can rely on
 setuptools automatic discovery, or use the provided tools, as explained in
 the following sections.
@@ -112,58 +157,12 @@ config>` and :doc:`py_modules ` configuration.
 To avoid confusion, file and folder names that are used by popular tools (or
 that correspond to well-known conventions, such as distributing documentation
 alongside the project code) are automatically filtered out in the case of
-*flat-layouts*:
+*flat-layouts* [#layout3]_:
 
 .. autoattribute:: setuptools.discovery.FlatLayoutPackageFinder.DEFAULT_EXCLUDE
 
 .. autoattribute:: setuptools.discovery.FlatLayoutModuleFinder.DEFAULT_EXCLUDE
 
-Also note that you can customise your project layout by explicitly setting
-``package_dir``:
-
-.. tab:: setup.cfg
-
-    .. code-block:: ini
-
-        [options]
-        # ...
-        package_dir =
-            = lib
-            # similar to "src-layout" but using the "lib" folder
-            # pkg.mod corresponds to lib/pkg/mod.py
-        # OR
-        package_dir =
-            pkg1 = lib1
-            # pkg1.mod corresponds to lib1/mod.py
-            # pkg1.subpkg.mod corresponds to lib1/subpkg/mod.py
-            pkg2 = lib2
-            # pkg2.mod corresponds to lib2/mod.py
-            pkg2.subpkg = lib3
-            # pkg2.subpkg.mod corresponds to lib3/mod.py
-
-.. tab:: setup.py
-
-    .. code-block:: python
-
-        setup(
-            # ...
-            package_dir = {"": "lib"}
-            # similar to "src-layout" but using the "lib" folder
-            # pkg.mod corresponds to lib/pkg/mod.py
-        )
-
-        # OR
-
-        setup(
-            # ...
-            package_dir = {
-                "pkg1": "lib1",  # pkg1.mod corresponds to lib1/mod.py
-                                 # pkg1.subpkg.mod corresponds to lib1/subpkg/mod.py
-                "pkg2": "lib2",   # pkg2.mod corresponds to lib2/mod.py
-                "pkg2.subpkg": "lib3"  # pkg2.subpkg.mod corresponds to lib3/mod.py
-                # ...
-        )
-
 .. important:: Automatic discovery will **only** be enabled if you don't
    provide any configuration for both ``packages`` and ``py_modules``.
    If at least one of them is explicitly set, automatic discovery will not take
@@ -252,8 +251,8 @@ in ``src`` that starts with the name ``pkg`` and not ``additional``:
 
 .. _Namespace Packages:
 
-Using ``find_namespace:`` or ``find_namespace_packages``
---------------------------------------------------------
+Using ``find_namespace:`` or ``find_namespace_packages:``
+---------------------------------------------------------
 ``setuptools``  provides the ``find_namespace:`` (``find_namespace_packages``)
 which behaves similarly to ``find:`` but works with namespace package. Before
 diving in, it is important to have a good understanding of what namespace
@@ -393,5 +392,10 @@ The project layout remains the same and ``setup.cfg`` remains the same.
 
 .. [#layout1] https://blog.ionelmc.ro/2014/05/25/python-packaging/#the-structure
 .. [#layout2] https://blog.ionelmc.ro/2017/09/25/rehashing-the-src-layout/
+.. [#layout3]
+   If you are using auto-discovery with *flat-layout* and have multiple folders
+   (other than ``tests`` and ``docs``) or Python files in your project root,
+   always check the created :term:`distribution archive `
+   to make sure files are not being distributed accidentally.
 
 .. _editable install: https://pip.pypa.io/en/stable/cli/pip_install/#editable-installs
-- 
cgit v1.2.1


From dcb136115373df161af02ec3d32aa97e38523742 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 14 Mar 2022 00:48:38 +0000
Subject: Add initial docs about pyproject.toml metadata

---
 docs/conf.py                          |   1 +
 docs/userguide/declarative_config.rst |  23 ++---
 docs/userguide/index.rst              |   1 +
 docs/userguide/package_discovery.rst  |   6 ++
 docs/userguide/pyproject_config.rst   | 188 ++++++++++++++++++++++++++++++++++
 5 files changed, 206 insertions(+), 13 deletions(-)
 create mode 100644 docs/userguide/pyproject_config.rst

diff --git a/docs/conf.py b/docs/conf.py
index 9b4841d1..ee833135 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -103,6 +103,7 @@ extlinks = {
     'pr': (f'{github_repo_url}/pull/%s', 'PR #%s'),  # noqa: WPS323
     'user': (f'{github_sponsors_url}/%s', '@'),  # noqa: WPS323
     'pypi': ('https://pypi.org/project/%s', '%s'),  # noqa: WPS323
+    'wiki': ('https://wikipedia.org/wiki/%s', '%s'),  # noqa: WPS323
 }
 extensions += ['sphinx.ext.extlinks']
 
diff --git a/docs/userguide/declarative_config.rst b/docs/userguide/declarative_config.rst
index 6f41d92b..52379dbf 100644
--- a/docs/userguide/declarative_config.rst
+++ b/docs/userguide/declarative_config.rst
@@ -1,8 +1,8 @@
 .. _declarative config:
 
------------------------------------------
-Configuring setup() using setup.cfg files
------------------------------------------
+------------------------------------------------
+Configuring setuptools using ``setup.cfg`` files
+------------------------------------------------
 
 .. note:: New in 30.3.0 (8 Dec 2016).
 
@@ -24,27 +24,22 @@ boilerplate code in some cases.
 
     [metadata]
     name = my_package
-    version = attr: src.VERSION
+    version = attr: my_package.VERSION
     description = My package description
     long_description = file: README.rst, CHANGELOG.rst, LICENSE.rst
     keywords = one, two
     license = BSD 3-Clause License
     classifiers =
         Framework :: Django
-        License :: OSI Approved :: BSD License
         Programming Language :: Python :: 3
-        Programming Language :: Python :: 3.5
 
     [options]
     zip_safe = False
     include_package_data = True
     packages = find:
-    scripts =
-        bin/first.py
-        bin/second.py
     install_requires =
         requests
-        importlib; python_version == "2.6"
+        importlib-metadata; python_version<"3.8"
 
     [options.package_data]
     * = *.txt, *.rst
@@ -52,7 +47,7 @@ boilerplate code in some cases.
 
     [options.entry_points]
     console_scripts =
-        executable-name = package.module:function
+        executable-name = my_package.module:function
 
     [options.extras_require]
     pdf = ReportLab>=1.2; RXP
@@ -60,8 +55,10 @@ boilerplate code in some cases.
 
     [options.packages.find]
     exclude =
-        src.subpackage1
-        src.subpackage2
+        examples*
+        tools*
+        docs*
+        my_package.tests*
 
 Metadata and options are set in the config sections of the same name.
 
diff --git a/docs/userguide/index.rst b/docs/userguide/index.rst
index eca5a85a..49655acd 100644
--- a/docs/userguide/index.rst
+++ b/docs/userguide/index.rst
@@ -31,6 +31,7 @@ quickstart provides an overview of the new workflow.
     distribution
     extension
     declarative_config
+    pyproject_config
     keywords
     commands
     functionalities_rewrite
diff --git a/docs/userguide/package_discovery.rst b/docs/userguide/package_discovery.rst
index 42bba92c..03663ea2 100644
--- a/docs/userguide/package_discovery.rst
+++ b/docs/userguide/package_discovery.rst
@@ -88,6 +88,8 @@ setuptools automatic discovery, or use the provided tools, as explained in
 the following sections.
 
 
+.. _auto-discovery:
+
 Automatic discovery
 ===================
 
@@ -98,6 +100,8 @@ Automatic discovery
 By default setuptools will consider 2 popular project layouts, each one with
 its own set of advantages and disadvantages [#layout1]_ [#layout2]_.
 
+.. _src-layout:
+
 src-layout:
     The project should contain a ``src`` directory under the project root and
     all modules and packages meant for distribution are placed inside this
@@ -121,6 +125,8 @@ src-layout:
     up the Python REPL and play with your package (you will need an
     `editable install`_ to be able to do that).
 
+.. _flat-layout:
+
 flat-layout (also known as "adhoc"):
     The package folder(s) are placed directly under the project root::
 
diff --git a/docs/userguide/pyproject_config.rst b/docs/userguide/pyproject_config.rst
new file mode 100644
index 00000000..f279d873
--- /dev/null
+++ b/docs/userguide/pyproject_config.rst
@@ -0,0 +1,188 @@
+.. _pyproject.toml config:
+
+-----------------------------------------------------
+Configuring setuptools using ``pyproject.toml`` files
+-----------------------------------------------------
+
+.. note:: New in 61.0.0 (**experimental**)
+
+.. warning::
+   Support for declaring :doc:`project metadata
+   ` or configuring
+   ``setuptools`` via ``pyproject.toml`` files is still experimental and might
+   change (or be removed) in future releases.
+
+Starting with :pep:`621`, the Python community selected ``pyproject.toml`` as
+a standard way of specifying *project metadata*.
+``Setuptools`` has adopted this standard and will use the information contained
+in this file as an input in the build process.
+
+The example bellow illustrates how to write a ``pyproject.toml`` file that can
+be used with ``setuptools``. It contains two TOML tables (identified by the
+``[table-header]`` syntax): ``build-system`` and ``project``.
+The ``build-system`` table is used to tell the build frontend (e.g.
+:pypi:`build` or :pypi:`pip`) to use ``setuptools`` and any other plugins (e.g.
+``setuptools-scm``) to build the package.
+The ``project`` table contains metadata fields as described by
+:doc:`PyPUG:specifications/declaring-project-metadata` guide.
+
+.. _example-pyproject-config:
+
+.. code-block:: toml
+
+   [build-system]
+   requires = ["setuptools", "setuptools-scm"]
+   build-backend = "setuptools.build_meta"
+
+   [project]
+   name = "my_package"
+   description = "My package description"
+   readme = "README.rst"
+   keywords = ["one", "two"]
+   license = {text = "BSD 3-Clause License"}
+   classifiers = [
+       "Framework :: Django",
+       "Programming Language :: Python :: 3",
+   ]
+   dependencies = [
+       "requests",
+       'importlib-metadata; python_version<"3.8"',
+   ]
+   dynamic = ["version"]
+
+   [project.optional-dependencies]
+   pdf = ["ReportLab>=1.2", "RXP"]
+   rest = ["docutils>=0.3", "pack ==1.1, ==1.3"]
+
+   [project.scripts]
+   my-script = "my_package.module:function"
+
+
+.. _setuptools-table:
+
+Setuptools-specific configuration
+=================================
+
+While the standard ``project`` table in the ``pyproject.toml`` file covers most
+of the metadata used during the packaging process, there are still some
+``setuptools``-specific configurations that can be set by users that require
+customization.
+These configurations are completely optional (and probably can be skipped when
+creating simple packages). They are equivalent to the :doc:`/references/keywords`
+used by the ``setup.py`` file:
+
+========================= =========================== =========================
+Key                       Value Type (TOML)           Notes
+========================= =========================== =========================
+``platforms``             array
+``zip-safe``              boolean
+``eager-resources``       array
+``py-modules``            array                       See tip bellow
+``packages``              array or ``find`` directive See tip bellow
+``package-dir``           table/inline-table          Used when explicitly listing ``packages``
+``namespace-packages``    array                       Not necessary if you use :pep:`420`
+``package-data``          table/inline-table          See :doc:`/userguide/datafiles`
+``include-package-data``  boolean                     ``True`` by default
+``exclude-package-data``  table/inline-table
+``license-files``         array of glob patterns      **Provisional** - likely to change with :pep:`639`
+                                                      (by default: ``['LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*']``)
+``data-files``            table/inline-table          **Deprecated** - check :doc:`/userguide/datafiles`
+``script-files``          array                       **Deprecated** - equivalent to the ``script`` keyword in ``setup.py``
+                                                      (should be avoided in favour of ``project.scripts``)
+``provides``              array                       **Ignored by pip**
+``obsoletes``             array                       **Ignored by pip**
+========================= =========================== =========================
+
+The `TOML value types`_ ``array`` and ``table/inline-table`` are roughly
+equivalent to the Python's :obj:`dict` and :obj:`list` data types.
+
+.. tip::
+   When both ``py-modules`` and ``packages`` are left unspecified,
+   ``setuptools`` will attempt to perform :ref:`auto-discovery`, which should
+   cover most popular project directory organization techniques, such as the
+   :ref:`src ` and :ref:`flat ` layouts.
+
+   However if your project does not follow these conventional layouts
+   (e.g. you want to use a ``flat-layout`` but at the same time have custom
+   directories at the root of your project), you might need to use the ``find``
+   directive as shown bellow:
+
+   .. code-block:: toml
+
+      [tool.setuptools.packages.find]
+      where = ["src"]  # list of folders that contain the packages (["."] by default)
+      include = ["my_package*"]  # package names should match these glob patterns (["*"] by default)
+      exclude = ["my_package.tests*"]  # exclude packages matching these glob patterns (empty by default)
+      namespaces = false  # to disable scanning PEP 420 namespaces (true by default)
+
+   Note that the glob patterns in the example above need to be matched
+   by the **entire** package name. This means that if you specify ``exclude = ["tests"]``,
+   modules like ``tests.my_package.test1`` will still be included in the distribution
+   (to remove them, add a wildcard to the end of the pattern: ``"tests*"``).
+
+   Alternatively, you can explicitly list the packages in modules:
+
+   .. code-block:: toml
+
+      [tool.setuptools]
+      packages = ["my_package"]
+
+
+.. _dynamic-pyproject-config:
+
+Dynamic Metadata
+================
+
+Note that in the first example of this page we use ``dynamic`` to identify
+which metadata fields are dynamically calculated during the build by either
+``setuptools`` itself or the selected plugins (e.g. ``setuptools-scm`` is
+capable of deriving the current project version directly from the ``git``
+:wiki:`version control` system).
+
+Currently the following fields can be used dynamically: ``version``,
+``classifiers``, ``description``, ``entry-points``, ``scripts``,
+``gui-scripts`` and ``readme``.
+When these fields are expected to be directly provided by ``setuptools`` a
+corresponding entry is required in the ``tool.setuptools.dynamic`` table
+[#entry-points]_. For example:
+
+.. code-block:: toml
+
+   # ...
+   [project]
+   name = "my_package"
+   dynamic = ["version", "readme"]
+   # ...
+   [tool.setuptools.dynamic]
+   version = {attr = "my_package.VERSION"}
+   readme = {file = ["README.rst", "USAGE.rst"]}
+
+In this example the ``attr`` attribute will read an attribute from the given
+module [#attr]_, while ``file`` will read all the given files and concatenate
+them in a single string.
+
+================= =================== =========================
+Key               Directive           Notes
+================= =================== =========================
+``version``       ``attr``, ``file``
+``readme``        ``file``
+``description``   ``file``            One-line text
+``classifiers``   ``file``            Multi-line text with one classifier per line
+``entry-points``  ``file``            INI format following :doc:`PyPUG:specifications/entry-points`
+                                      (``console_scripts`` and ``gui_scripts`` can be included)
+================= =================== =========================
+
+----
+
+.. [#entry-points] Dynamic ``scripts`` and ``gui-scripts`` are a special case.
+   When resolving these metadata keys, ``setuptools`` will look for
+   ``tool.setuptool.dynamic.entry-points``, and use the values of the
+   ``console_scripts`` and ``gui_scripts`` :doc:`entry-point groups
+   `.
+
+.. [#attr] ``attr`` is meant to be used when the module attribute is statically
+   specified (e.g. as a string, list or tuple). As a rule of thumb, the
+   attribute should be able to be parsed with :func:`ast.literal_eval`, and
+   should not be modified or re-assigned.
+
+.. _TOML value types: https://toml.io/en/v1.0.0
-- 
cgit v1.2.1


From bab2aae6326e4792e64d2dbe903f36f37fb9e363 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 14 Mar 2022 00:53:36 +0000
Subject: Add news fragment

---
 changelog.d/3172.doc.rst | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 changelog.d/3172.doc.rst

diff --git a/changelog.d/3172.doc.rst b/changelog.d/3172.doc.rst
new file mode 100644
index 00000000..1c179763
--- /dev/null
+++ b/changelog.d/3172.doc.rst
@@ -0,0 +1,2 @@
+Added initial documentation about configuring ``setuptools`` via ``pyproject.toml``
+(using standard project metadata).
-- 
cgit v1.2.1


From f12dba7c5307092b045ed87eeabb6586954e7fe5 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 14 Mar 2022 01:06:04 +0000
Subject: Add remark about editable installs

---
 docs/userguide/pyproject_config.rst | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/docs/userguide/pyproject_config.rst b/docs/userguide/pyproject_config.rst
index f279d873..29db36cb 100644
--- a/docs/userguide/pyproject_config.rst
+++ b/docs/userguide/pyproject_config.rst
@@ -12,6 +12,11 @@ Configuring setuptools using ``pyproject.toml`` files
    ``setuptools`` via ``pyproject.toml`` files is still experimental and might
    change (or be removed) in future releases.
 
+.. important::
+   For the time being, you still might require a ``setup.py`` file containing
+   a *arg-less* ``setup()`` function call to support
+   :doc:`editable installs `.
+
 Starting with :pep:`621`, the Python community selected ``pyproject.toml`` as
 a standard way of specifying *project metadata*.
 ``Setuptools`` has adopted this standard and will use the information contained
-- 
cgit v1.2.1


From 59f923e87920736509fe22a35e9a7047dfd43ee8 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 17 Mar 2022 00:27:09 +0000
Subject: Mention experimental pyproject.toml support in discovery docs

---
 docs/build_meta.rst                  |   2 +
 docs/userguide/package_discovery.rst | 288 +++++++++++++++++++++++++++--------
 2 files changed, 227 insertions(+), 63 deletions(-)

diff --git a/docs/build_meta.rst b/docs/build_meta.rst
index 1337bddb..cb372721 100644
--- a/docs/build_meta.rst
+++ b/docs/build_meta.rst
@@ -72,6 +72,8 @@ specify the package information::
     [options]
     packages = find:
 
+.. _building:
+
 Now generate the distribution. To build the package, use
 `PyPA build `_::
 
diff --git a/docs/userguide/package_discovery.rst b/docs/userguide/package_discovery.rst
index 03663ea2..8f2185da 100644
--- a/docs/userguide/package_discovery.rst
+++ b/docs/userguide/package_discovery.rst
@@ -39,6 +39,18 @@ Normally, you would specify the package to be included manually in the following
             packages=['mypkg1', 'mypkg2']
         )
 
+.. tab:: pyproject.toml
+
+    **EXPERIMENTAL** [#experimental]_
+
+    .. code-block:: toml
+
+        # ...
+        [tool.setuptools]
+        packages = ["mypkg1", "mypkg2"]
+        # ...
+
+
 If your packages are not in the root of the repository you also need to
 configure ``package_dir``:
 
@@ -59,7 +71,7 @@ configure ``package_dir``:
             mypkg2 = lib2
             # mypkg2.mod corresponds to lib2/mod.py
             mypkg2.subpkg = lib3
-            # pkg2.subpkg.mod corresponds to lib3/mod.py
+            # mypkg2.subpkg.mod corresponds to lib3/mod.py
 
 .. tab:: setup.py
 
@@ -76,13 +88,36 @@ configure ``package_dir``:
         setup(
             # ...
             package_dir = {
-                "mypkg1": "lib1",  # mypkg1.mod corresponds to lib1/mod.py
-                                 # mypkg1.subpkg.mod corresponds to lib1/subpkg/mod.py
+                "mypkg1": "lib1",   # mypkg1.mod corresponds to lib1/mod.py
+                                    # mypkg1.subpkg.mod corresponds to lib1/subpkg/mod.py
                 "mypkg2": "lib2",   # mypkg2.mod corresponds to lib2/mod.py
                 "mypkg2.subpkg": "lib3"  # mypkg2.subpkg.mod corresponds to lib3/mod.py
                 # ...
         )
 
+.. tab:: pyproject.toml
+
+    **EXPERIMENTAL** [#experimental]_
+
+    .. code-block:: toml
+
+        [tool.setuptools]
+        # ...
+        package-dir = {"" = "src"}
+            # directory containing all the packages (e.g.  src/mypkg1, src/mypkg2)
+
+        # OR
+
+        [tool.setuptools.package-dir]
+        mypkg1 = "lib1"
+            # mypkg1.mod corresponds to lib1/mod.py
+            # mypkg1.subpkg.mod corresponds to lib1/subpkg/mod.py
+        mypkg2 = "lib2"
+            # mypkg2.mod corresponds to lib2/mod.py
+        "mypkg2.subpkg" = "lib3"
+            # mypkg2.subpkg.mod corresponds to lib3/mod.py
+        # ...
+
 This can get tiresome really quickly. To speed things up, you can rely on
 setuptools automatic discovery, or use the provided tools, as explained in
 the following sections.
@@ -200,29 +235,43 @@ the provided tools for package discovery:
     .. code-block:: python
 
         from setuptools import find_packages
-
         # or
         from setuptools import find_namespace_packages
 
+.. tab:: pyproject.toml
 
-Using ``find:`` or ``find_packages``
-------------------------------------
-Let's start with the first tool. ``find:`` (``find_packages``) takes a source
-directory and two lists of package name patterns to exclude and include, and
-then return a list of ``str`` representing the packages it could find. To use
-it, consider the following directory
+    **EXPERIMENTAL** [#experimental]_
 
-.. code-block:: bash
+    .. code-block:: toml
 
-    mypkg/
-        src/
-            pkg1/__init__.py
-            pkg2/__init__.py
-            additional/__init__.py
+        # ...
+        [tool.setuptools.packages]
+        find = {}  # Scanning implicit namespaces is active by default
+        # OR
+        find = {namespace = false}  # Disable implicit namespaces
 
-        setup.cfg #or setup.py
 
-To have your setup.cfg or setup.py to automatically include packages found
+Finding simple packages
+-----------------------
+Let's start with the first tool. ``find:`` (``find_packages()``) takes a source
+directory and two lists of package name patterns to exclude and include, and
+then return a list of ``str`` representing the packages it could find. To use
+it, consider the following directory::
+
+    mypkg
+    ├── setup.cfg  # and/or setup.py, pyproject.toml
+    └── src
+        ├── pkg1
+        │   └── __init__.py
+        ├── pkg2
+        │   └── __init__.py
+        ├── aditional
+        │   └── __init__.py
+        └── pkg
+            └── namespace
+                └── __init__.py
+
+To have setuptools to automatically include packages found
 in ``src`` that starts with the name ``pkg`` and not ``additional``:
 
 .. tab:: setup.cfg
@@ -239,6 +288,10 @@ in ``src`` that starts with the name ``pkg`` and not ``additional``:
         include = pkg*
         exclude = additional
 
+    .. note::
+        ``pkg`` does not contain an ``__init__.py`` file, therefore
+        ``pkg.namespace`` is ignored by ``find:`` (see ``find_namespace:`` below).
+
 .. tab:: setup.py
 
     .. code-block:: python
@@ -255,16 +308,55 @@ in ``src`` that starts with the name ``pkg`` and not ``additional``:
         )
 
 
+    .. note::
+        ``pkg`` does not contain an ``__init__.py`` file, therefore
+        ``pkg.namespace`` is ignored by ``find_packages()``
+        (see ``find_namespace_packages()`` below).
+
+.. tab:: pyproject.toml
+
+    **EXPERIMENTAL** [#experimental]_
+
+    .. code-block:: toml
+
+        [tool.setuptools.packages.find]
+        where = ["src"]
+        include = ["pkg*"]
+        exclude = ["additional"]
+        namespaces = false
+
+    .. note::
+        When using ``tool.setuptools.packages.find`` in ``pyproject.toml``,
+        setuptools will consider :pep:`implicit namespaces <420>` by default when
+        scanning your project directory.
+        To avoid ``pkg.namespace`` from being added to your package list
+        you can set ``namespaces = false``. This will prevent any folder
+        without an ``__init__.py`` file from being scanned.
+
+.. important::
+   ``include`` and ``exclude`` accept strings representing :mod:`glob` patterns.
+   These patterns should match the **full** name of the Python module (as if it
+   was written in an ``import`` statement).
+
+   For example if you have ``util`` pattern, it will match
+   ``util/__init__.py`` but not ``util/files/__init__.py``.
+
+   The fact that the parent package is matched by the pattern will not dictate
+   if the submodule will be included or excluded from the distribution.
+   You will need to explicitly add a wildcard (e.g. ``util*``)
+   if you want the pattern to also match submodules.
+
 .. _Namespace Packages:
 
-Using ``find_namespace:`` or ``find_namespace_packages:``
----------------------------------------------------------
-``setuptools``  provides the ``find_namespace:`` (``find_namespace_packages``)
-which behaves similarly to ``find:`` but works with namespace package. Before
-diving in, it is important to have a good understanding of what namespace
-packages are. Here is a quick recap:
+Finding namespace packages
+--------------------------
+``setuptools``  provides the ``find_namespace:`` (``find_namespace_packages()``)
+which behaves similarly to ``find:`` but works with namespace package.
 
-Suppose you have two packages named as follows:
+Before diving in, it is important to have a good understanding of what
+:pep:`namespace packages <420>` are. Here is a quick recap.
+
+When you have two packages organized as follows:
 
 .. code-block:: bash
 
@@ -273,7 +365,7 @@ Suppose you have two packages named as follows:
 
 If both ``Desktop`` and ``Library`` are on your ``PYTHONPATH``, then a
 namespace package called ``timmins`` will be created automatically for you when
-you invoke the import mechanism, allowing you to accomplish the following
+you invoke the import mechanism, allowing you to accomplish the following:
 
 .. code-block:: pycon
 
@@ -282,49 +374,110 @@ you invoke the import mechanism, allowing you to accomplish the following
 
 as if there is only one ``timmins`` on your system. The two packages can then
 be distributed separately and installed individually without affecting the
-other one. Suppose you are packaging the ``foo`` part:
+other one.
 
-.. code-block:: bash
+Now, suppose you decide to package the ``foo`` part for distribution and start
+by creating a project directory organized as follows::
 
-    foo/
-        src/
-            timmins/foo/__init__.py
-        setup.cfg # or setup.py
+   foo
+   ├── setup.cfg  # and/or setup.py, pyproject.toml
+   └── src
+       └── timmins
+           └── foo
+               └── __init__.py
 
-and you want the ``foo`` to be automatically included, ``find:`` won't work
-because timmins doesn't contain ``__init__.py`` directly, instead, you have
-to use ``find_namespace:``:
+If you want the ``timmins.foo`` to be automatically included in the
+distribution, then you will need to specify:
 
-.. code-block:: ini
+.. tab:: setup.cfg
 
-    [options]
-    package_dir =
-        =src
-    packages = find_namespace:
+    .. code-block:: ini
 
-    [options.packages.find]
-    where = src
+        [options]
+        package_dir =
+            =src
+        packages = find_namespace:
 
-When you install the zipped distribution, ``timmins.foo`` would become
+        [options.packages.find]
+        where = src
+
+    ``find:`` won't work because timmins doesn't contain ``__init__.py``
+    directly, instead, you have to use ``find_namespace:``.
+
+    You can think of ``find_namespace:`` as identical to ``find:`` except it
+    would count a directory as a package even if it doesn't contain ``__init__.py``
+    file directly.
+
+.. tab:: setup.py
+
+    .. code-block:: python
+
+        setup(
+            # ...
+            packages=find_namespace_packages(where='src'),
+            package_dir={"": "src"}
+            # ...
+        )
+
+    When you use ``find_packages()``, all directories without an
+    ``__init__.py`` file will be disconsidered.
+    On the other hand, ``find_namespace_packages()`` will scan all
+    directories.
+
+.. tab:: pyproject.toml
+
+    **EXPERIMENTAL** [#experimental]_
+
+    .. code-block:: toml
+
+        [tool.setuptools.packages.find]
+        where = ["src"]
+
+    When using ``tool.setuptools.packages.find`` in ``pyproject.toml``,
+    setuptools will consider :pep:`implicit namespaces <420>` by default when
+    scanning your project directory.
+
+After installing the package distribution, ``timmins.foo`` would become
 available to your interpreter.
 
-You can think of ``find_namespace:`` as identical to ``find:`` except it
-would count a directory as a package even if it doesn't contain ``__init__.py``
-file directly. As a result, this creates an interesting side effect. If you
-organize your package like this:
+.. warning::
+   Please have in mind that ``find_namespace:`` (setup.cfg),
+   ``find_namespace_packages()`` (setup.py) and ``find`` (pyproject.toml) will
+   scan **all** folders that you have in your project directory if you use a
+   :ref:`flat-layout`.
 
-.. code-block:: bash
+   If used naïvely, this might result in unwanted files being added to your
+   final wheel. For example, with a project directory organized as follows::
+
+       foo
+       ├── docs
+       │   └── conf.py
+       ├── timmins
+       │   └── foo
+       │       └── __init__.py
+       └── tests
+           └── tests_foo
+               └── __init__.py
+
+   final users will end up installing not only ``timmins.foo``, but also
+   ``docs`` and ``tests.tests_foo``.
+
+   A simple way to fix this is to adopt the aforementioned :ref:`src-layout`,
+   or make sure to properly configure the ``include`` and/or ``exclude``
+   accordingly.
 
-    foo/
-        timmins/
-            foo/__init__.py
-        setup.cfg # or setup.py
-        tests/
-            test_foo/__init__.py
+.. tip::
+   After :ref:`building your package `, you can have a look if all
+   the files are correct (nothing missing or extra), by running the following
+   commands:
 
-a naive ``find_namespace:`` would include tests as part of your package to
-be installed. A simple way to fix it is to adopt the aforementioned
-``src`` layout.
+   .. code-block:: bash
+
+      tar tf dist/*.tar.gz
+      unzip -l dist/*.whl
+
+   This requires the ``tar`` and ``unzip`` to be installed in your OS.
+   On Windows you can also use a GUI program such as 7zip_.
 
 
 Legacy Namespace Packages
@@ -373,12 +526,13 @@ And your directory should look like this
 
 .. code-block:: bash
 
-    /foo/
-        src/
-            timmins/
-                __init__.py
-                foo/__init__.py
-        setup.cfg #or setup.py
+   foo
+   ├── setup.cfg  # and/or setup.py, pyproject.toml
+   └── src
+       └── timmins
+           ├── __init__.py
+           └── foo
+               └── __init__.py
 
 Repeat the same for other packages and you can achieve the same result as
 the previous section.
@@ -396,6 +550,13 @@ file contains the following:
 The project layout remains the same and ``setup.cfg`` remains the same.
 
 
+----
+
+
+.. [#experimental]
+   Support for specifying package metadata and build configuration options via
+   ``pyproject.toml`` is experimental and might change (or be completely
+   removed) in the future. See :doc:`/userguide/pyproject_config`.
 .. [#layout1] https://blog.ionelmc.ro/2014/05/25/python-packaging/#the-structure
 .. [#layout2] https://blog.ionelmc.ro/2017/09/25/rehashing-the-src-layout/
 .. [#layout3]
@@ -405,3 +566,4 @@ The project layout remains the same and ``setup.cfg`` remains the same.
    to make sure files are not being distributed accidentally.
 
 .. _editable install: https://pip.pypa.io/en/stable/cli/pip_install/#editable-installs
+.. _7zip: https://www.7-zip.org
-- 
cgit v1.2.1


From f14200550a97bf3f113a563e3502bc63883b1c6b Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 17 Mar 2022 00:39:39 +0000
Subject: Mention experimental pyproject config in the quickstart

---
 docs/userguide/quickstart.rst | 44 ++++++++++++++++++++++++++++++++++++-------
 1 file changed, 37 insertions(+), 7 deletions(-)

diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst
index f3183624..6267fe8b 100644
--- a/docs/userguide/quickstart.rst
+++ b/docs/userguide/quickstart.rst
@@ -35,9 +35,9 @@ package your project:
     requires = ["setuptools"]
     build-backend = "setuptools.build_meta"
 
-Then, you will need a ``setup.cfg`` or ``setup.py`` to specify your package
-information, such as metadata, contents, dependencies, etc. Here we demonstrate
-the minimum
+Then, you will need to specify your package information (either via
+``setup.cfg``, ``setup.py`` or ``pyproject.toml``), such as metadata, contents,
+dependencies, etc. Here we demonstrate the minimum
 
 .. tab:: setup.cfg
 
@@ -51,7 +51,9 @@ the minimum
         packages = mypackage
         install_requires =
             requests
-            importlib; python_version == "2.6"
+            importlib-metadata; python_version < "3.8"
+
+    See :doc:`/userguide/declarative_config` for more information.
 
 .. tab:: setup.py
 
@@ -65,10 +67,28 @@ the minimum
             packages=['mypackage'],
             install_requires=[
                 'requests',
-                'importlib; python_version == "2.6"',
+                'importlib-metadata; python_version == "3.8"',
             ],
         )
 
+    See :doc:`/keywords` for more information.
+
+.. tab:: pyproject.toml
+
+    **EXPERIMENTAL** [#experimental]_
+
+    .. code-block:: toml
+
+       [project]
+       name = "mypackage"
+       version = "0.0.1"
+       dependencies = [
+           "requests",
+           'importlib-metadata; python_version<"3.8"',
+       ]
+
+    See :doc:`/userguide/pyproject_config` for more information.
+
 This is what your project would look like::
 
     ~/mypackage/
@@ -220,8 +240,9 @@ Transitioning from ``setup.py`` to ``setup.cfg``
 To avoid executing arbitrary scripts and boilerplate code, we are transitioning
 into a full-fledged ``setup.cfg`` to declare your package information instead
 of running ``setup()``. This inevitably brings challenges due to a different
-syntax. Here we provide a quick guide to understanding how ``setup.cfg`` is
-parsed by ``setuptool`` to ease the pain of transition.
+syntax. :doc:`Here ` we provide a quick guide to
+understanding how ``setup.cfg`` is parsed by ``setuptool`` to ease the pain of
+transition.
 
 .. _packaging-resources:
 
@@ -234,3 +255,12 @@ up-to-date references that can help you when it is time to distribute your work.
 
 .. |MANIFEST.in| replace:: ``MANIFEST.in``
 .. _MANIFEST.in: https://packaging.python.org/en/latest/guides/using-manifest-in/
+
+
+----
+
+
+.. [#experimental]
+   Support for specifying package metadata and build configuration options via
+   ``pyproject.toml`` is experimental and might change (or be completely
+   removed) in the future. See :doc:`/userguide/pyproject_config`.
-- 
cgit v1.2.1


From 0a754916d77864e5dad08c4b59f60deb97ee200b Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 17 Mar 2022 00:47:01 +0000
Subject: Add some missing references in the quickstart

---
 docs/userguide/quickstart.rst | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst
index 6267fe8b..dd899428 100644
--- a/docs/userguide/quickstart.rst
+++ b/docs/userguide/quickstart.rst
@@ -137,7 +137,7 @@ it can find following the ``include``  (defaults to none), then removes
 those that match the ``exclude`` and returns a list of Python packages. Note
 that each entry in the ``[options.packages.find]`` is optional. The above
 setup also allows you to adopt a ``src/`` layout. For more details and advanced
-use, go to :ref:`package_discovery`
+use, go to :ref:`package_discovery`.
 
 
 Entry points and automatic script creation
@@ -182,7 +182,7 @@ additional keywords such as ``setup_requires`` that allows you to install
 dependencies before running the script, and ``extras_require`` that take
 care of those needed by automatically generated scripts. It also provides
 mechanisms to handle dependencies that are not in PyPI. For more advanced use,
-see :doc:`dependency_management`
+see :doc:`dependency_management`.
 
 
 .. _Including Data Files:
@@ -203,7 +203,7 @@ This tells setuptools to install any data files it finds in your packages.
 The data files must be specified via the distutils' |MANIFEST.in|_ file
 or automatically added by a :ref:`Revision Control System plugin
 `.
-For more details, see :doc:`datafiles`
+For more details, see :doc:`datafiles`.
 
 
 Development mode
@@ -231,8 +231,8 @@ Uploading your package to PyPI
 ==============================
 After generating the distribution files, the next step would be to upload your
 distribution so others can use it. This functionality is provided by
-`twine `_ and we will only demonstrate the
-basic use here.
+:pypi:`twine` and is documented in the :doc:`Python packaging tutorial
+`.
 
 
 Transitioning from ``setup.py`` to ``setup.cfg``
-- 
cgit v1.2.1


From 12a466735735074e230995e199aaca3fda73dd79 Mon Sep 17 00:00:00 2001
From: Isuru Fernando 
Date: Wed, 16 Mar 2022 20:38:25 -0500
Subject: Update test to check for correct value

---
 distutils/tests/test_unixccompiler.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/distutils/tests/test_unixccompiler.py b/distutils/tests/test_unixccompiler.py
index d9891189..53ef6fa4 100644
--- a/distutils/tests/test_unixccompiler.py
+++ b/distutils/tests/test_unixccompiler.py
@@ -245,8 +245,8 @@ class UnixCCompilerTestCase(support.TempdirManager, unittest.TestCase):
             self.assertEqual(self.cc.linker_so[0:2], ['ccache','my_cc'])
             self.cc.link(None, [], 'a.out', target_lang='c++')
             call_args = mock_spawn.call_args[0][0]
-            if len(call_args) >= 2:
-                assert(call_args[:2] != ['my_cxx', 'my_cc'])
+            assert len(call_args) >= 4
+            assert(call_args[:4] == ['g++-4.2', '-bundle', '-undefined', 'dynamic_lookup'])
 
     @unittest.skipIf(sys.platform == 'win32', "can't test on Windows")
     def test_explicit_ldshared(self):
-- 
cgit v1.2.1


From d0eba16088c749ef6c7b6eda1170ef036b430fd4 Mon Sep 17 00:00:00 2001
From: Isuru Fernando 
Date: Wed, 16 Mar 2022 20:42:44 -0500
Subject: Fix test

---
 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 53ef6fa4..7544a86e 100644
--- a/distutils/tests/test_unixccompiler.py
+++ b/distutils/tests/test_unixccompiler.py
@@ -246,7 +246,7 @@ class UnixCCompilerTestCase(support.TempdirManager, unittest.TestCase):
             self.cc.link(None, [], 'a.out', target_lang='c++')
             call_args = mock_spawn.call_args[0][0]
             assert len(call_args) >= 4
-            assert(call_args[:4] == ['g++-4.2', '-bundle', '-undefined', 'dynamic_lookup'])
+            assert(call_args[:4] == ['my_cxx', '-bundle', '-undefined', 'dynamic_lookup'])
 
     @unittest.skipIf(sys.platform == 'win32', "can't test on Windows")
     def test_explicit_ldshared(self):
-- 
cgit v1.2.1


From 6f5c018db98a033161b450744041019e7a4d2fc2 Mon Sep 17 00:00:00 2001
From: Isuru Fernando 
Date: Wed, 16 Mar 2022 22:04:18 -0500
Subject: Pass through PROGRAMDATA, PROGRAMFILES env variables

---
 tox.ini | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/tox.ini b/tox.ini
index 1590e308..235c7897 100644
--- a/tox.ini
+++ b/tox.ini
@@ -5,4 +5,8 @@ commands =
 	pytest {posargs}
 setenv =
     PYTHONPATH = {toxinidir}
+passenv =
+    PROGRAMDATA
+    PROGRAMFILES
+    PROGRAMFILES(X86)
 skip_install = True
-- 
cgit v1.2.1


From e43140ca8459de6c4651b0b9da17aaf79fe99f89 Mon Sep 17 00:00:00 2001
From: Isuru Fernando 
Date: Wed, 16 Mar 2022 22:04:57 -0500
Subject: Re-enable windows tests

---
 .github/workflows/main.yml | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 5beb799f..6fca2f69 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -22,8 +22,7 @@ jobs:
         platform:
         - ubuntu-latest
         - macos-latest
-        # disable tests on Windows due to pypa/distutils#118
-        # - windows-latest
+        - windows-latest
     runs-on: ${{ matrix.platform }}
     steps:
       - uses: actions/checkout@v2
-- 
cgit v1.2.1


From 203c2f89d7fd315017c0834f74f5eb7f0f501cc5 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 17 Mar 2022 09:46:01 +0000
Subject: Fix link to keywords

---
 docs/userguide/quickstart.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst
index dd899428..085e46c0 100644
--- a/docs/userguide/quickstart.rst
+++ b/docs/userguide/quickstart.rst
@@ -71,7 +71,7 @@ dependencies, etc. Here we demonstrate the minimum
             ],
         )
 
-    See :doc:`/keywords` for more information.
+    See :doc:`/references/keywords` for more information.
 
 .. tab:: pyproject.toml
 
-- 
cgit v1.2.1


From eea1ea59042f01a59af890c682a30d8158f54ca4 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 17 Mar 2022 10:19:26 +0000
Subject: Clarify deprecated fields for tool.setuptools

---
 docs/userguide/pyproject_config.rst | 23 +++++++++++++++--------
 1 file changed, 15 insertions(+), 8 deletions(-)

diff --git a/docs/userguide/pyproject_config.rst b/docs/userguide/pyproject_config.rst
index 29db36cb..597bc33c 100644
--- a/docs/userguide/pyproject_config.rst
+++ b/docs/userguide/pyproject_config.rst
@@ -73,14 +73,15 @@ of the metadata used during the packaging process, there are still some
 ``setuptools``-specific configurations that can be set by users that require
 customization.
 These configurations are completely optional (and probably can be skipped when
-creating simple packages). They are equivalent to the :doc:`/references/keywords`
-used by the ``setup.py`` file:
+creating simple packages) and can be set via the ``tool.setuptools`` table.
+They are equivalent to the :doc:`/references/keywords` used by the ``setup.py`` file:
 
 ========================= =========================== =========================
 Key                       Value Type (TOML)           Notes
 ========================= =========================== =========================
 ``platforms``             array
-``zip-safe``              boolean
+``zip-safe``              boolean                     If not specified, ``setuptools`` will try to guess
+                                                      a reasonable default for the package
 ``eager-resources``       array
 ``py-modules``            array                       See tip bellow
 ``packages``              array or ``find`` directive See tip bellow
@@ -98,8 +99,14 @@ Key                       Value Type (TOML)           Notes
 ``obsoletes``             array                       **Ignored by pip**
 ========================= =========================== =========================
 
-The `TOML value types`_ ``array`` and ``table/inline-table`` are roughly
-equivalent to the Python's :obj:`dict` and :obj:`list` data types.
+In the table above, the `TOML value types`_ ``array`` and
+``table/inline-table`` are roughly equivalent to the Python's :obj:`dict` and
+:obj:`list` data types.
+
+Please note that some of these configurations are deprecated or at least
+discouraged, but they are made available to ensure portability.
+New packages should avoid relying on them, and existing packages should
+consider alternatives.
 
 .. tip::
    When both ``py-modules`` and ``packages`` are left unspecified,
@@ -162,9 +169,9 @@ corresponding entry is required in the ``tool.setuptools.dynamic`` table
    version = {attr = "my_package.VERSION"}
    readme = {file = ["README.rst", "USAGE.rst"]}
 
-In this example the ``attr`` attribute will read an attribute from the given
-module [#attr]_, while ``file`` will read all the given files and concatenate
-them in a single string.
+In the ``dynamic`` table, the ``attr`` directive will read an attribute from
+the given module [#attr]_, while ``file`` will read all the given files and
+concatenate them in a single string.
 
 ================= =================== =========================
 Key               Directive           Notes
-- 
cgit v1.2.1


From 733c84345904804bf4719b9aa9414018b9ed3057 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 17 Mar 2022 11:04:57 +0000
Subject: Improve discovery section in the quickstart

---
 docs/userguide/quickstart.rst | 65 ++++++++++++++++++++++++++++++++++++-------
 1 file changed, 55 insertions(+), 10 deletions(-)

diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst
index 085e46c0..6219cd9a 100644
--- a/docs/userguide/quickstart.rst
+++ b/docs/userguide/quickstart.rst
@@ -118,27 +118,72 @@ 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. ``setuptools``
-therefore provides two convenient tools to ease the burden: :literal:`find:\ ` and
-:literal:`find_namespace:\ `. To use it in your project:
+therefore provides a convenient way to automatically list all the packages in
+your project directory:
 
-.. code-block:: ini
+.. tab:: setup.cfg
 
-    [options]
-    packages = find:
+    .. code-block:: ini
+
+        [options]
+        packages = find: # OR `find_namespaces:` if you want to use namespaces
 
-    [options.packages.find] #optional
-    include=pkg1, pkg2
-    exclude=pk3, pk4
+        [options.packages.find] (always `find` even if `find_namespaces:` was used before)
+        # This section is optional
+        # Each entry in this section is optional, and if not specified, the default values are:
+        # `where=.`, `include=*` and `exclude=` (empty).
+        include=mypackage*
+        exclude=mypackage.tests*
+
+.. tab:: setup.py
+
+    .. code-block:: python
+        from setuptools import find_packages  # or find_namespace_packages
+
+        setup(
+            # ...
+            packages=find_packages(
+                where='.',
+                include=['mypackage*'],  # ["*"] by default
+                exclude=['mypackage.tests'],  # empty by default
+            ),
+            # ...
+        )
+
+.. tab:: pyproject.toml
+
+    **EXPERIMENTAL** [#experimental]_
+
+    .. code-block:: toml
+
+        # ...
+        [tool.setuptools.packages]
+        find = {}  # Scan the project directory with the default parameters
+
+        # OR
+        [tool.setuptools.packages.find]
+        where = ["src"]  # ["."] by default
+        include = ["mypackage*"]  # ["*"] by default
+        exclude = ["mypackage.tests*"]  # empty by default
+        namespaces = false  # true by default
 
 When you pass the above information, alongside other necessary information,
 ``setuptools`` walks through the directory specified in ``where`` (omitted
 here as the package resides in the current directory) and filters the packages
 it can find following the ``include``  (defaults to none), then removes
-those that match the ``exclude`` and returns a list of Python packages. Note
-that each entry in the ``[options.packages.find]`` is optional. The above
+those that match the ``exclude`` and returns a list of Python packages. The above
 setup also allows you to adopt a ``src/`` layout. For more details and advanced
 use, go to :ref:`package_discovery`.
 
+.. tip::
+   Starting with version 60.10.0, setuptools' automatic discovery capabilities
+   have been improved to detect popular project layouts (such as the
+   :ref:`flat-layout` and :ref:`src-layout`) without requiring any
+   special configuration. Check out our :ref:`reference docs `
+   for more information, but please keep in mind that this functionality is
+   still considered **experimental** and might change (or even be removed) in
+   future releases.
+
 
 Entry points and automatic script creation
 ===========================================
-- 
cgit v1.2.1


From 912cb9cbfd677d4ad0de21b16ad9906de9ebef60 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 17 Mar 2022 11:06:34 +0000
Subject: Improve entry-points section in the quickstart

---
 docs/userguide/quickstart.rst | 48 +++++++++++++++++++++++++++++++++----------
 1 file changed, 37 insertions(+), 11 deletions(-)

diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst
index 6219cd9a..13d881cf 100644
--- a/docs/userguide/quickstart.rst
+++ b/docs/userguide/quickstart.rst
@@ -188,21 +188,47 @@ use, go to :ref:`package_discovery`.
 Entry points and automatic script creation
 ===========================================
 Setuptools supports automatic creation of scripts upon installation, that runs
-code within your package if you specify them with the ``entry_points`` keyword.
+code within your package if you specify them as :doc:`entry points
+`.
 This is what allows you to run commands like ``pip install`` instead of having
-to type ``python -m pip install``. To accomplish this, add the entry_points
-keyword in your ``setup.cfg``:
+to type ``python -m pip install``.
+The following configuration examples show how to accomplish this:
 
-.. code-block:: ini
+.. tab:: setup.cfg
+
+    .. code-block:: ini
+
+        [options.entry_points]
+        console_scripts =
+            cli-name = mypkg:some_func
+
+.. tab:: setup.py
+
+    .. code-block:: python
+
+        setup(
+            # ...
+            entry_points={
+                'console_scripts': [
+                    'cli-name = mypkg:some_func',
+                ]
+            }
+        )
+
+.. tab:: pyproject.toml
+
+    **EXPERIMENTAL** [#experimental]_
+
+    .. code-block:: toml
 
-    [options.entry_points]
-    console_scripts =
-        main = mypkg:some_func
+       [project.scripts]
+       cli-name = mypkg:some_func
 
-When this project is installed, a ``main`` script will be installed and will
-invoke the ``some_func`` in the ``__init__.py`` file when called by the user.
-For detailed usage, including managing the additional or optional dependencies,
-go to :doc:`entry_point`.
+When this project is installed, a ``cli-name`` executable will be installed and will
+invoke the ``some_func`` in the ``mypkg/__init__.py`` file when called by the user.
+Note that you can also use the ``entry-points`` mechanism to advertise
+components between installed packages and implement plugin systems.
+For detailed usage, go to :doc:`entry_point`.
 
 
 Dependency management
-- 
cgit v1.2.1


From c627a367ea62245631ba203096bd5a961851a42e Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 17 Mar 2022 11:28:38 +0000
Subject: Improve dependencies section in quickstart

---
 docs/userguide/quickstart.rst | 57 +++++++++++++++++++++++++++++++------------
 1 file changed, 41 insertions(+), 16 deletions(-)

diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst
index 13d881cf..7a02d20b 100644
--- a/docs/userguide/quickstart.rst
+++ b/docs/userguide/quickstart.rst
@@ -233,27 +233,52 @@ For detailed usage, go to :doc:`entry_point`.
 
 Dependency management
 =====================
-``setuptools`` supports automatically installing dependencies when a package is
-installed. The simplest way to include requirement specifiers is to use the
-``install_requires`` argument to ``setup.cfg``.  It takes a string or list of
-strings containing requirement specifiers (A version specifier is one of the
-operators <, >, <=, >=, == or !=, followed by a version identifier):
+Packages built with ``setuptools`` can specify dependencies to be automatically
+installed when the package itself is installed.
+The example bellow show how to configure this kind of dependencies:
 
-.. code-block:: ini
+.. tab:: setup.cfg
 
-    [options]
-    install_requires =
-        docutils >= 0.3
-        requests <= 0.4
+    .. code-block:: ini
+
+        [options]
+        install_requires =
+            docutils
+            requests <= 0.4
+
+.. tab:: setup.py
+
+    .. code-block:: python
+
+        setup(
+            # ...
+            install_requires=["docutils", "requests <= 0.4"],
+            # ...
+        )
+
+.. tab:: pyproject.toml
+
+    **EXPERIMENTAL** [#experimental]_
+
+    [project]
+    # ...
+    dependencies = [
+        "docutils",
+        "requires <= 0.4",
+    ]
+    # ...
+
+Each dependency is represented a string that can optionally contain version requirements
+(e.g. one of the operators <, >, <=, >=, == or !=, followed by a version identifier),
+and/or conditional environment markers, e.g. ``os_name = "windows"``
+(see :doc:`PyPUG:specifications/version-specifiers` for more information).
 
 When your project is installed, all of the dependencies not already installed
 will be located (via PyPI), downloaded, built (if necessary), and installed.
-This, of course, is a simplified scenarios. ``setuptools`` also provides
-additional keywords such as ``setup_requires`` that allows you to install
-dependencies before running the script, and ``extras_require`` that take
-care of those needed by automatically generated scripts. It also provides
-mechanisms to handle dependencies that are not in PyPI. For more advanced use,
-see :doc:`dependency_management`.
+This, of course, is a simplified scenarios. You can also specify groups of
+extra dependencies that are not strictly required by your package to work, but
+that will provide additional functionalities.
+For more advanced use, see :doc:`dependency_management`.
 
 
 .. _Including Data Files:
-- 
cgit v1.2.1


From c4c8fd1e5704d22199026be12d4a7fe959295bd6 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 17 Mar 2022 11:32:48 +0000
Subject: Improve data files section in quickstart

---
 docs/userguide/quickstart.rst | 30 +++++++++++++++++++++++++++---
 1 file changed, 27 insertions(+), 3 deletions(-)

diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst
index 7a02d20b..f8e5cc64 100644
--- a/docs/userguide/quickstart.rst
+++ b/docs/userguide/quickstart.rst
@@ -290,10 +290,34 @@ are placed in a platform-specific location. Setuptools offers three ways to
 specify data files to be included in your packages. For the simplest use, you
 can simply use the ``include_package_data`` keyword:
 
-.. code-block:: ini
+.. tab:: setup.cfg
+
+    .. code-block:: ini
+
+        [options]
+        include_package_data = True
+
+.. tab:: setup.py
+
+    .. code-block:: python
+
+        setup(
+            # ...
+            include_package_data=True,
+            # ...
+        )
+
+.. tab:: pyproject.toml
+
+    **EXPERIMENTAL** [#experimental]_
+
+    .. code-block:: toml
 
-    [options]
-    include_package_data = True
+        [tool.setuptools]
+        include-package-data = true
+        # This is already the default behaviour if your are using
+        # pyproject.toml to configure your build.
+        # You can deactivate that with `include-package-data = false`
 
 This tells setuptools to install any data files it finds in your packages.
 The data files must be specified via the distutils' |MANIFEST.in|_ file
-- 
cgit v1.2.1


From 508ce521dd874c60a1bb440a5a5163ef5060ecdd Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 17 Mar 2022 12:09:22 +0000
Subject: Add note about editable installs in quickstart

---
 docs/userguide/quickstart.rst | 18 +++++++++++++++++-
 1 file changed, 17 insertions(+), 1 deletion(-)

diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst
index f8e5cc64..3075a045 100644
--- a/docs/userguide/quickstart.rst
+++ b/docs/userguide/quickstart.rst
@@ -331,7 +331,23 @@ Development mode
 
 .. tip::
 
-    Prior to :ref:`pip v21.1 `, a ``setup.py`` script was
+    For the time being you might need to keep a ``setup.py``
+    file in your repository if you want to use editable installs
+    (depending how the project is configured). A simple script will suffice,
+    for example:
+
+    .. code-block:: python
+
+        from setuptools import setup
+
+        setup()
+
+    You can still keep all the configuration in :doc:`setup.cfg `
+    (or :doc:`pyproject.toml `, a ``setup.py`` script was
     required to be compatible with development mode. With late
     versions of pip, any project may be installed in this mode.
 
-- 
cgit v1.2.1


From 630fc123b33eefadcabe005383634a21a6556cd5 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 17 Mar 2022 12:24:25 +0000
Subject: Clarify editable installs note in quickstart

---
 docs/userguide/quickstart.rst | 43 ++++++++++++++++++++++---------------------
 1 file changed, 22 insertions(+), 21 deletions(-)

diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst
index 3075a045..f23295aa 100644
--- a/docs/userguide/quickstart.rst
+++ b/docs/userguide/quickstart.rst
@@ -329,21 +329,16 @@ For more details, see :doc:`datafiles`.
 Development mode
 ================
 
-.. tip::
-
-    For the time being you might need to keep a ``setup.py``
-    file in your repository if you want to use editable installs
-    (depending how the project is configured). A simple script will suffice,
-    for example:
-
-    .. code-block:: python
-
-        from setuptools import setup
+``setuptools`` allows you to install a package without copying any files
+to your interpreter directory (e.g. the ``site-packages`` directory).
+This allows you to modify your source code and have the changes take
+effect without you having to rebuild and reinstall.
+Here's how to do it::
 
-        setup()
+    pip install --editable .
 
-    You can still keep all the configuration in :doc:`setup.cfg `
-    (or :doc:`pyproject.toml `, or have version of ``pip`` older than :ref:`v21.1 `,
+    you might need to keep a ``setup.py`` file in file in your repository if
+    you want to use editable installs (for the time being).
 
-    pip install --editable .
+    A simple script will suffice, for example:
 
-This creates a link file in your interpreter site package directory which
-associate with your source code. For more information, see :doc:`development_mode`.
+    .. code-block:: python
+
+        from setuptools import setup
+
+        setup()
+
+    You can still keep all the configuration in :doc:`setup.cfg `
+    (or :doc:`pyproject.toml 
Date: Thu, 17 Mar 2022 13:03:30 +0000
Subject: Improve notes on quickstart

---
 docs/userguide/quickstart.rst | 84 ++++++++++++++++++++++++-------------------
 1 file changed, 47 insertions(+), 37 deletions(-)

diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst
index f23295aa..9f3288d6 100644
--- a/docs/userguide/quickstart.rst
+++ b/docs/userguide/quickstart.rst
@@ -35,9 +35,14 @@ package your project:
     requires = ["setuptools"]
     build-backend = "setuptools.build_meta"
 
-Then, you will need to specify your package information (either via
-``setup.cfg``, ``setup.py`` or ``pyproject.toml``), such as metadata, contents,
-dependencies, etc. Here we demonstrate the minimum
+Then, you will need to specify your package information such as metadata,
+contents, dependencies, etc.
+
+Setuptools currently support configurations from either ``setup.cfg``,
+``setup.py`` or ``pyproject.toml`` [#experimental]_ files, however, configuring new
+projects via ``setup.py`` is discouraged [#setup.py]_.
+
+The following example demonstrates a minimum configuration:
 
 .. tab:: setup.cfg
 
@@ -55,7 +60,7 @@ dependencies, etc. Here we demonstrate the minimum
 
     See :doc:`/userguide/declarative_config` for more information.
 
-.. tab:: setup.py
+.. tab:: setup.py [#setup.py]_
 
     .. code-block:: python
 
@@ -73,9 +78,7 @@ dependencies, etc. Here we demonstrate the minimum
 
     See :doc:`/references/keywords` for more information.
 
-.. tab:: pyproject.toml
-
-    **EXPERIMENTAL** [#experimental]_
+.. tab:: pyproject.toml (**EXPERIMENTAL**) [#experimental]_
 
     .. code-block:: toml
 
@@ -117,9 +120,9 @@ 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. ``setuptools``
-therefore provides a convenient way to automatically list all the packages in
-your project directory:
+it can be a big burden to keep the package list updated.
+Therefore, ``setuptoops`` provides a convenient way to automatically list all
+the packages in your project directory:
 
 .. tab:: setup.cfg
 
@@ -135,9 +138,10 @@ your project directory:
         include=mypackage*
         exclude=mypackage.tests*
 
-.. tab:: setup.py
+.. tab:: setup.py [#setup.py]_
 
     .. code-block:: python
+
         from setuptools import find_packages  # or find_namespace_packages
 
         setup(
@@ -150,9 +154,7 @@ your project directory:
             # ...
         )
 
-.. tab:: pyproject.toml
-
-    **EXPERIMENTAL** [#experimental]_
+.. tab:: pyproject.toml (**EXPERIMENTAL**) [#experimental]_
 
     .. code-block:: toml
 
@@ -178,7 +180,7 @@ use, go to :ref:`package_discovery`.
 .. tip::
    Starting with version 60.10.0, setuptools' automatic discovery capabilities
    have been improved to detect popular project layouts (such as the
-   :ref:`flat-layout` and :ref:`src-layout`) without requiring any
+   :ref:`flat-layout` and :ref:`src-layout` layouts) without requiring any
    special configuration. Check out our :ref:`reference docs `
    for more information, but please keep in mind that this functionality is
    still considered **experimental** and might change (or even be removed) in
@@ -202,7 +204,7 @@ The following configuration examples show how to accomplish this:
         console_scripts =
             cli-name = mypkg:some_func
 
-.. tab:: setup.py
+.. tab:: setup.py [#setup.py]_
 
     .. code-block:: python
 
@@ -215,9 +217,7 @@ The following configuration examples show how to accomplish this:
             }
         )
 
-.. tab:: pyproject.toml
-
-    **EXPERIMENTAL** [#experimental]_
+.. tab:: pyproject.toml (**EXPERIMENTAL**) [#experimental]_
 
     .. code-block:: toml
 
@@ -246,7 +246,7 @@ The example bellow show how to configure this kind of dependencies:
             docutils
             requests <= 0.4
 
-.. tab:: setup.py
+.. tab:: setup.py [#setup.py]_
 
     .. code-block:: python
 
@@ -256,17 +256,17 @@ The example bellow show how to configure this kind of dependencies:
             # ...
         )
 
-.. tab:: pyproject.toml
+.. tab:: pyproject.toml (**EXPERIMENTAL**) [#experimental]_
 
-    **EXPERIMENTAL** [#experimental]_
+    .. code-block:: toml
 
-    [project]
-    # ...
-    dependencies = [
-        "docutils",
-        "requires <= 0.4",
-    ]
-    # ...
+        [project]
+        # ...
+        dependencies = [
+            "docutils",
+            "requires <= 0.4",
+        ]
+        # ...
 
 Each dependency is represented a string that can optionally contain version requirements
 (e.g. one of the operators <, >, <=, >=, == or !=, followed by a version identifier),
@@ -297,7 +297,7 @@ can simply use the ``include_package_data`` keyword:
         [options]
         include_package_data = True
 
-.. tab:: setup.py
+.. tab:: setup.py [#setup.py]_
 
     .. code-block:: python
 
@@ -307,9 +307,7 @@ can simply use the ``include_package_data`` keyword:
             # ...
         )
 
-.. tab:: pyproject.toml
-
-    **EXPERIMENTAL** [#experimental]_
+.. tab:: pyproject.toml (**EXPERIMENTAL**) [#experimental]_
 
     .. code-block:: toml
 
@@ -361,7 +359,7 @@ associate with your source code. For more information, see :doc:`development_mod
         setup()
 
     You can still keep all the configuration in :doc:`setup.cfg `
-    (or :doc:`pyproject.toml `).
 
 
 Uploading your package to PyPI
@@ -396,8 +394,20 @@ up-to-date references that can help you when it is time to distribute your work.
 
 ----
 
+.. rubric:: Notes
+
+.. [#setup.py]
+   The ``setup.py`` file should be used only when absolutely necessary.
+   Examples are kept in this document to help people interested in maintaining or
+   contributing to existing packages that use ``setup.py``.
+   Note that you can still keep most of configuration declarative in
+   :doc:`setup.cfg ` or :doc:`pyproject.toml
+   ` and use ``setup.py`` only for the parts not
+   supported in those files (e.g. C extensions).
 
 .. [#experimental]
-   Support for specifying package metadata and build configuration options via
-   ``pyproject.toml`` is experimental and might change (or be completely
-   removed) in the future. See :doc:`/userguide/pyproject_config`.
+   While the ``[build-system]`` table should always be specified in the
+   ``pyproject.toml`` file, adding package metadata and build configuration
+   options via the ``[project]`` and ``[tool.setuptools]`` tables is still
+   experimental and might change (or be completely removed) in future releases.
+   See :doc:`/userguide/pyproject_config`.
-- 
cgit v1.2.1


From 1a5e7ec7ac11ca702735d4947a51ae084a921c72 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 17 Mar 2022 13:10:16 +0000
Subject: Fix references to layouts in docs

---
 docs/userguide/package_discovery.rst | 91 +++++++++++++++++++-----------------
 1 file changed, 49 insertions(+), 42 deletions(-)

diff --git a/docs/userguide/package_discovery.rst b/docs/userguide/package_discovery.rst
index 8f2185da..70ef3538 100644
--- a/docs/userguide/package_discovery.rst
+++ b/docs/userguide/package_discovery.rst
@@ -137,59 +137,66 @@ its own set of advantages and disadvantages [#layout1]_ [#layout2]_.
 
 .. _src-layout:
 
-src-layout:
-    The project should contain a ``src`` directory under the project root and
-    all modules and packages meant for distribution are placed inside this
-    directory::
-
-        project_root_directory
-        ├── pyproject.toml
-        ├── setup.cfg  # or setup.py
-        ├── ...
-        └── src/
-            └── mypkg/
-                ├── __init__.py
-                ├── ...
-                └── mymodule.py
-
-    This layout is very handy when you wish to use automatic discovery,
-    since you don't have to worry about other Python files or folders in your
-    project root being distributed by mistake. In some circumstances it can be
-    also less error-prone for testing or when using :pep:`420`-style packages.
-    On the other hand you cannot rely on the implicit ``PYTHONPATH=.`` to fire
-    up the Python REPL and play with your package (you will need an
-    `editable install`_ to be able to do that).
+src-layout
+----------
+The project should contain a ``src`` directory under the project root and
+all modules and packages meant for distribution are placed inside this
+directory::
+
+    project_root_directory
+    ├── pyproject.toml
+    ├── setup.cfg  # or setup.py
+    ├── ...
+    └── src/
+        └── mypkg/
+            ├── __init__.py
+            ├── ...
+            └── mymodule.py
+
+This layout is very handy when you wish to use automatic discovery,
+since you don't have to worry about other Python files or folders in your
+project root being distributed by mistake. In some circumstances it can be
+also less error-prone for testing or when using :pep:`420`-style packages.
+On the other hand you cannot rely on the implicit ``PYTHONPATH=.`` to fire
+up the Python REPL and play with your package (you will need an
+`editable install`_ to be able to do that).
 
 .. _flat-layout:
 
-flat-layout (also known as "adhoc"):
-    The package folder(s) are placed directly under the project root::
+flat-layout
+-----------
+*(also known as "adhoc")*
+
+The package folder(s) are placed directly under the project root::
 
-        project_root_directory
-        ├── pyproject.toml
-        ├── setup.cfg  # or setup.py
+    project_root_directory
+    ├── pyproject.toml
+    ├── setup.cfg  # or setup.py
+    ├── ...
+    └── mypkg/
+        ├── __init__.py
         ├── ...
-        └── mypkg/
-            ├── __init__.py
-            ├── ...
-            └── mymodule.py
+        └── 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
-    of folders or Python files hanging around your project root)
+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
+of folders or Python files hanging around your project root)
 
 There is also a handy variation of the *flat-layout* for utilities/libraries
 that can be implemented with a single Python file:
 
-single-module approach (or "few top-level modules"):
-    Standalone modules are placed directly under the project root, instead of
-    inside a package folder::
+single-module approach
+----------------------
+*(or "few top-level modules")*
 
-        project_root_directory
-        ├── pyproject.toml
-        ├── setup.cfg  # or setup.py
-        ├── ...
-        └── single_file_lib.py
+Standalone modules are placed directly under the project root, instead of
+inside a package folder::
+
+    project_root_directory
+    ├── pyproject.toml
+    ├── setup.cfg  # or setup.py
+    ├── ...
+    └── single_file_lib.py
 
 Setuptools will automatically scan your project directory looking for these
 layouts and try to guess the correct values for the :ref:`packages 
Date: Thu, 17 Mar 2022 13:20:15 +0000
Subject: Add tab for pyproject.toml in dependency management docs

---
 docs/userguide/dependency_management.rst | 79 ++++++++++++++++++++++++++++++--
 1 file changed, 76 insertions(+), 3 deletions(-)

diff --git a/docs/userguide/dependency_management.rst b/docs/userguide/dependency_management.rst
index ea2fc556..d2b77762 100644
--- a/docs/userguide/dependency_management.rst
+++ b/docs/userguide/dependency_management.rst
@@ -69,6 +69,18 @@ finesse to it, let's start with a simple example.
             ],
         )
 
+.. tab:: pyproject.toml (**EXPERIMENTAL**) [#experimental]_
+
+    .. code-block:: toml
+
+        [project]
+        # ...
+        dependencies = [
+            "docutils",
+            "BazSpam == 1.1",
+        ]
+        # ...
+
 
 When your project is installed (e.g. using pip), all of the dependencies not
 already installed will be located (via PyPI), downloaded, built (if necessary),
@@ -104,6 +116,17 @@ the Python version is older than 3.4. To accomplish this
             ],
         )
 
+.. tab:: pyproject.toml (**EXPERIMENTAL**) [#experimental]_
+
+    .. code-block:: toml
+
+        [project]
+        # ...
+        dependencies = [
+            "enum34; python_version<'3.4'",
+        ]
+        # ...
+
 Similarly, if you also wish to declare ``pywin32`` with a minimal version of 1.0
 and only install it if the user is using a Windows operating system:
 
@@ -129,6 +152,18 @@ and only install it if the user is using a Windows operating system:
             ],
         )
 
+.. tab:: pyproject.toml (**EXPERIMENTAL**) [#experimental]_
+
+    .. code-block:: toml
+
+        [project]
+        # ...
+        dependencies = [
+            "enum34; python_version<'3.4'",
+            "pywin32 >= 1.0; platform_system=='Windows'",
+        ]
+        # ...
+
 The environmental markers that may be used for testing platform types are
 detailed in `PEP 508 `_.
 
@@ -249,6 +284,14 @@ dependencies for it to work:
             },
         )
 
+.. tab:: pyproject.toml (**EXPERIMENTAL**) [#experimental]_
+
+    .. code-block:: toml
+
+        # ...
+        [project.optional-dependencies]
+        PDF = ["ReportLab>=1.2", "RXP"]
+
 The name ``PDF`` is an arbitrary identifier of such a list of dependencies, to
 which other components can refer and have them installed. There are two common
 use cases.
@@ -319,6 +362,17 @@ installed, it might declare the dependency like this:
             ...,
         )
 
+.. tab:: pyproject.toml (**EXPERIMENTAL**) [#experimental]_
+
+    .. code-block:: toml
+
+        [project]
+        name = "Project-B"
+        # ...
+        dependencies = [
+            "Project-A[PDF]"
+        ]
+
 This will cause ReportLab to be installed along with project A, if project B is
 installed -- even if project A was already installed.  In this way, a project
 can encapsulate groups of optional "downstream dependencies" under a feature
@@ -338,9 +392,7 @@ not need to change, but the right packages will still be installed if needed.
 Python requirement
 ==================
 In some cases, you might need to specify the minimum required python version.
-This is handled with the ``python_requires`` keyword supplied to ``setup.cfg``
-or ``setup.py``.
-
+This can be configured as shown in the example bellow.
 
 .. tab:: setup.cfg
 
@@ -363,3 +415,24 @@ or ``setup.py``.
             python_requires=">=3.6",
             ...,
         )
+
+
+.. tab:: pyproject.toml (**EXPERIMENTAL**) [#experimental]_
+
+    .. code-block:: toml
+
+        [project]
+        name = "Project-B"
+        requires-python = ">=3.6"
+        # ...
+
+----
+
+.. rubric:: Notes
+
+.. [#experimental]
+   While the ``[build-system]`` table should always be specified in the
+   ``pyproject.toml`` file, adding package metadata and build configuration
+   options via the ``[project]`` and ``[tool.setuptools]`` tables is still
+   experimental and might change (or be completely removed) in future releases.
+   See :doc:`/userguide/pyproject_config`.
-- 
cgit v1.2.1


From 6f5c5575f6f78bebcdf78a67cc0f05ca999ae45a Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 17 Mar 2022 13:30:13 +0000
Subject: Clarify extras in entry-points are deprecated

---
 docs/userguide/dependency_management.rst | 93 +++++++++++++++++---------------
 docs/userguide/quickstart.rst            |  2 +-
 2 files changed, 50 insertions(+), 45 deletions(-)

diff --git a/docs/userguide/dependency_management.rst b/docs/userguide/dependency_management.rst
index d2b77762..85545b7c 100644
--- a/docs/userguide/dependency_management.rst
+++ b/docs/userguide/dependency_management.rst
@@ -293,49 +293,9 @@ dependencies for it to work:
         PDF = ["ReportLab>=1.2", "RXP"]
 
 The name ``PDF`` is an arbitrary identifier of such a list of dependencies, to
-which other components can refer and have them installed. There are two common
-use cases.
+which other components can refer and have them installed.
 
-First is the console_scripts entry point:
-
-.. tab:: setup.cfg
-
-    .. code-block:: ini
-
-        [metadata]
-        name = Project A
-        #...
-
-        [options]
-        #...
-        entry_points=
-            [console_scripts]
-            rst2pdf = project_a.tools.pdfgen [PDF]
-            rst2html = project_a.tools.htmlgen
-
-.. tab:: setup.py
-
-    .. code-block:: python
-
-        setup(
-            name="Project-A",
-            ...,
-            entry_points={
-                "console_scripts": [
-                    "rst2pdf = project_a.tools.pdfgen [PDF]",
-                    "rst2html = project_a.tools.htmlgen",
-                ],
-            },
-        )
-
-This syntax indicates that the entry point (in this case a console script)
-is only valid when the PDF extra is installed. It is up to the installer
-to determine how to handle the situation where PDF was not indicated
-(e.g. omit the console script, provide a warning when attempting to load
-the entry point, assume the extras are present and let the implementation
-fail later).
-
-The second use case is that other package can use this "extra" for their
+A use case for this approach is that other package can use this "extra" for their
 own dependencies. For example, if "Project-B" needs "project A" with PDF support
 installed, it might declare the dependency like this:
 
@@ -383,11 +343,56 @@ ReportLab in order to provide PDF support, Project B's setup information does
 not need to change, but the right packages will still be installed if needed.
 
 .. note::
-    Best practice: if a project ends up not needing any other packages to
+    Best practice: if a project ends up no longer needing any other packages to
     support a feature, it should keep an empty requirements list for that feature
     in its ``extras_require`` argument, so that packages depending on that feature
     don't break (due to an invalid feature name).
 
+Historically ``setuptools`` also used to support extra dependencies in console
+scripts, for example:
+
+.. tab:: setup.cfg
+
+    .. code-block:: ini
+
+        [metadata]
+        name = Project A
+        #...
+
+        [options]
+        #...
+        entry_points=
+            [console_scripts]
+            rst2pdf = project_a.tools.pdfgen [PDF]
+            rst2html = project_a.tools.htmlgen
+
+.. tab:: setup.py
+
+    .. code-block:: python
+
+        setup(
+            name="Project-A",
+            ...,
+            entry_points={
+                "console_scripts": [
+                    "rst2pdf = project_a.tools.pdfgen [PDF]",
+                    "rst2html = project_a.tools.htmlgen",
+                ],
+            },
+        )
+
+This syntax indicates that the entry point (in this case a console script)
+is only valid when the PDF extra is installed. It is up to the installer
+to determine how to handle the situation where PDF was not indicated
+(e.g. omit the console script, provide a warning when attempting to load
+the entry point, assume the extras are present and let the implementation
+fail later).
+
+.. warning::
+   ``pip`` and other tools might not support this use case for extra
+   dependencies, therefore this practice is considered **deprecated**.
+   See :doc:`PyPUG:specifications/entry-points`.
+
 
 Python requirement
 ==================
@@ -432,7 +437,7 @@ This can be configured as shown in the example bellow.
 
 .. [#experimental]
    While the ``[build-system]`` table should always be specified in the
-   ``pyproject.toml`` file, adding package metadata and build configuration
+   ``pyproject.toml`` file, support for adding package metadata and build configuration
    options via the ``[project]`` and ``[tool.setuptools]`` tables is still
    experimental and might change (or be completely removed) in future releases.
    See :doc:`/userguide/pyproject_config`.
diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst
index 9f3288d6..3e048574 100644
--- a/docs/userguide/quickstart.rst
+++ b/docs/userguide/quickstart.rst
@@ -407,7 +407,7 @@ up-to-date references that can help you when it is time to distribute your work.
 
 .. [#experimental]
    While the ``[build-system]`` table should always be specified in the
-   ``pyproject.toml`` file, adding package metadata and build configuration
+   ``pyproject.toml`` file, support for adding package metadata and build configuration
    options via the ``[project]`` and ``[tool.setuptools]`` tables is still
    experimental and might change (or be completely removed) in future releases.
    See :doc:`/userguide/pyproject_config`.
-- 
cgit v1.2.1


From 54acea6c19266f7ab9abb1aa91513dba6bcb1ebf Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 17 Mar 2022 13:38:48 +0000
Subject: Add notes to pyproject_config docs

---
 docs/userguide/pyproject_config.rst | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/docs/userguide/pyproject_config.rst b/docs/userguide/pyproject_config.rst
index 597bc33c..761a5677 100644
--- a/docs/userguide/pyproject_config.rst
+++ b/docs/userguide/pyproject_config.rst
@@ -186,6 +186,8 @@ Key               Directive           Notes
 
 ----
 
+.. rubric:: Notes
+
 .. [#entry-points] Dynamic ``scripts`` and ``gui-scripts`` are a special case.
    When resolving these metadata keys, ``setuptools`` will look for
    ``tool.setuptool.dynamic.entry-points``, and use the values of the
-- 
cgit v1.2.1


From 5013bfed2e66c9a7f523da28b8950373666acb81 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 17 Mar 2022 13:56:37 +0000
Subject: Apply suggestions from code review

Co-authored-by: Steven Silvester 
---
 docs/userguide/pyproject_config.rst | 2 +-
 docs/userguide/quickstart.rst       | 6 +++---
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/docs/userguide/pyproject_config.rst b/docs/userguide/pyproject_config.rst
index 761a5677..8753761d 100644
--- a/docs/userguide/pyproject_config.rst
+++ b/docs/userguide/pyproject_config.rst
@@ -117,7 +117,7 @@ consider alternatives.
    However if your project does not follow these conventional layouts
    (e.g. you want to use a ``flat-layout`` but at the same time have custom
    directories at the root of your project), you might need to use the ``find``
-   directive as shown bellow:
+   directive as shown below:
 
    .. code-block:: toml
 
diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst
index 3e048574..3ddb84aa 100644
--- a/docs/userguide/quickstart.rst
+++ b/docs/userguide/quickstart.rst
@@ -38,7 +38,7 @@ package your project:
 Then, you will need to specify your package information such as metadata,
 contents, dependencies, etc.
 
-Setuptools currently support configurations from either ``setup.cfg``,
+Setuptools currently supports configurations from either ``setup.cfg``,
 ``setup.py`` or ``pyproject.toml`` [#experimental]_ files, however, configuring new
 projects via ``setup.py`` is discouraged [#setup.py]_.
 
@@ -275,7 +275,7 @@ and/or conditional environment markers, e.g. ``os_name = "windows"``
 
 When your project is installed, all of the dependencies not already installed
 will be located (via PyPI), downloaded, built (if necessary), and installed.
-This, of course, is a simplified scenarios. You can also specify groups of
+This, of course, is a simplified scenario. You can also specify groups of
 extra dependencies that are not strictly required by your package to work, but
 that will provide additional functionalities.
 For more advanced use, see :doc:`dependency_management`.
@@ -376,7 +376,7 @@ To avoid executing arbitrary scripts and boilerplate code, we are transitioning
 into a full-fledged ``setup.cfg`` to declare your package information instead
 of running ``setup()``. This inevitably brings challenges due to a different
 syntax. :doc:`Here ` we provide a quick guide to
-understanding how ``setup.cfg`` is parsed by ``setuptool`` to ease the pain of
+understanding how ``setup.cfg`` is parsed by ``setuptools`` to ease the pain of
 transition.
 
 .. _packaging-resources:
-- 
cgit v1.2.1


From a2230a509bcd0f4c308ee59bd1eeac6def0e5d2b Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 11 Mar 2022 23:58:47 +0000
Subject: Attempt to re-enable Windows tests

According to a comment in pypa/distutils#118 this problem might be
solved by allowing tox to pass some environment variables.
---
 .github/workflows/main.yml | 2 +-
 tox.ini                    | 8 ++++++++
 2 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 5be824c1..c680fb36 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -24,7 +24,7 @@ jobs:
         platform:
         - ubuntu-latest
         - macos-latest
-        - windows-2019
+        - windows-latest
         include:
         - platform: ubuntu-latest
           python: "3.10"
diff --git a/tox.ini b/tox.ini
index a56ea24b..ca29dbbb 100644
--- a/tox.ini
+++ b/tox.ini
@@ -20,6 +20,10 @@ passenv =
 	windir  # required for test_pkg_resources
 	# honor git config in pytest-perf
 	HOME
+	# Microsoft's compiler suite (pypa/distutils#118)
+	PROGRAMDATA
+	PROGRAMFILES
+	PROGRAMFILES(x86)
 
 [testenv:integration]
 deps = {[testenv]deps}
@@ -27,6 +31,10 @@ extras = testing-integration
 passenv =
 	{[testenv]passenv}
 	DOWNLOAD_PATH
+	# Microsoft's compiler suite (pypa/distutils#118)
+	PROGRAMDATA
+	PROGRAMFILES
+	PROGRAMFILES(x86)
 setenv =
     PROJECT_ROOT = {toxinidir}
 commands =
-- 
cgit v1.2.1


From 0739ae06e29986bf438c6cd38c8a85fe1d93636c Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 17 Mar 2022 17:55:00 +0000
Subject: Small doc improvements

---
 docs/userguide/pyproject_config.rst | 31 ++++++++++++++++---------------
 docs/userguide/quickstart.rst       |  2 +-
 2 files changed, 17 insertions(+), 16 deletions(-)

diff --git a/docs/userguide/pyproject_config.rst b/docs/userguide/pyproject_config.rst
index 8753761d..2f1b9146 100644
--- a/docs/userguide/pyproject_config.rst
+++ b/docs/userguide/pyproject_config.rst
@@ -72,9 +72,10 @@ While the standard ``project`` table in the ``pyproject.toml`` file covers most
 of the metadata used during the packaging process, there are still some
 ``setuptools``-specific configurations that can be set by users that require
 customization.
-These configurations are completely optional (and probably can be skipped when
-creating simple packages) and can be set via the ``tool.setuptools`` table.
-They are equivalent to the :doc:`/references/keywords` used by the ``setup.py`` file:
+These configurations are completely optional and probably can be skipped when
+creating simple packages.
+They are equivalent to the :doc:`/references/keywords` used by the ``setup.py``
+file, and can be set via the ``tool.setuptools`` table:
 
 ========================= =========================== =========================
 Key                       Value Type (TOML)           Notes
@@ -99,9 +100,9 @@ Key                       Value Type (TOML)           Notes
 ``obsoletes``             array                       **Ignored by pip**
 ========================= =========================== =========================
 
-In the table above, the `TOML value types`_ ``array`` and
-``table/inline-table`` are roughly equivalent to the Python's :obj:`dict` and
-:obj:`list` data types.
+.. note::
+   The `TOML value types`_ ``array`` and ``table/inline-table`` are roughly
+   equivalent to the Python's :obj:`dict` and :obj:`list` data types.
 
 Please note that some of these configurations are deprecated or at least
 discouraged, but they are made available to ensure portability.
@@ -112,7 +113,7 @@ consider alternatives.
    When both ``py-modules`` and ``packages`` are left unspecified,
    ``setuptools`` will attempt to perform :ref:`auto-discovery`, which should
    cover most popular project directory organization techniques, such as the
-   :ref:`src ` and :ref:`flat ` layouts.
+   :ref:`src-layout` and the :ref:`flat-layout`.
 
    However if your project does not follow these conventional layouts
    (e.g. you want to use a ``flat-layout`` but at the same time have custom
@@ -146,15 +147,15 @@ Dynamic Metadata
 ================
 
 Note that in the first example of this page we use ``dynamic`` to identify
-which metadata fields are dynamically calculated during the build by either
-``setuptools`` itself or the selected plugins (e.g. ``setuptools-scm`` is
-capable of deriving the current project version directly from the ``git``
-:wiki:`version control` system).
+which metadata fields are dynamically computed during the build by either
+``setuptools`` itself or the plugins installed via ``build-system.requires``
+(e.g. ``setuptools-scm`` is capable of deriving the current project version
+directly from the ``git`` :wiki:`version control` system).
 
-Currently the following fields can be used dynamically: ``version``,
+Currently the following fields can be listed as dynamic: ``version``,
 ``classifiers``, ``description``, ``entry-points``, ``scripts``,
 ``gui-scripts`` and ``readme``.
-When these fields are expected to be directly provided by ``setuptools`` a
+When these fields are expected to be provided by ``setuptools`` a
 corresponding entry is required in the ``tool.setuptools.dynamic`` table
 [#entry-points]_. For example:
 
@@ -170,8 +171,8 @@ corresponding entry is required in the ``tool.setuptools.dynamic`` table
    readme = {file = ["README.rst", "USAGE.rst"]}
 
 In the ``dynamic`` table, the ``attr`` directive will read an attribute from
-the given module [#attr]_, while ``file`` will read all the given files and
-concatenate them in a single string.
+the given module [#attr]_, while ``file`` will read the contents of all given
+files and concatenate them in a single string.
 
 ================= =================== =========================
 Key               Directive           Notes
diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst
index 3ddb84aa..276aaf73 100644
--- a/docs/userguide/quickstart.rst
+++ b/docs/userguide/quickstart.rst
@@ -180,7 +180,7 @@ use, go to :ref:`package_discovery`.
 .. tip::
    Starting with version 60.10.0, setuptools' automatic discovery capabilities
    have been improved to detect popular project layouts (such as the
-   :ref:`flat-layout` and :ref:`src-layout` layouts) without requiring any
+   :ref:`flat-layout` and :ref:`src-layout`) without requiring any
    special configuration. Check out our :ref:`reference docs `
    for more information, but please keep in mind that this functionality is
    still considered **experimental** and might change (or even be removed) in
-- 
cgit v1.2.1


From 089fed393d27bbe010c4a86ed27f2fe80fe2b20a Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 17 Mar 2022 18:08:23 +0000
Subject: Clarify directives in the context of pyproject.toml

---
 docs/userguide/pyproject_config.rst | 16 ++++++++++++----
 1 file changed, 12 insertions(+), 4 deletions(-)

diff --git a/docs/userguide/pyproject_config.rst b/docs/userguide/pyproject_config.rst
index 2f1b9146..45153c34 100644
--- a/docs/userguide/pyproject_config.rst
+++ b/docs/userguide/pyproject_config.rst
@@ -118,7 +118,7 @@ consider alternatives.
    However if your project does not follow these conventional layouts
    (e.g. you want to use a ``flat-layout`` but at the same time have custom
    directories at the root of your project), you might need to use the ``find``
-   directive as shown below:
+   directive [#directives]_ as shown below:
 
    .. code-block:: toml
 
@@ -170,9 +170,9 @@ corresponding entry is required in the ``tool.setuptools.dynamic`` table
    version = {attr = "my_package.VERSION"}
    readme = {file = ["README.rst", "USAGE.rst"]}
 
-In the ``dynamic`` table, the ``attr`` directive will read an attribute from
-the given module [#attr]_, while ``file`` will read the contents of all given
-files and concatenate them in a single string.
+In the ``dynamic`` table, the ``attr`` directive [#directives]_ will read an
+attribute from the given module [#attr]_, while ``file`` will read the contents
+of all given files and concatenate them in a single string.
 
 ================= =================== =========================
 Key               Directive           Notes
@@ -195,6 +195,14 @@ Key               Directive           Notes
    ``console_scripts`` and ``gui_scripts`` :doc:`entry-point groups
    `.
 
+.. [#directives] In the context of this document, *directives* are special TOML
+   values that are interpreted differently by ``setuptools`` (usually triggering an
+   associated function). Most of the times they correspond to a special TOML table
+   (or inline-table) with a single top-level key.
+   For example, you can have the ``{find = {where = ["src"], exclude=["tests*"]}}``
+   directive for ``tool.setuptools.packages``, or ``{attr = "mymodule.attr"}``
+   directive for ``tool.setuptools.dynamic.version``.
+
 .. [#attr] ``attr`` is meant to be used when the module attribute is statically
    specified (e.g. as a string, list or tuple). As a rule of thumb, the
    attribute should be able to be parsed with :func:`ast.literal_eval`, and
-- 
cgit v1.2.1


From 35421e774b2cb3a6c0572951d2505092e6173bad Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 17 Mar 2022 18:49:39 +0000
Subject: Add tests for dynamic classifiers on pyproject.toml

---
 setuptools/tests/config/test_pyprojecttoml.py | 44 +++++++++++++++++++++++----
 1 file changed, 38 insertions(+), 6 deletions(-)

diff --git a/setuptools/tests/config/test_pyprojecttoml.py b/setuptools/tests/config/test_pyprojecttoml.py
index 235876f0..a2b9da52 100644
--- a/setuptools/tests/config/test_pyprojecttoml.py
+++ b/setuptools/tests/config/test_pyprojecttoml.py
@@ -5,7 +5,11 @@ from inspect import cleandoc
 import pytest
 import tomli_w
 
-from setuptools.config.pyprojecttoml import read_configuration, expand_configuration
+from setuptools.config.pyprojecttoml import (
+    read_configuration,
+    expand_configuration,
+    validate,
+)
 
 EXAMPLE = """
 [project]
@@ -75,7 +79,7 @@ def create_example(path, pkg_root):
     files = [
         f"{pkg_root}/pkg/__init__.py",
         f"{pkg_root}/other/nested/__init__.py",  # ensure namespaces are discovered
-        "_files/file.txt"
+        "_files/file.txt",
     ]
     for file in files:
         (path / file).parent.mkdir(exist_ok=True, parents=True)
@@ -126,7 +130,7 @@ def test_read_configuration(tmp_path):
         (".", {}),
         ("src", {}),
         ("lib", {"packages": {"find": {"where": ["lib"]}}}),
-    ]
+    ],
 )
 def test_discovered_package_dir_with_attr_directive_in_config(tmp_path, pkg_root, opts):
     create_example(tmp_path, pkg_root)
@@ -177,6 +181,34 @@ def test_expand_entry_point(tmp_path):
     assert "gui-scripts" not in expanded_project
 
 
+def test_dynamic_classifiers(tmp_path):
+    # Let's create a project example that has dynamic classifiers
+    # coming from a txt file.
+    create_example(tmp_path, "src")
+    classifiers = """\
+    Framework :: Flask
+    Programming Language :: Haskell
+    """
+    (tmp_path / "classifiers.txt").write_text(cleandoc(classifiers))
+
+    pyproject = tmp_path / "pyproject.toml"
+    config = read_configuration(pyproject, expand=False)
+    dynamic = config["project"]["dynamic"]
+    config["project"]["dynamic"] = list({*dynamic, "classifiers"})
+    dynamic_config = config["tool"]["setuptools"]["dynamic"]
+    dynamic_config["classifiers"] = {"file": "classifiers.txt"}
+
+    # When the configuration is expanded,
+    # each line of the file should be an different classifier.
+    validate(config, pyproject)
+    expanded = expand_configuration(config, tmp_path)
+
+    assert set(expanded["project"]["classifiers"]) == {
+        "Framework :: Flask",
+        "Programming Language :: Haskell",
+    }
+
+
 @pytest.mark.parametrize(
     "example",
     (
@@ -188,7 +220,7 @@ def test_expand_entry_point(tmp_path):
         [my-tool.that-disrespect.pep518]
         value = 42
         """,
-    )
+    ),
 )
 def test_ignore_unrelated_config(tmp_path, example):
     pyproject = tmp_path / "pyproject.toml"
@@ -209,9 +241,9 @@ def test_ignore_unrelated_config(tmp_path, example):
             requires = ['pywin32; platform_system=="Windows"' ]
             """,
             "configuration error: `project` must not contain {'requires'} properties",
-            '"requires": ["pywin32; platform_system==\\"Windows\\""]'
+            '"requires": ["pywin32; platform_system==\\"Windows\\""]',
         ),
-    ]
+    ],
 )
 def test_invalid_example(tmp_path, caplog, example, error_msg, value_shown_in_debug):
     caplog.set_level(logging.DEBUG)
-- 
cgit v1.2.1


From e24e6e91b0f7ae117bf7a9db945cbc4a6ff59cc3 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 17 Mar 2022 18:50:12 +0000
Subject: Split lines for dynamic classifiers in pyproject.toml

---
 setuptools/config/pyprojecttoml.py | 21 ++++++++++++++++-----
 1 file changed, 16 insertions(+), 5 deletions(-)

diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index a4a54061..4ba234f8 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -214,18 +214,25 @@ def _expand_all_dynamic(
 ):
     silent = ignore_option_errors
     dynamic_cfg = setuptools_cfg.get("dynamic", {})
-    package_dir = setuptools_cfg["package-dir"]
-    special = ("readme", "version", "entry-points", "scripts", "gui-scripts")
+    pkg_dir = setuptools_cfg["package-dir"]
+    special = (
+        "readme",
+        "version",
+        "entry-points",
+        "scripts",
+        "gui-scripts",
+        "classifiers",
+    )
     # readme, version and entry-points need special handling
     dynamic = project_cfg.get("dynamic", [])
     regular_dynamic = (x for x in dynamic if x not in special)
 
     for field in regular_dynamic:
-        value = _expand_dynamic(dynamic_cfg, field, package_dir, root_dir, silent)
+        value = _expand_dynamic(dynamic_cfg, field, pkg_dir, root_dir, silent)
         project_cfg[field] = value
 
     if "version" in dynamic and "version" in dynamic_cfg:
-        version = _expand_dynamic(dynamic_cfg, "version", package_dir, root_dir, silent)
+        version = _expand_dynamic(dynamic_cfg, "version", pkg_dir, root_dir, silent)
         project_cfg["version"] = _expand.version(version)
 
     if "readme" in dynamic:
@@ -233,9 +240,13 @@ def _expand_all_dynamic(
 
     if "entry-points" in dynamic:
         field = "entry-points"
-        value = _expand_dynamic(dynamic_cfg, field, package_dir, root_dir, silent)
+        value = _expand_dynamic(dynamic_cfg, field, pkg_dir, root_dir, silent)
         project_cfg.update(_expand_entry_points(value, dynamic))
 
+    if "classifiers" in dynamic:
+        value = _expand_dynamic(dynamic_cfg, "classifiers", pkg_dir, root_dir, silent)
+        project_cfg["classifiers"] = value.splitlines()
+
 
 def _expand_dynamic(
     dynamic_cfg: dict,
-- 
cgit v1.2.1


From 585553fba189850935c4e3e0971b68b5a94765c3 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 18 Mar 2022 11:04:25 +0000
Subject: Add expectations about multiple packages for discovery

---
 setuptools/errors.py                      | 40 ++++++++++++++++++++++---------
 setuptools/tests/test_config_discovery.py | 40 +++++++++++++++++++++++++++++++
 2 files changed, 69 insertions(+), 11 deletions(-)

diff --git a/setuptools/errors.py b/setuptools/errors.py
index f4d35a63..ec7fb3b6 100644
--- a/setuptools/errors.py
+++ b/setuptools/errors.py
@@ -4,17 +4,6 @@ Provides exceptions used by setuptools modules.
 """
 
 from distutils import errors as _distutils_errors
-from distutils.errors import DistutilsError
-
-
-class RemovedCommandError(DistutilsError, RuntimeError):
-    """Error used for commands that have been removed in setuptools.
-
-    Since ``setuptools`` is built on ``distutils``, simply removing a command
-    from ``setuptools`` will make the behavior fall back to ``distutils``; this
-    error is raised if a command exists in ``distutils`` but has been actively
-    removed in ``setuptools``.
-    """
 
 
 # Re-export errors from distutils to facilitate the migration to PEP632
@@ -38,3 +27,32 @@ UnknownFileError = _distutils_errors.UnknownFileError
 
 # The root error class in the hierarchy
 BaseError = _distutils_errors.DistutilsError
+
+
+class RemovedCommandError(BaseError, RuntimeError):
+    """Error used for commands that have been removed in setuptools.
+
+    Since ``setuptools`` is built on ``distutils``, simply removing a command
+    from ``setuptools`` will make the behavior fall back to ``distutils``; this
+    error is raised if a command exists in ``distutils`` but has been actively
+    removed in ``setuptools``.
+    """
+
+
+class PackageDiscoveryError(BaseError, RuntimeError):
+    """Impossible to perform automatic discovery of packages and/or modules.
+
+    The current project layout or given discovery options can lead to problems when
+    scanning the project directory.
+
+    Setuptools might also refuse to complete auto-discovery if an error prone condition
+    is detected (e.g. when a project is organised as a flat-layout but contains
+    multiple directories that can be taken as top-level packages inside a single
+    distribution [*]_). In these situations the users are encouraged to be explicit
+    about which packages to include or to make the discovery parameters more specific.
+
+    .. [*] Since multi-package distributions are uncommon it is very likely that the
+       developers did not intend for all the directories to be packaged, and are just
+       leaving auxiliary code in the repository top-level, such as maintenance-related
+       scripts.
+    """
diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py
index d60513e3..5249ed53 100644
--- a/setuptools/tests/test_config_discovery.py
+++ b/setuptools/tests/test_config_discovery.py
@@ -6,6 +6,7 @@ from itertools import product
 from setuptools.command.sdist import sdist
 from setuptools.dist import Distribution
 from setuptools.discovery import find_package_path
+from setuptools.errors import PackageDiscoveryError
 
 import pytest
 from path import Path as _Path
@@ -152,6 +153,45 @@ class TestDiscoverPackagesAndPyModules:
             name = file.replace("src/", "")
             assert name not in wheel_files
 
+    @pytest.mark.parametrize(
+        "extra_files, pkgs",
+        [
+            (["venv/bin/simulate_venv"], {"pkg"}),
+            (["pkg-stubs/__init__.pyi"], {"pkg", "pkg-stubs"}),
+            (["other-stubs/__init__.pyi"], {"pkg", "other-stubs"}),
+            (
+                # Type stubs can also be namespaced
+                ["namespace-stubs/pkg/__init__.pyi"],
+                {"pkg", "namespace-stubs", "namespace-stubs.pkg"},
+            ),
+            (
+                # Just the top-level package can have `-stubs`, ignore nested ones
+                ["namespace-stubs/pkg-stubs/__init__.pyi"],
+                {"pkg", "namespace-stubs"}
+            ),
+            (["_hidden/file.py"], {"pkg"}),
+            (["news/finalize.py"], {"pkg"}),
+        ]
+    )
+    def test_flat_layout_with_extra_dirs(self, tmp_path, extra_files, pkgs):
+        files = self.FILES["flat"] + extra_files
+        _populate_project_dir(tmp_path, files, {})
+        dist, _ = _run_sdist_programatically(tmp_path, {})
+        assert set(dist.packages) == pkgs
+
+    @pytest.mark.parametrize(
+        "extra_files",
+        [
+            ["other/__init__.py"],
+            ["other/finalize.py"],
+        ]
+    )
+    def test_flat_layout_with_dangerous_extra_dirs(self, tmp_path, extra_files):
+        files = self.FILES["flat"] + extra_files
+        _populate_project_dir(tmp_path, files, {})
+        with pytest.raises(PackageDiscoveryError):
+            _run_sdist_programatically(tmp_path, {})
+
 
 class TestNoConfig:
     DEFAULT_VERSION = "0.0.0"  # Default version given by setuptools
-- 
cgit v1.2.1


From fd66feb094cc9885265fecae4c7b37911e880702 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 18 Mar 2022 14:47:30 +0000
Subject: Add other names to the list of excluded packages for auto-discovery

---
 setuptools/discovery.py | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/setuptools/discovery.py b/setuptools/discovery.py
index 1d1b3814..f15ebd6f 100644
--- a/setuptools/discovery.py
+++ b/setuptools/discovery.py
@@ -183,15 +183,24 @@ class FlatLayoutPackageFinder(PEP420PackageFinder):
         "doc",
         "docs",
         "documentation",
+        "manpages",
+        "news",
+        "changelog",
         "test",
         "tests",
+        "unit_test",
+        "unit_tests",
         "example",
         "examples",
         "scripts",
         "tools",
+        "util",
+        "utils",
         "build",
         "dist",
         "venv",
+        "env",
+        "requirements",
         # ---- Task runners / Build tools ----
         "tasks",  # invoke
         "fabfile",  # fabric
-- 
cgit v1.2.1


From 5e12f26fee168f2fe5168397757b54c433e6ff5d Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 18 Mar 2022 15:08:26 +0000
Subject: Allow type stubs for FlatLayoutPackageFinder

---
 setuptools/discovery.py | 15 +++++++++++----
 1 file changed, 11 insertions(+), 4 deletions(-)

diff --git a/setuptools/discovery.py b/setuptools/discovery.py
index f15ebd6f..8c7f506c 100644
--- a/setuptools/discovery.py
+++ b/setuptools/discovery.py
@@ -131,7 +131,7 @@ class PackageFinder(_Finder):
                 package = rel_path.replace(os.path.sep, '.')
 
                 # Skip directory trees that are not valid packages
-                if '.' in dir or not cls._looks_like_package(full_path):
+                if '.' in dir or not cls._looks_like_package(full_path, package):
                     continue
 
                 # Should this package be included?
@@ -143,14 +143,14 @@ class PackageFinder(_Finder):
                 dirs.append(dir)
 
     @staticmethod
-    def _looks_like_package(path):
+    def _looks_like_package(path, _package_name):
         """Does a directory look like a package?"""
         return os.path.isfile(os.path.join(path, '__init__.py'))
 
 
 class PEP420PackageFinder(PackageFinder):
     @staticmethod
-    def _looks_like_package(path):
+    def _looks_like_package(path, _package_name):
         return True
 
 
@@ -212,7 +212,14 @@ class FlatLayoutPackageFinder(PEP420PackageFinder):
     DEFAULT_EXCLUDE = tuple(chain_iter((p, f"{p}.*") for p in _EXCLUDE))
     """Reserved package names"""
 
-    _looks_like_package = staticmethod(_valid_name)
+    @staticmethod
+    def _looks_like_package(path, package_name):
+        names = package_name.split('.')
+        return names and (
+            # Consider PEP 561
+            (names[0].isidentifier() or names[0].endswith("-stubs"))
+            and all(name.isidentifier() for name in names[1:])
+        )
 
 
 class FlatLayoutModuleFinder(ModuleFinder):
-- 
cgit v1.2.1


From a8bcac8b0715213828adc275691915b1b84d0e3b Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 18 Mar 2022 15:41:13 +0000
Subject: Refactor function for finding top-level packages in auto-discovery

---
 setuptools/config/expand.py               | 21 +++------------------
 setuptools/discovery.py                   | 30 +++++++++++++++++++++++++++++-
 setuptools/tests/test_config_discovery.py | 18 +++++++++++++++++-
 3 files changed, 49 insertions(+), 20 deletions(-)

diff --git a/setuptools/config/expand.py b/setuptools/config/expand.py
index b12b263d..694476a0 100644
--- a/setuptools/config/expand.py
+++ b/setuptools/config/expand.py
@@ -288,6 +288,8 @@ def find_packages(
     :rtype: list
     """
 
+    from setuptools.discovery import remove_nested_packages
+
     if namespaces:
         from setuptools.discovery import PEP420PackageFinder as PackageFinder
     else:
@@ -304,30 +306,13 @@ def find_packages(
         pkgs = PackageFinder.find(_nest_path(root_dir, path), **kwargs)
         packages.extend(pkgs)
         if fill_package_dir.get("") != path:
-            parent_pkgs = _parent_packages(pkgs)
+            parent_pkgs = remove_nested_packages(pkgs)
             parent = {pkg: "/".join([path, *pkg.split(".")]) for pkg in parent_pkgs}
             fill_package_dir.update(parent)
 
     return packages
 
 
-def _parent_packages(packages: List[str]) -> List[str]:
-    """Remove children packages from the list
-    >>> _parent_packages(["a", "a.b1", "a.b2", "a.b1.c1"])
-    ['a']
-    >>> _parent_packages(["a", "b", "c.d", "c.d.e.f", "g.h", "a.a1"])
-    ['a', 'b', 'c.d', 'g.h']
-    """
-    pkgs = sorted(packages, key=len)
-    top_level = pkgs[:]
-    size = len(pkgs)
-    for i, name in enumerate(reversed(pkgs)):
-        if any(name.startswith(f"{other}.") for other in top_level):
-            top_level.pop(size - i - 1)
-
-    return top_level
-
-
 def _nest_path(parent: _Path, path: _Path) -> str:
     path = parent if path == "." else os.path.join(parent, path)
     return os.path.normpath(path)
diff --git a/setuptools/discovery.py b/setuptools/discovery.py
index 8c7f506c..7d80a26c 100644
--- a/setuptools/discovery.py
+++ b/setuptools/discovery.py
@@ -394,13 +394,32 @@ class ConfigDiscovery:
         return None
 
 
+def remove_nested_packages(packages: List[str]) -> List[str]:
+    """Remove nested packages from the list of packages.
+
+    >>> remove_nested_packages(["a", "a.b1", "a.b2", "a.b1.c1"])
+    ['a']
+    >>> remove_nested_packages(["a", "b", "c.d", "c.d.e.f", "g.h", "a.a1"])
+    ['a', 'b', 'c.d', 'g.h']
+    """
+    pkgs = sorted(packages, key=len)
+    top_level = pkgs[:]
+    size = len(pkgs)
+    for i, name in enumerate(reversed(pkgs)):
+        if any(name.startswith(f"{other}.") for other in top_level):
+            top_level.pop(size - i - 1)
+
+    return top_level
+
+
 def find_parent_package(
     packages: List[str], package_dir: Dict[str, str], root_dir: _Path
 ) -> Optional[str]:
+    """Find the parent package that is not a namespace."""
     packages = sorted(packages, key=len)
     common_ancestors = []
     for i, name in enumerate(packages):
-        if not all(n.startswith(name) for n in packages[i+1:]):
+        if not all(n.startswith(f"{name}.") for n in packages[i+1:]):
             # Since packages are sorted by length, this condition is able
             # to find a list of all common ancestors.
             # When there is divergence (e.g. multiple root packages)
@@ -420,6 +439,15 @@ def find_parent_package(
 def find_package_path(name: str, package_dir: Dict[str, str], root_dir: _Path) -> str:
     """Given a package name, return the path where it should be found on
     disk, considering the ``package_dir`` option.
+
+    >>> find_package_path("my.pkg", {"": "root/is/nested"}, ".")
+    './root/is/nested/my/pkg'
+    >>> find_package_path("my.pkg", {"my": "root/is/nested"}, ".")
+    './root/is/nested/pkg'
+    >>> find_package_path("my.pkg", {"my.pkg": "root/is/nested"}, ".")
+    './root/is/nested'
+    >>> find_package_path("other.pkg", {"my.pkg": "root/is/nested"}, ".")
+    './other/pkg'
     """
     parts = name.split(".")
     for i in range(len(parts), 0, -1):
diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py
index 5249ed53..92cc0d79 100644
--- a/setuptools/tests/test_config_discovery.py
+++ b/setuptools/tests/test_config_discovery.py
@@ -5,7 +5,7 @@ from itertools import product
 
 from setuptools.command.sdist import sdist
 from setuptools.dist import Distribution
-from setuptools.discovery import find_package_path
+from setuptools.discovery import find_package_path, find_parent_package
 from setuptools.errors import PackageDiscoveryError
 
 import pytest
@@ -16,6 +16,22 @@ from .integration.helpers import get_sdist_members, get_wheel_members, run
 from .textwrap import DALS
 
 
+def test_find_parent_package(tmp_path):
+    (tmp_path / "src/namespace/pkg/nested").mkdir(exist_ok=True, parents=True)
+    (tmp_path / "src/namespace/pkg/nested/__init__.py").touch()
+    (tmp_path / "src/namespace/pkg/__init__.py").touch()
+    packages = ["namespace", "namespace.pkg", "namespace.pkg.nested"]
+    assert find_parent_package(packages, {"": "src"}, tmp_path) == "namespace.pkg"
+
+
+def test_find_parent_package_multiple_toplevel(tmp_path):
+    multiple = ["pkg", "pkg1", "pkg2"]
+    for name in multiple:
+        (tmp_path / f"src/{name}").mkdir(exist_ok=True, parents=True)
+        (tmp_path / f"src/{name}/__init__.py").touch()
+    assert find_parent_package(multiple, {"": "src"}, tmp_path) is None
+
+
 class TestDiscoverPackagesAndPyModules:
     """Make sure discovered values for ``packages`` and ``py_modules`` work
     similarly to explicit configuration for the simple scenarios.
-- 
cgit v1.2.1


From ab64032d062b9c093ea0c9bfa5f7fd79e8b774e5 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 18 Mar 2022 17:45:46 +0000
Subject: Add function to remove stubs from a list of packages

---
 setuptools/discovery.py | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/setuptools/discovery.py b/setuptools/discovery.py
index 7d80a26c..ada877db 100644
--- a/setuptools/discovery.py
+++ b/setuptools/discovery.py
@@ -395,7 +395,7 @@ class ConfigDiscovery:
 
 
 def remove_nested_packages(packages: List[str]) -> List[str]:
-    """Remove nested packages from the list of packages.
+    """Remove nested packages from a list of packages.
 
     >>> remove_nested_packages(["a", "a.b1", "a.b2", "a.b1.c1"])
     ['a']
@@ -412,6 +412,15 @@ def remove_nested_packages(packages: List[str]) -> List[str]:
     return top_level
 
 
+def remove_stubs(packages: List[str]) -> List[str]:
+    """Remove type stubs from a list of packages.
+
+    >>> remove_stubs(["a", "a.b", "a-stubs", "a-stubs.b.c", "b", "c-stubs"])
+    ['a', 'a.b', 'b']
+    """
+    return [pkg for pkg in packages if not pkg.split(".")[0].endswith("-stubs")]
+
+
 def find_parent_package(
     packages: List[str], package_dir: Dict[str, str], root_dir: _Path
 ) -> Optional[str]:
-- 
cgit v1.2.1


From d73b4446cdf5eeabca6dcc26e0de50a4b290c7c3 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 18 Mar 2022 17:55:21 +0000
Subject: Remove stubs when trying name auto-discovery

---
 setuptools/discovery.py                   | 2 +-
 setuptools/tests/test_config_discovery.py | 5 ++++-
 2 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/setuptools/discovery.py b/setuptools/discovery.py
index ada877db..e0c406e3 100644
--- a/setuptools/discovery.py
+++ b/setuptools/discovery.py
@@ -382,7 +382,7 @@ class ConfigDiscovery:
         if not self.dist.packages:
             return None
 
-        packages = sorted(self.dist.packages, key=len)
+        packages = remove_stubs(sorted(self.dist.packages, key=len))
         package_dir = self.dist.package_dir or {}
 
         parent_pkg = find_parent_package(packages, package_dir, self._root_dir)
diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py
index 92cc0d79..80553175 100644
--- a/setuptools/tests/test_config_discovery.py
+++ b/setuptools/tests/test_config_discovery.py
@@ -215,7 +215,10 @@ class TestNoConfig:
     EXAMPLES = {
         "pkg1": ["src/pkg1.py"],
         "pkg2": ["src/pkg2/__init__.py"],
-        "ns.nested.pkg3": ["src/ns/nested/pkg3/__init__.py"]
+        "pkg3": ["src/pkg3/__init__.py", "src/pkg3-stubs/__init__.py"],
+        "pkg4": ["pkg4/__init__.py", "pkg4-stubs/__init__.py"],
+        "ns.nested.pkg1": ["src/ns/nested/pkg1/__init__.py"],
+        "ns.nested.pkg2": ["ns/nested/pkg2/__init__.py"],
     }
 
     @pytest.mark.parametrize("example", EXAMPLES.keys())
-- 
cgit v1.2.1


From 5d30507883d7c7892f7fd4f38f99e8a1e5c0de08 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 18 Mar 2022 18:00:51 +0000
Subject: Avoid running build unless necessary in test for discovery

---
 setuptools/tests/test_config_discovery.py | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py
index 80553175..27db4e29 100644
--- a/setuptools/tests/test_config_discovery.py
+++ b/setuptools/tests/test_config_discovery.py
@@ -224,9 +224,15 @@ class TestNoConfig:
     @pytest.mark.parametrize("example", EXAMPLES.keys())
     def test_discover_name(self, tmp_path, example):
         _populate_project_dir(tmp_path, self.EXAMPLES[example], {})
+        dist, _ = _run_sdist_programatically(tmp_path, {})
+        dist.get_name() == example
+
+    def test_build_with_discovered_name(self, tmp_path):
+        files = ["src/ns/nested/pkg/__init__.py"]
+        _populate_project_dir(tmp_path, files, {})
         _run_build(tmp_path, "--sdist")
         # Expected distribution file
-        dist_file = tmp_path / f"dist/{example}-{self.DEFAULT_VERSION}.tar.gz"
+        dist_file = tmp_path / f"dist/ns.nested.pkg-{self.DEFAULT_VERSION}.tar.gz"
         assert dist_file.is_file()
 
 
-- 
cgit v1.2.1


From c43968a036f50ccff1e9dd9998c1cc7d4805b4ea Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 18 Mar 2022 19:05:58 +0000
Subject: Prevent accidental multi-package dist with auto-discovery

As discussed in
https://discuss.python.org/t/help-testing-experimental-features-in-setuptools/13821/41
automatically scanning all the directories might be very error-prone.

One way of avoiding that is to error when multiple top-level packages are
automatically discovered.
---
 setuptools/discovery.py                   | 41 ++++++++++++++++++++++++++++---
 setuptools/tests/test_config_discovery.py | 21 +++++++++++++---
 2 files changed, 56 insertions(+), 6 deletions(-)

diff --git a/setuptools/discovery.py b/setuptools/discovery.py
index e0c406e3..ae1dc165 100644
--- a/setuptools/discovery.py
+++ b/setuptools/discovery.py
@@ -336,18 +336,53 @@ class ConfigDiscovery:
         if not os.path.isdir(src_dir):
             return False
 
+        log.debug(f"`src-layout` detected -- analysing {src_dir}")
         package_dir.setdefault("", os.path.basename(src_dir))
         self.dist.packages = PEP420PackageFinder.find(src_dir)
         self.dist.py_modules = ModuleFinder.find(src_dir)
-        log.debug(f"`src-layout` detected -- analysing {src_dir}")
+        log.debug(f"discovered packages -- {self.dist.packages}")
+        log.debug(f"discovered py_modules -- {self.dist.py_modules}")
         return True
 
     def _analyse_flat_layout(self):
         """Try to find all packages and modules under the project root"""
+        log.debug(f"`flat-layout` detected -- analysing {self._root_dir}")
+        return self._analyse_flat_packages() or self._analyse_flat_modules()
+
+    def _analyse_flat_packages(self):
         self.dist.packages = FlatLayoutPackageFinder.find(self._root_dir)
+        top_level = remove_nested_packages(remove_stubs(self.dist.packages))
+        log.debug(f"discovered packages -- {self.dist.packages}")
+        self._ensure_no_accidental_inclusion(top_level, "packages")
+        return bool(top_level)
+
+    def _analyse_flat_modules(self):
         self.dist.py_modules = FlatLayoutModuleFinder.find(self._root_dir)
-        log.debug(f"`flat-layout` detected -- analysing {self._root_dir}")
-        return True
+        log.debug(f"discovered py_modules -- {self.dist.py_modules}")
+        self._ensure_no_accidental_inclusion(self.dist.py_modules, "modules")
+        return bool(self.dist.py_modules)
+
+    def _ensure_no_accidental_inclusion(self, detected: List[str], kind: str):
+        if len(detected) > 1:
+            from inspect import cleandoc
+            from setuptools.errors import PackageDiscoveryError
+
+            msg = f"""Multiple top-level {kind} discovered in a flat-layout: {detected}.
+
+            To avoid accidental inclusion of unwanted files or directories,
+            setuptools will not proceed with this build.
+
+            If you are trying to create a single distribution with multiple {kind}
+            on purpose, you should not rely on automatic discovery.
+            Instead, consider the following options:
+
+            1. set up custom discovery (`find` directive with `include` or `exclude`)
+            2. use a `src-layout`
+            3. explicitly set `py_modules` or `packages` with a list of names
+
+            To find more information, look for "package discovery" on setuptools docs.
+            """
+            raise PackageDiscoveryError(cleandoc(msg))
 
     def analyse_name(self):
         """The packages/modules are the essential contribution of the author.
diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py
index 27db4e29..cfc5cf56 100644
--- a/setuptools/tests/test_config_discovery.py
+++ b/setuptools/tests/test_config_discovery.py
@@ -17,6 +17,7 @@ from .textwrap import DALS
 
 
 def test_find_parent_package(tmp_path):
+    # find_parent_package should find a non-namespace parent package
     (tmp_path / "src/namespace/pkg/nested").mkdir(exist_ok=True, parents=True)
     (tmp_path / "src/namespace/pkg/nested/__init__.py").touch()
     (tmp_path / "src/namespace/pkg/__init__.py").touch()
@@ -25,6 +26,8 @@ def test_find_parent_package(tmp_path):
 
 
 def test_find_parent_package_multiple_toplevel(tmp_path):
+    # find_parent_package should return null if the given list of packages does not
+    # have a single parent package
     multiple = ["pkg", "pkg1", "pkg2"]
     for name in multiple:
         (tmp_path / f"src/{name}").mkdir(exist_ok=True, parents=True)
@@ -189,7 +192,7 @@ class TestDiscoverPackagesAndPyModules:
             (["news/finalize.py"], {"pkg"}),
         ]
     )
-    def test_flat_layout_with_extra_dirs(self, tmp_path, extra_files, pkgs):
+    def test_flat_layout_with_extra_files(self, tmp_path, extra_files, pkgs):
         files = self.FILES["flat"] + extra_files
         _populate_project_dir(tmp_path, files, {})
         dist, _ = _run_sdist_programatically(tmp_path, {})
@@ -202,10 +205,22 @@ class TestDiscoverPackagesAndPyModules:
             ["other/finalize.py"],
         ]
     )
-    def test_flat_layout_with_dangerous_extra_dirs(self, tmp_path, extra_files):
+    def test_flat_layout_with_dangerous_extra_files(self, tmp_path, extra_files):
         files = self.FILES["flat"] + extra_files
         _populate_project_dir(tmp_path, files, {})
-        with pytest.raises(PackageDiscoveryError):
+        with pytest.raises(PackageDiscoveryError, match="multiple (packages|modules)"):
+            _run_sdist_programatically(tmp_path, {})
+
+    def test_flat_layout_with_single_module(self, tmp_path):
+        files = self.FILES["single_module"] + ["invalid-module-name.py"]
+        _populate_project_dir(tmp_path, files, {})
+        dist, _ = _run_sdist_programatically(tmp_path, {})
+        assert set(dist.py_modules) == {"pkg"}
+
+    def test_flat_layout_with_multiple_modules(self, tmp_path):
+        files = self.FILES["single_module"] + ["valid_module_name.py"]
+        _populate_project_dir(tmp_path, files, {})
+        with pytest.raises(PackageDiscoveryError, match="multiple (packages|modules)"):
             _run_sdist_programatically(tmp_path, {})
 
 
-- 
cgit v1.2.1


From 141607086a74ebb47df8f2112e06cbd2ffead78f Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 18 Mar 2022 19:54:11 +0000
Subject: Adequate existing tests to the new errors for auto-discovery

---
 .../tests/config/test_apply_pyprojecttoml.py       | 18 ++++++++++------
 setuptools/tests/config/test_pyprojecttoml.py      | 25 ++++++++++++++--------
 setuptools/tests/test_dist.py                      |  3 +--
 3 files changed, 28 insertions(+), 18 deletions(-)

diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py
index 181be475..38c9d1dc 100644
--- a/setuptools/tests/config/test_apply_pyprojecttoml.py
+++ b/setuptools/tests/config/test_apply_pyprojecttoml.py
@@ -21,6 +21,10 @@ EXAMPLE_URLS = [x for x in EXAMPLES.splitlines() if not x.startswith("#")]
 DOWNLOAD_DIR = Path(__file__).parent / "downloads"
 
 
+def makedist(path):
+    return Distribution({"src_root": path})
+
+
 @pytest.mark.parametrize("url", EXAMPLE_URLS)
 @pytest.mark.filterwarnings("ignore")
 @pytest.mark.uses_network
@@ -31,8 +35,8 @@ def test_apply_pyproject_equivalent_to_setupcfg(url, monkeypatch, tmp_path):
     toml_config = Translator().translate(setupcfg_example.read_text(), "setup.cfg")
     pyproject_example.write_text(toml_config)
 
-    dist_toml = pyprojecttoml.apply_configuration(Distribution(), pyproject_example)
-    dist_cfg = setupcfg.apply_configuration(Distribution(), setupcfg_example)
+    dist_toml = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject_example)
+    dist_cfg = setupcfg.apply_configuration(makedist(tmp_path), setupcfg_example)
 
     pkg_info_toml = core_metadata(dist_toml)
     pkg_info_cfg = core_metadata(dist_cfg)
@@ -146,7 +150,7 @@ def _pep621_example_project(tmp_path, readme="README.rst"):
 def test_pep621_example(tmp_path):
     """Make sure the example in PEP 621 works"""
     pyproject = _pep621_example_project(tmp_path)
-    dist = pyprojecttoml.apply_configuration(Distribution(), pyproject)
+    dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
     assert dist.metadata.license == "--- LICENSE stub ---"
     assert set(dist.metadata.license_files) == {"LICENSE.txt"}
 
@@ -161,19 +165,19 @@ def test_pep621_example(tmp_path):
 )
 def test_readme_content_type(tmp_path, readme, ctype):
     pyproject = _pep621_example_project(tmp_path, readme)
-    dist = pyprojecttoml.apply_configuration(Distribution(), pyproject)
+    dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
     assert dist.metadata.long_description_content_type == ctype
 
 
 def test_undefined_content_type(tmp_path):
     pyproject = _pep621_example_project(tmp_path, "README.tex")
     with pytest.raises(ValueError, match="Undefined content type for README.tex"):
-        pyprojecttoml.apply_configuration(Distribution(), pyproject)
+        pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
 
 
 def test_no_explicit_content_type_for_missing_extension(tmp_path):
     pyproject = _pep621_example_project(tmp_path, "README")
-    dist = pyprojecttoml.apply_configuration(Distribution(), pyproject)
+    dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
     assert dist.metadata.long_description_content_type is None
 
 
@@ -196,7 +200,7 @@ def test_license_and_license_files(tmp_path):
     # by being explicit. On the other hand, its contents should be added to `license`
     (tmp_path / "LICENSE.txt").write_text("LicenseRef-Proprietary\n", encoding="utf-8")
 
-    dist = pyprojecttoml.apply_configuration(Distribution(), pyproject)
+    dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
     assert set(dist.metadata.license_files) == {"_FILE.rst", "_FILE.txt"}
     assert dist.metadata.license == "LicenseRef-Proprietary\n"
 
diff --git a/setuptools/tests/config/test_pyprojecttoml.py b/setuptools/tests/config/test_pyprojecttoml.py
index a2b9da52..463048ed 100644
--- a/setuptools/tests/config/test_pyprojecttoml.py
+++ b/setuptools/tests/config/test_pyprojecttoml.py
@@ -78,9 +78,12 @@ def create_example(path, pkg_root):
 
     files = [
         f"{pkg_root}/pkg/__init__.py",
-        f"{pkg_root}/other/nested/__init__.py",  # ensure namespaces are discovered
         "_files/file.txt",
     ]
+    if pkg_root != ".":  # flat-layout will raise error for multi-package dist
+        # Ensure namespaces are discovered
+        files.append(f"{pkg_root}/other/nested/__init__.py")
+
     for file in files:
         (path / file).parent.mkdir(exist_ok=True, parents=True)
         (path / file).touch()
@@ -92,7 +95,7 @@ def create_example(path, pkg_root):
     (path / f"{pkg_root}/pkg/__main__.py").write_text("def exec(): print('hello')")
 
 
-def verify_example(config, path):
+def verify_example(config, path, pkg_root):
     pyproject = path / "pyproject.toml"
     pyproject.write_text(tomli_w.dumps(config), encoding="utf-8")
     expanded = expand_configuration(config, path)
@@ -101,11 +104,15 @@ def verify_example(config, path):
     assert expanded_project["version"] == "3.10"
     assert expanded_project["readme"]["text"] == "hello world"
     assert "packages" in expanded["tool"]["setuptools"]
-    assert set(expanded["tool"]["setuptools"]["packages"]) == {
-        "pkg",
-        "other",
-        "other.nested",
-    }
+    if pkg_root == ".":
+        # Auto-discovery will raise error for multi-package dist
+        assert set(expanded["tool"]["setuptools"]["packages"]) == {"pkg"}
+    else:
+        assert set(expanded["tool"]["setuptools"]["packages"]) == {
+            "pkg",
+            "other",
+            "other.nested",
+        }
     assert "" in expanded["tool"]["setuptools"]["package-data"]
     assert "*" not in expanded["tool"]["setuptools"]["package-data"]
     assert expanded["tool"]["setuptools"]["data-files"] == [
@@ -121,7 +128,7 @@ def test_read_configuration(tmp_path):
     assert config["project"].get("version") is None
     assert config["project"].get("readme") is None
 
-    verify_example(config, tmp_path)
+    verify_example(config, tmp_path, "src")
 
 
 @pytest.mark.parametrize(
@@ -144,7 +151,7 @@ def test_discovered_package_dir_with_attr_directive_in_config(tmp_path, pkg_root
     config["tool"]["setuptools"].pop("package-dir", None)
 
     config["tool"]["setuptools"].update(opts)
-    verify_example(config, tmp_path)
+    verify_example(config, tmp_path, pkg_root)
 
 
 ENTRY_POINTS = {
diff --git a/setuptools/tests/test_dist.py b/setuptools/tests/test_dist.py
index 049576a7..e7d2f5ca 100644
--- a/setuptools/tests/test_dist.py
+++ b/setuptools/tests/test_dist.py
@@ -489,8 +489,7 @@ def test_dist_default_packages(
             ["lib/__init__.py", "lib/nested/__init__.pyt", "lib2/__init__.py"],
         ),
         # Should not try to guess a name from multiple py_modules/packages
-        ("UNKNOWN", None, ["mod1.py", "mod2.py"]),
-        ("UNKNOWN", None, ["pkg1/__ini__.py", "pkg2/__init__.py"]),
+        ("UNKNOWN", None, ["src/mod1.py", "src/mod2.py"]),
         ("UNKNOWN", None, ["src/pkg1/__ini__.py", "src/pkg2/__init__.py"]),
     ]
 )
-- 
cgit v1.2.1


From 7dc83cbc141131d1439ee25e5fcf9eba072ebca2 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 18 Mar 2022 19:59:57 +0000
Subject: Improve logs/docstrings for setuptools.discovery

---
 setuptools/discovery.py | 13 +++++++++++--
 1 file changed, 11 insertions(+), 2 deletions(-)

diff --git a/setuptools/discovery.py b/setuptools/discovery.py
index ae1dc165..e3ef6bf5 100644
--- a/setuptools/discovery.py
+++ b/setuptools/discovery.py
@@ -312,12 +312,13 @@ class ConfigDiscovery:
         if not package_dir:
             return False
 
+        log.debug(f"`explicit-layout` detected -- analysing {package_dir}")
         pkgs = chain_iter(
             _find_packages_within(pkg, os.path.join(root_dir, parent_dir))
             for pkg, parent_dir in package_dir.items()
         )
         self.dist.packages = list(pkgs)
-        log.debug(f"`explicit-layout` detected -- analysing {package_dir}")
+        log.debug(f"discovered packages -- {self.dist.packages}")
         return True
 
     def _analyse_src_layout(self):
@@ -345,7 +346,15 @@ class ConfigDiscovery:
         return True
 
     def _analyse_flat_layout(self):
-        """Try to find all packages and modules under the project root"""
+        """Try to find all packages and modules under the project root.
+
+        Since the ``flat-layout`` is more dangerous in terms of accidentally including
+        extra files/directories, this function is more conservative and will raise an
+        error if multiple packages or modules are found.
+
+        This assumes that multi-package dists are uncommon and refuse to support that
+        use case in order to be able to prevent unintended errors.
+        """
         log.debug(f"`flat-layout` detected -- analysing {self._root_dir}")
         return self._analyse_flat_packages() or self._analyse_flat_modules()
 
-- 
cgit v1.2.1


From 7985ea4bd728c2c947fa9c45368a439104c667f0 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 18 Mar 2022 20:34:34 +0000
Subject: Add type hints to setuptools.discovery

This helps to increase confidence in the code
---
 setuptools/discovery.py | 71 +++++++++++++++++++++++++++++--------------------
 1 file changed, 42 insertions(+), 29 deletions(-)

diff --git a/setuptools/discovery.py b/setuptools/discovery.py
index e3ef6bf5..75c5bf42 100644
--- a/setuptools/discovery.py
+++ b/setuptools/discovery.py
@@ -41,19 +41,25 @@ import itertools
 import os
 from fnmatch import fnmatchcase
 from glob import glob
+from typing import TYPE_CHECKING
+from typing import Callable, Dict, Iterator, Iterable, List, Optional, Tuple, Union
 
 import _distutils_hack.override  # noqa: F401
 
 from distutils import log
 from distutils.util import convert_path
 
-from typing import Dict, List, Optional, Union
 _Path = Union[str, os.PathLike]
+_Filter = Callable[[str], bool]
+StrIter = Iterator[str]
 
 chain_iter = itertools.chain.from_iterable
 
+if TYPE_CHECKING:
+    from setuptools import Distribution  # noqa
 
-def _valid_name(path):
+
+def _valid_name(path: _Path) -> bool:
     # Ignore invalid names that cannot be imported directly
     return os.path.basename(path).isidentifier()
 
@@ -61,11 +67,16 @@ def _valid_name(path):
 class _Finder:
     """Base class that exposes functionality for module/package finders"""
 
-    ALWAYS_EXCLUDE = ()
-    DEFAULT_EXCLUDE = ()
+    ALWAYS_EXCLUDE: Tuple[str, ...] = ()
+    DEFAULT_EXCLUDE: Tuple[str, ...] = ()
 
     @classmethod
-    def find(cls, where='.', exclude=(), include=('*',)):
+    def find(
+        cls,
+        where: _Path = '.',
+        exclude: Iterable[str] = (),
+        include: Iterable[str] = ('*',)
+    ) -> List[str]:
         """Return a list of all Python items (packages or modules, depending on
         the finder implementation) found within directory 'where'.
 
@@ -95,11 +106,11 @@ class _Finder:
         )
 
     @classmethod
-    def _find_iter(cls, where, exclude, include):
+    def _find_iter(cls, where: _Path, exclude: _Filter, include: _Filter) -> StrIter:
         raise NotImplementedError
 
     @staticmethod
-    def _build_filter(*patterns):
+    def _build_filter(*patterns: str) -> _Filter:
         """
         Given a list of patterns, return a callable that will be true only if
         the input matches at least one of the patterns.
@@ -115,12 +126,12 @@ class PackageFinder(_Finder):
     ALWAYS_EXCLUDE = ("ez_setup", "*__pycache__")
 
     @classmethod
-    def _find_iter(cls, where, exclude, include):
+    def _find_iter(cls, where: _Path, exclude: _Filter, include: _Filter) -> StrIter:
         """
         All the packages found in 'where' that pass the 'include' filter, but
         not the 'exclude' filter.
         """
-        for root, dirs, files in os.walk(where, followlinks=True):
+        for root, dirs, files in os.walk(str(where), followlinks=True):
             # Copy dirs to iterate over it, then empty dirs.
             all_dirs = dirs[:]
             dirs[:] = []
@@ -143,14 +154,14 @@ class PackageFinder(_Finder):
                 dirs.append(dir)
 
     @staticmethod
-    def _looks_like_package(path, _package_name):
+    def _looks_like_package(path: _Path, _package_name: str) -> bool:
         """Does a directory look like a package?"""
         return os.path.isfile(os.path.join(path, '__init__.py'))
 
 
 class PEP420PackageFinder(PackageFinder):
     @staticmethod
-    def _looks_like_package(path, _package_name):
+    def _looks_like_package(_path: _Path, _package_name: str) -> bool:
         return True
 
 
@@ -160,7 +171,7 @@ class ModuleFinder(_Finder):
     """
 
     @classmethod
-    def _find_iter(cls, where, exclude, include):
+    def _find_iter(cls, where: _Path, exclude: _Filter, include: _Filter) -> StrIter:
         for file in glob(os.path.join(where, "*.py")):
             module, _ext = os.path.splitext(os.path.basename(file))
 
@@ -213,12 +224,14 @@ class FlatLayoutPackageFinder(PEP420PackageFinder):
     """Reserved package names"""
 
     @staticmethod
-    def _looks_like_package(path, package_name):
+    def _looks_like_package(path: _Path, package_name: str) -> bool:
         names = package_name.split('.')
-        return names and (
-            # Consider PEP 561
-            (names[0].isidentifier() or names[0].endswith("-stubs"))
-            and all(name.isidentifier() for name in names[1:])
+        return bool(
+            names and (
+                # Consider PEP 561
+                (names[0].isidentifier() or names[0].endswith("-stubs"))
+                and all(name.isidentifier() for name in names[1:])
+            )
         )
 
 
@@ -247,7 +260,7 @@ class FlatLayoutModuleFinder(ModuleFinder):
     """Reserved top-level module names"""
 
 
-def _find_packages_within(root_pkg, pkg_dir):
+def _find_packages_within(root_pkg: str, pkg_dir: _Path) -> List[str]:
     nested = PEP420PackageFinder.find(pkg_dir)
     return [root_pkg] + [".".join((root_pkg, n)) for n in nested]
 
@@ -257,10 +270,10 @@ class ConfigDiscovery:
     (from other metadata/options, the file system or conventions)
     """
 
-    def __init__(self, distribution):
+    def __init__(self, distribution: "Distribution"):
         self.dist = distribution
         self._called = False
-        self._root_dir = None  # delay so `src_root` can be set in dist
+        self._root_dir: _Path  # delay so `src_root` can be set in dist
 
     def __call__(self, force=False, name=True):
         """Automatically discover missing configuration fields
@@ -285,11 +298,11 @@ class ConfigDiscovery:
 
         self._called = True
 
-    def _analyse_package_layout(self):
+    def _analyse_package_layout(self) -> bool:
         if self.dist.packages is not None or self.dist.py_modules is not None:
             # For backward compatibility, just try to find modules/packages
             # when nothing is given
-            return None
+            return True
 
         log.debug(
             "No `packages` or `py_modules` configuration, performing "
@@ -303,7 +316,7 @@ class ConfigDiscovery:
             or self._analyse_flat_layout()
         )
 
-    def _analyse_explicit_layout(self):
+    def _analyse_explicit_layout(self) -> bool:
         """The user can explicitly give a package layout via ``package_dir``"""
         package_dir = (self.dist.package_dir or {}).copy()
         package_dir.pop("", None)  # This falls under the "src-layout" umbrella
@@ -321,7 +334,7 @@ class ConfigDiscovery:
         log.debug(f"discovered packages -- {self.dist.packages}")
         return True
 
-    def _analyse_src_layout(self):
+    def _analyse_src_layout(self) -> bool:
         """Try to find all packages or modules under the ``src`` directory
         (or anything pointed by ``package_dir[""]``).
 
@@ -345,7 +358,7 @@ class ConfigDiscovery:
         log.debug(f"discovered py_modules -- {self.dist.py_modules}")
         return True
 
-    def _analyse_flat_layout(self):
+    def _analyse_flat_layout(self) -> bool:
         """Try to find all packages and modules under the project root.
 
         Since the ``flat-layout`` is more dangerous in terms of accidentally including
@@ -358,14 +371,14 @@ class ConfigDiscovery:
         log.debug(f"`flat-layout` detected -- analysing {self._root_dir}")
         return self._analyse_flat_packages() or self._analyse_flat_modules()
 
-    def _analyse_flat_packages(self):
+    def _analyse_flat_packages(self) -> bool:
         self.dist.packages = FlatLayoutPackageFinder.find(self._root_dir)
         top_level = remove_nested_packages(remove_stubs(self.dist.packages))
         log.debug(f"discovered packages -- {self.dist.packages}")
         self._ensure_no_accidental_inclusion(top_level, "packages")
         return bool(top_level)
 
-    def _analyse_flat_modules(self):
+    def _analyse_flat_modules(self) -> bool:
         self.dist.py_modules = FlatLayoutModuleFinder.find(self._root_dir)
         log.debug(f"discovered py_modules -- {self.dist.py_modules}")
         self._ensure_no_accidental_inclusion(self.dist.py_modules, "modules")
@@ -411,7 +424,7 @@ class ConfigDiscovery:
             self.dist.metadata.name = name
             self.dist.name = name
 
-    def _find_name_single_package_or_module(self):
+    def _find_name_single_package_or_module(self) -> Optional[str]:
         """Exactly one module or package"""
         for field in ('packages', 'py_modules'):
             items = getattr(self.dist, field, None) or []
@@ -421,7 +434,7 @@ class ConfigDiscovery:
 
         return None
 
-    def _find_name_from_packages(self):
+    def _find_name_from_packages(self) -> Optional[str]:
         """Try to find the root package that is not a PEP 420 namespace"""
         if not self.dist.packages:
             return None
-- 
cgit v1.2.1


From a0eb605343de400943102589e896318d87028b57 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 18 Mar 2022 20:37:48 +0000
Subject: Fix type error in setuptools.config

---
 setuptools/config/pyprojecttoml.py | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index 4ba234f8..d57edddb 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -4,7 +4,7 @@ import os
 import warnings
 from contextlib import contextmanager
 from functools import partial
-from typing import TYPE_CHECKING, Callable, Optional, Tuple, Union
+from typing import TYPE_CHECKING, Callable, Dict, Optional, Tuple, Union
 
 from setuptools.errors import FileError, OptionError
 
@@ -268,10 +268,12 @@ def _expand_dynamic(
     return None
 
 
-def _expand_readme(dynamic_cfg: dict, root_dir: _Path, ignore_option_errors: bool):
+def _expand_readme(
+    dynamic_cfg: dict, root_dir: _Path, ignore_option_errors: bool
+) -> Dict[str, str]:
     silent = ignore_option_errors
     return {
-        "text": _expand_dynamic(dynamic_cfg, "readme", None, root_dir, silent),
+        "text": _expand_dynamic(dynamic_cfg, "readme", {}, root_dir, silent),
         "content-type": dynamic_cfg["readme"].get("content-type", "text/x-rst"),
     }
 
-- 
cgit v1.2.1


From feaea561591af122d3380e1f5bfc144bfacae31d Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 18 Mar 2022 21:24:49 +0000
Subject: Update package discovery docs to reflect latest changes

---
 docs/userguide/package_discovery.rst | 64 ++++++++++++++++++++----------------
 1 file changed, 35 insertions(+), 29 deletions(-)

diff --git a/docs/userguide/package_discovery.rst b/docs/userguide/package_discovery.rst
index 70ef3538..46fb2a8e 100644
--- a/docs/userguide/package_discovery.rst
+++ b/docs/userguide/package_discovery.rst
@@ -132,8 +132,18 @@ Automatic discovery
    (or be completely removed) in the future.
    See :ref:`custom-discovery` for a stable way of configuring ``setuptools``.
 
-By default setuptools will consider 2 popular project layouts, each one with
-its own set of advantages and disadvantages [#layout1]_ [#layout2]_.
+By default ``setuptools`` will consider 2 popular project layouts, each one with
+its own set of advantages and disadvantages [#layout1]_ [#layout2]_ as
+discussed in the following sections.
+
+Setuptools will automatically scan your project directory looking for these
+layouts and try to guess the correct values for the :ref:`packages ` and :doc:`py_modules ` configuration.
+
+.. important::
+   Automatic discovery will **only** be enabled if you don't provide any
+   configuration for both ``packages`` and ``py_modules``.
+   If at least one of them is explicitly set, automatic discovery will not take place.
 
 .. _src-layout:
 
@@ -182,14 +192,33 @@ 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
 of folders or Python files hanging around your project root)
 
+To avoid confusion, file and folder names that are used by popular tools (or
+that correspond to well-known conventions, such as distributing documentation
+alongside the project code) are automatically filtered out in the case of
+*flat-layout*:
+
+.. autoattribute:: setuptools.discovery.FlatLayoutPackageFinder.DEFAULT_EXCLUDE
+
+.. autoattribute:: setuptools.discovery.FlatLayoutModuleFinder.DEFAULT_EXCLUDE
+
+.. warning::
+   If you are using auto-discovery with *flat-layout*, ``setuptools`` will
+   refuse to create :term:`distribution archives ` with
+   multiple top-level packages or modules.
+
+   This is done to prevent common errors such as accidentally publishing code
+   not meant for distribution (e.g. maintenance-related scripts).
+
+   Users that purposefully want to create multi-package distributions are
+   advised to use :ref:`custom-discovery` or the ``src-layout``.
+
 There is also a handy variation of the *flat-layout* for utilities/libraries
 that can be implemented with a single Python file:
 
-single-module approach
-----------------------
-*(or "few top-level modules")*
+single-module distribution
+^^^^^^^^^^^^^^^^^^^^^^^^^^
 
-Standalone modules are placed directly under the project root, instead of
+A standalone module is placed directly under the project root, instead of
 inside a package folder::
 
     project_root_directory
@@ -198,24 +227,6 @@ inside a package folder::
     ├── ...
     └── single_file_lib.py
 
-Setuptools will automatically scan your project directory looking for these
-layouts and try to guess the correct values for the :ref:`packages ` and :doc:`py_modules ` configuration.
-
-To avoid confusion, file and folder names that are used by popular tools (or
-that correspond to well-known conventions, such as distributing documentation
-alongside the project code) are automatically filtered out in the case of
-*flat-layouts* [#layout3]_:
-
-.. autoattribute:: setuptools.discovery.FlatLayoutPackageFinder.DEFAULT_EXCLUDE
-
-.. autoattribute:: setuptools.discovery.FlatLayoutModuleFinder.DEFAULT_EXCLUDE
-
-.. important:: Automatic discovery will **only** be enabled if you don't
-   provide any configuration for both ``packages`` and ``py_modules``.
-   If at least one of them is explicitly set, automatic discovery will not take
-   place.
-
 
 .. _custom-discovery:
 
@@ -566,11 +577,6 @@ The project layout remains the same and ``setup.cfg`` remains the same.
    removed) in the future. See :doc:`/userguide/pyproject_config`.
 .. [#layout1] https://blog.ionelmc.ro/2014/05/25/python-packaging/#the-structure
 .. [#layout2] https://blog.ionelmc.ro/2017/09/25/rehashing-the-src-layout/
-.. [#layout3]
-   If you are using auto-discovery with *flat-layout* and have multiple folders
-   (other than ``tests`` and ``docs``) or Python files in your project root,
-   always check the created :term:`distribution archive `
-   to make sure files are not being distributed accidentally.
 
 .. _editable install: https://pip.pypa.io/en/stable/cli/pip_install/#editable-installs
 .. _7zip: https://www.7-zip.org
-- 
cgit v1.2.1


From 78c82c60a1760a68192c05c8efa177f225dfd67d Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 18 Mar 2022 21:31:40 +0000
Subject: Fix error in doctest on Windows

---
 setuptools/discovery.py | 15 +++++++++++----
 1 file changed, 11 insertions(+), 4 deletions(-)

diff --git a/setuptools/discovery.py b/setuptools/discovery.py
index 75c5bf42..5c21199e 100644
--- a/setuptools/discovery.py
+++ b/setuptools/discovery.py
@@ -506,13 +506,20 @@ def find_package_path(name: str, package_dir: Dict[str, str], root_dir: _Path) -
     """Given a package name, return the path where it should be found on
     disk, considering the ``package_dir`` option.
 
-    >>> find_package_path("my.pkg", {"": "root/is/nested"}, ".")
+    >>> path = find_package_path("my.pkg", {"": "root/is/nested"}, ".")
+    >>> path.replace(os.sep, "/")
     './root/is/nested/my/pkg'
-    >>> find_package_path("my.pkg", {"my": "root/is/nested"}, ".")
+
+    >>> path = find_package_path("my.pkg", {"my": "root/is/nested"}, ".")
+    >>> path.replace(os.sep, "/")
     './root/is/nested/pkg'
-    >>> find_package_path("my.pkg", {"my.pkg": "root/is/nested"}, ".")
+
+    >>> path = find_package_path("my.pkg", {"my.pkg": "root/is/nested"}, ".")
+    >>> path.replace(os.sep, "/")
     './root/is/nested'
-    >>> find_package_path("other.pkg", {"my.pkg": "root/is/nested"}, ".")
+
+    >>> path = find_package_path("other.pkg", {"my.pkg": "root/is/nested"}, ".")
+    >>> path.replace(os.sep, "/")
     './other/pkg'
     """
     parts = name.split(".")
-- 
cgit v1.2.1


From 4fe0e898761e179c987c2ddafcf11971a46a9105 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 18 Mar 2022 21:36:53 +0000
Subject: Change tabs in discovery docs to be similar to quickstart

---
 docs/userguide/package_discovery.rst | 20 +++++---------------
 1 file changed, 5 insertions(+), 15 deletions(-)

diff --git a/docs/userguide/package_discovery.rst b/docs/userguide/package_discovery.rst
index 46fb2a8e..fd688824 100644
--- a/docs/userguide/package_discovery.rst
+++ b/docs/userguide/package_discovery.rst
@@ -39,9 +39,7 @@ Normally, you would specify the package to be included manually in the following
             packages=['mypkg1', 'mypkg2']
         )
 
-.. tab:: pyproject.toml
-
-    **EXPERIMENTAL** [#experimental]_
+.. tab:: pyproject.toml (**EXPERIMENTAL**) [#experimental]_
 
     .. code-block:: toml
 
@@ -95,9 +93,7 @@ configure ``package_dir``:
                 # ...
         )
 
-.. tab:: pyproject.toml
-
-    **EXPERIMENTAL** [#experimental]_
+.. tab:: pyproject.toml (**EXPERIMENTAL**) [#experimental]_
 
     .. code-block:: toml
 
@@ -256,9 +252,7 @@ the provided tools for package discovery:
         # or
         from setuptools import find_namespace_packages
 
-.. tab:: pyproject.toml
-
-    **EXPERIMENTAL** [#experimental]_
+.. tab:: pyproject.toml (**EXPERIMENTAL**) [#experimental]_
 
     .. code-block:: toml
 
@@ -331,9 +325,7 @@ in ``src`` that starts with the name ``pkg`` and not ``additional``:
         ``pkg.namespace`` is ignored by ``find_packages()``
         (see ``find_namespace_packages()`` below).
 
-.. tab:: pyproject.toml
-
-    **EXPERIMENTAL** [#experimental]_
+.. tab:: pyproject.toml (**EXPERIMENTAL**) [#experimental]_
 
     .. code-block:: toml
 
@@ -442,9 +434,7 @@ distribution, then you will need to specify:
     On the other hand, ``find_namespace_packages()`` will scan all
     directories.
 
-.. tab:: pyproject.toml
-
-    **EXPERIMENTAL** [#experimental]_
+.. tab:: pyproject.toml (**EXPERIMENTAL**) [#experimental]_
 
     .. code-block:: toml
 
-- 
cgit v1.2.1


From 20a95398a3ba68bb8829539d0dda31ee79056a8b Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 19 Mar 2022 03:37:05 +0000
Subject: Fix problem caused by mispelling of py_modules for pyproject.toml

---
 setuptools/config/pyprojecttoml.py        |  2 +-
 setuptools/tests/test_config_discovery.py | 86 ++++++++++++++++++++++---------
 2 files changed, 62 insertions(+), 26 deletions(-)

diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index d57edddb..2b430787 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -197,7 +197,7 @@ def _fill_discovered_attrs(
 
     # Set `py_modules` and `packages` in dist to short-circuit auto-discovery,
     # but avoid overwriting empty lists purposefully set by users.
-    if isinstance(setuptools_cfg.get("py_modules"), list) and dist.py_modules is None:
+    if isinstance(setuptools_cfg.get("py-modules"), list) and dist.py_modules is None:
         dist.py_modules = setuptools_cfg["py-modules"]
     if isinstance(setuptools_cfg.get("packages"), list) and dist.packages is None:
         dist.packages = setuptools_cfg["packages"]
diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py
index cfc5cf56..655e2a9f 100644
--- a/setuptools/tests/test_config_discovery.py
+++ b/setuptools/tests/test_config_discovery.py
@@ -8,6 +8,9 @@ from setuptools.dist import Distribution
 from setuptools.discovery import find_package_path, find_parent_package
 from setuptools.errors import PackageDiscoveryError
 
+import setuptools  # noqa -- force distutils.core to be patched
+import distutils.core
+
 import pytest
 from path import Path as _Path
 
@@ -145,32 +148,53 @@ class TestDiscoverPackagesAndPyModules:
             [build-system]
             requires = []
             build-backend = 'setuptools.build_meta'
+
+            [project]
+            name = "myproj"
+            version = "0.0.0"
+
+            [tool.setuptools]
+            {param} = []
+            """
+        ),
+        "template-pyproject.toml": DALS(
+            """
+            [build-system]
+            requires = []
+            build-backend = 'setuptools.build_meta'
             """
         )
     }
 
     @pytest.mark.parametrize(
         "config_file, param, circumstance",
-        product(["setup.cfg", "setup.py"], ["packages", "py_modules"], FILES.keys())
+        product(
+            ["setup.cfg", "setup.py", "pyproject.toml"],
+            ["packages", "py_modules"],
+            FILES.keys()
+        )
     )
     def test_purposefully_empty(self, tmp_path, config_file, param, circumstance):
-        files = self.FILES[circumstance]
+        files = self.FILES[circumstance] + ["mod.py", "other.py", "src/pkg/__init__.py"]
         _populate_project_dir(tmp_path, files, {})
-        config = self.PURPOSEFULLY_EMPY[config_file].format(param=param)
-        (tmp_path / config_file).write_text(config)
 
-        # Make sure build works with or without setup.cfg
-        pyproject = self.PURPOSEFULLY_EMPY["pyproject.toml"]
-        (tmp_path / "pyproject.toml").write_text(pyproject)
+        if config_file == "pyproject.toml":
+            template_param = param.replace("_", "-")
+        else:
+            # Make sure build works with or without setup.cfg
+            pyproject = self.PURPOSEFULLY_EMPY["template-pyproject.toml"]
+            (tmp_path / "pyproject.toml").write_text(pyproject)
+            template_param = param
 
-        _run_build(tmp_path)
+        config = self.PURPOSEFULLY_EMPY[config_file].format(param=template_param)
+        (tmp_path / config_file).write_text(config)
 
-        wheel_files = get_wheel_members(next(tmp_path.glob("dist/*.whl")))
-        print("~~~~~ wheel_members ~~~~~")
-        print('\n'.join(wheel_files))
-        for file in files:
-            name = file.replace("src/", "")
-            assert name not in wheel_files
+        dist = _get_dist(tmp_path, {})
+        # When either parameter package or py_modules is an empty list,
+        # then there should be no discovery
+        assert getattr(dist, param) == []
+        other = {"py_modules": "packages", "packages": "py_modules"}[param]
+        assert getattr(dist, other) is None
 
     @pytest.mark.parametrize(
         "extra_files, pkgs",
@@ -195,7 +219,7 @@ class TestDiscoverPackagesAndPyModules:
     def test_flat_layout_with_extra_files(self, tmp_path, extra_files, pkgs):
         files = self.FILES["flat"] + extra_files
         _populate_project_dir(tmp_path, files, {})
-        dist, _ = _run_sdist_programatically(tmp_path, {})
+        dist = _get_dist(tmp_path, {})
         assert set(dist.packages) == pkgs
 
     @pytest.mark.parametrize(
@@ -209,19 +233,19 @@ class TestDiscoverPackagesAndPyModules:
         files = self.FILES["flat"] + extra_files
         _populate_project_dir(tmp_path, files, {})
         with pytest.raises(PackageDiscoveryError, match="multiple (packages|modules)"):
-            _run_sdist_programatically(tmp_path, {})
+            _get_dist(tmp_path, {})
 
     def test_flat_layout_with_single_module(self, tmp_path):
         files = self.FILES["single_module"] + ["invalid-module-name.py"]
         _populate_project_dir(tmp_path, files, {})
-        dist, _ = _run_sdist_programatically(tmp_path, {})
+        dist = _get_dist(tmp_path, {})
         assert set(dist.py_modules) == {"pkg"}
 
     def test_flat_layout_with_multiple_modules(self, tmp_path):
         files = self.FILES["single_module"] + ["valid_module_name.py"]
         _populate_project_dir(tmp_path, files, {})
         with pytest.raises(PackageDiscoveryError, match="multiple (packages|modules)"):
-            _run_sdist_programatically(tmp_path, {})
+            _get_dist(tmp_path, {})
 
 
 class TestNoConfig:
@@ -239,7 +263,7 @@ class TestNoConfig:
     @pytest.mark.parametrize("example", EXAMPLES.keys())
     def test_discover_name(self, tmp_path, example):
         _populate_project_dir(tmp_path, self.EXAMPLES[example], {})
-        dist, _ = _run_sdist_programatically(tmp_path, {})
+        dist = _get_dist(tmp_path, {})
         dist.get_name() == example
 
     def test_build_with_discovered_name(self, tmp_path):
@@ -266,7 +290,7 @@ def test_discovered_package_dir_with_attr_directive_in_config(tmp_path, folder,
         + (tmp_path / "setup.cfg").read_text()
     )
 
-    dist, _ = _run_sdist_programatically(tmp_path, {})
+    dist = _get_dist(tmp_path, {})
     assert dist.get_name() == "pkg"
     assert dist.get_version() == "42"
     assert dist.package_dir
@@ -321,15 +345,27 @@ def _run_build(path, *flags):
     return run(cmd, env={'DISTUTILS_DEBUG': '1'})
 
 
-def _run_sdist_programatically(dist_path, attrs):
+def _get_dist(dist_path, attrs):
     root = "/".join(os.path.split(dist_path))  # POSIX-style
-    dist = Distribution({**attrs, "src_root": root})
-    dist.script_name = 'setup.py'
 
-    if (dist_path / "setup.cfg").exists():
-        dist.parse_config_files([dist_path / "setup.cfg"])
+    script = dist_path / 'setup.py'
+    if script.exists():
+        with _Path(dist_path):
+            dist = distutils.core.run_setup("setup.py", {}, stop_after="init")
+    else:
+        dist = Distribution(attrs)
+
+    dist.src_root = root
+    dist.script_name = "setup.py"
+    with _Path(dist_path):
+        dist.parse_config_files()
 
     dist.set_defaults()
+    return dist
+
+
+def _run_sdist_programatically(dist_path, attrs):
+    dist = _get_dist(dist_path, attrs)
     cmd = sdist(dist)
     cmd.ensure_finalized()
     assert cmd.distribution.packages or cmd.distribution.py_modules
-- 
cgit v1.2.1


From 1cdda8476f1f0ac99932494c1a129c021c5a9ccd Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 19 Mar 2022 03:53:09 +0000
Subject: Prevent setup_requires patches from activating auto-discovery

---
 setuptools/__init__.py              |  2 ++
 setuptools/discovery.py             |  7 ++++++-
 setuptools/tests/test_build_meta.py | 24 ++++++++++++++++++++++++
 3 files changed, 32 insertions(+), 1 deletion(-)

diff --git a/setuptools/__init__.py b/setuptools/__init__.py
index 15b1786e..187e7329 100644
--- a/setuptools/__init__.py
+++ b/setuptools/__init__.py
@@ -53,6 +53,8 @@ def _install_setup_requires(attrs):
             _incl = 'dependency_links', 'setup_requires'
             filtered = {k: attrs[k] for k in set(_incl) & set(attrs)}
             super().__init__(filtered)
+            # Prevent accidentally triggering discovery with incomplete set of attrs
+            self.set_defaults._disable()
 
         def finalize_options(self):
             """
diff --git a/setuptools/discovery.py b/setuptools/discovery.py
index 5c21199e..5ec5d584 100644
--- a/setuptools/discovery.py
+++ b/setuptools/discovery.py
@@ -273,8 +273,13 @@ class ConfigDiscovery:
     def __init__(self, distribution: "Distribution"):
         self.dist = distribution
         self._called = False
+        self._disabled = False
         self._root_dir: _Path  # delay so `src_root` can be set in dist
 
+    def _disable(self):
+        """Internal API to disable automatic discovery"""
+        self._disabled = True
+
     def __call__(self, force=False, name=True):
         """Automatically discover missing configuration fields
         and modifies the given ``distribution`` object in-place.
@@ -286,7 +291,7 @@ class ConfigDiscovery:
         directory changes), please use ``force=True`` (or create a new
         ``ConfigDiscovery`` instance).
         """
-        if force is False and self._called:
+        if force is False and (self._called or self._disabled):
             # Avoid overhead of multiple calls
             return
 
diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py
index 628d601e..36940e76 100644
--- a/setuptools/tests/test_build_meta.py
+++ b/setuptools/tests/test_build_meta.py
@@ -662,6 +662,30 @@ class TestBuildMetaBackend:
 
         assert expected == sorted(actual)
 
+    def test_setup_requires_with_auto_discovery(self, tmpdir_cwd):
+        # Make sure patches introduced to retrieve setup_requires don't accidentally
+        # activate auto-discovery and cause problems due to the incomplete set of
+        # attributes passed to MinimalDistribution
+        files = {
+            'pyproject.toml': DALS("""
+                [project]
+                name = "proj"
+                version = "42"
+            """),
+            "setup.py": DALS("""
+                __import__('setuptools').setup(
+                    setup_requires=["foo"],
+                    py_modules = ["hello", "world"]
+                )
+            """),
+            'hello.py': "'hello'",
+            'world.py': "'world'",
+        }
+        path.build(files)
+        build_backend = self.get_build_backend()
+        setup_requires = build_backend.get_requires_for_build_wheel()
+        assert setup_requires == ["wheel", "foo"]
+
     def test_dont_install_setup_requires(self, tmpdir_cwd):
         files = {
             'setup.py': DALS("""
-- 
cgit v1.2.1


From ddca7988e951021340ee445994d3c6c55a957eee Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 19 Mar 2022 08:58:32 -0400
Subject: Remove reference to non-existent doc.

---
 docs/deprecated/index.rst | 1 -
 1 file changed, 1 deletion(-)

diff --git a/docs/deprecated/index.rst b/docs/deprecated/index.rst
index ce2ac006..59fc7bef 100644
--- a/docs/deprecated/index.rst
+++ b/docs/deprecated/index.rst
@@ -13,7 +13,6 @@ objectives.
 .. toctree::
     :maxdepth: 1
 
-    python3
     python_eggs
     easy_install
     distutils/index
-- 
cgit v1.2.1


From e109eff86e98c0570a5c86f83e470ea942aab5a5 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 19 Mar 2022 09:10:41 -0400
Subject: Fix warnings for pypi references.

---
 CHANGES.rst                      | 14 ++++++--------
 changelog.d/README.rst           |  3 +--
 docs/deprecated/easy_install.rst |  7 ++-----
 docs/pkg_resources.rst           |  4 ++--
 docs/userguide/entry_point.rst   |  2 +-
 docs/userguide/extension.rst     |  4 ++--
 docs/userguide/quickstart.rst    |  2 +-
 7 files changed, 15 insertions(+), 21 deletions(-)

diff --git a/CHANGES.rst b/CHANGES.rst
index 3c724e47..9d1e22f1 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -718,7 +718,7 @@ Changes
   ``license_file`` (deprecated) and ``license_files`` options,
   relative to ``.dist-info``. - by :user:`cdce8p`
 * #2678: Moved Setuptools' own entry points into declarative config.
-* #2680: Vendored `more_itertools `_ for Setuptools.
+* #2680: Vendored :pypi:`more_itertools` for Setuptools.
 * #2681: Setuptools own setup.py no longer declares setup_requires, but instead expects wheel to be installed as declared by pyproject.toml.
 
 Misc
@@ -1646,7 +1646,7 @@ Breaking Changes
    * eggs are not supported
    * no support for the ``allow_hosts`` easy_install option (``index_url``/``find_links`` are still honored)
    * pip environment variables are honored (and take precedence over easy_install options)
-* #1898: Removed the "upload" and "register" commands in favor of `twine `_.
+* #1898: Removed the "upload" and "register" commands in favor of :pypi:`twine`.
 
 Changes
 ^^^^^^^
@@ -1656,7 +1656,7 @@ Changes
   * add support for manylinux2010
   * fix use of removed 'm' ABI flag in Python 3.8 on Windows
 * #1861: Fix empty namespace package installation from wheel.
-* #1877: Setuptools now exposes a new entry point hook "setuptools.finalize_distribution_options", enabling plugins like `setuptools_scm `_ to configure options on the distribution at finalization time.
+* #1877: Setuptools now exposes a new entry point hook "setuptools.finalize_distribution_options", enabling plugins like :pypi:`setuptools_scm` to configure options on the distribution at finalization time.
 
 
 v41.6.0
@@ -2923,7 +2923,7 @@ v26.1.0
 -------
 
 * #763: ``pkg_resources.get_default_cache`` now defers to the
-  `appdirs project `_ to
+  :pypi:`appdirs` project to
   resolve the cache directory. Adds a vendored dependency on
   appdirs to pkg_resources.
 
@@ -3915,8 +3915,7 @@ process to fail and PyPI uploads no longer accept files for 13.0.
 
 * Issue #313: Removed built-in support for subversion. Projects wishing to
   retain support for subversion will need to use a third party library. The
-  extant implementation is being ported to `setuptools_svn
-  `_.
+  extant implementation is being ported to :pypi:`setuptools_svn`.
 * Issue #315: Updated setuptools to hide its own loaded modules during
   installation of another package. This change will enable setuptools to
   upgrade (or downgrade) itself even when its own metadata and implementation
@@ -4420,8 +4419,7 @@ process to fail and PyPI uploads no longer accept files for 13.0.
 
 * Address security vulnerability in SSL match_hostname check as reported in
   Python #17997.
-* Prefer `backports.ssl_match_hostname
-  `_ for backport
+* Prefer :pypi:`backports.ssl_match_hostname` for backport
   implementation if present.
 * Correct NameError in ``ssl_support`` module (``socket.error``).
 
diff --git a/changelog.d/README.rst b/changelog.d/README.rst
index 49b4d563..6def76b5 100644
--- a/changelog.d/README.rst
+++ b/changelog.d/README.rst
@@ -21,8 +21,7 @@ recorded in the Git history rather than a changelog.
 Alright! So how to add a news fragment?
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
-``setuptools`` uses `towncrier `_
-for changelog management.
+``setuptools`` uses :pypi:`towncrier` for changelog management.
 To submit a change note about your PR, add a text file into the
 ``changelog.d/`` folder. It should contain an
 explanation of what applying this PR will change in the way
diff --git a/docs/deprecated/easy_install.rst b/docs/deprecated/easy_install.rst
index 76c3f608..3cf3bea9 100644
--- a/docs/deprecated/easy_install.rst
+++ b/docs/deprecated/easy_install.rst
@@ -34,7 +34,7 @@ Using "Easy Install"
 Installing "Easy Install"
 -------------------------
 
-Please see the `setuptools PyPI page `_
+Please see the :pypi:`setuptools` on the package index
 for download links and basic installation instructions for each of the
 supported platforms.
 
@@ -1020,10 +1020,7 @@ of the User installation scheme.  "virtualenv" provides a version of ``easy_inst
 scoped to the cloned python install and is used in the normal way. "virtualenv" does offer various features
 that the User installation scheme alone does not provide, e.g. the ability to hide the main python site-packages.
 
-Please refer to the `virtualenv`_ documentation for more details.
-
-.. _virtualenv: https://pypi.org/project/virtualenv/
-
+Please refer to the :pypi:`virtualenv` documentation for more details.
 
 
 Package Index "API"
diff --git a/docs/pkg_resources.rst b/docs/pkg_resources.rst
index c1158189..21ff6dc1 100644
--- a/docs/pkg_resources.rst
+++ b/docs/pkg_resources.rst
@@ -13,8 +13,8 @@ packages.
 Use of ``pkg_resources`` is discouraged in favor of
 `importlib.resources `_,
 `importlib.metadata `_,
-and their backports (`resources `_,
-`metadata `_).
+and their backports (:pypi:`importlib_resources`,
+:pypi:`importlib_metadata`).
 Please consider using those libraries instead of pkg_resources.
 
 
diff --git a/docs/userguide/entry_point.rst b/docs/userguide/entry_point.rst
index ea73bb5e..b97419c4 100644
--- a/docs/userguide/entry_point.rst
+++ b/docs/userguide/entry_point.rst
@@ -120,7 +120,7 @@ and tools like ``pip`` create wrapper scripts that invoke those commands.
 For a project wishing to solicit entry points, Setuptools recommends the
 `importlib.metadata `_
 module (part of stdlib since Python 3.8) or its backport,
-`importlib_metadata `_.
+:pypi:`importlib_metadata`.
 
 For example, to find the console script entry points from the example above:
 
diff --git a/docs/userguide/extension.rst b/docs/userguide/extension.rst
index d74ca3fe..21fb05b6 100644
--- a/docs/userguide/extension.rst
+++ b/docs/userguide/extension.rst
@@ -194,8 +194,8 @@ Adding Support for Revision Control Systems
 If the files you want to include in the source distribution are tracked using
 Git, Mercurial or SVN, you can use the following packages to achieve that:
 
-- Git and Mercurial: `setuptools_scm `_
-- SVN: `setuptools_svn `_
+- Git and Mercurial: :pypi:`setuptools_scm`
+- SVN: :pypi:`setuptools_svn`
 
 If you would like to create a plugin for ``setuptools`` to find files tracked
 by another revision control system, you can do so by adding an entry point to
diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst
index f3183624..4fb59b14 100644
--- a/docs/userguide/quickstart.rst
+++ b/docs/userguide/quickstart.rst
@@ -211,7 +211,7 @@ Uploading your package to PyPI
 ==============================
 After generating the distribution files, the next step would be to upload your
 distribution so others can use it. This functionality is provided by
-`twine `_ and we will only demonstrate the
+:pypi:`twine` and we will only demonstrate the
 basic use here.
 
 
-- 
cgit v1.2.1


From 6fe084a20d3ef436387ff66afe3cf1280e9aa06e Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 19 Mar 2022 09:13:49 -0400
Subject: Fix docs build errors in changelog.

---
 CHANGES.rst | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/CHANGES.rst b/CHANGES.rst
index 9d1e22f1..68b68fbd 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -15,7 +15,7 @@ Documentation changes
   and more prominent mentions to using a revision control system plugin as an
   alternative.
 * #3148: Removed mention to ``pkg_resources`` as the recommended way of accessing data
-  files, in favour of :doc:`importlib.resources`.
+  files, in favour of importlib.resources.
   Additionally more emphasis was put on the fact that *package data files* reside
   **inside** the *package directory* (and therefore should be *read-only*).
 
@@ -207,7 +207,7 @@ v60.4.0
 
 Changes
 ^^^^^^^
-* #2839: Removed `requires` sorting when installing wheels as an egg dir.
+* #2839: Removed ``requires`` sorting when installing wheels as an egg dir.
 * #2953: Fixed a bug that easy install incorrectly parsed Python 3.10 version string.
 * #3006: Fixed startup performance issue of Python interpreter due to imports of
   costly modules in ``_distutils_hack`` -- by :user:`tiran`
@@ -646,7 +646,7 @@ v57.5.0
 
 Changes
 ^^^^^^^
-* #2712: Added implicit globbing support for `[options.data_files]` values.
+* #2712: Added implicit globbing support for ``[options.data_files]`` values.
 
 Documentation changes
 ^^^^^^^^^^^^^^^^^^^^^
-- 
cgit v1.2.1


From 829009ba19393bdd74bf88c3ecba1d36cf8201db Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 19 Mar 2022 10:16:54 -0400
Subject: Extract _linker_params function to capture the concerns about
 matching the compiler.

---
 distutils/unixccompiler.py | 54 +++++++++++++++++++++++++---------------------
 1 file changed, 30 insertions(+), 24 deletions(-)

diff --git a/distutils/unixccompiler.py b/distutils/unixccompiler.py
index 4a260654..422719b3 100644
--- a/distutils/unixccompiler.py
+++ b/distutils/unixccompiler.py
@@ -74,6 +74,34 @@ def _split_aix(cmd):
     return cmd[:pivot], cmd[pivot:]
 
 
+def _linker_params(linker_cmd, compiler_cmd):
+    """
+    The linker command usually begins with the compiler
+    command (possibly multiple elements), followed by zero or more
+    params for shared library building.
+
+    If the LDSHARED env variable overrides the linker command,
+    however, the commands may not match.
+
+    Return the best guess of the linker parameters by stripping
+    the linker command. If the compiler command does not
+    match the linker command, assume the linker command is
+    just the first element.
+
+    >>> _linker_params('gcc foo bar'.split(), ['gcc'])
+    ['foo', 'bar']
+    >>> _linker_params('gcc foo bar'.split(), ['other'])
+    ['foo', 'bar']
+    >>> _linker_params('ccache gcc foo bar'.split(), 'ccache gcc'.split())
+    ['foo', 'bar']
+    >>> _linker_params(['gcc'], ['gcc'])
+    []
+    """
+    c_len = len(compiler_cmd)
+    pivot = c_len if linker_cmd[:c_len] == compiler_cmd else 1
+    return linker_cmd[pivot:]
+
+
 class UnixCCompiler(CCompiler):
 
     compiler_type = 'unix'
@@ -220,30 +248,8 @@ class UnixCCompiler(CCompiler):
                     _, compiler_cxx_ne = _split_env(self.compiler_cxx)
                     _, linker_exe_ne = _split_env(self.linker_exe)
 
-                    # Linker command given by linker_na usually starts with
-                    # with the C compiler given by linker_exe_ne and then
-                    # some options for shared library building if we are
-                    # building a shared library.
-                    # This may not always be true because the user can use
-                    # LDSHARED env variable to override the linker command.
-                    # When building C++ extensions, we need to replace all of
-                    # the C compiler which can be multiple words with the
-                    # C++ compiler.
-                    # To ensure that we are replacing the C compiler, we first
-                    # check that the linker command starts with the C compiler
-                    # and replace that part with the C++ compiler.
-                    if len(linker_na) >= len(linker_exe_ne) and \
-                            linker_na[:len(linker_exe_ne)] == linker_exe_ne:
-                        linker_na = compiler_cxx_ne + \
-                            linker_na[len(linker_exe_ne):]
-                    else:
-                        # This occurs if the user has set LDSHARED env variable
-                        # and we do not know how to plug in the C++ compiler
-                        # in this case. Therefore we fallback to the previous
-                        # potentially buggy functionality.
-                        linker_na[0] = compiler_cxx_ne[0]
-
-                    linker = env + aix + linker_na
+                    params = _linker_params(linker_na, linker_exe_ne)
+                    linker = env + aix + compiler_cxx_ne + params
 
                 if sys.platform == 'darwin':
                     linker = _osx_support.compiler_fixup(linker, ld_args)
-- 
cgit v1.2.1


From b41c06607d3a61a2612c1e44598612a44e7a55b7 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 19 Mar 2022 10:26:37 -0400
Subject: Reword comment and refactor logic to add context and use imperative
 voice.

---
 distutils/unixccompiler.py | 15 ++++++---------
 1 file changed, 6 insertions(+), 9 deletions(-)

diff --git a/distutils/unixccompiler.py b/distutils/unixccompiler.py
index 422719b3..b9f1fbfc 100644
--- a/distutils/unixccompiler.py
+++ b/distutils/unixccompiler.py
@@ -233,15 +233,12 @@ class UnixCCompiler(CCompiler):
                 ld_args.extend(extra_postargs)
             self.mkpath(os.path.dirname(output_filename))
             try:
-                # If we are building an executable, use the C compiler
-                # given by linker_exe as the linker command,
-                # else use the C compiler + shared options given by
-                # linker_so.
-                linker = (
-                    self.linker_exe
-                    if target_desc == CCompiler.EXECUTABLE else
-                    self.linker_so
-                )[:]
+                # Select a linker based on context: linker_exe when
+                # building an executable or linker_so (with shared options)
+                # when building a shared library.
+                building_exe = target_desc == CCompiler.EXECUTABLE
+                linker = (self.linker_exe if building_exe else self.linker_so)[:]
+
                 if target_lang == "c++" and self.compiler_cxx:
                     env, linker_ne = _split_env(linker)
                     aix, linker_na = _split_aix(linker_ne)
-- 
cgit v1.2.1


From 57968b965d06126c17f96a4f502df9436f726031 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 19 Mar 2022 10:35:33 -0400
Subject: Prefer docstrings to describe test intentions.

---
 distutils/tests/test_unixccompiler.py | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/distutils/tests/test_unixccompiler.py b/distutils/tests/test_unixccompiler.py
index 7544a86e..0e637af3 100644
--- a/distutils/tests/test_unixccompiler.py
+++ b/distutils/tests/test_unixccompiler.py
@@ -217,9 +217,12 @@ class UnixCCompilerTestCase(support.TempdirManager, unittest.TestCase):
 
     @unittest.skipIf(sys.platform == 'win32', "can't test on Windows")
     def test_cc_overrides_ldshared_for_cxx_correctly(self):
-        # Issur https://github.com/pypa/distutils/issues/126
-        # ensure that setting CC env variable also changes default linker
-        # correctly when C++ extensions are built
+        """
+        Ensure that setting CC env variable also changes default linker
+        correctly when building C++ extensions.
+
+        pypa/distutils#126
+        """
         def gcv(v):
             if v == 'LDSHARED':
                 return 'gcc-4.2 -bundle -undefined dynamic_lookup '
-- 
cgit v1.2.1


From a823ebaa791afa69c4c22177b578a302c25f1fbe Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 19 Mar 2022 10:37:52 -0400
Subject: =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins=20(delint).?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 distutils/tests/test_unixccompiler.py | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/distutils/tests/test_unixccompiler.py b/distutils/tests/test_unixccompiler.py
index 0e637af3..c8b4c149 100644
--- a/distutils/tests/test_unixccompiler.py
+++ b/distutils/tests/test_unixccompiler.py
@@ -238,18 +238,18 @@ class UnixCCompilerTestCase(support.TempdirManager, unittest.TestCase):
         sysconfig.get_config_var = gcv
         sysconfig.get_config_vars = gcvs
         with patch.object(self.cc, 'spawn', return_value=None) as mock_spawn, \
-                patch.object(self.cc, '_need_link', return_value=True) as mock_need, \
-                patch.object(self.cc, 'mkpath', return_value=None) as mock_mkpath, \
+                patch.object(self.cc, '_need_link', return_value=True), \
+                patch.object(self.cc, 'mkpath', return_value=None), \
                 EnvironmentVarGuard() as env:
             env['CC'] = 'ccache my_cc'
             env['CXX'] = 'my_cxx'
             del env['LDSHARED']
             sysconfig.customize_compiler(self.cc)
-            self.assertEqual(self.cc.linker_so[0:2], ['ccache','my_cc'])
+            self.assertEqual(self.cc.linker_so[0:2], ['ccache', 'my_cc'])
             self.cc.link(None, [], 'a.out', target_lang='c++')
             call_args = mock_spawn.call_args[0][0]
-            assert len(call_args) >= 4
-            assert(call_args[:4] == ['my_cxx', '-bundle', '-undefined', 'dynamic_lookup'])
+            expected = ['my_cxx', '-bundle', '-undefined', 'dynamic_lookup']
+            assert call_args[:4] == expected
 
     @unittest.skipIf(sys.platform == 'win32', "can't test on Windows")
     def test_explicit_ldshared(self):
-- 
cgit v1.2.1


From 4484edf5472bf692f49c73538bcb920c1a42db68 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 19 Mar 2022 11:34:58 -0400
Subject: Add upstream reference.

---
 tox.ini | 1 +
 1 file changed, 1 insertion(+)

diff --git a/tox.ini b/tox.ini
index 235c7897..83d54b2f 100644
--- a/tox.ini
+++ b/tox.ini
@@ -6,6 +6,7 @@ commands =
 setenv =
     PYTHONPATH = {toxinidir}
 passenv =
+    # workaround for tox-dev/tox#2382
     PROGRAMDATA
     PROGRAMFILES
     PROGRAMFILES(X86)
-- 
cgit v1.2.1


From 6b869254181c712a73415d3fe41b1ca13bfdc004 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sat, 19 Mar 2022 11:39:25 -0400
Subject: Update changelog

---
 changelog.d/3179.change.rst | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/3179.change.rst

diff --git a/changelog.d/3179.change.rst b/changelog.d/3179.change.rst
new file mode 100644
index 00000000..791a327b
--- /dev/null
+++ b/changelog.d/3179.change.rst
@@ -0,0 +1 @@
+Merge with pypa/distutils@267dbd25ac
-- 
cgit v1.2.1


From 252ff9affbec758a12e6a103049f5d1771060d44 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 20 Mar 2022 13:55:56 +0000
Subject: Simplify package name condition for flat layout

---
 setuptools/discovery.py | 14 +++++---------
 1 file changed, 5 insertions(+), 9 deletions(-)

diff --git a/setuptools/discovery.py b/setuptools/discovery.py
index 5ec5d584..837cea9e 100644
--- a/setuptools/discovery.py
+++ b/setuptools/discovery.py
@@ -224,15 +224,11 @@ class FlatLayoutPackageFinder(PEP420PackageFinder):
     """Reserved package names"""
 
     @staticmethod
-    def _looks_like_package(path: _Path, package_name: str) -> bool:
+    def _looks_like_package(_path: _Path, package_name: str) -> bool:
         names = package_name.split('.')
-        return bool(
-            names and (
-                # Consider PEP 561
-                (names[0].isidentifier() or names[0].endswith("-stubs"))
-                and all(name.isidentifier() for name in names[1:])
-            )
-        )
+        # Consider PEP 561
+        root_pkg_is_valid = names[0].isidentifier() or names[0].endswith("-stubs")
+        return root_pkg_is_valid and all(name.isidentifier() for name in names[1:])
 
 
 class FlatLayoutModuleFinder(ModuleFinder):
@@ -475,7 +471,7 @@ def remove_nested_packages(packages: List[str]) -> List[str]:
 
 
 def remove_stubs(packages: List[str]) -> List[str]:
-    """Remove type stubs from a list of packages.
+    """Remove type stubs (:pep:`561`) from a list of packages.
 
     >>> remove_stubs(["a", "a.b", "a-stubs", "a-stubs.b.c", "b", "c-stubs"])
     ['a', 'a.b', 'b']
-- 
cgit v1.2.1


From 3b3bbfdd5c793f2c414c7906181cd95d9674f916 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 20 Mar 2022 19:29:56 +0000
Subject: Use the same comment as distutils

---
 tox.ini | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/tox.ini b/tox.ini
index ca29dbbb..22c796ff 100644
--- a/tox.ini
+++ b/tox.ini
@@ -20,7 +20,7 @@ passenv =
 	windir  # required for test_pkg_resources
 	# honor git config in pytest-perf
 	HOME
-	# Microsoft's compiler suite (pypa/distutils#118)
+	# workaround for tox-dev/tox#2382
 	PROGRAMDATA
 	PROGRAMFILES
 	PROGRAMFILES(x86)
@@ -31,7 +31,7 @@ extras = testing-integration
 passenv =
 	{[testenv]passenv}
 	DOWNLOAD_PATH
-	# Microsoft's compiler suite (pypa/distutils#118)
+	# workaround for tox-dev/tox#2382
 	PROGRAMDATA
 	PROGRAMFILES
 	PROGRAMFILES(x86)
-- 
cgit v1.2.1


From 32caf7312860ecdb54a5d70067f2c0a914c73b25 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 20 Mar 2022 19:54:30 +0000
Subject: Attempt to clarify which url is missing for pyproject-metadata builds

When the user does not specify `Homepage` (or any variant such as
`home-page`), distutils will warn the following message:

    warning: check: missing required meta-data: url

This message is fine for `setup.cfg` builds because the field there is
called `url`, but it does not work well for builds using pyproject.toml
metadata.

The change implemented here will add some other logging information that
try to point the user in the correct direction for solving this issue.

This problem was first identified in:
https://discuss.python.org/t/help-testing-experimental-features-in-setuptools/13821
---
 setuptools/config/_apply_pyprojecttoml.py | 20 +++++++++++++++-----
 1 file changed, 15 insertions(+), 5 deletions(-)

diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py
index ce638c62..300b5d71 100644
--- a/setuptools/config/_apply_pyprojecttoml.py
+++ b/setuptools/config/_apply_pyprojecttoml.py
@@ -5,6 +5,7 @@ The distribution and metadata objects are modeled after (an old version of)
 core metadata, therefore configs in the format specified for ``pyproject.toml``
 need to be processed before being applied.
 """
+import logging
 import os
 from collections.abc import Mapping
 from email.headerregistry import Address
@@ -24,6 +25,8 @@ _DictOrStr = Union[dict, str]
 _CorrespFn = Callable[["Distribution", Any, _Path], None]
 _Correspondence = Union[str, _CorrespFn]
 
+_logger = logging.getLogger(__name__)
+
 
 def apply(dist: "Distribution", config: dict, filename: _Path) -> "Distribution":
     """Apply configuration dict read with :func:`read_configuration`"""
@@ -140,6 +143,16 @@ def _project_urls(dist: "Distribution", val: dict, _root_dir):
     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. It is advisable to link some kind of reference "
+            "for your project (e.g. source code or documentation)."
+        )
+        _logger.warning(msg)
     _set_config(dist, "project_urls", val.copy())
 
 
@@ -166,8 +179,6 @@ def _unify_entry_points(project_table: dict):
 
 
 def _copy_command_options(pyproject: dict, dist: "Distribution", filename: _Path):
-    from distutils import log
-
     tool_table = pyproject.get("tool", {})
     cmdclass = tool_table.get("setuptools", {}).get("cmdclass", {})
     valid_options = _valid_command_options(cmdclass)
@@ -183,7 +194,7 @@ def _copy_command_options(pyproject: dict, dist: "Distribution", filename: _Path
             if key not in valid:
                 # To avoid removing options that are specified dynamically we
                 # just log a warn...
-                log.warn(f"Command option {cmd}.{key} is not defined")
+                _logger.warning(f"Command option {cmd}.{key} is not defined")
 
 
 def _valid_command_options(cmdclass: Mapping = EMPTY) -> Dict[str, Set[str]]:
@@ -208,9 +219,8 @@ def _load_ep(ep: "metadata.EntryPoint") -> Optional[Tuple[str, Type]]:
     try:
         return (ep.name, ep.load())
     except Exception as ex:
-        from distutils import log
         msg = f"{ex.__class__.__name__} while trying to load entry-point {ep.name}"
-        log.warn(f"{msg}: {ex}")
+        _logger.warning(f"{msg}: {ex}")
         return None
 
 
-- 
cgit v1.2.1


From 38c7a6ed5d62f4ee93ee37717b89ed8bbce8a4d1 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 20 Mar 2022 20:33:17 +0000
Subject: Use blank lines to emphasize warnings

This matches the level of emphasis used by distutils.
---
 setuptools/config/_apply_pyprojecttoml.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py
index 300b5d71..c8ddab4b 100644
--- a/setuptools/config/_apply_pyprojecttoml.py
+++ b/setuptools/config/_apply_pyprojecttoml.py
@@ -149,8 +149,8 @@ def _project_urls(dist: "Distribution", val: dict, _root_dir):
     # Let's add a warning before distutils check to help users understand the problem:
     if not dist.metadata.url:
         msg = (
-            "Missing `Homepage` url. It is advisable to link some kind of reference "
-            "for your project (e.g. source code or documentation)."
+            "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())
-- 
cgit v1.2.1


From 533115f08eab629a4f92c0f9f5f8d296153cd765 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 21 Mar 2022 08:34:44 +0000
Subject: Refactor ConfigDiscovery._root_dir as a property

---
 setuptools/discovery.py | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/setuptools/discovery.py b/setuptools/discovery.py
index 837cea9e..b9aedfb0 100644
--- a/setuptools/discovery.py
+++ b/setuptools/discovery.py
@@ -270,12 +270,16 @@ class ConfigDiscovery:
         self.dist = distribution
         self._called = False
         self._disabled = False
-        self._root_dir: _Path  # delay so `src_root` can be set in dist
 
     def _disable(self):
         """Internal API to disable automatic discovery"""
         self._disabled = True
 
+    @property
+    def _root_dir(self) -> _Path:
+        # The best is to wait until `src_root` is set in dist, before using _root_dir.
+        return self.dist.src_root or os.curdir
+
     def __call__(self, force=False, name=True):
         """Automatically discover missing configuration fields
         and modifies the given ``distribution`` object in-place.
@@ -291,8 +295,6 @@ class ConfigDiscovery:
             # Avoid overhead of multiple calls
             return
 
-        self._root_dir = self.dist.src_root or os.curdir
-
         self._analyse_package_layout()
         if name:
             self.analyse_name()  # depends on ``packages`` and ``py_modules``
-- 
cgit v1.2.1


From e4649ea6c503b3eda7c29abf7990417ccd4fcd46 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 21 Mar 2022 12:32:38 +0000
Subject: Fix test missing assertion

---
 setuptools/tests/test_config_discovery.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py
index 655e2a9f..069e819a 100644
--- a/setuptools/tests/test_config_discovery.py
+++ b/setuptools/tests/test_config_discovery.py
@@ -264,7 +264,7 @@ class TestNoConfig:
     def test_discover_name(self, tmp_path, example):
         _populate_project_dir(tmp_path, self.EXAMPLES[example], {})
         dist = _get_dist(tmp_path, {})
-        dist.get_name() == example
+        assert dist.get_name() == example
 
     def test_build_with_discovered_name(self, tmp_path):
         files = ["src/ns/nested/pkg/__init__.py"]
-- 
cgit v1.2.1


From 7f29cd5b84ffee9417aa0d0642bba5e6d97cd836 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 21 Mar 2022 14:04:04 +0000
Subject: Improve interaction between pyproject.toml metadata and discovery

---
 setuptools/config/expand.py        |  72 +++++++++++++++++++-----
 setuptools/config/pyprojecttoml.py | 112 ++++++++++++++++++-------------------
 setuptools/config/setupcfg.py      |   4 +-
 3 files changed, 115 insertions(+), 73 deletions(-)

diff --git a/setuptools/config/expand.py b/setuptools/config/expand.py
index 694476a0..94c9ee38 100644
--- a/setuptools/config/expand.py
+++ b/setuptools/config/expand.py
@@ -29,9 +29,12 @@ from typing import (
     Callable,
     Dict,
     Iterable,
+    Iterator,
     List,
+    Mapping,
     Optional,
     Tuple,
+    TypeVar,
     Union,
     cast
 )
@@ -46,6 +49,8 @@ if TYPE_CHECKING:
 
 chain_iter = chain.from_iterable
 _Path = Union[str, os.PathLike]
+_K = TypeVar("_K")
+_V = TypeVar("_V", covariant=True)
 
 
 class StaticModule:
@@ -146,7 +151,7 @@ def _assert_local(filepath: _Path, root_dir: str):
 
 def read_attr(
     attr_desc: str,
-    package_dir: Optional[dict] = None,
+    package_dir: Optional[Mapping[str, str]] = None,
     root_dir: Optional[_Path] = None
 ):
     """Reads the value of an attribute from a module.
@@ -203,7 +208,7 @@ def _load_spec(spec: ModuleSpec, module_name: str) -> ModuleType:
 
 
 def _find_module(
-    module_name: str, package_dir: Optional[dict], root_dir: _Path
+    module_name: str, package_dir: Optional[Mapping[str, str]], root_dir: _Path
 ) -> Tuple[_Path, Optional[str], str]:
     """Given a module (that could normally be imported by ``module_name``
     after the build is complete), find the path to the parent directory where
@@ -238,7 +243,7 @@ def _find_module(
 
 def resolve_class(
     qualified_class_name: str,
-    package_dir: Optional[dict] = None,
+    package_dir: Optional[Mapping[str, str]] = None,
     root_dir: Optional[_Path] = None
 ) -> Callable:
     """Given a qualified class name, return the associated class object"""
@@ -254,7 +259,7 @@ def resolve_class(
 
 def cmdclass(
     values: Dict[str, str],
-    package_dir: Optional[dict] = None,
+    package_dir: Optional[Mapping[str, str]] = None,
     root_dir: Optional[_Path] = None
 ) -> Dict[str, Callable]:
     """Given a dictionary mapping command names to strings for qualified class
@@ -378,12 +383,10 @@ class EnsurePackagesDiscovered:
     """Some expand functions require all the packages to already be discovered before
     they run, e.g. :func:`read_attr`, :func:`resolve_class`, :func:`cmdclass`.
 
-    Therefore in some cases we will need to run autodiscovery during the parsing of the
-    configuration. However, it is better to postpone calling package discovery as much
-    as possible.
-
-    We should only run the discovery if absolutely necessary, otherwise we can miss
-    files that define important configuration (like ``package_dir``) are processed.
+    Therefore in some cases we will need to run autodiscovery during the evaluation of
+    the configuration. However, it is better to postpone calling package discovery as
+    much as possible, because some parameters can influence it (e.g. ``package_dir``),
+    and those might not have been processed yet.
     """
 
     def __init__(self, distribution: "Distribution"):
@@ -391,9 +394,10 @@ class EnsurePackagesDiscovered:
         self._called = False
 
     def __call__(self):
-        self._called = True
-        self._dist.set_defaults(name=False)  # Skip name since we are parsing metadata
-        return self._dist.package_dir
+        """Trigger the automatic package discovery, if it is still necessary."""
+        if not self._called:
+            self._called = True
+            self._dist.set_defaults(name=False)  # Skip name, we can still be parsing
 
     def __enter__(self):
         return self
@@ -401,3 +405,45 @@ class EnsurePackagesDiscovered:
     def __exit__(self, _exc_type, _exc_value, _traceback):
         if self._called:
             self._dist.set_defaults.analyse_name()  # Now we can set a default name
+
+    def _get_package_dir(self) -> Mapping[str, str]:
+        self()
+        return self._dist.package_dir
+
+    @property
+    def package_dir(self) -> Mapping[str, str]:
+        """Proxy to ``package_dir`` that may trigger auto-discovery when used."""
+        return LazyMappingProxy(self._get_package_dir)
+
+
+class LazyMappingProxy(Mapping[_K, _V]):
+    """Mapping proxy that delays resolving the target object, until really needed.
+
+    >>> def obtain_mapping():
+    ...     print("Running expensive function!")
+    ...     return {"key": "value", "other key": "other value"}
+    >>> mapping = LazyMappingProxy(obtain_mapping)
+    >>> mapping["key"]
+    Running expensive function!
+    'value'
+    >>> mapping["other key"]
+    'other value'
+    """
+
+    def __init__(self, obtain_mapping_value: Callable[[], Mapping[_K, _V]]):
+        self._obtain = obtain_mapping_value
+        self._value: Optional[Mapping[_K, _V]] = None
+
+    def _target(self) -> Mapping[_K, _V]:
+        if self._value is None:
+            self._value = self._obtain()
+        return self._value
+
+    def __getitem__(self, key: _K) -> _V:
+        return self._target()[key]
+
+    def __len__(self) -> int:
+        return len(self._target())
+
+    def __iter__(self) -> Iterator[_K]:
+        return iter(self._target())
diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index 2b430787..7867cd52 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -4,7 +4,7 @@ import os
 import warnings
 from contextlib import contextmanager
 from functools import partial
-from typing import TYPE_CHECKING, Callable, Dict, Optional, Tuple, Union
+from typing import TYPE_CHECKING, Callable, Dict, Optional, Mapping, Union
 
 from setuptools.errors import FileError, OptionError
 
@@ -137,84 +137,80 @@ def expand_configuration(
     root_dir = root_dir or os.getcwd()
     project_cfg = config.get("project", {})
     setuptools_cfg = config.get("tool", {}).get("setuptools", {})
+    silent = ignore_option_errors
 
-    # A distribution object is required for discovering the correct package_dir
-    dist, setuptools_cfg = _ensure_dist_and_package_dir(
-        dist, project_cfg, setuptools_cfg, root_dir
-    )
-
-    _expand_packages(setuptools_cfg, root_dir, ignore_option_errors)
+    _expand_packages(setuptools_cfg, root_dir, silent)
     _canonic_package_data(setuptools_cfg)
     _canonic_package_data(setuptools_cfg, "exclude-package-data")
 
-    with _expand.EnsurePackagesDiscovered(dist) as ensure_discovered:
-        _fill_discovered_attrs(dist, setuptools_cfg, ensure_discovered)
-        package_dir = setuptools_cfg["package-dir"]
+    # A distribution object is required for discovering the correct package_dir
+    dist = _ensure_dist(dist, project_cfg, root_dir)
 
-        process = partial(_process_field, ignore_option_errors=ignore_option_errors)
+    with _EnsurePackagesDiscovered(dist, setuptools_cfg) as ensure_discovered:
+        package_dir = ensure_discovered.package_dir
+        process = partial(_process_field, ignore_option_errors=silent)
         cmdclass = partial(_expand.cmdclass, package_dir=package_dir, root_dir=root_dir)
         data_files = partial(_expand.canonic_data_files, root_dir=root_dir)
 
         process(setuptools_cfg, "data-files", data_files)
         process(setuptools_cfg, "cmdclass", cmdclass)
-        _expand_all_dynamic(project_cfg, setuptools_cfg, root_dir, ignore_option_errors)
+        _expand_all_dynamic(project_cfg, setuptools_cfg, package_dir, root_dir, silent)
 
     return config
 
 
-def _ensure_dist_and_package_dir(
-    dist: Optional["Distribution"],
-    project_cfg: dict,
-    setuptools_cfg: dict,
-    root_dir: _Path,
-) -> Tuple["Distribution", dict]:
+def _ensure_dist(
+    dist: Optional["Distribution"], project_cfg: dict, root_dir: _Path
+) -> "Distribution":
     from setuptools.dist import Distribution
 
     attrs = {"src_root": root_dir, "name": project_cfg.get("name", None)}
-    dist = dist or Distribution(attrs)
-
-    # dist and setuptools_cfg should use the same package_dir
-    if dist.package_dir is None:
-        dist.package_dir = setuptools_cfg.get("package-dir", {})
-    if setuptools_cfg.get("package-dir") is None:
-        setuptools_cfg["package-dir"] = dist.package_dir
-
-    return dist, setuptools_cfg
-
-
-def _fill_discovered_attrs(
-    dist: "Distribution",
-    setuptools_cfg: dict,
-    ensure_discovered: _expand.EnsurePackagesDiscovered,
-):
-    """When entering the context, the values of ``packages``, ``py_modules`` and
-    ``package_dir`` that are missing in ``dist`` are copied from ``setuptools_cfg``.
-    When existing the context, if these values are missing in ``setuptools_cfg``, they
-    will be copied from ``dist``.
-    """
-    package_dir = setuptools_cfg["package-dir"]
-    dist.package_dir = package_dir  # need to be the same object
-
-    # Set `py_modules` and `packages` in dist to short-circuit auto-discovery,
-    # but avoid overwriting empty lists purposefully set by users.
-    if isinstance(setuptools_cfg.get("py-modules"), list) and dist.py_modules is None:
-        dist.py_modules = setuptools_cfg["py-modules"]
-    if isinstance(setuptools_cfg.get("packages"), list) and dist.packages is None:
-        dist.packages = setuptools_cfg["packages"]
-
-    package_dir.update(ensure_discovered())
-
-    # If anything was discovered set them back, so they count in the final config.
-    setuptools_cfg.setdefault("packages", dist.packages)
-    setuptools_cfg.setdefault("py-modules", dist.py_modules)
+    return dist or Distribution(attrs)
+
+
+class _EnsurePackagesDiscovered(_expand.EnsurePackagesDiscovered):
+    def __init__(self, distribution: "Distribution", setuptools_cfg: dict):
+        super().__init__(distribution)
+        self._setuptools_cfg = setuptools_cfg
+
+    def __enter__(self):
+        """When entering the context, the values of ``packages``, ``py_modules`` and
+        ``package_dir`` that are missing in ``dist`` are copied from ``setuptools_cfg``.
+        """
+        dist, cfg = self._dist, self._setuptools_cfg
+        package_dir: Dict[str, str] = cfg.setdefault("package-dir", {})
+        package_dir.update(dist.package_dir or {})
+        dist.package_dir = package_dir  # needs to be the same object
+
+        # Set `py_modules` and `packages` in dist to short-circuit auto-discovery,
+        # but avoid overwriting empty lists purposefully set by users.
+        if dist.py_modules is None:
+            dist.py_modules = cfg.get("py-modules")
+        if dist.packages is None:
+            dist.packages = cfg.get("packages")
+
+        return super().__enter__()
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        """When exiting the context, if values of ``packages``, ``py_modules`` and
+        ``package_dir`` are missing in ``setuptools_cfg``, copy from ``dist``.
+        """
+        # If anything was discovered set them back, so they count in the final config.
+        self._setuptools_cfg.setdefault("packages", self._dist.packages)
+        self._setuptools_cfg.setdefault("py-modules", self._dist.py_modules)
+        return super().__exit__(exc_type, exc_value, traceback)
 
 
 def _expand_all_dynamic(
-    project_cfg: dict, setuptools_cfg: dict, root_dir: _Path, ignore_option_errors: bool
+    project_cfg: dict,
+    setuptools_cfg: dict,
+    package_dir: Mapping[str, str],
+    root_dir: _Path,
+    ignore_option_errors: bool,
 ):
     silent = ignore_option_errors
     dynamic_cfg = setuptools_cfg.get("dynamic", {})
-    pkg_dir = setuptools_cfg["package-dir"]
+    pkg_dir = package_dir
     special = (
         "readme",
         "version",
@@ -251,7 +247,7 @@ def _expand_all_dynamic(
 def _expand_dynamic(
     dynamic_cfg: dict,
     field: str,
-    package_dir: dict,
+    package_dir: Mapping[str, str],
     root_dir: _Path,
     ignore_option_errors: bool,
 ):
@@ -296,7 +292,7 @@ def _expand_packages(setuptools_cfg: dict, root_dir: _Path, ignore_option_errors
     find = packages.get("find")
     if isinstance(find, dict):
         find["root_dir"] = root_dir
-        find["fill_package_dir"] = setuptools_cfg["package-dir"]
+        find["fill_package_dir"] = setuptools_cfg.setdefault("package-dir", {})
         with _ignore_errors(ignore_option_errors):
             setuptools_cfg["packages"] = _expand.find_packages(**find)
 
diff --git a/setuptools/config/setupcfg.py b/setuptools/config/setupcfg.py
index 36460d95..5ecf6269 100644
--- a/setuptools/config/setupcfg.py
+++ b/setuptools/config/setupcfg.py
@@ -368,7 +368,7 @@ class ConfigHandler(Generic[Target]):
         attr_desc = value.replace(attr_directive, '')
 
         # Make sure package_dir is populated correctly, so `attr:` directives can work
-        package_dir.update(self.ensure_discovered())
+        package_dir.update(self.ensure_discovered.package_dir)
         return expand.read_attr(attr_desc, package_dir, root_dir)
 
     @classmethod
@@ -596,7 +596,7 @@ class ConfigOptionsHandler(ConfigHandler["Distribution"]):
         }
 
     def _parse_cmdclass(self, value):
-        package_dir = self.ensure_discovered()
+        package_dir = self.ensure_discovered.package_dir
         return expand.cmdclass(self._parse_dict(value), package_dir, self.root_dir)
 
     def _parse_packages(self, value):
-- 
cgit v1.2.1


From 599777036b06c735913a8745ae31ab83acaf9ef2 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 21 Mar 2022 14:23:38 +0000
Subject: Rename variable alias

Rename 'silent' to 'ignore', because it seems more appropriate.
---
 setuptools/config/pyprojecttoml.py | 24 ++++++++++++------------
 1 file changed, 12 insertions(+), 12 deletions(-)

diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index 7867cd52..609b07f5 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -137,9 +137,9 @@ def expand_configuration(
     root_dir = root_dir or os.getcwd()
     project_cfg = config.get("project", {})
     setuptools_cfg = config.get("tool", {}).get("setuptools", {})
-    silent = ignore_option_errors
+    ignore = ignore_option_errors
 
-    _expand_packages(setuptools_cfg, root_dir, silent)
+    _expand_packages(setuptools_cfg, root_dir, ignore)
     _canonic_package_data(setuptools_cfg)
     _canonic_package_data(setuptools_cfg, "exclude-package-data")
 
@@ -148,13 +148,13 @@ def expand_configuration(
 
     with _EnsurePackagesDiscovered(dist, setuptools_cfg) as ensure_discovered:
         package_dir = ensure_discovered.package_dir
-        process = partial(_process_field, ignore_option_errors=silent)
+        process = partial(_process_field, ignore_option_errors=ignore)
         cmdclass = partial(_expand.cmdclass, package_dir=package_dir, root_dir=root_dir)
         data_files = partial(_expand.canonic_data_files, root_dir=root_dir)
 
         process(setuptools_cfg, "data-files", data_files)
         process(setuptools_cfg, "cmdclass", cmdclass)
-        _expand_all_dynamic(project_cfg, setuptools_cfg, package_dir, root_dir, silent)
+        _expand_all_dynamic(project_cfg, setuptools_cfg, package_dir, root_dir, ignore)
 
     return config
 
@@ -208,7 +208,7 @@ def _expand_all_dynamic(
     root_dir: _Path,
     ignore_option_errors: bool,
 ):
-    silent = ignore_option_errors
+    ignore = ignore_option_errors
     dynamic_cfg = setuptools_cfg.get("dynamic", {})
     pkg_dir = package_dir
     special = (
@@ -224,23 +224,23 @@ def _expand_all_dynamic(
     regular_dynamic = (x for x in dynamic if x not in special)
 
     for field in regular_dynamic:
-        value = _expand_dynamic(dynamic_cfg, field, pkg_dir, root_dir, silent)
+        value = _expand_dynamic(dynamic_cfg, field, pkg_dir, root_dir, ignore)
         project_cfg[field] = value
 
     if "version" in dynamic and "version" in dynamic_cfg:
-        version = _expand_dynamic(dynamic_cfg, "version", pkg_dir, root_dir, silent)
+        version = _expand_dynamic(dynamic_cfg, "version", pkg_dir, root_dir, ignore)
         project_cfg["version"] = _expand.version(version)
 
     if "readme" in dynamic:
-        project_cfg["readme"] = _expand_readme(dynamic_cfg, root_dir, silent)
+        project_cfg["readme"] = _expand_readme(dynamic_cfg, root_dir, ignore)
 
     if "entry-points" in dynamic:
         field = "entry-points"
-        value = _expand_dynamic(dynamic_cfg, field, pkg_dir, root_dir, silent)
+        value = _expand_dynamic(dynamic_cfg, field, pkg_dir, root_dir, ignore)
         project_cfg.update(_expand_entry_points(value, dynamic))
 
     if "classifiers" in dynamic:
-        value = _expand_dynamic(dynamic_cfg, "classifiers", pkg_dir, root_dir, silent)
+        value = _expand_dynamic(dynamic_cfg, "classifiers", pkg_dir, root_dir, ignore)
         project_cfg["classifiers"] = value.splitlines()
 
 
@@ -267,9 +267,9 @@ def _expand_dynamic(
 def _expand_readme(
     dynamic_cfg: dict, root_dir: _Path, ignore_option_errors: bool
 ) -> Dict[str, str]:
-    silent = ignore_option_errors
+    ignore = ignore_option_errors
     return {
-        "text": _expand_dynamic(dynamic_cfg, "readme", {}, root_dir, silent),
+        "text": _expand_dynamic(dynamic_cfg, "readme", {}, root_dir, ignore),
         "content-type": dynamic_cfg["readme"].get("content-type", "text/x-rst"),
     }
 
-- 
cgit v1.2.1


From 4c26a65c810d977c3f904e809b0d2f8accd695d1 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 21 Mar 2022 20:13:41 +0000
Subject: Ensure empty package_dir is not replaced on auto-discovery

---
 setuptools/discovery.py | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/setuptools/discovery.py b/setuptools/discovery.py
index b9aedfb0..1672d013 100644
--- a/setuptools/discovery.py
+++ b/setuptools/discovery.py
@@ -280,6 +280,12 @@ class ConfigDiscovery:
         # The best is to wait until `src_root` is set in dist, before using _root_dir.
         return self.dist.src_root or os.curdir
 
+    @property
+    def _package_dir(self) -> Dict[str, str]:
+        if self.dist.package_dir is None:
+            return {}
+        return self.dist.package_dir
+
     def __call__(self, force=False, name=True):
         """Automatically discover missing configuration fields
         and modifies the given ``distribution`` object in-place.
@@ -321,7 +327,7 @@ class ConfigDiscovery:
 
     def _analyse_explicit_layout(self) -> bool:
         """The user can explicitly give a package layout via ``package_dir``"""
-        package_dir = (self.dist.package_dir or {}).copy()
+        package_dir = self._package_dir.copy()  # don't modify directly
         package_dir.pop("", None)  # This falls under the "src-layout" umbrella
         root_dir = self._root_dir
 
@@ -348,13 +354,14 @@ class ConfigDiscovery:
         If ``package_dir[""]`` is not given, but the ``src`` directory exists,
         this function will set ``package_dir[""] = "src"``.
         """
-        package_dir = self.dist.package_dir = self.dist.package_dir or {}
+        package_dir = self._package_dir
         src_dir = os.path.join(self._root_dir, package_dir.get("", "src"))
         if not os.path.isdir(src_dir):
             return False
 
         log.debug(f"`src-layout` detected -- analysing {src_dir}")
         package_dir.setdefault("", os.path.basename(src_dir))
+        self.dist.package_dir = package_dir  # persist eventual modifications
         self.dist.packages = PEP420PackageFinder.find(src_dir)
         self.dist.py_modules = ModuleFinder.find(src_dir)
         log.debug(f"discovered packages -- {self.dist.packages}")
-- 
cgit v1.2.1


From 05c00ae948fa5059ba445727fa450f7dfb6dda29 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 21 Mar 2022 20:59:02 +0000
Subject: Make sure to ignore option errors with MinimalDistribution

---
 setuptools/config/pyprojecttoml.py        | 15 +++++++++------
 setuptools/dist.py                        |  2 +-
 setuptools/tests/test_config_discovery.py | 12 ++++++++++++
 3 files changed, 22 insertions(+), 7 deletions(-)

diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index 609b07f5..834d5a35 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -43,11 +43,13 @@ def validate(config: dict, filepath: _Path):
         raise error from None
 
 
-def apply_configuration(dist: "Distribution", filepath: _Path) -> "Distribution":
+def apply_configuration(
+    dist: "Distribution", filepath: _Path, ignore_option_errors=False,
+) -> "Distribution":
     """Apply the configuration from a ``pyproject.toml`` file into an existing
     distribution object.
     """
-    config = read_configuration(filepath, dist=dist)
+    config = read_configuration(filepath, True, ignore_option_errors, dist)
     return apply(dist, config, filepath)
 
 
@@ -253,10 +255,11 @@ def _expand_dynamic(
 ):
     if field in dynamic_cfg:
         directive = dynamic_cfg[field]
-        if "file" in directive:
-            return _expand.read_files(directive["file"], root_dir)
-        if "attr" in directive:
-            return _expand.read_attr(directive["attr"], package_dir, root_dir)
+        with _ignore_errors(ignore_option_errors):
+            if "file" in directive:
+                return _expand.read_files(directive["file"], root_dir)
+            if "attr" in directive:
+                return _expand.read_attr(directive["attr"], package_dir, root_dir)
     elif not ignore_option_errors:
         msg = f"Impossible to expand dynamic value of {field!r}. "
         msg += f"No configuration found for `tool.setuptools.dynamic.{field}`"
diff --git a/setuptools/dist.py b/setuptools/dist.py
index 1cdb7472..865a19dd 100644
--- a/setuptools/dist.py
+++ b/setuptools/dist.py
@@ -833,7 +833,7 @@ class Distribution(_Distribution):
             self, self.command_options, ignore_option_errors=ignore_option_errors
         )
         for filename in tomlfiles:
-            pyprojecttoml.apply_configuration(self, filename)
+            pyprojecttoml.apply_configuration(self, filename, ignore_option_errors)
 
         self._finalize_requires()
         self._finalize_license_files()
diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py
index 069e819a..2715f769 100644
--- a/setuptools/tests/test_config_discovery.py
+++ b/setuptools/tests/test_config_discovery.py
@@ -303,6 +303,18 @@ def test_discovered_package_dir_with_attr_directive_in_config(tmp_path, folder,
     assert dist_file.is_file()
 
 
+def test_discovered_package_dir_with_attr_in_pyproject_config(tmp_path):
+    _populate_project_dir(tmp_path, ["src/pkg/__init__.py"], {})
+    (tmp_path / "src/pkg/__init__.py").write_text("version = 42")
+    (tmp_path / "pyproject.toml").write_text(
+        "[project]\nname = 'pkg'\ndynamic = ['version']\n"
+        "[tool.setuptools.dynamic]\nversion = {attr = 'pkg.version'}\n"
+    )
+    dist = _get_dist(tmp_path, {})
+    assert dist.get_version() == "42"
+    assert dist.package_dir == {"": "src"}
+
+
 def _populate_project_dir(root, files, options):
     # NOTE: Currently pypa/build will refuse to build the project if no
     # `pyproject.toml` or `setup.py` is found. So it is impossible to do
-- 
cgit v1.2.1


From 14180bab5f8d88333a1636f30d6092e5c6bd6f0d Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 22 Mar 2022 09:50:02 +0000
Subject: Test discovery when ext_modules are provided

This example is based on the way the kiwisolver package is organised.
---
 setuptools/tests/test_config_discovery.py | 52 +++++++++++++++++++++++++++++++
 1 file changed, 52 insertions(+)

diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py
index 2715f769..053a605b 100644
--- a/setuptools/tests/test_config_discovery.py
+++ b/setuptools/tests/test_config_discovery.py
@@ -2,6 +2,7 @@ import os
 import sys
 from configparser import ConfigParser
 from itertools import product
+from inspect import cleandoc
 
 from setuptools.command.sdist import sdist
 from setuptools.dist import Distribution
@@ -315,6 +316,57 @@ def test_discovered_package_dir_with_attr_in_pyproject_config(tmp_path):
     assert dist.package_dir == {"": "src"}
 
 
+def test_skip_when_extensions_are_provided(tmp_path):
+    """Ensure that auto-discovery is not triggered when the project is based on
+    C-Extensions only.
+    """
+    # This example is based on: https://github.com/nucleic/kiwi/tree/1.4.0
+    files = [
+        "benchmarks/file.py",
+        "docs/Makefile",
+        "docs/requirements.txt",
+        "docs/source/conf.py",
+        "proj/header.h",
+        "proj/file.py",
+        "py/proj.cpp",
+        "py/other.cpp",
+        "py/file.py",
+        "py/py.typed",
+        "py/tests/test_proj.py",
+        "README.rst",
+    ]
+    _populate_project_dir(tmp_path, files, {})
+
+    pyproject = """
+        [project]
+        name = 'proj'
+        version = '42'
+    """
+    (tmp_path / "pyproject.toml").write_text(cleandoc(pyproject))
+
+    setup_script = """
+        from setuptools import Extension, setup
+
+        ext_modules = [
+            Extension(
+                "proj",
+                ["py/proj.cpp", "py/other.cpp"],
+                include_dirs=["."],
+                language="c++",
+            ),
+        ]
+        setup(ext_modules=ext_modules)
+    """
+    (tmp_path / "setup.py").write_text(cleandoc(setup_script))
+    dist = _get_dist(tmp_path, {})
+    assert dist.get_name() == "proj"
+    assert dist.get_version() == "42"
+    assert dist.py_modules is None
+    assert dist.packages is None
+    assert len(dist.ext_modules) == 1
+    assert dist.ext_modules[0].name == "proj"
+
+
 def _populate_project_dir(root, files, options):
     # NOTE: Currently pypa/build will refuse to build the project if no
     # `pyproject.toml` or `setup.py` is found. So it is impossible to do
-- 
cgit v1.2.1


From d793ba0b93d87eaee81cc99a5b52b7fa87f8e15d Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 22 Mar 2022 09:50:49 +0000
Subject: Skip discover when ext_modules are provided

---
 setuptools/discovery.py | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/setuptools/discovery.py b/setuptools/discovery.py
index 1672d013..b444cc57 100644
--- a/setuptools/discovery.py
+++ b/setuptools/discovery.py
@@ -307,8 +307,16 @@ class ConfigDiscovery:
 
         self._called = True
 
+    def _explicitly_specified(self) -> bool:
+        """``True`` if the user has specified some form of package/module listing"""
+        return (
+            self.dist.packages is not None
+            or self.dist.py_modules is not None
+            or self.dist.ext_modules is not None
+        )
+
     def _analyse_package_layout(self) -> bool:
-        if self.dist.packages is not None or self.dist.py_modules is not None:
+        if self._explicitly_specified():
             # For backward compatibility, just try to find modules/packages
             # when nothing is given
             return True
-- 
cgit v1.2.1


From e267806d3764ca84eb5a5a384f9e617a53a3811d Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 22 Mar 2022 10:06:43 +0000
Subject: Update discovery docs to mention ext_modules

---
 changelog.d/2887.change.1.rst        | 4 ++--
 changelog.d/2894.breaking.rst        | 7 ++++---
 docs/userguide/package_discovery.rst | 4 ++--
 3 files changed, 8 insertions(+), 7 deletions(-)

diff --git a/changelog.d/2887.change.1.rst b/changelog.d/2887.change.1.rst
index 66832176..e7e96e58 100644
--- a/changelog.d/2887.change.1.rst
+++ b/changelog.d/2887.change.1.rst
@@ -10,8 +10,8 @@ the project root).
 The automatic discovery will also respect layouts that are explicitly
 configured using the ``package_dir`` option.
 
-For backward-compatibility, this behavior will be observed **only if both**
-``py_modules`` **and** ``packages`` **are not set**.
+For backward-compatibility, this behavior will be observed **only if none of
+the following is set**: ``packages``, ``py_modules``, ``ext_modules``.
 
 If setuptools detects modules or packages that are not supposed to be in the
 distribution, please manually set ``py_modules`` and ``packages`` in your
diff --git a/changelog.d/2894.breaking.rst b/changelog.d/2894.breaking.rst
index 687ae511..3f2dfe7e 100644
--- a/changelog.d/2894.breaking.rst
+++ b/changelog.d/2894.breaking.rst
@@ -2,9 +2,10 @@ If you purposefully want to create an *"empty distribution"*, please be aware
 that some Python files (or general folders) might be automatically detected and
 included.
 
-Projects that currently don't specify both ``packages`` and ``py_modules`` in their
-configuration and have extra Python files and folders (not meant for distribution),
-might see these files being included in the wheel archive.
+Projects that currently don't specify ``packages``, ``py_modules`` and
+``ext_modules`` in their configuration and have extra Python files and folders
+(not meant for distribution), might see these files being included in the wheel
+archive.
 
 You can check details about the automatic discovery behaviour (and
 how to configure a different one) in :doc:`/userguide/package_discovery`.
diff --git a/docs/userguide/package_discovery.rst b/docs/userguide/package_discovery.rst
index fd688824..1fe17650 100644
--- a/docs/userguide/package_discovery.rst
+++ b/docs/userguide/package_discovery.rst
@@ -137,8 +137,8 @@ layouts and try to guess the correct values for the :ref:`packages ` and :doc:`py_modules ` configuration.
 
 .. important::
-   Automatic discovery will **only** be enabled if you don't provide any
-   configuration for both ``packages`` and ``py_modules``.
+   Automatic discovery will **only** be enabled if you **don't** provide any
+   configuration for ``packages``, ``py_modules`` and ``ext_modules``.
    If at least one of them is explicitly set, automatic discovery will not take place.
 
 .. _src-layout:
-- 
cgit v1.2.1


From fbfc92b6d896db536469fab594064f9f3eb81204 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 22 Mar 2022 11:07:12 +0000
Subject: Ignore ext-modules for auto-discovery with pyproject.toml metadata

---
 setuptools/config/pyprojecttoml.py        |   2 +
 setuptools/discovery.py                   |  27 +++++--
 setuptools/tests/test_config_discovery.py | 124 +++++++++++++++++++-----------
 3 files changed, 100 insertions(+), 53 deletions(-)

diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index 834d5a35..9a7c9fe6 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -184,6 +184,8 @@ class _EnsurePackagesDiscovered(_expand.EnsurePackagesDiscovered):
         package_dir.update(dist.package_dir or {})
         dist.package_dir = package_dir  # needs to be the same object
 
+        dist.set_defaults._ignore_ext_modules()  # pyproject.toml-specific behaviour
+
         # Set `py_modules` and `packages` in dist to short-circuit auto-discovery,
         # but avoid overwriting empty lists purposefully set by users.
         if dist.py_modules is None:
diff --git a/setuptools/discovery.py b/setuptools/discovery.py
index b444cc57..00d9065a 100644
--- a/setuptools/discovery.py
+++ b/setuptools/discovery.py
@@ -270,11 +270,24 @@ class ConfigDiscovery:
         self.dist = distribution
         self._called = False
         self._disabled = False
+        self._skip_ext_modules = False
 
     def _disable(self):
         """Internal API to disable automatic discovery"""
         self._disabled = True
 
+    def _ignore_ext_modules(self):
+        """Internal API to disregard ext_modules.
+
+        Normally auto-discovery would not be triggered if ``ext_modules`` are set
+        (this is done for backward compatibility with existing packages relying on
+        ``setup.py`` or ``setup.cfg``). However, ``setuptools`` can call this function
+        to ignore given ``ext_modules`` and proceed with the auto-discovery if
+        ``packages`` and ``py_modules`` are not given (e.g. when using pyproject.toml
+        metadata).
+        """
+        self._skip_ext_modules = True
+
     @property
     def _root_dir(self) -> _Path:
         # The best is to wait until `src_root` is set in dist, before using _root_dir.
@@ -286,7 +299,7 @@ class ConfigDiscovery:
             return {}
         return self.dist.package_dir
 
-    def __call__(self, force=False, name=True):
+    def __call__(self, force=False, name=True, ignore_ext_modules=False):
         """Automatically discover missing configuration fields
         and modifies the given ``distribution`` object in-place.
 
@@ -301,22 +314,24 @@ class ConfigDiscovery:
             # Avoid overhead of multiple calls
             return
 
-        self._analyse_package_layout()
+        self._analyse_package_layout(ignore_ext_modules)
         if name:
             self.analyse_name()  # depends on ``packages`` and ``py_modules``
 
         self._called = True
 
-    def _explicitly_specified(self) -> bool:
+    def _explicitly_specified(self, ignore_ext_modules: bool) -> bool:
         """``True`` if the user has specified some form of package/module listing"""
+        ignore_ext_modules = ignore_ext_modules or self._skip_ext_modules
+        ext_modules = not (self.dist.ext_modules is None or ignore_ext_modules)
         return (
             self.dist.packages is not None
             or self.dist.py_modules is not None
-            or self.dist.ext_modules is not None
+            or ext_modules
         )
 
-    def _analyse_package_layout(self) -> bool:
-        if self._explicitly_specified():
+    def _analyse_package_layout(self, ignore_ext_modules: bool) -> bool:
+        if self._explicitly_specified(ignore_ext_modules):
             # For backward compatibility, just try to find modules/packages
             # when nothing is given
             return True
diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py
index 053a605b..cbfd0188 100644
--- a/setuptools/tests/test_config_discovery.py
+++ b/setuptools/tests/test_config_discovery.py
@@ -2,7 +2,6 @@ import os
 import sys
 from configparser import ConfigParser
 from itertools import product
-from inspect import cleandoc
 
 from setuptools.command.sdist import sdist
 from setuptools.dist import Distribution
@@ -316,55 +315,86 @@ def test_discovered_package_dir_with_attr_in_pyproject_config(tmp_path):
     assert dist.package_dir == {"": "src"}
 
 
-def test_skip_when_extensions_are_provided(tmp_path):
-    """Ensure that auto-discovery is not triggered when the project is based on
-    C-Extensions only.
-    """
-    # This example is based on: https://github.com/nucleic/kiwi/tree/1.4.0
-    files = [
-        "benchmarks/file.py",
-        "docs/Makefile",
-        "docs/requirements.txt",
-        "docs/source/conf.py",
-        "proj/header.h",
-        "proj/file.py",
-        "py/proj.cpp",
-        "py/other.cpp",
-        "py/file.py",
-        "py/py.typed",
-        "py/tests/test_proj.py",
-        "README.rst",
-    ]
-    _populate_project_dir(tmp_path, files, {})
+class TestWithCExtension:
+    def _simulate_package_with_extension(self, tmp_path):
+        # This example is based on: https://github.com/nucleic/kiwi/tree/1.4.0
+        files = [
+            "benchmarks/file.py",
+            "docs/Makefile",
+            "docs/requirements.txt",
+            "docs/source/conf.py",
+            "proj/header.h",
+            "proj/file.py",
+            "py/proj.cpp",
+            "py/other.cpp",
+            "py/file.py",
+            "py/py.typed",
+            "py/tests/test_proj.py",
+            "README.rst",
+        ]
+        _populate_project_dir(tmp_path, files, {})
 
-    pyproject = """
-        [project]
-        name = 'proj'
-        version = '42'
-    """
-    (tmp_path / "pyproject.toml").write_text(cleandoc(pyproject))
+        setup_script = """
+            from setuptools import Extension, setup
+
+            ext_modules = [
+                Extension(
+                    "proj",
+                    ["py/proj.cpp", "py/other.cpp"],
+                    include_dirs=["."],
+                    language="c++",
+                ),
+            ]
+            setup(ext_modules=ext_modules)
+        """
+        (tmp_path / "setup.py").write_text(DALS(setup_script))
+
+    def test_skip_discovery_with_setupcfg_metadata(self, tmp_path):
+        """Ensure that auto-discovery is not triggered when the project is based on
+        C-extensions only, for backward compatibility.
+        """
+        self._simulate_package_with_extension(tmp_path)
+
+        pyproject = """
+            [build-system]
+            requires = []
+            build-backend = 'setuptools.build_meta'
+        """
+        (tmp_path / "pyproject.toml").write_text(DALS(pyproject))
 
-    setup_script = """
-        from setuptools import Extension, setup
+        setupcfg = """
+            [metadata]
+            name = proj
+            version = 42
+        """
+        (tmp_path / "setup.cfg").write_text(DALS(setupcfg))
 
-        ext_modules = [
-            Extension(
-                "proj",
-                ["py/proj.cpp", "py/other.cpp"],
-                include_dirs=["."],
-                language="c++",
-            ),
-        ]
-        setup(ext_modules=ext_modules)
-    """
-    (tmp_path / "setup.py").write_text(cleandoc(setup_script))
-    dist = _get_dist(tmp_path, {})
-    assert dist.get_name() == "proj"
-    assert dist.get_version() == "42"
-    assert dist.py_modules is None
-    assert dist.packages is None
-    assert len(dist.ext_modules) == 1
-    assert dist.ext_modules[0].name == "proj"
+        dist = _get_dist(tmp_path, {})
+        assert dist.get_name() == "proj"
+        assert dist.get_version() == "42"
+        assert dist.py_modules is None
+        assert dist.packages is None
+        assert len(dist.ext_modules) == 1
+        assert dist.ext_modules[0].name == "proj"
+
+    def test_dont_skip_discovery_with_pyproject_metadata(self, tmp_path):
+        """When opting-in to pyproject.toml metadata, auto-discovery will be active if
+        the package lists C-extensions, but does not configure py-modules or packages.
+
+        This way we ensure users with complex package layouts that would lead to the
+        discovery of multiple top-level modules/packages see errors and are forced to
+        explicitly set ``packages`` or ``py-modules``.
+        """
+        self._simulate_package_with_extension(tmp_path)
+
+        pyproject = """
+            [project]
+            name = 'proj'
+            version = '42'
+        """
+        (tmp_path / "pyproject.toml").write_text(DALS(pyproject))
+        with pytest.raises(PackageDiscoveryError, match="multiple (packages|modules)"):
+            _get_dist(tmp_path, {})
 
 
 def _populate_project_dir(root, files, options):
-- 
cgit v1.2.1


From fab53fac0cbcbf6d87e23f64d27975e992d766c0 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 22 Mar 2022 11:32:44 +0000
Subject: Adequate docs to the latest changes

---
 changelog.d/2887.change.1.rst        | 8 +++++---
 changelog.d/2894.breaking.rst        | 7 +++----
 docs/userguide/package_discovery.rst | 6 +++++-
 3 files changed, 13 insertions(+), 8 deletions(-)

diff --git a/changelog.d/2887.change.1.rst b/changelog.d/2887.change.1.rst
index e7e96e58..eeb5471e 100644
--- a/changelog.d/2887.change.1.rst
+++ b/changelog.d/2887.change.1.rst
@@ -4,14 +4,16 @@
 Setuptools will try to find these values assuming that the package uses either
 the *src-layout* (a ``src`` directory containing all the packages or modules),
 the *flat-layout* (package directories directly under the project root),
-or the *single-module* approach (isolated Python files, directly under
+or the *single-module* approach (an isolated Python file, directly under
 the project root).
 
 The automatic discovery will also respect layouts that are explicitly
 configured using the ``package_dir`` option.
 
-For backward-compatibility, this behavior will be observed **only if none of
-the following is set**: ``packages``, ``py_modules``, ``ext_modules``.
+For backward-compatibility, this behavior will be observed **only if both**
+``py_modules`` **and** ``packages`` **are not set**.
+(**Note**: specifying ``ext_modules`` might also prevent auto-discover from
+taking place)
 
 If setuptools detects modules or packages that are not supposed to be in the
 distribution, please manually set ``py_modules`` and ``packages`` in your
diff --git a/changelog.d/2894.breaking.rst b/changelog.d/2894.breaking.rst
index 3f2dfe7e..687ae511 100644
--- a/changelog.d/2894.breaking.rst
+++ b/changelog.d/2894.breaking.rst
@@ -2,10 +2,9 @@ If you purposefully want to create an *"empty distribution"*, please be aware
 that some Python files (or general folders) might be automatically detected and
 included.
 
-Projects that currently don't specify ``packages``, ``py_modules`` and
-``ext_modules`` in their configuration and have extra Python files and folders
-(not meant for distribution), might see these files being included in the wheel
-archive.
+Projects that currently don't specify both ``packages`` and ``py_modules`` in their
+configuration and have extra Python files and folders (not meant for distribution),
+might see these files being included in the wheel archive.
 
 You can check details about the automatic discovery behaviour (and
 how to configure a different one) in :doc:`/userguide/package_discovery`.
diff --git a/docs/userguide/package_discovery.rst b/docs/userguide/package_discovery.rst
index 1fe17650..ee8e9836 100644
--- a/docs/userguide/package_discovery.rst
+++ b/docs/userguide/package_discovery.rst
@@ -138,9 +138,13 @@ config>` and :doc:`py_modules ` configuration.
 
 .. important::
    Automatic discovery will **only** be enabled if you **don't** provide any
-   configuration for ``packages``, ``py_modules`` and ``ext_modules``.
+   configuration for ``packages`` and ``py_modules``.
    If at least one of them is explicitly set, automatic discovery will not take place.
 
+   **Note**: specifying ``ext_modules`` might also prevent auto-discover from
+   taking place, unless your opt into :doc:`pyproject_config` (which will
+   disable the backward compatible behaviour).
+
 .. _src-layout:
 
 src-layout
-- 
cgit v1.2.1


From 5c0b4b23759a5b2e8dca8153e222416da2eea54a Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 22 Mar 2022 12:35:01 +0000
Subject: Improve organisation of test_config_discovery

---
 setuptools/tests/test_config_discovery.py | 107 +++++++++++++++---------------
 1 file changed, 54 insertions(+), 53 deletions(-)

diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py
index cbfd0188..e6ed632e 100644
--- a/setuptools/tests/test_config_discovery.py
+++ b/setuptools/tests/test_config_discovery.py
@@ -19,23 +19,23 @@ from .integration.helpers import get_sdist_members, get_wheel_members, run
 from .textwrap import DALS
 
 
-def test_find_parent_package(tmp_path):
-    # find_parent_package should find a non-namespace parent package
-    (tmp_path / "src/namespace/pkg/nested").mkdir(exist_ok=True, parents=True)
-    (tmp_path / "src/namespace/pkg/nested/__init__.py").touch()
-    (tmp_path / "src/namespace/pkg/__init__.py").touch()
-    packages = ["namespace", "namespace.pkg", "namespace.pkg.nested"]
-    assert find_parent_package(packages, {"": "src"}, tmp_path) == "namespace.pkg"
-
-
-def test_find_parent_package_multiple_toplevel(tmp_path):
-    # find_parent_package should return null if the given list of packages does not
-    # have a single parent package
-    multiple = ["pkg", "pkg1", "pkg2"]
-    for name in multiple:
-        (tmp_path / f"src/{name}").mkdir(exist_ok=True, parents=True)
-        (tmp_path / f"src/{name}/__init__.py").touch()
-    assert find_parent_package(multiple, {"": "src"}, tmp_path) is None
+class TestFindParentPackage:
+    def test_single_package(self, tmp_path):
+        # find_parent_package should find a non-namespace parent package
+        (tmp_path / "src/namespace/pkg/nested").mkdir(exist_ok=True, parents=True)
+        (tmp_path / "src/namespace/pkg/nested/__init__.py").touch()
+        (tmp_path / "src/namespace/pkg/__init__.py").touch()
+        packages = ["namespace", "namespace.pkg", "namespace.pkg.nested"]
+        assert find_parent_package(packages, {"": "src"}, tmp_path) == "namespace.pkg"
+
+    def test_multiple_toplevel(self, tmp_path):
+        # find_parent_package should return null if the given list of packages does not
+        # have a single parent package
+        multiple = ["pkg", "pkg1", "pkg2"]
+        for name in multiple:
+            (tmp_path / f"src/{name}").mkdir(exist_ok=True, parents=True)
+            (tmp_path / f"src/{name}/__init__.py").touch()
+        assert find_parent_package(multiple, {"": "src"}, tmp_path) is None
 
 
 class TestDiscoverPackagesAndPyModules:
@@ -275,44 +275,45 @@ class TestNoConfig:
         assert dist_file.is_file()
 
 
-@pytest.mark.parametrize(
-    "folder, opts",
-    [
-        ("src", {}),
-        ("lib", {"packages": "find:", "packages.find": {"where": "lib"}}),
-    ]
-)
-def test_discovered_package_dir_with_attr_directive_in_config(tmp_path, folder, opts):
-    _populate_project_dir(tmp_path, [f"{folder}/pkg/__init__.py", "setup.cfg"], opts)
-    (tmp_path / folder / "pkg/__init__.py").write_text("version = 42")
-    (tmp_path / "setup.cfg").write_text(
-        "[metadata]\nversion = attr: pkg.version\n"
-        + (tmp_path / "setup.cfg").read_text()
+class TestWithAttrDirective:
+    @pytest.mark.parametrize(
+        "folder, opts",
+        [
+            ("src", {}),
+            ("lib", {"packages": "find:", "packages.find": {"where": "lib"}}),
+        ]
     )
+    def test_setupcfg_metadata(self, tmp_path, folder, opts):
+        files = [f"{folder}/pkg/__init__.py", "setup.cfg"]
+        _populate_project_dir(tmp_path, files, opts)
+        (tmp_path / folder / "pkg/__init__.py").write_text("version = 42")
+        (tmp_path / "setup.cfg").write_text(
+            "[metadata]\nversion = attr: pkg.version\n"
+            + (tmp_path / "setup.cfg").read_text()
+        )
 
-    dist = _get_dist(tmp_path, {})
-    assert dist.get_name() == "pkg"
-    assert dist.get_version() == "42"
-    assert dist.package_dir
-    package_path = find_package_path("pkg", dist.package_dir, tmp_path)
-    assert os.path.exists(package_path)
-    assert folder in _Path(package_path).parts()
-
-    _run_build(tmp_path, "--sdist")
-    dist_file = tmp_path / "dist/pkg-42.tar.gz"
-    assert dist_file.is_file()
-
-
-def test_discovered_package_dir_with_attr_in_pyproject_config(tmp_path):
-    _populate_project_dir(tmp_path, ["src/pkg/__init__.py"], {})
-    (tmp_path / "src/pkg/__init__.py").write_text("version = 42")
-    (tmp_path / "pyproject.toml").write_text(
-        "[project]\nname = 'pkg'\ndynamic = ['version']\n"
-        "[tool.setuptools.dynamic]\nversion = {attr = 'pkg.version'}\n"
-    )
-    dist = _get_dist(tmp_path, {})
-    assert dist.get_version() == "42"
-    assert dist.package_dir == {"": "src"}
+        dist = _get_dist(tmp_path, {})
+        assert dist.get_name() == "pkg"
+        assert dist.get_version() == "42"
+        assert dist.package_dir
+        package_path = find_package_path("pkg", dist.package_dir, tmp_path)
+        assert os.path.exists(package_path)
+        assert folder in _Path(package_path).parts()
+
+        _run_build(tmp_path, "--sdist")
+        dist_file = tmp_path / "dist/pkg-42.tar.gz"
+        assert dist_file.is_file()
+
+    def test_pyproject_metadata(self, tmp_path):
+        _populate_project_dir(tmp_path, ["src/pkg/__init__.py"], {})
+        (tmp_path / "src/pkg/__init__.py").write_text("version = 42")
+        (tmp_path / "pyproject.toml").write_text(
+            "[project]\nname = 'pkg'\ndynamic = ['version']\n"
+            "[tool.setuptools.dynamic]\nversion = {attr = 'pkg.version'}\n"
+        )
+        dist = _get_dist(tmp_path, {})
+        assert dist.get_version() == "42"
+        assert dist.package_dir == {"": "src"}
 
 
 class TestWithCExtension:
-- 
cgit v1.2.1


From cf0236229e79b6a4e59af7c7ed5feb527dbc998e Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 22 Mar 2022 17:02:42 +0000
Subject: Add a few other reserved package/module names to discovery

---
 setuptools/discovery.py | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/setuptools/discovery.py b/setuptools/discovery.py
index 00d9065a..410d503f 100644
--- a/setuptools/discovery.py
+++ b/setuptools/discovery.py
@@ -190,6 +190,7 @@ class ModuleFinder(_Finder):
 
 class FlatLayoutPackageFinder(PEP420PackageFinder):
     _EXCLUDE = (
+        "ci",
         "bin",
         "doc",
         "docs",
@@ -207,6 +208,7 @@ class FlatLayoutPackageFinder(PEP420PackageFinder):
         "tools",
         "util",
         "utils",
+        "python",
         "build",
         "dist",
         "venv",
@@ -216,6 +218,11 @@ class FlatLayoutPackageFinder(PEP420PackageFinder):
         "tasks",  # invoke
         "fabfile",  # fabric
         "site_scons",  # SCons
+        # ---- Other tools ----
+        "benchmark",
+        "benchmarks",
+        "exercise",
+        "exercises",
         # ---- Hidden directories/Private packages ----
         "[._]*",
     )
@@ -250,6 +257,10 @@ class FlatLayoutModuleFinder(ModuleFinder):
         "[Ss][Cc]onstruct",  # SCons
         "conanfile",  # Connan: C/C++ build tool
         "manage",  # Django
+        "benchmark",
+        "benchmarks",
+        "exercise",
+        "exercises",
         # ---- Hidden files/Private modules ----
         "[._]*",
     )
-- 
cgit v1.2.1


From 88504d3d755d3a5e0e95ab84b5df41953cb4f016 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 22 Mar 2022 18:39:14 +0000
Subject: Add test for default include-package-data with 'pyproject.toml'

---
 setuptools/tests/config/test_pyprojecttoml.py | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/setuptools/tests/config/test_pyprojecttoml.py b/setuptools/tests/config/test_pyprojecttoml.py
index 463048ed..0157b2ad 100644
--- a/setuptools/tests/config/test_pyprojecttoml.py
+++ b/setuptools/tests/config/test_pyprojecttoml.py
@@ -113,6 +113,7 @@ def verify_example(config, path, pkg_root):
             "other",
             "other.nested",
         }
+    assert expanded["tool"]["setuptools"]["include-package-data"] is True
     assert "" in expanded["tool"]["setuptools"]["package-data"]
     assert "*" not in expanded["tool"]["setuptools"]["package-data"]
     assert expanded["tool"]["setuptools"]["data-files"] == [
@@ -279,3 +280,15 @@ def test_empty(tmp_path, config):
 
     # Make sure no error is raised
     assert read_configuration(pyproject) == {}
+
+
+@pytest.mark.parametrize("config", ("[project]\nname = 'myproj'\nversion='42'\n",))
+def test_include_package_data_by_default(tmp_path, config):
+    """Builds with ``pyproject.toml`` should consider ``include-package-data=True`` as
+    default.
+    """
+    pyproject = tmp_path / "pyproject.toml"
+    pyproject.write_text(config)
+
+    config = read_configuration(pyproject)
+    assert config["tool"]["setuptools"]["include-package-data"] is True
-- 
cgit v1.2.1


From bb01ab7c7470dc9ccd5c7196e727a8046ef88250 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 22 Mar 2022 18:39:35 +0000
Subject: Fix default include-package-data with 'pyproject.toml'

---
 setuptools/config/pyprojecttoml.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index 9a7c9fe6..e0a8946f 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -103,6 +103,7 @@ def read_configuration(
     # `ini2toml` backfills include_package_data=False when nothing is explicitly given,
     # therefore setting a default here is backwards compatible.
     tool_table.setdefault("include-package-data", True)
+    asdict.setdefault("tool", {})["setuptools"] = tool_table  # persist changes
 
     with _ignore_errors(ignore_option_errors):
         # Don't complain about unrelated errors (e.g. tools not using the "tool" table)
-- 
cgit v1.2.1


From 88e613772ad54a97b8fedf3a4ebebf5a9d2678de Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 22 Mar 2022 18:44:24 +0000
Subject: Use better variable naming

---
 setuptools/config/pyprojecttoml.py | 15 +++++++++------
 1 file changed, 9 insertions(+), 6 deletions(-)

diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index e0a8946f..bc76b111 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -87,11 +87,12 @@ def read_configuration(
 
     asdict = load_file(filepath) or {}
     project_table = asdict.get("project", {})
-    tool_table = asdict.get("tool", {}).get("setuptools", {})
-    if not asdict or not (project_table or tool_table):
+    tool_table = asdict.get("tool", {})
+    setuptools_table = tool_table.get("setuptools", {})
+    if not asdict or not (project_table or setuptools_table):
         return {}  # User is not using pyproject to configure setuptools
 
-    # TODO: Remove once the future stabilizes
+    # TODO: Remove once the feature stabilizes
     msg = (
         "Support for project metadata in `pyproject.toml` is still experimental "
         "and may be removed (or change) in future releases."
@@ -102,12 +103,14 @@ def read_configuration(
     # the default would be an improvement.
     # `ini2toml` backfills include_package_data=False when nothing is explicitly given,
     # therefore setting a default here is backwards compatible.
-    tool_table.setdefault("include-package-data", True)
-    asdict.setdefault("tool", {})["setuptools"] = tool_table  # persist changes
+    setuptools_table.setdefault("include-package-data", True)
+    # Persist changes:
+    asdict["tool"] = tool_table
+    tool_table["setuptools"] = setuptools_table
 
     with _ignore_errors(ignore_option_errors):
         # Don't complain about unrelated errors (e.g. tools not using the "tool" table)
-        subset = {"project": project_table, "tool": {"setuptools": tool_table}}
+        subset = {"project": project_table, "tool": {"setuptools": setuptools_table}}
         validate(subset, filepath)
 
     if expand:
-- 
cgit v1.2.1


From 06ada30c3319307a0354903e592e667e39a2cf89 Mon Sep 17 00:00:00 2001
From: Andrew Murray 
Date: Wed, 23 Mar 2022 19:12:23 +1100
Subject: Fixed typos [ci skip]

---
 CHANGES.rst | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/CHANGES.rst b/CHANGES.rst
index 3c724e47..98d86c5e 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -933,7 +933,7 @@ Changes
 * #2481: Define ``create_module()`` and ``exec_module()`` methods in ``VendorImporter``
   to get rid of ``ImportWarning`` -- by :user:`hroncok`
 * #2489: ``pkg_resources`` behavior for zipimport now matches the regular behavior, and finds
-  ``.egg-info`` (previoulsy would only find ``.dist-info``) -- by :user:`thatch`
+  ``.egg-info`` (previously would only find ``.dist-info``) -- by :user:`thatch`
 * #2529: Fixed an issue where version tags may be added multiple times
 
 
@@ -944,7 +944,7 @@ v51.2.0
 Changes
 ^^^^^^^
 * #2493: Use importlib.import_module() rather than the deprecated loader.load_module()
-  in pkg_resources namespace delaration -- by :user:`encukou`
+  in pkg_resources namespace declaration -- by :user:`encukou`
 
 Documentation changes
 ^^^^^^^^^^^^^^^^^^^^^
-- 
cgit v1.2.1


From a48561e8e71dad450a913ec3b8ee465b1e31ff75 Mon Sep 17 00:00:00 2001
From: Andrew Murray 
Date: Sat, 19 Mar 2022 15:27:52 +1100
Subject: Only import ctypes when necessary

---
 setuptools/windows_support.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setuptools/windows_support.py b/setuptools/windows_support.py
index cb977cff..1ca64fbb 100644
--- a/setuptools/windows_support.py
+++ b/setuptools/windows_support.py
@@ -1,5 +1,4 @@
 import platform
-import ctypes
 
 
 def windows_only(func):
@@ -17,6 +16,7 @@ def hide_file(path):
 
     `path` must be text.
     """
+    import ctypes
     __import__('ctypes.wintypes')
     SetFileAttributes = ctypes.windll.kernel32.SetFileAttributesW
     SetFileAttributes.argtypes = ctypes.wintypes.LPWSTR, ctypes.wintypes.DWORD
-- 
cgit v1.2.1


From b777ab4feb81a998d97ab375b22335c71858562c Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 23 Mar 2022 09:26:35 +0000
Subject: Add news fragment

---
 changelog.d/3178.change.rst | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 changelog.d/3178.change.rst

diff --git a/changelog.d/3178.change.rst b/changelog.d/3178.change.rst
new file mode 100644
index 00000000..20f04010
--- /dev/null
+++ b/changelog.d/3178.change.rst
@@ -0,0 +1,2 @@
+Postponed importing ``ctypes`` when hidding files on Windows.
+This helps to prevent errors in systems that might not have `libffi` installed.
\ No newline at end of file
-- 
cgit v1.2.1


From 5fd0ccf9d47e7e3de4a7b96b6149788bfd777ece Mon Sep 17 00:00:00 2001
From: Andrew Murray <3112309+radarhere@users.noreply.github.com>
Date: Wed, 23 Mar 2022 20:31:17 +1100
Subject: Fixed typo

---
 changelog.d/3178.change.rst | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/changelog.d/3178.change.rst b/changelog.d/3178.change.rst
index 20f04010..dfb2d33b 100644
--- a/changelog.d/3178.change.rst
+++ b/changelog.d/3178.change.rst
@@ -1,2 +1,2 @@
-Postponed importing ``ctypes`` when hidding files on Windows.
-This helps to prevent errors in systems that might not have `libffi` installed.
\ No newline at end of file
+Postponed importing ``ctypes`` when hiding files on Windows.
+This helps to prevent errors in systems that might not have `libffi` installed.
-- 
cgit v1.2.1


From 64351e5f008276a6ca5a1efd65771ebc6325c067 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=81ukasz=20Daniluk?= 
Date: Sun, 6 Mar 2022 23:11:06 +0100
Subject: Add tests for normalized package name resolution
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Due to PEP 503 package requirements might be specified using normalized
name which won't be resolved by WorkingSet.

Signed-off-by: Łukasz Daniluk 
---
 pkg_resources/tests/test_working_set.py | 21 ++++++++++++++++++++-
 1 file changed, 20 insertions(+), 1 deletion(-)

diff --git a/pkg_resources/tests/test_working_set.py b/pkg_resources/tests/test_working_set.py
index db13c714..575656ee 100644
--- a/pkg_resources/tests/test_working_set.py
+++ b/pkg_resources/tests/test_working_set.py
@@ -42,7 +42,7 @@ def parse_distributions(s):
             continue
         fields = spec.split('\n', 1)
         assert 1 <= len(fields) <= 2
-        name, version = fields.pop(0).split('-')
+        name, version = fields.pop(0).rsplit('-', 1)
         if fields:
             requires = textwrap.dedent(fields.pop(0))
             metadata = Metadata(('requires.txt', requires))
@@ -465,6 +465,25 @@ def parametrize_test_working_set_resolve(*test_list):
     # resolved [replace conflicting]
     VersionConflict
     ''',
+
+    '''
+    # id
+    wanted_normalized_name_installed_canonical
+
+    # installed
+    foo.bar-3.6
+
+    # installable
+
+    # wanted
+    foo-bar==3.6
+
+    # resolved
+    foo.bar-3.6
+
+    # resolved [replace conflicting]
+    foo.bar-3.6
+    ''',
 )
 def test_working_set_resolve(installed_dists, installable_dists, requirements,
                              replace_conflicting, resolved_dists_or_exception):
-- 
cgit v1.2.1


From 3669910426979f6e7f136bd89d53df9be6a7700a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=81ukasz=20Daniluk?= 
Date: Sun, 6 Mar 2022 23:11:12 +0100
Subject: Add matching of normalized requirements to canonical packages
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Łukasz Daniluk 
---
 changelog.d/3153.change.rst |  1 +
 pkg_resources/__init__.py   | 19 ++++++++++++++++---
 2 files changed, 17 insertions(+), 3 deletions(-)
 create mode 100644 changelog.d/3153.change.rst

diff --git a/changelog.d/3153.change.rst b/changelog.d/3153.change.rst
new file mode 100644
index 00000000..d7e0755b
--- /dev/null
+++ b/changelog.d/3153.change.rst
@@ -0,0 +1 @@
+When resolving requirements use both canonical and normalized names -- by :user:`ldaniluk`
diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py
index 852476e2..d59226af 100644
--- a/pkg_resources/__init__.py
+++ b/pkg_resources/__init__.py
@@ -83,6 +83,7 @@ __import__('pkg_resources.extern.packaging.version')
 __import__('pkg_resources.extern.packaging.specifiers')
 __import__('pkg_resources.extern.packaging.requirements')
 __import__('pkg_resources.extern.packaging.markers')
+__import__('pkg_resources.extern.packaging.utils')
 
 if sys.version_info < (3, 5):
     raise RuntimeError("Python 3.5 or later is required")
@@ -554,6 +555,7 @@ class WorkingSet:
         self.entries = []
         self.entry_keys = {}
         self.by_key = {}
+        self.normalized_to_canonical_keys = {}
         self.callbacks = []
 
         if entries is None:
@@ -634,6 +636,14 @@ class WorkingSet:
         is returned.
         """
         dist = self.by_key.get(req.key)
+
+        if dist is None:
+            canonical_key = self.normalized_to_canonical_keys.get(req.key)
+
+            if canonical_key is not None:
+                req.key = canonical_key
+                dist = self.by_key.get(canonical_key)
+
         if dist is not None and dist not in req:
             # XXX add more info
             raise VersionConflict(dist, req)
@@ -702,6 +712,8 @@ class WorkingSet:
             return
 
         self.by_key[dist.key] = dist
+        normalized_name = packaging.utils.canonicalize_name(dist.key)
+        self.normalized_to_canonical_keys[normalized_name] = dist.key
         if dist.key not in keys:
             keys.append(dist.key)
         if dist.key not in keys2:
@@ -922,14 +934,15 @@ class WorkingSet:
     def __getstate__(self):
         return (
             self.entries[:], self.entry_keys.copy(), self.by_key.copy(),
-            self.callbacks[:]
+            self.normalized_to_canonical_keys.copy(), self.callbacks[:]
         )
 
-    def __setstate__(self, e_k_b_c):
-        entries, keys, by_key, callbacks = e_k_b_c
+    def __setstate__(self, e_k_b_n_c):
+        entries, keys, by_key, normalized_to_canonical_keys, callbacks = e_k_b_n_c
         self.entries = entries[:]
         self.entry_keys = keys.copy()
         self.by_key = by_key.copy()
+        self.normalized_to_canonical_keys = normalized_to_canonical_keys.copy()
         self.callbacks = callbacks[:]
 
 
-- 
cgit v1.2.1


From 5af72b9c332551756681be31d92a165f737ddf12 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 24 Mar 2022 11:16:03 +0000
Subject: Restore tip about editable installs

Experiments with pip 21.1 confirm that it can use editable mode even
when `setup.py` is missing.
---
 docs/userguide/quickstart.rst | 23 +++--------------------
 1 file changed, 3 insertions(+), 20 deletions(-)

diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst
index 276aaf73..79b5d13e 100644
--- a/docs/userguide/quickstart.rst
+++ b/docs/userguide/quickstart.rst
@@ -338,28 +338,11 @@ Here's how to do it::
 This creates a link file in your interpreter site package directory which
 associate with your source code. For more information, see :doc:`development_mode`.
 
-..
-    TODO: Restore the following note once PEP 660 lands in setuptools.
-    tip: Prior to :ref:`pip v21.1 `, a ``setup.py`` script was
-    required to be compatible with development mode. With late
-    versions of pip, any project may be installed in this mode.
-
 .. tip::
-    If you are experimenting with :doc:`configuration using
-    `, or have version of ``pip`` older than :ref:`v21.1 `,
-    you might need to keep a ``setup.py`` file in file in your repository if
-    you want to use editable installs (for the time being).
-
-    A simple script will suffice, for example:
-
-    .. code-block:: python
 
-        from setuptools import setup
-
-        setup()
-
-    You can still keep all the configuration in :doc:`setup.cfg `
-    (or :doc:`pyproject.toml `).
+    Prior to :ref:`pip v21.1 `, a ``setup.py`` script was
+    required to be compatible with development mode. With late
+    versions of pip, any project may be installed in this mode.
 
 
 Uploading your package to PyPI
-- 
cgit v1.2.1


From e18496d4a58c098f0513f24f52da561f4a93e27b Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 24 Mar 2022 11:44:54 +0000
Subject: Fix example of environment maker for dependencies in quickstart

---
 docs/userguide/quickstart.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst
index 79b5d13e..b363b509 100644
--- a/docs/userguide/quickstart.rst
+++ b/docs/userguide/quickstart.rst
@@ -270,7 +270,7 @@ The example bellow show how to configure this kind of dependencies:
 
 Each dependency is represented a string that can optionally contain version requirements
 (e.g. one of the operators <, >, <=, >=, == or !=, followed by a version identifier),
-and/or conditional environment markers, e.g. ``os_name = "windows"``
+and/or conditional environment markers, e.g. ``sys_platform == "win32"``
 (see :doc:`PyPUG:specifications/version-specifiers` for more information).
 
 When your project is installed, all of the dependencies not already installed
-- 
cgit v1.2.1


From 4def932c937ffab43f4a9eea5aa57ad0d1a18272 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 24 Mar 2022 11:50:54 +0000
Subject: Improve text about CLI entry-point in quickstart

---
 docs/userguide/quickstart.rst | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst
index b363b509..67c4bc81 100644
--- a/docs/userguide/quickstart.rst
+++ b/docs/userguide/quickstart.rst
@@ -224,8 +224,9 @@ The following configuration examples show how to accomplish this:
        [project.scripts]
        cli-name = mypkg:some_func
 
-When this project is installed, a ``cli-name`` executable will be installed and will
-invoke the ``some_func`` in the ``mypkg/__init__.py`` file when called by the user.
+When this project is installed, a ``cli-name`` executable will be created.
+``cli-name`` will invoke the function ``some_func`` in the
+``mypkg/__init__.py`` file when called by the user.
 Note that you can also use the ``entry-points`` mechanism to advertise
 components between installed packages and implement plugin systems.
 For detailed usage, go to :doc:`entry_point`.
-- 
cgit v1.2.1


From 9bc0c4a26f82940c213fb07de5e6ebce0b65dd87 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 24 Mar 2022 11:53:37 +0000
Subject: Add module to entry_point example in quickstart

---
 docs/userguide/quickstart.rst | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst
index 67c4bc81..4a24f337 100644
--- a/docs/userguide/quickstart.rst
+++ b/docs/userguide/quickstart.rst
@@ -202,7 +202,7 @@ The following configuration examples show how to accomplish this:
 
         [options.entry_points]
         console_scripts =
-            cli-name = mypkg:some_func
+            cli-name = mypkg.mymodule:some_func
 
 .. tab:: setup.py [#setup.py]_
 
@@ -212,7 +212,7 @@ The following configuration examples show how to accomplish this:
             # ...
             entry_points={
                 'console_scripts': [
-                    'cli-name = mypkg:some_func',
+                    'cli-name = mypkg.mymodule:some_func',
                 ]
             }
         )
@@ -222,11 +222,11 @@ The following configuration examples show how to accomplish this:
     .. code-block:: toml
 
        [project.scripts]
-       cli-name = mypkg:some_func
+       cli-name = mypkg.mymodule:some_func
 
 When this project is installed, a ``cli-name`` executable will be created.
 ``cli-name`` will invoke the function ``some_func`` in the
-``mypkg/__init__.py`` file when called by the user.
+``mypkg/mymodule.py`` file when called by the user.
 Note that you can also use the ``entry-points`` mechanism to advertise
 components between installed packages and implement plugin systems.
 For detailed usage, go to :doc:`entry_point`.
-- 
cgit v1.2.1


From 8f4c6e6bb3b23359f63ffeff1974da6a437cca96 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 24 Mar 2022 11:55:01 +0000
Subject: Add missing preposition

---
 docs/userguide/quickstart.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst
index 4a24f337..cfc97758 100644
--- a/docs/userguide/quickstart.rst
+++ b/docs/userguide/quickstart.rst
@@ -269,7 +269,7 @@ The example bellow show how to configure this kind of dependencies:
         ]
         # ...
 
-Each dependency is represented a string that can optionally contain version requirements
+Each dependency is represented by a string that can optionally contain version requirements
 (e.g. one of the operators <, >, <=, >=, == or !=, followed by a version identifier),
 and/or conditional environment markers, e.g. ``sys_platform == "win32"``
 (see :doc:`PyPUG:specifications/version-specifiers` for more information).
-- 
cgit v1.2.1


From aa33fdce0feb19f25831865daaa2b7c353131f06 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 24 Mar 2022 11:57:58 +0000
Subject: Improve note about setup.py

---
 docs/userguide/quickstart.rst | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst
index cfc97758..79cfb2ef 100644
--- a/docs/userguide/quickstart.rst
+++ b/docs/userguide/quickstart.rst
@@ -381,7 +381,8 @@ up-to-date references that can help you when it is time to distribute your work.
 .. rubric:: Notes
 
 .. [#setup.py]
-   The ``setup.py`` file should be used only when absolutely necessary.
+   The ``setup.py`` file should be used only when custom scripting during the
+   build is necessary.
    Examples are kept in this document to help people interested in maintaining or
    contributing to existing packages that use ``setup.py``.
    Note that you can still keep most of configuration declarative in
-- 
cgit v1.2.1


From bd8e1ba6a37b58e2674578baac627f25bc3ceb89 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 24 Mar 2022 12:06:23 +0000
Subject: Remove note about setup.py being required for editable installs

---
 docs/userguide/pyproject_config.rst | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/docs/userguide/pyproject_config.rst b/docs/userguide/pyproject_config.rst
index 45153c34..1fe93528 100644
--- a/docs/userguide/pyproject_config.rst
+++ b/docs/userguide/pyproject_config.rst
@@ -12,11 +12,6 @@ Configuring setuptools using ``pyproject.toml`` files
    ``setuptools`` via ``pyproject.toml`` files is still experimental and might
    change (or be removed) in future releases.
 
-.. important::
-   For the time being, you still might require a ``setup.py`` file containing
-   a *arg-less* ``setup()`` function call to support
-   :doc:`editable installs `.
-
 Starting with :pep:`621`, the Python community selected ``pyproject.toml`` as
 a standard way of specifying *project metadata*.
 ``Setuptools`` has adopted this standard and will use the information contained
-- 
cgit v1.2.1


From c7489da160cd4e0169f6c8b1eb275fc99a773e29 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 24 Mar 2022 12:09:32 +0000
Subject: Clarify that only deprecated fields should be avoided in
 pyproject_config

---
 docs/userguide/pyproject_config.rst | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/docs/userguide/pyproject_config.rst b/docs/userguide/pyproject_config.rst
index 1fe93528..a139fa73 100644
--- a/docs/userguide/pyproject_config.rst
+++ b/docs/userguide/pyproject_config.rst
@@ -101,8 +101,8 @@ Key                       Value Type (TOML)           Notes
 
 Please note that some of these configurations are deprecated or at least
 discouraged, but they are made available to ensure portability.
-New packages should avoid relying on them, and existing packages should
-consider alternatives.
+New packages should avoid relying on deprecated/discouraged fields, and
+existing packages should consider alternatives.
 
 .. tip::
    When both ``py-modules`` and ``packages`` are left unspecified,
-- 
cgit v1.2.1


From 2ec868c8a8ce69dd43a9a00eea5426159f625c44 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 24 Mar 2022 12:13:09 +0000
Subject: Specify Python 3 after Python 2 in intersphinx mapping

Hopefully it will make links to Python 3 the default.
---
 docs/conf.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/conf.py b/docs/conf.py
index ee833135..21451447 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -170,8 +170,8 @@ nitpick_ignore = [
 
 # Allow linking objects on other Sphinx sites seamlessly:
 intersphinx_mapping.update(
-    python=('https://docs.python.org/3', None),
     python2=('https://docs.python.org/2', None),
+    python=('https://docs.python.org/3', None),
 )
 
 # Add support for the unreleased "next-version" change notes
-- 
cgit v1.2.1


From 7f7ac349329d748bfa784540fe861a682bf3df50 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 24 Mar 2022 12:19:54 +0000
Subject: Avoid extlink for issue to prevent verbose warnings

---
 changelog.d/3068.change.rst | 2 +-
 docs/conf.py                | 6 ++++--
 2 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/changelog.d/3068.change.rst b/changelog.d/3068.change.rst
index 26ec747b..bedc5958 100644
--- a/changelog.d/3068.change.rst
+++ b/changelog.d/3068.change.rst
@@ -4,7 +4,7 @@ standards are handled in the ``[tool.setuptools]`` sub-table.
 
 In the future, existing ``setup.cfg`` configuration
 may be automatically converted into the ``pyproject.toml`` equivalent before taking effect
-(as proposed in :issue:`1688`). Meanwhile users can use automated tools like
+(as proposed in #1688). Meanwhile users can use automated tools like
 :pypi:`ini2toml` to help in the transition.
 
 Please note that the legacy backend is not guaranteed to work with
diff --git a/docs/conf.py b/docs/conf.py
index 21451447..4ebb521c 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -9,6 +9,10 @@ link_files = {
             GH='https://github.com',
         ),
         replace=[
+            dict(
+                pattern=r'(?\d+)',
+                url='{package_url}/pull/{pull}',
+            ),
             dict(
                 pattern=r'(?\d+)',
                 url='{package_url}/issues/{issue}',
@@ -99,8 +103,6 @@ github_repo_slug = f'{github_repo_org}/{github_repo_name}'
 github_repo_url = f'{github_url}/{github_repo_slug}'
 github_sponsors_url = f'{github_url}/sponsors'
 extlinks = {
-    'issue': (f'{github_repo_url}/issues/%s', 'issue #%s'),  # noqa: WPS323
-    'pr': (f'{github_repo_url}/pull/%s', 'PR #%s'),  # noqa: WPS323
     'user': (f'{github_sponsors_url}/%s', '@'),  # noqa: WPS323
     'pypi': ('https://pypi.org/project/%s', '%s'),  # noqa: WPS323
     'wiki': ('https://wikipedia.org/wiki/%s', '%s'),  # noqa: WPS323
-- 
cgit v1.2.1


From bce326f48f2d6e5d5727ee59155212dfedab9b68 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 24 Mar 2022 12:23:06 +0000
Subject: Fix wrong version reference in quickstart

---
 docs/userguide/quickstart.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst
index 79cfb2ef..71d44370 100644
--- a/docs/userguide/quickstart.rst
+++ b/docs/userguide/quickstart.rst
@@ -178,7 +178,7 @@ setup also allows you to adopt a ``src/`` layout. For more details and advanced
 use, go to :ref:`package_discovery`.
 
 .. tip::
-   Starting with version 60.10.0, setuptools' automatic discovery capabilities
+   Starting with version 61.0.0, setuptools' automatic discovery capabilities
    have been improved to detect popular project layouts (such as the
    :ref:`flat-layout` and :ref:`src-layout`) without requiring any
    special configuration. Check out our :ref:`reference docs `
-- 
cgit v1.2.1


From 5ad4e0b566bca38b3e02da231f17f08ca921f79f Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 24 Mar 2022 12:35:13 +0000
Subject: Fix edge case of package discovery

---
 setuptools/config/expand.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/setuptools/config/expand.py b/setuptools/config/expand.py
index 94c9ee38..3985040c 100644
--- a/setuptools/config/expand.py
+++ b/setuptools/config/expand.py
@@ -408,7 +408,8 @@ class EnsurePackagesDiscovered:
 
     def _get_package_dir(self) -> Mapping[str, str]:
         self()
-        return self._dist.package_dir
+        pkg_dir = self._dist.package_dir
+        return {} if pkg_dir is None else pkg_dir
 
     @property
     def package_dir(self) -> Mapping[str, str]:
-- 
cgit v1.2.1


From 5080d60e6bb80ea0e003da163fa6628c3f395d40 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 24 Mar 2022 12:39:38 +0000
Subject: Add the upcomming toxfile.py to the list of ignored modules for
 flat-layout

---
 setuptools/discovery.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/setuptools/discovery.py b/setuptools/discovery.py
index 410d503f..b787a0fd 100644
--- a/setuptools/discovery.py
+++ b/setuptools/discovery.py
@@ -248,6 +248,7 @@ class FlatLayoutModuleFinder(ModuleFinder):
         "examples",
         "build",
         # ---- Task runners ----
+        "toxfile",
         "noxfile",
         "pavement",
         "dodo",
-- 
cgit v1.2.1


From 3ea30666fa635a962f21a89dd3534cd0b6fabf8a Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 24 Mar 2022 12:44:55 +0000
Subject: Specify that some builds may break due to improper configuration

---
 changelog.d/2894.breaking.rst | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/changelog.d/2894.breaking.rst b/changelog.d/2894.breaking.rst
index 687ae511..8ee780b6 100644
--- a/changelog.d/2894.breaking.rst
+++ b/changelog.d/2894.breaking.rst
@@ -3,8 +3,9 @@ that some Python files (or general folders) might be automatically detected and
 included.
 
 Projects that currently don't specify both ``packages`` and ``py_modules`` in their
-configuration and have extra Python files and folders (not meant for distribution),
-might see these files being included in the wheel archive.
+configuration and contain extra folders or Python files (not meant for distribution),
+might see these files being included in the wheel archive or even experience
+the build to fail.
 
-You can check details about the automatic discovery behaviour (and
-how to configure a different one) in :doc:`/userguide/package_discovery`.
+You can check details about the automatic discovery (and how to configure a
+different behaviour) in :doc:`/userguide/package_discovery`.
-- 
cgit v1.2.1


From 7cd56d3c92be8fc45b029eb4c01b581d8dcf84f0 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 24 Mar 2022 14:09:51 +0000
Subject: Add back notes about editable install and pyproject metadata

On further examination, `pip` seems to fail if `setup.py` is missing.
---
 docs/userguide/pyproject_config.rst | 12 ++++++++++++
 docs/userguide/quickstart.rst       | 18 +++++++++++++++++-
 2 files changed, 29 insertions(+), 1 deletion(-)

diff --git a/docs/userguide/pyproject_config.rst b/docs/userguide/pyproject_config.rst
index a139fa73..3988db2f 100644
--- a/docs/userguide/pyproject_config.rst
+++ b/docs/userguide/pyproject_config.rst
@@ -12,6 +12,18 @@ Configuring setuptools using ``pyproject.toml`` files
    ``setuptools`` via ``pyproject.toml`` files is still experimental and might
    change (or be removed) in future releases.
 
+.. important::
+   For the time being, ``pip`` still might require a ``setup.py`` file
+   to support :doc:`editable installs `.
+
+   A simple script will suffice, for example:
+
+   .. code-block:: python
+
+       from setuptools import setup
+
+       setup()
+
 Starting with :pep:`621`, the Python community selected ``pyproject.toml`` as
 a standard way of specifying *project metadata*.
 ``Setuptools`` has adopted this standard and will use the information contained
diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst
index 71d44370..3af8aaa8 100644
--- a/docs/userguide/quickstart.rst
+++ b/docs/userguide/quickstart.rst
@@ -343,7 +343,23 @@ associate with your source code. For more information, see :doc:`development_mod
 
     Prior to :ref:`pip v21.1 `, a ``setup.py`` script was
     required to be compatible with development mode. With late
-    versions of pip, any project may be installed in this mode.
+    versions of pip, ``setup.cfg``-only projects may be installed in this mode.
+
+    If you are experimenting with :doc:`configuration using `,
+    or have version of ``pip`` older than v21.1, you might need to keep a
+    ``setup.py`` file in file in your repository if you want to use editable
+    installs (for the time being).
+
+    A simple script will suffice, for example:
+
+    .. code-block:: python
+
+        from setuptools import setup
+
+        setup()
+
+    You can still keep all the configuration in :doc:`setup.cfg `
+    (or :doc:`pyproject.toml `).
 
 
 Uploading your package to PyPI
-- 
cgit v1.2.1


From f86e9346300f9e893c9b473839af1e0e04e5dc65 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 23 Mar 2022 11:51:26 +0000
Subject: Add unit test for read_attr

Closes #3176
---
 setuptools/tests/config/test_expand.py | 68 ++++++++++++++++++++++------------
 1 file changed, 44 insertions(+), 24 deletions(-)

diff --git a/setuptools/tests/config/test_expand.py b/setuptools/tests/config/test_expand.py
index a7b0c21d..96f499dd 100644
--- a/setuptools/tests/config/test_expand.py
+++ b/setuptools/tests/config/test_expand.py
@@ -56,30 +56,50 @@ def test_read_files(tmp_path, monkeypatch):
         expand.read_files(["../a.txt"], tmp_path)
 
 
-def test_read_attr(tmp_path, monkeypatch):
-    files = {
-        "pkg/__init__.py": "",
-        "pkg/sub/__init__.py": "VERSION = '0.1.1'",
-        "pkg/sub/mod.py": (
-            "VALUES = {'a': 0, 'b': {42}, 'c': (0, 1, 1)}\n"
-            "raise SystemExit(1)"
-        ),
-    }
-    write_files(files, tmp_path)
-
-    with monkeypatch.context() as m:
-        m.chdir(tmp_path)
-        # Make sure it can read the attr statically without evaluating the module
-        assert expand.read_attr('pkg.sub.VERSION') == '0.1.1'
-        values = expand.read_attr('lib.mod.VALUES', {'lib': 'pkg/sub'})
-
-    assert values['a'] == 0
-    assert values['b'] == {42}
-
-    # Make sure the same APIs work outside cwd
-    assert expand.read_attr('pkg.sub.VERSION', root_dir=tmp_path) == '0.1.1'
-    values = expand.read_attr('lib.mod.VALUES', {'lib': 'pkg/sub'}, tmp_path)
-    assert values['c'] == (0, 1, 1)
+class TestReadAttr:
+    def test_read_attr(self, tmp_path, monkeypatch):
+        files = {
+            "pkg/__init__.py": "",
+            "pkg/sub/__init__.py": "VERSION = '0.1.1'",
+            "pkg/sub/mod.py": (
+                "VALUES = {'a': 0, 'b': {42}, 'c': (0, 1, 1)}\n"
+                "raise SystemExit(1)"
+            ),
+        }
+        write_files(files, tmp_path)
+
+        with monkeypatch.context() as m:
+            m.chdir(tmp_path)
+            # Make sure it can read the attr statically without evaluating the module
+            assert expand.read_attr('pkg.sub.VERSION') == '0.1.1'
+            values = expand.read_attr('lib.mod.VALUES', {'lib': 'pkg/sub'})
+
+        assert values['a'] == 0
+        assert values['b'] == {42}
+
+        # Make sure the same APIs work outside cwd
+        assert expand.read_attr('pkg.sub.VERSION', root_dir=tmp_path) == '0.1.1'
+        values = expand.read_attr('lib.mod.VALUES', {'lib': 'pkg/sub'}, tmp_path)
+        assert values['c'] == (0, 1, 1)
+
+    def test_import_order(self, tmp_path):
+        """
+        Sometimes the import machinery will import the parent package of a nested
+        module, which triggers side-effects and might create problems (see issue #3176)
+
+        ``read_attr`` should bypass these limitations by resolving modules statically
+        (via ast.literal_eval).
+        """
+        files = {
+            "src/pkg/__init__.py": "from .main import func\nfrom .about import version",
+            "src/pkg/main.py": "import super_complicated_dep\ndef func(): return 42",
+            "src/pkg/about.py": "version = '42'",
+        }
+        write_files(files, tmp_path)
+        attr_desc = "pkg.about.version"
+        pkg_dir = {"": "src"}
+        # `import super_complicated_dep` should not run, otherwise the build fails
+        assert expand.read_attr(attr_desc, pkg_dir, tmp_path) == "42"
 
 
 def test_resolve_class():
-- 
cgit v1.2.1


From d489141419688ae3cb87d70506c774011aa8a3cb Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 24 Mar 2022 10:44:04 +0000
Subject: Change tests for resolve_class to consider different layouts

Although this situation is different from the one described in #3000,
that issue served as inspiration behind this change.
---
 setuptools/tests/config/test_expand.py | 21 ++++++++++++++++-----
 1 file changed, 16 insertions(+), 5 deletions(-)

diff --git a/setuptools/tests/config/test_expand.py b/setuptools/tests/config/test_expand.py
index 96f499dd..d8078d0a 100644
--- a/setuptools/tests/config/test_expand.py
+++ b/setuptools/tests/config/test_expand.py
@@ -3,7 +3,6 @@ import os
 import pytest
 
 from distutils.errors import DistutilsOptionError
-from setuptools.command.sdist import sdist
 from setuptools.config import expand
 from setuptools.discovery import find_package_path
 
@@ -97,13 +96,25 @@ class TestReadAttr:
         }
         write_files(files, tmp_path)
         attr_desc = "pkg.about.version"
-        pkg_dir = {"": "src"}
+        package_dir = {"": "src"}
         # `import super_complicated_dep` should not run, otherwise the build fails
-        assert expand.read_attr(attr_desc, pkg_dir, tmp_path) == "42"
+        assert expand.read_attr(attr_desc, package_dir, tmp_path) == "42"
 
 
-def test_resolve_class():
-    assert expand.resolve_class("setuptools.command.sdist.sdist") == sdist
+@pytest.mark.parametrize(
+    'package_dir, file, module, return_value',
+    [
+        ({"": "src"}, "src/pkg/main.py", "pkg.main", 42),
+        ({"pkg": "lib"}, "lib/main.py", "pkg.main", 13),
+        ({}, "single_module.py", "single_module", 70),
+        ({}, "flat_layout/pkg.py", "flat_layout.pkg", 836),
+    ]
+)
+def test_resolve_class(tmp_path, package_dir, file, module, return_value):
+    files = {file: f"class Custom:\n    def testing(self): return {return_value}"}
+    write_files(files, tmp_path)
+    cls = expand.resolve_class(f"{module}.Custom", package_dir, tmp_path)
+    assert cls().testing() == return_value
 
 
 @pytest.mark.parametrize(
-- 
cgit v1.2.1


From 4923d1139be9366b3010a6d6e56d4c4980a0ab6e Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 24 Mar 2022 18:28:25 +0000
Subject: =?UTF-8?q?Bump=20version:=2060.10.0=20=E2=86=92=2061.0.0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .bumpversion.cfg                 |   2 +-
 CHANGES.rst                      | 108 +++++++++++++++++++++++++++++++++++++++
 changelog.d/2887.change.1.rst    |  22 --------
 changelog.d/2887.change.2.rst    |   9 ----
 changelog.d/2894.breaking.rst    |  11 ----
 changelog.d/3065.misc.rst        |   4 --
 changelog.d/3066.change.rst      |   3 --
 changelog.d/3068.change.rst      |  13 -----
 changelog.d/3068.deprecation.rst |   8 ---
 changelog.d/3125.change.rst      |  10 ----
 changelog.d/3152.change.rst      |   4 --
 changelog.d/3172.doc.rst         |   2 -
 changelog.d/3178.change.rst      |   2 -
 changelog.d/3179.change.rst      |   1 -
 setup.cfg                        |   2 +-
 15 files changed, 110 insertions(+), 91 deletions(-)
 delete mode 100644 changelog.d/2887.change.1.rst
 delete mode 100644 changelog.d/2887.change.2.rst
 delete mode 100644 changelog.d/2894.breaking.rst
 delete mode 100644 changelog.d/3065.misc.rst
 delete mode 100644 changelog.d/3066.change.rst
 delete mode 100644 changelog.d/3068.change.rst
 delete mode 100644 changelog.d/3068.deprecation.rst
 delete mode 100644 changelog.d/3125.change.rst
 delete mode 100644 changelog.d/3152.change.rst
 delete mode 100644 changelog.d/3172.doc.rst
 delete mode 100644 changelog.d/3178.change.rst
 delete mode 100644 changelog.d/3179.change.rst

diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index fd32042d..1b6f189f 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 60.10.0
+current_version = 61.0.0
 commit = True
 tag = True
 
diff --git a/CHANGES.rst b/CHANGES.rst
index 86b82911..3c6dc17a 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,3 +1,111 @@
+v61.0.0
+-------
+
+
+Deprecations
+^^^^^^^^^^^^
+* #3068: Deprecated ``setuptools.config.read_configuration``,
+  ``setuptools.config.parse_configuration`` and other functions or classes
+  from ``setuptools.config``.
+
+  Users that still need to parse and process configuration from ``setup.cfg`` can
+  import a direct replacement from ``setuptools.config.setupcfg``, however this
+  module is transitional and might be removed in the future
+  (the ``setup.cfg`` configuration format itself is likely to be deprecated in the future).
+
+Breaking Changes
+^^^^^^^^^^^^^^^^
+* #2894: If you purposefully want to create an *"empty distribution"*, please be aware
+  that some Python files (or general folders) might be automatically detected and
+  included.
+
+  Projects that currently don't specify both ``packages`` and ``py_modules`` in their
+  configuration and contain extra folders or Python files (not meant for distribution),
+  might see these files being included in the wheel archive or even experience
+  the build to fail.
+
+  You can check details about the automatic discovery (and how to configure a
+  different behaviour) in :doc:`/userguide/package_discovery`.
+
+Changes
+^^^^^^^
+* #2887: **[EXPERIMENTAL]** Added automatic discovery for ``py_modules`` and ``packages``
+  -- by :user:`abravalheri`.
+
+  Setuptools will try to find these values assuming that the package uses either
+  the *src-layout* (a ``src`` directory containing all the packages or modules),
+  the *flat-layout* (package directories directly under the project root),
+  or the *single-module* approach (an isolated Python file, directly under
+  the project root).
+
+  The automatic discovery will also respect layouts that are explicitly
+  configured using the ``package_dir`` option.
+
+  For backward-compatibility, this behavior will be observed **only if both**
+  ``py_modules`` **and** ``packages`` **are not set**.
+  (**Note**: specifying ``ext_modules`` might also prevent auto-discover from
+  taking place)
+
+  If setuptools detects modules or packages that are not supposed to be in the
+  distribution, please manually set ``py_modules`` and ``packages`` in your
+  ``setup.cfg`` or ``setup.py`` file.
+  If you are using a *flat-layout*, you can also consider switching to
+  *src-layout*.
+* #2887: **[EXPERIMENTAL]** Added automatic configuration for the ``name`` metadata
+  -- by :user:`abravalheri`.
+
+  Setuptools will adopt the name of the top-level package (or module in the case
+  of single-module distributions), **only when** ``name`` **is not explicitly
+  provided**.
+
+  Please note that it is not possible to automatically derive a single name when
+  the distribution consists of multiple top-level packages or modules.
+* #3066: Added vendored dependencies for :pypi:`tomli`, :pypi:`validate-pyproject`.
+
+  These dependencies are used to read ``pyproject.toml`` files and validate them.
+* #3068: **[EXPERIMENTAL]** Add support for ``pyproject.toml`` configuration
+  (as introduced by :pep:`621`). Configuration parameters not covered by
+  standards are handled in the ``[tool.setuptools]`` sub-table.
+
+  In the future, existing ``setup.cfg`` configuration
+  may be automatically converted into the ``pyproject.toml`` equivalent before taking effect
+  (as proposed in #1688). Meanwhile users can use automated tools like
+  :pypi:`ini2toml` to help in the transition.
+
+  Please note that the legacy backend is not guaranteed to work with
+  ``pyproject.toml`` configuration.
+
+  -- by :user:`abravalheri`
+* #3125: Implicit namespaces (as introduced in :pep:`420`) are now considered by default
+  during :doc:`package discovery `, when
+  ``setuptools`` configuration and project metadata are added to the
+  ``pyproject.toml`` file.
+
+  To disable this behaviour, use ``namespaces = False`` when explicitly setting
+  the ``[tool.setuptools.packages.find]`` section in ``pyproject.toml``.
+
+  This change is backwards compatible and does not affect the behaviour of
+  configuration done in ``setup.cfg`` or ``setup.py``.
+* #3152: **[EXPERIMENTAL]** Added support for ``attr:`` and ``cmdclass`` configurations
+  in ``setup.cfg`` and ``pyproject.toml`` when ``package_dir`` is implicitly
+  found via auto-discovery.
+* #3178: Postponed importing ``ctypes`` when hiding files on Windows.
+  This helps to prevent errors in systems that might not have `libffi` installed.
+* #3179: Merge with pypa/distutils@267dbd25ac
+
+Documentation changes
+^^^^^^^^^^^^^^^^^^^^^
+* #3172: Added initial documentation about configuring ``setuptools`` via ``pyproject.toml``
+  (using standard project metadata).
+
+Misc
+^^^^
+* #3065: Refactored ``setuptools.config`` by separating configuration parsing (specific
+  to the configuration file format, e.g. ``setup.cfg``) and post-processing
+  (which includes directives such as ``file:`` that can be used across different
+  configuration formats).
+
+
 v60.10.0
 --------
 
diff --git a/changelog.d/2887.change.1.rst b/changelog.d/2887.change.1.rst
deleted file mode 100644
index eeb5471e..00000000
--- a/changelog.d/2887.change.1.rst
+++ /dev/null
@@ -1,22 +0,0 @@
-**[EXPERIMENTAL]** Added automatic discovery for ``py_modules`` and ``packages``
--- by :user:`abravalheri`.
-
-Setuptools will try to find these values assuming that the package uses either
-the *src-layout* (a ``src`` directory containing all the packages or modules),
-the *flat-layout* (package directories directly under the project root),
-or the *single-module* approach (an isolated Python file, directly under
-the project root).
-
-The automatic discovery will also respect layouts that are explicitly
-configured using the ``package_dir`` option.
-
-For backward-compatibility, this behavior will be observed **only if both**
-``py_modules`` **and** ``packages`` **are not set**.
-(**Note**: specifying ``ext_modules`` might also prevent auto-discover from
-taking place)
-
-If setuptools detects modules or packages that are not supposed to be in the
-distribution, please manually set ``py_modules`` and ``packages`` in your
-``setup.cfg`` or ``setup.py`` file.
-If you are using a *flat-layout*, you can also consider switching to
-*src-layout*.
diff --git a/changelog.d/2887.change.2.rst b/changelog.d/2887.change.2.rst
deleted file mode 100644
index 1e3cc182..00000000
--- a/changelog.d/2887.change.2.rst
+++ /dev/null
@@ -1,9 +0,0 @@
-**[EXPERIMENTAL]** Added automatic configuration for the ``name`` metadata
--- by :user:`abravalheri`.
-
-Setuptools will adopt the name of the top-level package (or module in the case
-of single-module distributions), **only when** ``name`` **is not explicitly
-provided**.
-
-Please note that it is not possible to automatically derive a single name when
-the distribution consists of multiple top-level packages or modules.
diff --git a/changelog.d/2894.breaking.rst b/changelog.d/2894.breaking.rst
deleted file mode 100644
index 8ee780b6..00000000
--- a/changelog.d/2894.breaking.rst
+++ /dev/null
@@ -1,11 +0,0 @@
-If you purposefully want to create an *"empty distribution"*, please be aware
-that some Python files (or general folders) might be automatically detected and
-included.
-
-Projects that currently don't specify both ``packages`` and ``py_modules`` in their
-configuration and contain extra folders or Python files (not meant for distribution),
-might see these files being included in the wheel archive or even experience
-the build to fail.
-
-You can check details about the automatic discovery (and how to configure a
-different behaviour) in :doc:`/userguide/package_discovery`.
diff --git a/changelog.d/3065.misc.rst b/changelog.d/3065.misc.rst
deleted file mode 100644
index 31b9d59c..00000000
--- a/changelog.d/3065.misc.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-Refactored ``setuptools.config`` by separating configuration parsing (specific
-to the configuration file format, e.g. ``setup.cfg``) and post-processing
-(which includes directives such as ``file:`` that can be used across different
-configuration formats).
diff --git a/changelog.d/3066.change.rst b/changelog.d/3066.change.rst
deleted file mode 100644
index e672351f..00000000
--- a/changelog.d/3066.change.rst
+++ /dev/null
@@ -1,3 +0,0 @@
-Added vendored dependencies for :pypi:`tomli`, :pypi:`validate-pyproject`.
-
-These dependencies are used to read ``pyproject.toml`` files and validate them.
diff --git a/changelog.d/3068.change.rst b/changelog.d/3068.change.rst
deleted file mode 100644
index bedc5958..00000000
--- a/changelog.d/3068.change.rst
+++ /dev/null
@@ -1,13 +0,0 @@
-**[EXPERIMENTAL]** Add support for ``pyproject.toml`` configuration
-(as introduced by :pep:`621`). Configuration parameters not covered by
-standards are handled in the ``[tool.setuptools]`` sub-table.
-
-In the future, existing ``setup.cfg`` configuration
-may be automatically converted into the ``pyproject.toml`` equivalent before taking effect
-(as proposed in #1688). Meanwhile users can use automated tools like
-:pypi:`ini2toml` to help in the transition.
-
-Please note that the legacy backend is not guaranteed to work with
-``pyproject.toml`` configuration.
-
--- by :user:`abravalheri`
diff --git a/changelog.d/3068.deprecation.rst b/changelog.d/3068.deprecation.rst
deleted file mode 100644
index 3bae915c..00000000
--- a/changelog.d/3068.deprecation.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-Deprecated ``setuptools.config.read_configuration``,
-``setuptools.config.parse_configuration`` and other functions or classes
-from ``setuptools.config``.
-
-Users that still need to parse and process configuration from ``setup.cfg`` can
-import a direct replacement from ``setuptools.config.setupcfg``, however this
-module is transitional and might be removed in the future
-(the ``setup.cfg`` configuration format itself is likely to be deprecated in the future).
diff --git a/changelog.d/3125.change.rst b/changelog.d/3125.change.rst
deleted file mode 100644
index 716e95c0..00000000
--- a/changelog.d/3125.change.rst
+++ /dev/null
@@ -1,10 +0,0 @@
-Implicit namespaces (as introduced in :pep:`420`) are now considered by default
-during :doc:`package discovery `, when
-``setuptools`` configuration and project metadata are added to the
-``pyproject.toml`` file.
-
-To disable this behaviour, use ``namespaces = False`` when explicitly setting
-the ``[tool.setuptools.packages.find]`` section in ``pyproject.toml``.
-
-This change is backwards compatible and does not affect the behaviour of
-configuration done in ``setup.cfg`` or ``setup.py``.
diff --git a/changelog.d/3152.change.rst b/changelog.d/3152.change.rst
deleted file mode 100644
index 802a39ca..00000000
--- a/changelog.d/3152.change.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-**[EXPERIMENTAL]** Added support for ``attr:`` and ``cmdclass`` configurations
-in ``setup.cfg`` and ``pyproject.toml`` when ``package_dir`` is implicitly
-found via auto-discovery.
-
diff --git a/changelog.d/3172.doc.rst b/changelog.d/3172.doc.rst
deleted file mode 100644
index 1c179763..00000000
--- a/changelog.d/3172.doc.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Added initial documentation about configuring ``setuptools`` via ``pyproject.toml``
-(using standard project metadata).
diff --git a/changelog.d/3178.change.rst b/changelog.d/3178.change.rst
deleted file mode 100644
index dfb2d33b..00000000
--- a/changelog.d/3178.change.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Postponed importing ``ctypes`` when hiding files on Windows.
-This helps to prevent errors in systems that might not have `libffi` installed.
diff --git a/changelog.d/3179.change.rst b/changelog.d/3179.change.rst
deleted file mode 100644
index 791a327b..00000000
--- a/changelog.d/3179.change.rst
+++ /dev/null
@@ -1 +0,0 @@
-Merge with pypa/distutils@267dbd25ac
diff --git a/setup.cfg b/setup.cfg
index 7f300542..6183185c 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = setuptools
-version = 60.10.0
+version = 61.0.0
 author = Python Packaging Authority
 author_email = distutils-sig@python.org
 description = Easily download, build, install, upgrade, and uninstall Python packages
-- 
cgit v1.2.1


From 135e7d2a491dd68d9be6ac06fc74d4fe727e915d Mon Sep 17 00:00:00 2001
From: Mathieu Kniewallner 
Date: Thu, 24 Mar 2022 22:15:29 +0100
Subject: Fix `bellow` typo in docs

---
 docs/userguide/dependency_management.rst               | 2 +-
 docs/userguide/pyproject_config.rst                    | 6 +++---
 docs/userguide/quickstart.rst                          | 2 +-
 setuptools/_vendor/_validate_pyproject/NOTICE          | 2 +-
 setuptools/tests/integration/test_pip_install_sdist.py | 2 +-
 5 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/docs/userguide/dependency_management.rst b/docs/userguide/dependency_management.rst
index 85545b7c..279f794d 100644
--- a/docs/userguide/dependency_management.rst
+++ b/docs/userguide/dependency_management.rst
@@ -397,7 +397,7 @@ fail later).
 Python requirement
 ==================
 In some cases, you might need to specify the minimum required python version.
-This can be configured as shown in the example bellow.
+This can be configured as shown in the example below.
 
 .. tab:: setup.cfg
 
diff --git a/docs/userguide/pyproject_config.rst b/docs/userguide/pyproject_config.rst
index 3988db2f..47c4511e 100644
--- a/docs/userguide/pyproject_config.rst
+++ b/docs/userguide/pyproject_config.rst
@@ -29,7 +29,7 @@ a standard way of specifying *project metadata*.
 ``Setuptools`` has adopted this standard and will use the information contained
 in this file as an input in the build process.
 
-The example bellow illustrates how to write a ``pyproject.toml`` file that can
+The example below illustrates how to write a ``pyproject.toml`` file that can
 be used with ``setuptools``. It contains two TOML tables (identified by the
 ``[table-header]`` syntax): ``build-system`` and ``project``.
 The ``build-system`` table is used to tell the build frontend (e.g.
@@ -91,8 +91,8 @@ Key                       Value Type (TOML)           Notes
 ``zip-safe``              boolean                     If not specified, ``setuptools`` will try to guess
                                                       a reasonable default for the package
 ``eager-resources``       array
-``py-modules``            array                       See tip bellow
-``packages``              array or ``find`` directive See tip bellow
+``py-modules``            array                       See tip below
+``packages``              array or ``find`` directive See tip below
 ``package-dir``           table/inline-table          Used when explicitly listing ``packages``
 ``namespace-packages``    array                       Not necessary if you use :pep:`420`
 ``package-data``          table/inline-table          See :doc:`/userguide/datafiles`
diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst
index 3af8aaa8..5be1078a 100644
--- a/docs/userguide/quickstart.rst
+++ b/docs/userguide/quickstart.rst
@@ -236,7 +236,7 @@ Dependency management
 =====================
 Packages built with ``setuptools`` can specify dependencies to be automatically
 installed when the package itself is installed.
-The example bellow show how to configure this kind of dependencies:
+The example below show how to configure this kind of dependencies:
 
 .. tab:: setup.cfg
 
diff --git a/setuptools/_vendor/_validate_pyproject/NOTICE b/setuptools/_vendor/_validate_pyproject/NOTICE
index fd64608b..8ed8325e 100644
--- a/setuptools/_vendor/_validate_pyproject/NOTICE
+++ b/setuptools/_vendor/_validate_pyproject/NOTICE
@@ -31,7 +31,7 @@ by the same projects:
 - `__init__.py`
 - `fastjsonschema_validations.py`
 
-The relevant copyright notes and licenses are included bellow.
+The relevant copyright notes and licenses are included below.
 
 
 ***
diff --git a/setuptools/tests/integration/test_pip_install_sdist.py b/setuptools/tests/integration/test_pip_install_sdist.py
index 0177c22d..9d11047b 100644
--- a/setuptools/tests/integration/test_pip_install_sdist.py
+++ b/setuptools/tests/integration/test_pip_install_sdist.py
@@ -53,7 +53,7 @@ EXAMPLES = [
     ("brotli", LATEST),  # not in the list but used by urllib3
 
     # When adding packages to this list, make sure they expose a `__version__`
-    # attribute, or modify the tests bellow
+    # attribute, or modify the tests below
 ]
 
 
-- 
cgit v1.2.1


From fac2737b118356f37e99e3448dd5366ee58b6fa1 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 25 Mar 2022 12:51:34 +0000
Subject: Avoid unnecessarily changing package_dir

And also avoid using './' paths
---
 setuptools/config/expand.py               |  24 +++----
 setuptools/discovery.py                   |   7 +++
 setuptools/tests/test_config_discovery.py | 100 +++++++++++++++++++++++++++++-
 3 files changed, 118 insertions(+), 13 deletions(-)

diff --git a/setuptools/config/expand.py b/setuptools/config/expand.py
index 3985040c..04442bd8 100644
--- a/setuptools/config/expand.py
+++ b/setuptools/config/expand.py
@@ -292,8 +292,8 @@ def find_packages(
 
     :rtype: list
     """
-
-    from setuptools.discovery import remove_nested_packages
+    from setuptools.discovery import construct_package_dir
+    from setuptools.extern.more_itertools import unique_everseen, always_iterable
 
     if namespaces:
         from setuptools.discovery import PEP420PackageFinder as PackageFinder
@@ -302,18 +302,18 @@ def find_packages(
 
     root_dir = root_dir or os.curdir
     where = kwargs.pop('where', ['.'])
-    if isinstance(where, str):
-        where = [where]
-
-    packages = []
+    packages: List[str] = []
     fill_package_dir = {} if fill_package_dir is None else fill_package_dir
-    for path in where:
-        pkgs = PackageFinder.find(_nest_path(root_dir, path), **kwargs)
+
+    for path in unique_everseen(always_iterable(where)):
+        package_path = _nest_path(root_dir, path)
+        pkgs = PackageFinder.find(package_path, **kwargs)
         packages.extend(pkgs)
-        if fill_package_dir.get("") != path:
-            parent_pkgs = remove_nested_packages(pkgs)
-            parent = {pkg: "/".join([path, *pkg.split(".")]) for pkg in parent_pkgs}
-            fill_package_dir.update(parent)
+        if pkgs and not (
+            fill_package_dir.get("") == path
+            or os.path.samefile(package_path, root_dir)
+        ):
+            fill_package_dir.update(construct_package_dir(pkgs, path))
 
     return packages
 
diff --git a/setuptools/discovery.py b/setuptools/discovery.py
index b787a0fd..22f4fc4e 100644
--- a/setuptools/discovery.py
+++ b/setuptools/discovery.py
@@ -41,6 +41,7 @@ import itertools
 import os
 from fnmatch import fnmatchcase
 from glob import glob
+from pathlib import Path
 from typing import TYPE_CHECKING
 from typing import Callable, Dict, Iterator, Iterable, List, Optional, Tuple, Union
 
@@ -577,3 +578,9 @@ def find_package_path(name: str, package_dir: Dict[str, str], root_dir: _Path) -
 
     parent = package_dir.get("") or ""
     return os.path.join(root_dir, *parent.split("/"), *parts)
+
+
+def construct_package_dir(packages: List[str], package_path: _Path) -> Dict[str, str]:
+    parent_pkgs = remove_nested_packages(packages)
+    prefix = Path(package_path).parts
+    return {pkg: "/".join([*prefix, *pkg.split(".")]) for pkg in parent_pkgs}
diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py
index e6ed632e..5e70d524 100644
--- a/setuptools/tests/test_config_discovery.py
+++ b/setuptools/tests/test_config_discovery.py
@@ -12,6 +12,7 @@ import setuptools  # noqa -- force distutils.core to be patched
 import distutils.core
 
 import pytest
+import jaraco.path
 from path import Path as _Path
 
 from .contexts import quiet
@@ -398,6 +399,103 @@ class TestWithCExtension:
             _get_dist(tmp_path, {})
 
 
+class TestWithPackageData:
+    def _simulate_package_with_data_files(self, tmp_path, src_root):
+        files = [
+            f"{src_root}/proj/__init__.py",
+            f"{src_root}/proj/file1.txt",
+            f"{src_root}/proj/nested/file2.txt",
+        ]
+        _populate_project_dir(tmp_path, files, {})
+
+        manifest = """
+            global-include *.py *.txt
+        """
+        (tmp_path / "MANIFEST.in").write_text(DALS(manifest))
+
+    EXAMPLE_SETUPCFG = """
+    [metadata]
+    name = proj
+    version = 42
+
+    [options]
+    include_package_data = True
+    """
+    EXAMPLE_PYPROJECT = """
+    [project]
+    name = "proj"
+    version = "42"
+    """
+
+    PYPROJECT_PACKAGE_DIR = """
+    [tool.setuptools]
+    package-dir = {"" = "src"}
+    """
+
+    @pytest.mark.parametrize(
+        "src_root, files",
+        [
+            (".", {"setup.cfg": DALS(EXAMPLE_SETUPCFG)}),
+            (".", {"pyproject.toml": DALS(EXAMPLE_PYPROJECT)}),
+            ("src", {"setup.cfg": DALS(EXAMPLE_SETUPCFG)}),
+            ("src", {"pyproject.toml": DALS(EXAMPLE_PYPROJECT)}),
+            (
+                "src",
+                {
+                    "setup.cfg": DALS(EXAMPLE_SETUPCFG) + DALS(
+                        """
+                        packages = find:
+                        package_dir =
+                            =src
+
+                        [options.packages.find]
+                        where = src
+                        """
+                    )
+                }
+            ),
+            (
+                "src",
+                {
+                    "pyproject.toml": DALS(EXAMPLE_PYPROJECT) + DALS(
+                        """
+                        [tool.setuptools]
+                        package-dir = {"" = "src"}
+                        """
+                    )
+                },
+            ),
+        ]
+    )
+    def test_include_package_data(self, tmp_path, src_root, files):
+        """
+        Make sure auto-discovery does not affect package include_package_data.
+        See issue #3196.
+        """
+        jaraco.path.build(files, prefix=str(tmp_path))
+        self._simulate_package_with_data_files(tmp_path, src_root)
+
+        expected = {
+            os.path.normpath(f"{src_root}/proj/file1.txt"),
+            os.path.normpath(f"{src_root}/proj/nested/file2.txt"),
+        }
+
+        _run_build(tmp_path)
+        from pprint import pprint
+        pprint(files)
+
+        sdist_files = get_sdist_members(next(tmp_path.glob("dist/*.tar.gz")))
+        print("~~~~~ sdist_members ~~~~~")
+        print('\n'.join(sdist_files))
+        assert sdist_files >= expected
+
+        wheel_files = get_wheel_members(next(tmp_path.glob("dist/*.whl")))
+        print("~~~~~ wheel_members ~~~~~")
+        print('\n'.join(wheel_files))
+        orig_files = {f.replace("src/", "").replace("lib/", "") for f in expected}
+        assert wheel_files >= orig_files
+
+
 def _populate_project_dir(root, files, options):
     # NOTE: Currently pypa/build will refuse to build the project if no
     # `pyproject.toml` or `setup.py` is found. So it is impossible to do
@@ -437,7 +535,7 @@ def _write_setupcfg(root, options):
 
 def _run_build(path, *flags):
     cmd = [sys.executable, "-m", "build", "--no-isolation", *flags, str(path)]
-    return run(cmd, env={'DISTUTILS_DEBUG': '1'})
+    return run(cmd, env={'DISTUTILS_DEBUG': ''})
 
 
 def _get_dist(dist_path, attrs):
-- 
cgit v1.2.1


From 7d5fc95b7b8a6c2680990738b3554b70c4fff121 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 25 Mar 2022 13:04:54 +0000
Subject: Add news fragment

---
 changelog.d/3202.change.rst | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 changelog.d/3202.change.rst

diff --git a/changelog.d/3202.change.rst b/changelog.d/3202.change.rst
new file mode 100644
index 00000000..cb36190e
--- /dev/null
+++ b/changelog.d/3202.change.rst
@@ -0,0 +1,2 @@
+Changed behaviour of auto-discovery to not explicitly expand ``package_dir``
+for flat-layouts and to not use relative paths starting with ``./``.
-- 
cgit v1.2.1


From 47f506e758b513e59f60a27325aa65d05b429f4c Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 25 Mar 2022 13:36:00 +0000
Subject: Attempt to solve pathsep problems in windows tests

---
 setuptools/tests/test_config_discovery.py | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py
index 5e70d524..fd5a3239 100644
--- a/setuptools/tests/test_config_discovery.py
+++ b/setuptools/tests/test_config_discovery.py
@@ -476,13 +476,11 @@ class TestWithPackageData:
         self._simulate_package_with_data_files(tmp_path, src_root)
 
         expected = {
-            os.path.normpath(f"{src_root}/proj/file1.txt"),
-            os.path.normpath(f"{src_root}/proj/nested/file2.txt"),
+            os.path.normpath(f"{src_root}/proj/file1.txt").replace(os.sep, "/"),
+            os.path.normpath(f"{src_root}/proj/nested/file2.txt").replace(os.sep, "/"),
         }
 
         _run_build(tmp_path)
-        from pprint import pprint
-        pprint(files)
 
         sdist_files = get_sdist_members(next(tmp_path.glob("dist/*.tar.gz")))
         print("~~~~~ sdist_members ~~~~~")
-- 
cgit v1.2.1


From 51d5124b929ecff3ca590a565b994b9cd4922158 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 25 Mar 2022 13:40:32 +0000
Subject: Add missing breaking change note

---
 CHANGES.rst | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/CHANGES.rst b/CHANGES.rst
index 3c6dc17a..23ca2085 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -26,6 +26,15 @@ Breaking Changes
 
   You can check details about the automatic discovery (and how to configure a
   different behaviour) in :doc:`/userguide/package_discovery`.
+* #3067: If the file ``pyproject.toml`` exists and it includes project
+  metadata/config (via ``[project]`` table or ``[tool.setuptools]``),
+  a series of new behaviors that are not backward compatible may take place:
+
+  - The default value of ``include_package_data`` will be considered to be ``True``.
+  - Setuptools will attempt to validate the ``pyproject.toml`` file according
+    to PEP 621 specification.
+  - The values specified in ``pyproject.toml`` will take precedence over those
+    specified in ``setup.cfg`` or ``setup.py``.
 
 Changes
 ^^^^^^^
@@ -63,6 +72,8 @@ Changes
 * #3066: Added vendored dependencies for :pypi:`tomli`, :pypi:`validate-pyproject`.
 
   These dependencies are used to read ``pyproject.toml`` files and validate them.
+* #3067: **[EXPERIMENTAL]** When using ``pyproject.toml`` metadata,
+  the default value of ``include_package_data`` is changed to ``True``.
 * #3068: **[EXPERIMENTAL]** Add support for ``pyproject.toml`` configuration
   (as introduced by :pep:`621`). Configuration parameters not covered by
   standards are handled in the ``[tool.setuptools]`` sub-table.
-- 
cgit v1.2.1


From 9bc0d0a68fbe47c758fa733fdd9484ed0fb0c7b7 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 25 Mar 2022 13:56:08 +0000
Subject: Test setup.py' include_package_data not ignored when parsing
 pyproject

---
 setuptools/tests/config/test_pyprojecttoml.py | 23 +++++++++++++++++++++++
 1 file changed, 23 insertions(+)

diff --git a/setuptools/tests/config/test_pyprojecttoml.py b/setuptools/tests/config/test_pyprojecttoml.py
index 0157b2ad..0fdca253 100644
--- a/setuptools/tests/config/test_pyprojecttoml.py
+++ b/setuptools/tests/config/test_pyprojecttoml.py
@@ -4,6 +4,7 @@ from inspect import cleandoc
 
 import pytest
 import tomli_w
+from path import Path as _Path
 
 from setuptools.config.pyprojecttoml import (
     read_configuration,
@@ -11,6 +12,9 @@ from setuptools.config.pyprojecttoml import (
     validate,
 )
 
+import setuptools  # noqa -- force distutils.core to be patched
+import distutils.core
+
 EXAMPLE = """
 [project]
 name = "myproj"
@@ -292,3 +296,22 @@ def test_include_package_data_by_default(tmp_path, config):
 
     config = read_configuration(pyproject)
     assert config["tool"]["setuptools"]["include-package-data"] is True
+
+
+def test_include_package_data_in_setuppy(tmp_path):
+    """Builds with ``pyproject.toml`` should consider ``include_package_data`` set in
+    ``setup.py``.
+
+    See https://github.com/pypa/setuptools/issues/3197#issuecomment-1079023889
+    """
+    pyproject = tmp_path / "pyproject.toml"
+    pyproject.write_text("[project]\nname = 'myproj'\nversion='42'\n")
+    setuppy = tmp_path / "setup.py"
+    setuppy.write_text("__import__('setuptools').setup(include_package_data=False)")
+
+    with _Path(tmp_path):
+        dist = distutils.core.run_setup("setup.py", {}, stop_after="config")
+
+    assert dist.get_name() == "myproj"
+    assert dist.get_version() == "42"
+    assert dist.include_package_data is False
-- 
cgit v1.2.1


From 073141f7da97eee10a038f8cb05c4a1773106717 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 25 Mar 2022 13:57:54 +0000
Subject: Avoid overwritting dist.include_package_data with default

---
 setuptools/config/pyprojecttoml.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index bc76b111..de29b515 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -103,7 +103,10 @@ def read_configuration(
     # the default would be an improvement.
     # `ini2toml` backfills include_package_data=False when nothing is explicitly given,
     # therefore setting a default here is backwards compatible.
-    setuptools_table.setdefault("include-package-data", True)
+    if dist and getattr(dist, "include_package_data") is not None:
+        setuptools_table.setdefault("include-package-data", dist.include_package_data)
+    else:
+        setuptools_table.setdefault("include-package-data", True)
     # Persist changes:
     asdict["tool"] = tool_table
     tool_table["setuptools"] = setuptools_table
-- 
cgit v1.2.1


From 3780557be29c773a375d0ae2178ba16205d63eb3 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 25 Mar 2022 14:01:57 +0000
Subject: Add news fragment

---
 changelog.d/3203.change.rst | 3 +++
 1 file changed, 3 insertions(+)
 create mode 100644 changelog.d/3203.change.rst

diff --git a/changelog.d/3203.change.rst b/changelog.d/3203.change.rst
new file mode 100644
index 00000000..9c95a99e
--- /dev/null
+++ b/changelog.d/3203.change.rst
@@ -0,0 +1,3 @@
+Prevented ``pyproject.toml`` parsing from overwriting
+``dist.include_package_data`` explicitly set in ``setup.py`` with default
+value.
-- 
cgit v1.2.1


From 6e462c7a18c32089bfd4e8c6cb0f3382b7f25c7a Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 25 Mar 2022 14:34:03 +0000
Subject: Add back convert_path as deprecated function

---
 setuptools/__init__.py              | 15 +++++++++++++++
 setuptools/tests/test_setuptools.py |  5 +++++
 2 files changed, 20 insertions(+)

diff --git a/setuptools/__init__.py b/setuptools/__init__.py
index 187e7329..502d2a2e 100644
--- a/setuptools/__init__.py
+++ b/setuptools/__init__.py
@@ -3,11 +3,13 @@
 import functools
 import os
 import re
+import warnings
 
 import _distutils_hack.override  # noqa: F401
 
 import distutils.core
 from distutils.errors import DistutilsOptionError
+from distutils.util import convert_path as _convert_path
 
 from ._deprecation_warning import SetuptoolsDeprecationWarning
 
@@ -158,6 +160,19 @@ def findall(dir=os.curdir):
     return list(files)
 
 
+@functools.wraps(_convert_path)
+def convert_path(pathname):
+    from inspect import cleandoc
+
+    msg = """
+    The function `convert_path` is considered internal and not part of the public API.
+    Its direct usage by 3rd-party packages is considered deprecated and the function
+    may be removed in the future.
+    """
+    warnings.warn(cleandoc(msg), SetuptoolsDeprecationWarning)
+    return _convert_path(pathname)
+
+
 class sic(str):
     """Treat this string as-is (https://en.wikipedia.org/wiki/Sic)"""
 
diff --git a/setuptools/tests/test_setuptools.py b/setuptools/tests/test_setuptools.py
index b97faf17..0640f49d 100644
--- a/setuptools/tests/test_setuptools.py
+++ b/setuptools/tests/test_setuptools.py
@@ -303,3 +303,8 @@ def test_its_own_wheel_does_not_contain_tests(setuptools_wheel):
 
     for member in contents:
         assert '/tests/' not in member
+
+
+def test_convert_path_deprecated():
+    with pytest.warns(setuptools.SetuptoolsDeprecationWarning):
+        setuptools.convert_path('setuptools/tests')
-- 
cgit v1.2.1


From 26145049f9b4d9aeac926c17679bd01e9b7c41f4 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 25 Mar 2022 14:40:05 +0000
Subject: Add news fragment

---
 changelog.d/3206.deprecation.rst | 3 +++
 1 file changed, 3 insertions(+)
 create mode 100644 changelog.d/3206.deprecation.rst

diff --git a/changelog.d/3206.deprecation.rst b/changelog.d/3206.deprecation.rst
new file mode 100644
index 00000000..2ad90f37
--- /dev/null
+++ b/changelog.d/3206.deprecation.rst
@@ -0,0 +1,3 @@
+Changed ``setuptools.convert_path`` to an internal function that is not exposed
+as part of setuptools API.
+Future releases of ``setuptools`` are likely to remove this function.
-- 
cgit v1.2.1


From 7c9761ac1a1608300280d916927dbdb9e6d39974 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 25 Mar 2022 16:38:05 +0000
Subject: Make sure dynamic classifiers don't fail on unexisting files

---
 setuptools/config/expand.py                        | 18 +++--
 setuptools/config/pyprojecttoml.py                 |  2 +-
 .../tests/config/test_apply_pyprojecttoml.py       |  2 +-
 setuptools/tests/config/test_expand.py             | 12 ++--
 setuptools/tests/config/test_pyprojecttoml.py      | 83 +++++++++++++++-------
 setuptools/tests/config/test_setupcfg.py           |  7 +-
 6 files changed, 86 insertions(+), 38 deletions(-)

diff --git a/setuptools/config/expand.py b/setuptools/config/expand.py
index 3985040c..70f72468 100644
--- a/setuptools/config/expand.py
+++ b/setuptools/config/expand.py
@@ -20,6 +20,7 @@ import importlib
 import io
 import os
 import sys
+import warnings
 from glob import iglob
 from configparser import ConfigParser
 from importlib.machinery import ModuleSpec
@@ -124,18 +125,25 @@ def read_files(filepaths: Union[str, bytes, Iterable[_Path]], root_dir=None) ->
 
     (By default ``root_dir`` is the current directory).
     """
-    if isinstance(filepaths, (str, bytes)):
-        filepaths = [filepaths]  # type: ignore
+    from setuptools.extern.more_itertools import always_iterable
 
     root_dir = os.path.abspath(root_dir or os.getcwd())
-    _filepaths = (os.path.join(root_dir, path) for path in filepaths)
+    _filepaths = (os.path.join(root_dir, path) for path in always_iterable(filepaths))
     return '\n'.join(
         _read_file(path)
-        for path in _filepaths
-        if _assert_local(path, root_dir) and os.path.isfile(path)
+        for path in _filter_existing_files(_filepaths)
+        if _assert_local(path, root_dir)
     )
 
 
+def _filter_existing_files(filepaths: Iterable[_Path]) -> Iterator[_Path]:
+    for path in filepaths:
+        if os.path.isfile(path):
+            yield path
+        else:
+            warnings.warn(f"File {path!r} cannot be found")
+
+
 def _read_file(filepath: Union[bytes, _Path]) -> str:
     with io.open(filepath, encoding='utf-8') as f:
         return f.read()
diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index bc76b111..b3a6518e 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -249,7 +249,7 @@ def _expand_all_dynamic(
 
     if "classifiers" in dynamic:
         value = _expand_dynamic(dynamic_cfg, "classifiers", pkg_dir, root_dir, ignore)
-        project_cfg["classifiers"] = value.splitlines()
+        project_cfg["classifiers"] = (value or "").splitlines()
 
 
 def _expand_dynamic(
diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py
index 38c9d1dc..044f801c 100644
--- a/setuptools/tests/config/test_apply_pyprojecttoml.py
+++ b/setuptools/tests/config/test_apply_pyprojecttoml.py
@@ -141,7 +141,7 @@ def _pep621_example_project(tmp_path, readme="README.rst"):
         text = text.replace(orig, subst)
     pyproject.write_text(text)
 
-    (tmp_path / "README.rst").write_text("hello world")
+    (tmp_path / readme).write_text("hello world")
     (tmp_path / "LICENSE.txt").write_text("--- LICENSE stub ---")
     (tmp_path / "spam.py").write_text(PEP621_EXAMPLE_SCRIPT)
     return pyproject
diff --git a/setuptools/tests/config/test_expand.py b/setuptools/tests/config/test_expand.py
index d8078d0a..3a59edbb 100644
--- a/setuptools/tests/config/test_expand.py
+++ b/setuptools/tests/config/test_expand.py
@@ -34,15 +34,19 @@ def test_glob_relative(tmp_path, monkeypatch):
 
 
 def test_read_files(tmp_path, monkeypatch):
+
+    dir_ = tmp_path / "dir_"
+    (tmp_path / "_dir").mkdir(exist_ok=True)
+    (tmp_path / "a.txt").touch()
     files = {
         "a.txt": "a",
         "dir1/b.txt": "b",
         "dir1/dir2/c.txt": "c"
     }
-    write_files(files, tmp_path)
+    write_files(files, dir_)
 
     with monkeypatch.context() as m:
-        m.chdir(tmp_path)
+        m.chdir(dir_)
         assert expand.read_files(list(files)) == "a\nb\nc"
 
         cannot_access_msg = r"Cannot access '.*\.\..a\.txt'"
@@ -50,9 +54,9 @@ def test_read_files(tmp_path, monkeypatch):
             expand.read_files(["../a.txt"])
 
     # Make sure the same APIs work outside cwd
-    assert expand.read_files(list(files), tmp_path) == "a\nb\nc"
+    assert expand.read_files(list(files), dir_) == "a\nb\nc"
     with pytest.raises(DistutilsOptionError, match=cannot_access_msg):
-        expand.read_files(["../a.txt"], tmp_path)
+        expand.read_files(["../a.txt"], dir_)
 
 
 class TestReadAttr:
diff --git a/setuptools/tests/config/test_pyprojecttoml.py b/setuptools/tests/config/test_pyprojecttoml.py
index 0157b2ad..5e8f6cbe 100644
--- a/setuptools/tests/config/test_pyprojecttoml.py
+++ b/setuptools/tests/config/test_pyprojecttoml.py
@@ -10,6 +10,8 @@ from setuptools.config.pyprojecttoml import (
     expand_configuration,
     validate,
 )
+from setuptools.errors import OptionError
+
 
 EXAMPLE = """
 [project]
@@ -189,32 +191,63 @@ def test_expand_entry_point(tmp_path):
     assert "gui-scripts" not in expanded_project
 
 
-def test_dynamic_classifiers(tmp_path):
-    # Let's create a project example that has dynamic classifiers
-    # coming from a txt file.
-    create_example(tmp_path, "src")
-    classifiers = """\
-    Framework :: Flask
-    Programming Language :: Haskell
-    """
-    (tmp_path / "classifiers.txt").write_text(cleandoc(classifiers))
+class TestClassifiers:
+    def test_dynamic(self, tmp_path):
+        # Let's create a project example that has dynamic classifiers
+        # coming from a txt file.
+        create_example(tmp_path, "src")
+        classifiers = """\
+        Framework :: Flask
+        Programming Language :: Haskell
+        """
+        (tmp_path / "classifiers.txt").write_text(cleandoc(classifiers))
+
+        pyproject = tmp_path / "pyproject.toml"
+        config = read_configuration(pyproject, expand=False)
+        dynamic = config["project"]["dynamic"]
+        config["project"]["dynamic"] = list({*dynamic, "classifiers"})
+        dynamic_config = config["tool"]["setuptools"]["dynamic"]
+        dynamic_config["classifiers"] = {"file": "classifiers.txt"}
+
+        # When the configuration is expanded,
+        # each line of the file should be an different classifier.
+        validate(config, pyproject)
+        expanded = expand_configuration(config, tmp_path)
+
+        assert set(expanded["project"]["classifiers"]) == {
+            "Framework :: Flask",
+            "Programming Language :: Haskell",
+        }
 
-    pyproject = tmp_path / "pyproject.toml"
-    config = read_configuration(pyproject, expand=False)
-    dynamic = config["project"]["dynamic"]
-    config["project"]["dynamic"] = list({*dynamic, "classifiers"})
-    dynamic_config = config["tool"]["setuptools"]["dynamic"]
-    dynamic_config["classifiers"] = {"file": "classifiers.txt"}
-
-    # When the configuration is expanded,
-    # each line of the file should be an different classifier.
-    validate(config, pyproject)
-    expanded = expand_configuration(config, tmp_path)
-
-    assert set(expanded["project"]["classifiers"]) == {
-        "Framework :: Flask",
-        "Programming Language :: Haskell",
-    }
+    def test_dynamic_without_config(self, tmp_path):
+        config = """
+        [project]
+        name = "myproj"
+        version = '42'
+        dynamic = ["classifiers"]
+        """
+
+        pyproject = tmp_path / "pyproject.toml"
+        pyproject.write_text(cleandoc(config))
+        with pytest.raises(OptionError, match="No configuration found"):
+            read_configuration(pyproject)
+
+    def test_dynamic_without_file(self, tmp_path):
+        config = """
+        [project]
+        name = "myproj"
+        version = '42'
+        dynamic = ["classifiers"]
+
+        [tool.setuptools.dynamic]
+        classifiers = {file = ["classifiers.txt"]}
+        """
+
+        pyproject = tmp_path / "pyproject.toml"
+        pyproject.write_text(cleandoc(config))
+        with pytest.warns(UserWarning, match="File .*classifiers.txt. cannot be found"):
+            expanded = read_configuration(pyproject)
+        assert not expanded["project"]["classifiers"]
 
 
 @pytest.mark.parametrize(
diff --git a/setuptools/tests/config/test_setupcfg.py b/setuptools/tests/config/test_setupcfg.py
index 8cd3ae7f..1f35f836 100644
--- a/setuptools/tests/config/test_setupcfg.py
+++ b/setuptools/tests/config/test_setupcfg.py
@@ -185,9 +185,12 @@ class TestMetadata:
 
     def test_file_sandboxed(self, tmpdir):
 
-        fake_env(tmpdir, '[metadata]\n' 'long_description = file: ../../README\n')
+        tmpdir.ensure("README")
+        project = tmpdir.join('depth1', 'depth2')
+        project.ensure(dir=True)
+        fake_env(project, '[metadata]\n' 'long_description = file: ../../README\n')
 
-        with get_dist(tmpdir, parse=False) as dist:
+        with get_dist(project, parse=False) as dist:
             with pytest.raises(DistutilsOptionError):
                 dist.parse_config_files()  # file: out of sandbox
 
-- 
cgit v1.2.1


From 4569a87030b1e905303b06b72827ad8475223c86 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 25 Mar 2022 16:41:34 +0000
Subject: Add news fragment

---
 changelog.d/3208.change.1.rst | 2 ++
 changelog.d/3208.change.2.rst | 2 ++
 2 files changed, 4 insertions(+)
 create mode 100644 changelog.d/3208.change.1.rst
 create mode 100644 changelog.d/3208.change.2.rst

diff --git a/changelog.d/3208.change.1.rst b/changelog.d/3208.change.1.rst
new file mode 100644
index 00000000..fa2b73c4
--- /dev/null
+++ b/changelog.d/3208.change.1.rst
@@ -0,0 +1,2 @@
+Added a warning for non existing files listed with the ``file`` directive in
+``setup.cfg`` and ``pyproject.toml``.
diff --git a/changelog.d/3208.change.2.rst b/changelog.d/3208.change.2.rst
new file mode 100644
index 00000000..86e13177
--- /dev/null
+++ b/changelog.d/3208.change.2.rst
@@ -0,0 +1,2 @@
+Added a default value for dynamic ``classifiers`` in ``pyproject.toml`` when
+files are missing and errors being ignored.
-- 
cgit v1.2.1


From 6b0a021cc772fb67d275407fffaf96895e8be04a Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 25 Mar 2022 17:21:24 +0000
Subject: Disable auto-discovery when the 'configuration' attribute is passed

---
 changelog.d/3211.change.rst               | 12 ++++++++++++
 setuptools/discovery.py                   |  2 ++
 setuptools/tests/test_config_discovery.py | 14 ++++++++++++++
 3 files changed, 28 insertions(+)
 create mode 100644 changelog.d/3211.change.rst

diff --git a/changelog.d/3211.change.rst b/changelog.d/3211.change.rst
new file mode 100644
index 00000000..a6a9ffb3
--- /dev/null
+++ b/changelog.d/3211.change.rst
@@ -0,0 +1,12 @@
+Disabled auto-discovery when distribution class has a ``configuration`` field
+(e.g. when the ``setup.py`` script contains ``setup(..., configuration=...)``).
+This is done to ensure extension-only packages created with
+``numpy.distutils.misc_util.Configuration`` are not broken by the safe guard
+behaviour to avoid accidental multiple top-level packages in a flat-layout.
+
+**Note** - Users that don't set ``packages``, ``py_modules``, or
+``configuration`` are still likely to observe the auto-discovery behavior,
+which may interrupt the build if the project contains multiple directories and/or
+multiple Python files directly under the project root.
+For projects that don't use the ``[project]`` table in their ``pyproject.toml``
+setting ``ext_modules`` will also disable auto-discovery.
diff --git a/setuptools/discovery.py b/setuptools/discovery.py
index 22f4fc4e..95c3c7f8 100644
--- a/setuptools/discovery.py
+++ b/setuptools/discovery.py
@@ -341,6 +341,8 @@ class ConfigDiscovery:
             self.dist.packages is not None
             or self.dist.py_modules is not None
             or ext_modules
+            or hasattr(self.dist, "configuration") and self.dist.configuration
+            # ^ Some projects use numpy.distutils.misc_util.Configuration
         )
 
     def _analyse_package_layout(self, ignore_ext_modules: bool) -> bool:
diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py
index fd5a3239..fac365f4 100644
--- a/setuptools/tests/test_config_discovery.py
+++ b/setuptools/tests/test_config_discovery.py
@@ -494,6 +494,20 @@ class TestWithPackageData:
         assert wheel_files >= orig_files
 
 
+def test_compatible_with_numpy_configuration(tmp_path):
+    files = [
+        "dir1/__init__.py",
+        "dir2/__init__.py",
+        "file.py",
+    ]
+    _populate_project_dir(tmp_path, files, {})
+    dist = Distribution({})
+    dist.configuration = object()
+    dist.set_defaults()
+    assert dist.py_modules is None
+    assert dist.packages is None
+
+
 def _populate_project_dir(root, files, options):
     # NOTE: Currently pypa/build will refuse to build the project if no
     # `pyproject.toml` or `setup.py` is found. So it is impossible to do
-- 
cgit v1.2.1


From a72fb093bd70292a5d63ebba7285014e3ce13bdb Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 25 Mar 2022 18:07:46 +0000
Subject: Update news fragment

---
 changelog.d/3211.change.rst | 23 +++++++++++++----------
 1 file changed, 13 insertions(+), 10 deletions(-)

diff --git a/changelog.d/3211.change.rst b/changelog.d/3211.change.rst
index a6a9ffb3..3c4c4b47 100644
--- a/changelog.d/3211.change.rst
+++ b/changelog.d/3211.change.rst
@@ -1,12 +1,15 @@
-Disabled auto-discovery when distribution class has a ``configuration`` field
-(e.g. when the ``setup.py`` script contains ``setup(..., configuration=...)``).
-This is done to ensure extension-only packages created with
-``numpy.distutils.misc_util.Configuration`` are not broken by the safe guard
+Disabled auto-discovery when distribution class has a ``configuration``
+attribute (e.g. when the ``setup.py`` script contains ``setup(...,
+configuration=...)``).  This is done to ensure extension-only packages created
+with ``numpy.distutils.misc_util.Configuration`` are not broken by the safe
+guard
 behaviour to avoid accidental multiple top-level packages in a flat-layout.
 
-**Note** - Users that don't set ``packages``, ``py_modules``, or
-``configuration`` are still likely to observe the auto-discovery behavior,
-which may interrupt the build if the project contains multiple directories and/or
-multiple Python files directly under the project root.
-For projects that don't use the ``[project]`` table in their ``pyproject.toml``
-setting ``ext_modules`` will also disable auto-discovery.
+.. note::
+   Users that don't set ``packages``, ``py_modules``, or ``configuration`` are
+   still likely to observe the auto-discovery behavior, which may halt the
+   build if the project contains multiple directories and/or multiple Python
+   files directly under the project root.
+
+   To disable auto-discovery please explicitly set either ``packages`` or
+   ``py_modules``. Alternatively you can also configure :ref:`custom-discovery`.
-- 
cgit v1.2.1


From 6310e32e6a5456a75f982ca67ddf49f559f9cd69 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 25 Mar 2022 19:22:59 +0000
Subject: =?UTF-8?q?Bump=20version:=2061.0.0=20=E2=86=92=2061.1.0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .bumpversion.cfg                 |  2 +-
 CHANGES.rst                      | 38 ++++++++++++++++++++++++++++++++++++++
 changelog.d/3202.change.rst      |  2 --
 changelog.d/3203.change.rst      |  3 ---
 changelog.d/3206.deprecation.rst |  3 ---
 changelog.d/3208.change.1.rst    |  2 --
 changelog.d/3208.change.2.rst    |  2 --
 changelog.d/3211.change.rst      | 15 ---------------
 setup.cfg                        |  2 +-
 9 files changed, 40 insertions(+), 29 deletions(-)
 delete mode 100644 changelog.d/3202.change.rst
 delete mode 100644 changelog.d/3203.change.rst
 delete mode 100644 changelog.d/3206.deprecation.rst
 delete mode 100644 changelog.d/3208.change.1.rst
 delete mode 100644 changelog.d/3208.change.2.rst
 delete mode 100644 changelog.d/3211.change.rst

diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 1b6f189f..30fa709d 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 61.0.0
+current_version = 61.1.0
 commit = True
 tag = True
 
diff --git a/CHANGES.rst b/CHANGES.rst
index 23ca2085..2c76dad4 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,3 +1,41 @@
+v61.1.0
+-------
+
+
+Deprecations
+^^^^^^^^^^^^
+* #3206: Changed ``setuptools.convert_path`` to an internal function that is not exposed
+  as part of setuptools API.
+  Future releases of ``setuptools`` are likely to remove this function.
+
+Changes
+^^^^^^^
+* #3202: Changed behaviour of auto-discovery to not explicitly expand ``package_dir``
+  for flat-layouts and to not use relative paths starting with ``./``.
+* #3203: Prevented ``pyproject.toml`` parsing from overwriting
+  ``dist.include_package_data`` explicitly set in ``setup.py`` with default
+  value.
+* #3208: Added a warning for non existing files listed with the ``file`` directive in
+  ``setup.cfg`` and ``pyproject.toml``.
+* #3208: Added a default value for dynamic ``classifiers`` in ``pyproject.toml`` when
+  files are missing and errors being ignored.
+* #3211: Disabled auto-discovery when distribution class has a ``configuration``
+  attribute (e.g. when the ``setup.py`` script contains ``setup(...,
+  configuration=...)``).  This is done to ensure extension-only packages created
+  with ``numpy.distutils.misc_util.Configuration`` are not broken by the safe
+  guard
+  behaviour to avoid accidental multiple top-level packages in a flat-layout.
+
+  .. note::
+     Users that don't set ``packages``, ``py_modules``, or ``configuration`` are
+     still likely to observe the auto-discovery behavior, which may halt the
+     build if the project contains multiple directories and/or multiple Python
+     files directly under the project root.
+
+     To disable auto-discovery please explicitly set either ``packages`` or
+     ``py_modules``. Alternatively you can also configure :ref:`custom-discovery`.
+
+
 v61.0.0
 -------
 
diff --git a/changelog.d/3202.change.rst b/changelog.d/3202.change.rst
deleted file mode 100644
index cb36190e..00000000
--- a/changelog.d/3202.change.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Changed behaviour of auto-discovery to not explicitly expand ``package_dir``
-for flat-layouts and to not use relative paths starting with ``./``.
diff --git a/changelog.d/3203.change.rst b/changelog.d/3203.change.rst
deleted file mode 100644
index 9c95a99e..00000000
--- a/changelog.d/3203.change.rst
+++ /dev/null
@@ -1,3 +0,0 @@
-Prevented ``pyproject.toml`` parsing from overwriting
-``dist.include_package_data`` explicitly set in ``setup.py`` with default
-value.
diff --git a/changelog.d/3206.deprecation.rst b/changelog.d/3206.deprecation.rst
deleted file mode 100644
index 2ad90f37..00000000
--- a/changelog.d/3206.deprecation.rst
+++ /dev/null
@@ -1,3 +0,0 @@
-Changed ``setuptools.convert_path`` to an internal function that is not exposed
-as part of setuptools API.
-Future releases of ``setuptools`` are likely to remove this function.
diff --git a/changelog.d/3208.change.1.rst b/changelog.d/3208.change.1.rst
deleted file mode 100644
index fa2b73c4..00000000
--- a/changelog.d/3208.change.1.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Added a warning for non existing files listed with the ``file`` directive in
-``setup.cfg`` and ``pyproject.toml``.
diff --git a/changelog.d/3208.change.2.rst b/changelog.d/3208.change.2.rst
deleted file mode 100644
index 86e13177..00000000
--- a/changelog.d/3208.change.2.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Added a default value for dynamic ``classifiers`` in ``pyproject.toml`` when
-files are missing and errors being ignored.
diff --git a/changelog.d/3211.change.rst b/changelog.d/3211.change.rst
deleted file mode 100644
index 3c4c4b47..00000000
--- a/changelog.d/3211.change.rst
+++ /dev/null
@@ -1,15 +0,0 @@
-Disabled auto-discovery when distribution class has a ``configuration``
-attribute (e.g. when the ``setup.py`` script contains ``setup(...,
-configuration=...)``).  This is done to ensure extension-only packages created
-with ``numpy.distutils.misc_util.Configuration`` are not broken by the safe
-guard
-behaviour to avoid accidental multiple top-level packages in a flat-layout.
-
-.. note::
-   Users that don't set ``packages``, ``py_modules``, or ``configuration`` are
-   still likely to observe the auto-discovery behavior, which may halt the
-   build if the project contains multiple directories and/or multiple Python
-   files directly under the project root.
-
-   To disable auto-discovery please explicitly set either ``packages`` or
-   ``py_modules``. Alternatively you can also configure :ref:`custom-discovery`.
diff --git a/setup.cfg b/setup.cfg
index 6183185c..e5c484bb 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = setuptools
-version = 61.0.0
+version = 61.1.0
 author = Python Packaging Authority
 author_email = distutils-sig@python.org
 description = Easily download, build, install, upgrade, and uninstall Python packages
-- 
cgit v1.2.1


From a5658e826c1191eb1a40bff894fb625af7cccaa9 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 25 Mar 2022 22:47:31 +0000
Subject: Add test for setup.py install and dependencies

---
 setuptools/command/easy_install.py    |  4 ++-
 setuptools/tests/test_easy_install.py | 55 ++++++++++++++++++++++++++++++++++-
 2 files changed, 57 insertions(+), 2 deletions(-)

diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py
index 107850a9..77dcd25c 100644
--- a/setuptools/command/easy_install.py
+++ b/setuptools/command/easy_install.py
@@ -298,7 +298,9 @@ class easy_install(Command):
 
         if not self.editable:
             self.check_site_dir()
-        self.index_url = self.index_url or "https://pypi.org/simple/"
+        default_index = os.getenv("__EASYINSTALL_INDEX", "https://pypi.org/simple/")
+        # ^ Private API for testing purposes only
+        self.index_url = self.index_url or default_index
         self.shadow_path = self.all_site_dirs[:]
         for path_item in self.install_dir, normalize_path(self.script_dir):
             if path_item not in self.shadow_path:
diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py
index 5831b267..74044e20 100644
--- a/setuptools/tests/test_easy_install.py
+++ b/setuptools/tests/test_easy_install.py
@@ -448,6 +448,59 @@ class TestDistutilsPackage:
         run_setup('setup.py', ['bdist_egg'])
 
 
+class TestInstallRequires:
+    def test_setup_install_includes_dependencis(self, tmp_path, mock_index):
+        """
+        When ``python setup.py install`` is called directly, it will use easy_install
+        to fetch dependencies.
+        """
+        # TODO: Remove these tests once `setup.py install` is completely removed
+        # create an sdist that has a install-time dependency.
+        project_root = tmp_path / "project"
+        project_root.mkdir(exist_ok=True)
+        install_root = tmp_path / "project"
+        install_root.mkdir(exist_ok=True)
+
+        self.create_project(project_root)
+        cmd = [
+            sys.executable,
+            '-c', '__import__("setuptools").setup()',
+            'install',
+            '--install-base', str(install_root),
+            '--install-lib', str(install_root),
+            '--install-headers', str(install_root),
+            '--install-scripts', str(install_root),
+            '--install-data', str(install_root),
+            '--install-purelib', str(install_root),
+            '--install-platlib', str(install_root),
+        ]
+        env = {"PYTHONPATH": str(install_root), "__EASYINSTALL_INDEX": mock_index.url}
+        with pytest.raises(subprocess.CalledProcessError) as exc_info:
+            subprocess.check_output(
+                cmd, cwd=str(project_root), env=env, stderr=subprocess.STDOUT, text=True
+            )
+        assert next(
+            line
+            for line in exc_info.value.output.splitlines()
+            if "not find suitable distribution for" in line
+            and "does-not-exist" in line
+        )
+        assert '/does-not-exist/' in {r.path for r in mock_index.requests}
+
+    def create_project(self, root):
+        config = """
+        [metadata]
+        name = project
+        version = 42
+
+        [options]
+        install_requires = does-not-exist
+        py_modules = mod
+        """
+        (root / 'setup.cfg').write_text(DALS(config), encoding="utf-8")
+        (root / 'mod.py').touch()
+
+
 class TestSetupRequires:
 
     def test_setup_requires_honors_fetch_params(self, mock_index, monkeypatch):
@@ -466,7 +519,7 @@ class TestSetupRequires:
                     with contexts.environment(PYTHONPATH=temp_install_dir):
                         cmd = [
                             sys.executable,
-                            '-m', 'setup',
+                            '-c', '__import__("setuptools").setup()',
                             'easy_install',
                             '--index-url', mock_index.url,
                             '--exclude-scripts',
-- 
cgit v1.2.1


From 93a24585683944a9369d8fd37a824c0bca345af4 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 25 Mar 2022 22:48:21 +0000
Subject: Make install consider dist.run_command is overwritten in v61.0.0

Starting in v61, setuptools.dist overwrites distutils.dist.run_command
to add auto-discovery functionality on top of the original
implementation.

This change modifies the existing code in setuptools.command.install to
consider that previous change when trying to decide if the install
command was called directly from `setup.py` or not.
---
 setuptools/command/install.py | 23 +++++++++++++++--------
 1 file changed, 15 insertions(+), 8 deletions(-)

diff --git a/setuptools/command/install.py b/setuptools/command/install.py
index 35e54d20..55fdb124 100644
--- a/setuptools/command/install.py
+++ b/setuptools/command/install.py
@@ -91,14 +91,21 @@ class install(orig.install):
                 msg = "For best results, pass -X:Frames to enable call stack."
                 warnings.warn(msg)
             return True
-        res = inspect.getouterframes(run_frame)[2]
-        caller, = res[:1]
-        info = inspect.getframeinfo(caller)
-        caller_module = caller.f_globals.get('__name__', '')
-        return (
-            caller_module == 'distutils.dist'
-            and info.function == 'run_commands'
-        )
+
+        frames = inspect.getouterframes(run_frame)
+        for frame in frames[2:4]:
+            caller, = frame[:1]
+            info = inspect.getframeinfo(caller)
+            caller_module = caller.f_globals.get('__name__', '')
+
+            if caller_module == "setuptools.dist" and info.function == "run_command":
+                # Starting from v61.0.0 setuptools overwrites dist.run_command
+                continue
+
+            return (
+                caller_module == 'distutils.dist'
+                and info.function == 'run_commands'
+            )
 
     def do_egg_install(self):
 
-- 
cgit v1.2.1


From feecce27e54bcb46cda600a835265db2ecfb8777 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 25 Mar 2022 23:09:18 +0000
Subject: Add news fragment

---
 CHANGES.rst               | 2 ++
 changelog.d/3212.misc.rst | 4 ++++
 2 files changed, 6 insertions(+)
 create mode 100644 changelog.d/3212.misc.rst

diff --git a/CHANGES.rst b/CHANGES.rst
index 2c76dad4..fa181dec 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -704,6 +704,8 @@ Documentation changes
   :user:`abravalheri`
 
 
+.. _setup_install_deprecation_note:
+
 v58.3.0
 -------
 
diff --git a/changelog.d/3212.misc.rst b/changelog.d/3212.misc.rst
new file mode 100644
index 00000000..e54b6324
--- /dev/null
+++ b/changelog.d/3212.misc.rst
@@ -0,0 +1,4 @@
+Fixed missing dependencies when running ``setup.py install``.
+Note that calling ``setup.py install`` directly is still deprecated and support
+for this command will be removed in future versions of ``setuptools``.
+Please check the release notes for :ref:`setup_install_deprecation_note`.
-- 
cgit v1.2.1


From a30f65f5222286f6b0c646ca8d55d5eadedcf931 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 25 Mar 2022 23:35:57 +0000
Subject: Add workaround for PyPy

---
 setuptools/tests/test_easy_install.py | 17 +++++++++++------
 1 file changed, 11 insertions(+), 6 deletions(-)

diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py
index 74044e20..c2de336e 100644
--- a/setuptools/tests/test_easy_install.py
+++ b/setuptools/tests/test_easy_install.py
@@ -479,13 +479,18 @@ class TestInstallRequires:
             subprocess.check_output(
                 cmd, cwd=str(project_root), env=env, stderr=subprocess.STDOUT, text=True
             )
-        assert next(
-            line
-            for line in exc_info.value.output.splitlines()
-            if "not find suitable distribution for" in line
-            and "does-not-exist" in line
-        )
         assert '/does-not-exist/' in {r.path for r in mock_index.requests}
+        try:
+            assert next(
+                line
+                for line in exc_info.value.output.splitlines()
+                if "not find suitable distribution for" in line
+                and "does-not-exist" in line
+            )
+        except StopIteration:
+            if not hasattr(sys, 'pypy_version_info'):
+                # Let's skip PyPy for now in the test
+                raise
 
     def create_project(self, root):
         config = """
-- 
cgit v1.2.1


From 0407fd8920980a55074ca5d514066851545edc57 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 26 Mar 2022 00:13:39 +0000
Subject: Temporarily disable test for Windows+PyPy

---
 setuptools/tests/test_easy_install.py | 24 ++++++++++++------------
 1 file changed, 12 insertions(+), 12 deletions(-)

diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py
index c2de336e..ea216d6b 100644
--- a/setuptools/tests/test_easy_install.py
+++ b/setuptools/tests/test_easy_install.py
@@ -449,7 +449,12 @@ class TestDistutilsPackage:
 
 
 class TestInstallRequires:
-    def test_setup_install_includes_dependencis(self, tmp_path, mock_index):
+    @pytest.mark.xfail(
+        hasattr(sys, "pypy_version_info") and sys.platform == "win32",
+        reason="temporary disable test for pypy and windows "
+        "(seems to present problems in the CI)"
+    )
+    def test_setup_install_includes_dependencies(self, tmp_path, mock_index):
         """
         When ``python setup.py install`` is called directly, it will use easy_install
         to fetch dependencies.
@@ -480,17 +485,12 @@ class TestInstallRequires:
                 cmd, cwd=str(project_root), env=env, stderr=subprocess.STDOUT, text=True
             )
         assert '/does-not-exist/' in {r.path for r in mock_index.requests}
-        try:
-            assert next(
-                line
-                for line in exc_info.value.output.splitlines()
-                if "not find suitable distribution for" in line
-                and "does-not-exist" in line
-            )
-        except StopIteration:
-            if not hasattr(sys, 'pypy_version_info'):
-                # Let's skip PyPy for now in the test
-                raise
+        assert next(
+            line
+            for line in exc_info.value.output.splitlines()
+            if "not find suitable distribution for" in line
+            and "does-not-exist" in line
+        )
 
     def create_project(self, root):
         config = """
-- 
cgit v1.2.1


From 54bb069a936d23b1bf4b7f980a0e153c522df0c3 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 26 Mar 2022 06:35:52 +0000
Subject: Add debug statements for test on Windows

---
 setuptools/tests/test_easy_install.py | 25 +++++++++++++------------
 1 file changed, 13 insertions(+), 12 deletions(-)

diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py
index ea216d6b..6f4befe0 100644
--- a/setuptools/tests/test_easy_install.py
+++ b/setuptools/tests/test_easy_install.py
@@ -449,11 +449,6 @@ class TestDistutilsPackage:
 
 
 class TestInstallRequires:
-    @pytest.mark.xfail(
-        hasattr(sys, "pypy_version_info") and sys.platform == "win32",
-        reason="temporary disable test for pypy and windows "
-        "(seems to present problems in the CI)"
-    )
     def test_setup_install_includes_dependencies(self, tmp_path, mock_index):
         """
         When ``python setup.py install`` is called directly, it will use easy_install
@@ -484,13 +479,19 @@ class TestInstallRequires:
             subprocess.check_output(
                 cmd, cwd=str(project_root), env=env, stderr=subprocess.STDOUT, text=True
             )
-        assert '/does-not-exist/' in {r.path for r in mock_index.requests}
-        assert next(
-            line
-            for line in exc_info.value.output.splitlines()
-            if "not find suitable distribution for" in line
-            and "does-not-exist" in line
-        )
+        try:
+            assert '/does-not-exist/' in {r.path for r in mock_index.requests}
+            assert next(
+                line
+                for line in exc_info.value.output.splitlines()
+                if "not find suitable distribution for" in line
+                and "does-not-exist" in line
+            )
+        except Exception:
+            if sys.platform == "win32":
+                print("Problems in running the test on Windows")
+                print(exc_info.value.output)
+            raise
 
     def create_project(self, root):
         config = """
-- 
cgit v1.2.1


From 01c3b52dcc4dd575b37fe0139b3b81dc6c129475 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 26 Mar 2022 06:57:58 +0000
Subject: XFAIL test due to uncorrelated reason

---
 setuptools/tests/test_easy_install.py | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py
index 6f4befe0..ecfd2a8d 100644
--- a/setuptools/tests/test_easy_install.py
+++ b/setuptools/tests/test_easy_install.py
@@ -488,9 +488,8 @@ class TestInstallRequires:
                 and "does-not-exist" in line
             )
         except Exception:
-            if sys.platform == "win32":
-                print("Problems in running the test on Windows")
-                print(exc_info.value.output)
+            if "failed to get random numbers" in exc_info.value.output:
+                pytest.xfail(f"{sys.platform} failure - {exc_info.value.output}")
             raise
 
     def create_project(self, root):
-- 
cgit v1.2.1


From bf5c69fd390cd99ca01af2ba7d8e8214d59b4483 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 26 Mar 2022 07:46:25 +0000
Subject: Fix test for setup.py

---
 setuptools/tests/test_easy_install.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py
index ecfd2a8d..0d26dc73 100644
--- a/setuptools/tests/test_easy_install.py
+++ b/setuptools/tests/test_easy_install.py
@@ -455,10 +455,9 @@ class TestInstallRequires:
         to fetch dependencies.
         """
         # TODO: Remove these tests once `setup.py install` is completely removed
-        # create an sdist that has a install-time dependency.
         project_root = tmp_path / "project"
         project_root.mkdir(exist_ok=True)
-        install_root = tmp_path / "project"
+        install_root = tmp_path / "install"
         install_root.mkdir(exist_ok=True)
 
         self.create_project(project_root)
-- 
cgit v1.2.1


From 24b449fd5763eb95505ff9258227f4d362d196ca Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 26 Mar 2022 09:33:19 +0000
Subject: Improve news fragment

---
 changelog.d/3212.misc.rst | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/changelog.d/3212.misc.rst b/changelog.d/3212.misc.rst
index e54b6324..3fabab41 100644
--- a/changelog.d/3212.misc.rst
+++ b/changelog.d/3212.misc.rst
@@ -1,4 +1,4 @@
 Fixed missing dependencies when running ``setup.py install``.
-Note that calling ``setup.py install`` directly is still deprecated and support
-for this command will be removed in future versions of ``setuptools``.
+Note that calling ``setup.py install`` directly is still deprecated and
+will be removed in future versions of ``setuptools``.
 Please check the release notes for :ref:`setup_install_deprecation_note`.
-- 
cgit v1.2.1


From cc910e2235268963a5f05433f29dacc7804676c5 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 26 Mar 2022 09:33:29 +0000
Subject: =?UTF-8?q?Bump=20version:=2061.1.0=20=E2=86=92=2061.1.1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .bumpversion.cfg          |  2 +-
 CHANGES.rst               | 12 ++++++++++++
 changelog.d/3212.misc.rst |  4 ----
 setup.cfg                 |  2 +-
 4 files changed, 14 insertions(+), 6 deletions(-)
 delete mode 100644 changelog.d/3212.misc.rst

diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 30fa709d..70ba4d79 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 61.1.0
+current_version = 61.1.1
 commit = True
 tag = True
 
diff --git a/CHANGES.rst b/CHANGES.rst
index fa181dec..676cd15e 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,3 +1,15 @@
+v61.1.1
+-------
+
+
+Misc
+^^^^
+* #3212: Fixed missing dependencies when running ``setup.py install``.
+  Note that calling ``setup.py install`` directly is still deprecated and
+  will be removed in future versions of ``setuptools``.
+  Please check the release notes for :ref:`setup_install_deprecation_note`.
+
+
 v61.1.0
 -------
 
diff --git a/changelog.d/3212.misc.rst b/changelog.d/3212.misc.rst
deleted file mode 100644
index 3fabab41..00000000
--- a/changelog.d/3212.misc.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-Fixed missing dependencies when running ``setup.py install``.
-Note that calling ``setup.py install`` directly is still deprecated and
-will be removed in future versions of ``setuptools``.
-Please check the release notes for :ref:`setup_install_deprecation_note`.
diff --git a/setup.cfg b/setup.cfg
index e5c484bb..fa376efd 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = setuptools
-version = 61.1.0
+version = 61.1.1
 author = Python Packaging Authority
 author_email = distutils-sig@python.org
 description = Easily download, build, install, upgrade, and uninstall Python packages
-- 
cgit v1.2.1


From 1c47ae18c8c8c58a2e2b09bfc01028d747acfd66 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 26 Mar 2022 13:54:56 +0000
Subject: Test popular invalid pyproject patterns

---
 setuptools/tests/config/test_pyprojecttoml.py | 41 +++++++++++++++++++++++++++
 1 file changed, 41 insertions(+)

diff --git a/setuptools/tests/config/test_pyprojecttoml.py b/setuptools/tests/config/test_pyprojecttoml.py
index 1b5b90e2..c0ee2378 100644
--- a/setuptools/tests/config/test_pyprojecttoml.py
+++ b/setuptools/tests/config/test_pyprojecttoml.py
@@ -9,8 +9,11 @@ from path import Path as _Path
 from setuptools.config.pyprojecttoml import (
     read_configuration,
     expand_configuration,
+    apply_configuration,
     validate,
+    _InvalidFile,
 )
+from setuptools.dist import Distribution
 from setuptools.errors import OptionError
 
 
@@ -348,3 +351,41 @@ def test_include_package_data_in_setuppy(tmp_path):
     assert dist.get_name() == "myproj"
     assert dist.get_version() == "42"
     assert dist.include_package_data is False
+
+
+class TestSkipBadConfig:
+    @pytest.mark.parametrize(
+        "setup_attrs",
+        [
+            {"name": "myproj"},
+            {"install_requires": ["does-not-exist"]},
+        ],
+    )
+    @pytest.mark.parametrize(
+        "pyproject_content",
+        [
+            "[project]\nrequires-python = '>=3.7'\n",
+            "[project]\nversion = '42'\nrequires-python = '>=3.7'\n",
+            pytest.param(
+                "[project]\nname='othername'\nrequires-python = '>=3.7'\n",
+                marks=pytest.mark.xfail(reason="abravalheri/validate-pyproject#28")
+            ),
+        ],
+    )
+    def test_popular_config(self, tmp_path, pyproject_content, setup_attrs):
+        # See pypa/setuptools#3199 and pypa/cibuildwheel#1064
+        pyproject = tmp_path / "pyproject.toml"
+        pyproject.write_text(pyproject_content)
+        dist = Distribution(attrs=setup_attrs)
+
+        prev_name = dist.get_name()
+        prev_deps = dist.install_requires
+        print(f"{dist=}, {prev_name=}, {prev_deps=}")
+
+        with pytest.warns(_InvalidFile, match=r"DO NOT include.*\[project\].* table"):
+            dist = apply_configuration(dist, pyproject)
+
+        assert dist.get_name() != "othername"
+        assert dist.get_name() == prev_name
+        assert dist.python_requires is None
+        assert set(dist.install_requires) == set(prev_deps)
-- 
cgit v1.2.1


From 8946f664ea4fec781b9d4636ee37675223d9cb11 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 26 Mar 2022 13:55:52 +0000
Subject: Temporarily forgive popular patterns on invalid pyproject.toml

---
 setuptools/config/_apply_pyprojecttoml.py |  3 ++
 setuptools/config/pyprojecttoml.py        | 65 ++++++++++++++++++++++++++++++-
 2 files changed, 66 insertions(+), 2 deletions(-)

diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py
index c8ddab4b..5496502a 100644
--- a/setuptools/config/_apply_pyprojecttoml.py
+++ b/setuptools/config/_apply_pyprojecttoml.py
@@ -31,6 +31,9 @@ _logger = logging.getLogger(__name__)
 def apply(dist: "Distribution", config: dict, filename: _Path) -> "Distribution":
     """Apply configuration dict read with :func:`read_configuration`"""
 
+    if not config:
+        return dist  # short-circuit unrelated pyproject.toml file
+
     root_dir = os.path.dirname(filename) or "."
     tool_table = config.get("tool", {}).get("setuptools", {})
     project_table = config.get("project", {}).copy()
diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index c7f8cb6e..def6a651 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -92,7 +92,7 @@ def read_configuration(
     if not asdict or not (project_table or setuptools_table):
         return {}  # User is not using pyproject to configure setuptools
 
-    # TODO: Remove once the feature stabilizes
+    # TODO: Remove the following once the feature stabilizes:
     msg = (
         "Support for project metadata in `pyproject.toml` is still experimental "
         "and may be removed (or change) in future releases."
@@ -103,6 +103,7 @@ def read_configuration(
     # the default would be an improvement.
     # `ini2toml` backfills include_package_data=False when nothing is explicitly given,
     # therefore setting a default here is backwards compatible.
+    orig_setuptools_table = setuptools_table.copy()
     if dist and getattr(dist, "include_package_data") is not None:
         setuptools_table.setdefault("include-package-data", dist.include_package_data)
     else:
@@ -111,10 +112,17 @@ def read_configuration(
     asdict["tool"] = tool_table
     tool_table["setuptools"] = setuptools_table
 
-    with _ignore_errors(ignore_option_errors):
+    try:
         # Don't complain about unrelated errors (e.g. tools not using the "tool" table)
         subset = {"project": project_table, "tool": {"setuptools": setuptools_table}}
         validate(subset, filepath)
+    except Exception as ex:
+        if ignore_option_errors:
+            _logger.debug(f"ignored error: {ex.__class__.__name__} - {ex}")
+
+        # TODO: Remove the following once the feature stabilizes:
+        if _skip_bad_config(project_table, orig_setuptools_table, dist):
+            return {}
 
     if expand:
         root_dir = os.path.dirname(filepath)
@@ -123,6 +131,36 @@ def read_configuration(
     return asdict
 
 
+def _skip_bad_config(
+    project_cfg: dict, setuptools_cfg: dict, dist: Optional["Distribution"]
+) -> bool:
+    """Be temporarily forgiving with invalid ``pyproject.toml``"""
+    # See pypa/setuptools#3199 and pypa/cibuildwheel#1064
+
+    if dist is None or (
+        dist.metadata.name is None
+        and dist.metadata.version is None
+        and dist.install_requires is None
+    ):
+        # It seems that the build is not getting any configuration from other places
+        return False
+
+    if setuptools_cfg:
+        # If `[tool.setuptools]` is set, then `pyproject.toml` config is intentional
+        return False
+
+    given_config = set(project_cfg.keys())
+    popular_subset = {"name", "version", "python_requires", "requires-python"}
+    if given_config <= popular_subset:
+        # It seems that the docs in cibuildtool has been inadvertently encouraging users
+        # to create `pyproject.toml` files that are not compliant with the standards.
+        # Let's be forgiving for the time being.
+        warnings.warn(_InvalidFile.message(), _InvalidFile, stacklevel=2)
+        return True
+
+    return False
+
+
 def expand_configuration(
     config: dict,
     root_dir: Optional[_Path] = None,
@@ -336,3 +374,26 @@ def _ignore_errors(ignore_option_errors: bool):
 
 class _ExperimentalProjectMetadata(UserWarning):
     """Explicitly inform users that `pyproject.toml` configuration is experimental"""
+
+
+class _InvalidFile(UserWarning):
+    """Inform users that the given `pyproject.toml` is experimental.
+    !!\n\n
+    ############################
+    # Invalid `pyproject.toml` #
+    ############################
+
+    Any configurations in `pyproject.toml` will be ignored.
+    Please note that future releases of setuptools will halt the build process
+    if an invalid file is given.
+
+    To prevent setuptools from considering `pyproject.toml` please
+    DO NOT include the `[project]` or `[tool.setuptools]` tables in your file.
+    \n\n!!
+    """
+
+    @classmethod
+    def message(cls):
+        from inspect import cleandoc
+        msg = "\n".join(cls.__doc__.splitlines()[1:])
+        return cleandoc(msg)
-- 
cgit v1.2.1


From 18f9d0b6c50cb493e20afeedb38410ae35a27a86 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 26 Mar 2022 14:50:30 +0000
Subject: Add news fragment

---
 changelog.d/3215.change.rst | 7 +++++++
 1 file changed, 7 insertions(+)
 create mode 100644 changelog.d/3215.change.rst

diff --git a/changelog.d/3215.change.rst b/changelog.d/3215.change.rst
new file mode 100644
index 00000000..a086799e
--- /dev/null
+++ b/changelog.d/3215.change.rst
@@ -0,0 +1,7 @@
+Ignored a subgroup of invalid ``pyproject.toml`` files that use the ``[project]``
+table to specify only ``requires-python`` (**transitional**).
+
+.. warning::
+   Please note that future releases of setuptools will halt the build process
+   if a ``pyproject.toml`` file that does not match doc:`the PyPA Specification
+   ` is given.
-- 
cgit v1.2.1


From 67000543719e41e6dd7298e25757487029a8b511 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 26 Mar 2022 15:23:48 +0000
Subject: Add missing re-raise statement

---
 setuptools/config/pyprojecttoml.py | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index def6a651..b8cd0c51 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -117,12 +117,15 @@ def read_configuration(
         subset = {"project": project_table, "tool": {"setuptools": setuptools_table}}
         validate(subset, filepath)
     except Exception as ex:
-        if ignore_option_errors:
-            _logger.debug(f"ignored error: {ex.__class__.__name__} - {ex}")
-
         # TODO: Remove the following once the feature stabilizes:
         if _skip_bad_config(project_table, orig_setuptools_table, dist):
             return {}
+        # TODO: After the previous statement is removed the try/except can be replaced
+        # by the _ignore_errors context manager.
+        if ignore_option_errors:
+            _logger.debug(f"ignored error: {ex.__class__.__name__} - {ex}")
+        else:
+            raise  # re-raise exception
 
     if expand:
         root_dir = os.path.dirname(filepath)
-- 
cgit v1.2.1


From c275a1216a4e9d9490ca6abe38da4e906ec1252f Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 26 Mar 2022 15:32:18 +0000
Subject: Update pyproject validation as generated by validate-pyproject==0.6.1

---
 changelog.d/3215.change.2.rst                      |  1 +
 .../fastjsonschema_validations.py                  | 51 ++++++++++++----------
 setuptools/_vendor/vendored.txt                    |  2 +-
 3 files changed, 29 insertions(+), 25 deletions(-)
 create mode 100644 changelog.d/3215.change.2.rst

diff --git a/changelog.d/3215.change.2.rst b/changelog.d/3215.change.2.rst
new file mode 100644
index 00000000..b3a67f53
--- /dev/null
+++ b/changelog.d/3215.change.2.rst
@@ -0,0 +1 @@
+Updated ``pyproject.toml`` validation, as generated by ``validate-pyproject==0.6.1``.
diff --git a/setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py b/setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py
index 3feda6a8..3ad1edd0 100644
--- a/setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py
+++ b/setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py
@@ -30,7 +30,7 @@ def validate(data, custom_formats={}, name_prefix=None):
 
 def validate_https___packaging_python_org_en_latest_specifications_declaring_build_dependencies(data, custom_formats={}, name_prefix=None):
     if not isinstance(data, (dict)):
-        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-build-dependencies/', 'title': 'Data structure for ``pyproject.toml`` files', '$$description': ['File format containing build-time configurations for the Python ecosystem. ', ':pep:`517` initially defined a build-system independent format for source trees', 'which was complemented by :pep:`518` to provide a way of specifying dependencies ', 'for building Python projects.', 'Please notice the ``project`` table (as initially defined in  :pep:`621`) is not included', 'in this schema and should be considered separately.'], 'type': 'object', 'additionalProperties': False, 'properties': {'build-system': {'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, 'tool': {'type': 'object', 'properties': {'distutils': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://docs.python.org/3/install/', 'title': '``tool.distutils`` table', '$$description': ['Originally, ``distutils`` allowed developers to configure arguments for', '``setup.py`` scripts via `distutils configuration files', '`_.', '``tool.distutils`` subtables could be used with the same purpose', '(NOT CURRENTLY IMPLEMENTED).'], 'type': 'object', 'properties': {'global': {'type': 'object', 'description': 'Global options applied to all ``distutils`` commands'}}, 'patternProperties': {'.+': {'type': 'object'}}, '$comment': 'TODO: Is there a practical way of making this schema more specific?'}, 'setuptools': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$ref': '#/definitions/find-directive'}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might become standard with PEP 639).'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Probably ``project.license-files``?'}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'additionalProperties': False, 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'$ref': '#/definitions/attr-directive'}, {'$ref': '#/definitions/file-directive'}]}, 'classifiers': {'$ref': '#/definitions/file-directive'}, 'description': {'$ref': '#/definitions/file-directive'}, 'entry-points': {'$ref': '#/definitions/file-directive'}, 'readme': {'anyOf': [{'$ref': '#/definitions/file-directive'}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}}}}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, rule='type')
+        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-build-dependencies/', 'title': 'Data structure for ``pyproject.toml`` files', '$$description': ['File format containing build-time configurations for the Python ecosystem. ', ':pep:`517` initially defined a build-system independent format for source trees', 'which was complemented by :pep:`518` to provide a way of specifying dependencies ', 'for building Python projects.', 'Please notice the ``project`` table (as initially defined in  :pep:`621`) is not included', 'in this schema and should be considered separately.'], 'type': 'object', 'additionalProperties': False, 'properties': {'build-system': {'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['dynamic'], 'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}}}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'required': ['version'], '$$description': ['version should be statically defined in the ``version`` field']}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, 'tool': {'type': 'object', 'properties': {'distutils': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://docs.python.org/3/install/', 'title': '``tool.distutils`` table', '$$description': ['Originally, ``distutils`` allowed developers to configure arguments for', '``setup.py`` scripts via `distutils configuration files', '`_.', '``tool.distutils`` subtables could be used with the same purpose', '(NOT CURRENTLY IMPLEMENTED).'], 'type': 'object', 'properties': {'global': {'type': 'object', 'description': 'Global options applied to all ``distutils`` commands'}}, 'patternProperties': {'.+': {'type': 'object'}}, '$comment': 'TODO: Is there a practical way of making this schema more specific?'}, 'setuptools': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$ref': '#/definitions/find-directive'}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might become standard with PEP 639).'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Probably ``project.license-files``?'}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'additionalProperties': False, 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'$ref': '#/definitions/attr-directive'}, {'$ref': '#/definitions/file-directive'}]}, 'classifiers': {'$ref': '#/definitions/file-directive'}, 'description': {'$ref': '#/definitions/file-directive'}, 'entry-points': {'$ref': '#/definitions/file-directive'}, 'readme': {'anyOf': [{'$ref': '#/definitions/file-directive'}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}}}}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['dynamic'], 'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}}}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'required': ['version'], '$$description': ['version should be statically defined in the ``version`` field']}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, rule='type')
     data_is_dict = isinstance(data, dict)
     if data_is_dict:
         data_keys = set(data.keys())
@@ -98,7 +98,7 @@ def validate_https___packaging_python_org_en_latest_specifications_declaring_bui
                     data__tool__setuptools = data__tool["setuptools"]
                     validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data__tool__setuptools, custom_formats, (name_prefix or "data") + ".tool.setuptools")
         if data_keys:
-            raise JsonSchemaValueException("" + (name_prefix or "data") + " must not contain "+str(data_keys)+" properties", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-build-dependencies/', 'title': 'Data structure for ``pyproject.toml`` files', '$$description': ['File format containing build-time configurations for the Python ecosystem. ', ':pep:`517` initially defined a build-system independent format for source trees', 'which was complemented by :pep:`518` to provide a way of specifying dependencies ', 'for building Python projects.', 'Please notice the ``project`` table (as initially defined in  :pep:`621`) is not included', 'in this schema and should be considered separately.'], 'type': 'object', 'additionalProperties': False, 'properties': {'build-system': {'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, 'tool': {'type': 'object', 'properties': {'distutils': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://docs.python.org/3/install/', 'title': '``tool.distutils`` table', '$$description': ['Originally, ``distutils`` allowed developers to configure arguments for', '``setup.py`` scripts via `distutils configuration files', '`_.', '``tool.distutils`` subtables could be used with the same purpose', '(NOT CURRENTLY IMPLEMENTED).'], 'type': 'object', 'properties': {'global': {'type': 'object', 'description': 'Global options applied to all ``distutils`` commands'}}, 'patternProperties': {'.+': {'type': 'object'}}, '$comment': 'TODO: Is there a practical way of making this schema more specific?'}, 'setuptools': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$ref': '#/definitions/find-directive'}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might become standard with PEP 639).'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Probably ``project.license-files``?'}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'additionalProperties': False, 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'$ref': '#/definitions/attr-directive'}, {'$ref': '#/definitions/file-directive'}]}, 'classifiers': {'$ref': '#/definitions/file-directive'}, 'description': {'$ref': '#/definitions/file-directive'}, 'entry-points': {'$ref': '#/definitions/file-directive'}, 'readme': {'anyOf': [{'$ref': '#/definitions/file-directive'}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}}}}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, rule='additionalProperties')
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must not contain "+str(data_keys)+" properties", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-build-dependencies/', 'title': 'Data structure for ``pyproject.toml`` files', '$$description': ['File format containing build-time configurations for the Python ecosystem. ', ':pep:`517` initially defined a build-system independent format for source trees', 'which was complemented by :pep:`518` to provide a way of specifying dependencies ', 'for building Python projects.', 'Please notice the ``project`` table (as initially defined in  :pep:`621`) is not included', 'in this schema and should be considered separately.'], 'type': 'object', 'additionalProperties': False, 'properties': {'build-system': {'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['dynamic'], 'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}}}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'required': ['version'], '$$description': ['version should be statically defined in the ``version`` field']}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, 'tool': {'type': 'object', 'properties': {'distutils': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://docs.python.org/3/install/', 'title': '``tool.distutils`` table', '$$description': ['Originally, ``distutils`` allowed developers to configure arguments for', '``setup.py`` scripts via `distutils configuration files', '`_.', '``tool.distutils`` subtables could be used with the same purpose', '(NOT CURRENTLY IMPLEMENTED).'], 'type': 'object', 'properties': {'global': {'type': 'object', 'description': 'Global options applied to all ``distutils`` commands'}}, 'patternProperties': {'.+': {'type': 'object'}}, '$comment': 'TODO: Is there a practical way of making this schema more specific?'}, 'setuptools': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$ref': '#/definitions/find-directive'}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might become standard with PEP 639).'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Probably ``project.license-files``?'}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'additionalProperties': False, 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'$ref': '#/definitions/attr-directive'}, {'$ref': '#/definitions/file-directive'}]}, 'classifiers': {'$ref': '#/definitions/file-directive'}, 'description': {'$ref': '#/definitions/file-directive'}, 'entry-points': {'$ref': '#/definitions/file-directive'}, 'readme': {'anyOf': [{'$ref': '#/definitions/file-directive'}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}}}}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['dynamic'], 'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}}}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'required': ['version'], '$$description': ['version should be statically defined in the ``version`` field']}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, rule='additionalProperties')
     return data
 
 def validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data, custom_formats={}, name_prefix=None):
@@ -620,12 +620,12 @@ def validate_https___docs_python_org_3_install(data, custom_formats={}, name_pre
 
 def validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata(data, custom_formats={}, name_prefix=None):
     if not isinstance(data, (dict)):
-        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'gui-scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, rule='type')
+        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'gui-scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['dynamic'], 'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}}}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'required': ['version'], '$$description': ['version should be statically defined in the ``version`` field']}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, rule='type')
     data_is_dict = isinstance(data, dict)
     if data_is_dict:
         data_len = len(data)
         if not all(prop in data for prop in ['name']):
-            raise JsonSchemaValueException("" + (name_prefix or "data") + " must contain ['name'] properties", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'gui-scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, rule='required')
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must contain ['name'] properties", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'gui-scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['dynamic'], 'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}}}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'required': ['version'], '$$description': ['version should be statically defined in the ``version`` field']}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, rule='required')
         data_keys = set(data.keys())
         if "name" in data_keys:
             data_keys.remove("name")
@@ -906,38 +906,41 @@ def validate_https___packaging_python_org_en_latest_specifications_declaring_pro
                     if data__dynamic_item not in ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']:
                         raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic[{data__dynamic_x}]".format(**locals()) + " must be one of ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']", value=data__dynamic_item, name="" + (name_prefix or "data") + ".dynamic[{data__dynamic_x}]".format(**locals()) + "", definition={'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}, rule='enum')
         if data_keys:
-            raise JsonSchemaValueException("" + (name_prefix or "data") + " must not contain "+str(data_keys)+" properties", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'gui-scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}}}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, rule='additionalProperties')
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must not contain "+str(data_keys)+" properties", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'gui-scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['dynamic'], 'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}}}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'required': ['version'], '$$description': ['version should be statically defined in the ``version`` field']}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, rule='additionalProperties')
     try:
         try:
             data_is_dict = isinstance(data, dict)
             if data_is_dict:
                 data_len = len(data)
-                if not all(prop in data for prop in ['version']):
-                    raise JsonSchemaValueException("" + (name_prefix or "data") + " must contain ['version'] properties", value=data, name="" + (name_prefix or "data") + "", definition={'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, rule='required')
+                if not all(prop in data for prop in ['dynamic']):
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + " must contain ['dynamic'] properties", value=data, name="" + (name_prefix or "data") + "", definition={'required': ['dynamic'], 'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}}}, rule='required')
+                data_keys = set(data.keys())
+                if "dynamic" in data_keys:
+                    data_keys.remove("dynamic")
+                    data__dynamic = data["dynamic"]
+                    data__dynamic_is_list = isinstance(data__dynamic, (list, tuple))
+                    if data__dynamic_is_list:
+                        data__dynamic_contains = False
+                        for data__dynamic_key in data__dynamic:
+                            try:
+                                if data__dynamic_key != "version":
+                                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic must be same as const definition: version", value=data__dynamic_key, name="" + (name_prefix or "data") + ".dynamic", definition={'const': 'version'}, rule='const')
+                                data__dynamic_contains = True
+                                break
+                            except JsonSchemaValueException: pass
+                        if not data__dynamic_contains:
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic must contain one of contains definition", value=data__dynamic, name="" + (name_prefix or "data") + ".dynamic", definition={'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}, rule='contains')
         except JsonSchemaValueException: pass
         else:
-            raise JsonSchemaValueException("" + (name_prefix or "data") + " must NOT match a disallowed definition", value=data, name="" + (name_prefix or "data") + "", definition={'not': {'required': ['version'], '$$description': ['version is statically defined in the ``version`` field']}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, rule='not')
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must NOT match a disallowed definition", value=data, name="" + (name_prefix or "data") + "", definition={'not': {'required': ['dynamic'], 'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}}}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, rule='not')
     except JsonSchemaValueException:
         pass
     else:
         data_is_dict = isinstance(data, dict)
         if data_is_dict:
-            data_keys = set(data.keys())
-            if "dynamic" in data_keys:
-                data_keys.remove("dynamic")
-                data__dynamic = data["dynamic"]
-                data__dynamic_is_list = isinstance(data__dynamic, (list, tuple))
-                if data__dynamic_is_list:
-                    data__dynamic_contains = False
-                    for data__dynamic_key in data__dynamic:
-                        try:
-                            if data__dynamic_key != "version":
-                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic must be same as const definition: version", value=data__dynamic_key, name="" + (name_prefix or "data") + ".dynamic", definition={'const': 'version'}, rule='const')
-                            data__dynamic_contains = True
-                            break
-                        except JsonSchemaValueException: pass
-                    if not data__dynamic_contains:
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic must contain one of contains definition", value=data__dynamic, name="" + (name_prefix or "data") + ".dynamic", definition={'contains': {'const': 'version'}, '$$description': ['version should be listed in ``dynamic``']}, rule='contains')
+            data_len = len(data)
+            if not all(prop in data for prop in ['version']):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + " must contain ['version'] properties", value=data, name="" + (name_prefix or "data") + "", definition={'required': ['version'], '$$description': ['version should be statically defined in the ``version`` field']}, rule='required')
     return data
 
 def validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_dependency(data, custom_formats={}, name_prefix=None):
diff --git a/setuptools/_vendor/vendored.txt b/setuptools/_vendor/vendored.txt
index cf0e531d..798e2bab 100644
--- a/setuptools/_vendor/vendored.txt
+++ b/setuptools/_vendor/vendored.txt
@@ -11,4 +11,4 @@ typing_extensions==4.0.1
 # required for importlib_resources and _metadata on older Pythons
 zipp==3.7.0
 tomli==2.0.1
-# validate-pyproject[all]==0.6  # Special handling in tools/vendored, don't uncomment or remove
+# validate-pyproject[all]==0.6.1  # Special handling in tools/vendored, don't uncomment or remove
-- 
cgit v1.2.1


From ef424dd6cb9da8bd1daed32590d415f4af2cfc9b Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 26 Mar 2022 16:09:28 +0000
Subject: Remove no longer necessary xfail mark

---
 setuptools/tests/config/test_pyprojecttoml.py | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/setuptools/tests/config/test_pyprojecttoml.py b/setuptools/tests/config/test_pyprojecttoml.py
index c0ee2378..8cf006a6 100644
--- a/setuptools/tests/config/test_pyprojecttoml.py
+++ b/setuptools/tests/config/test_pyprojecttoml.py
@@ -366,10 +366,7 @@ class TestSkipBadConfig:
         [
             "[project]\nrequires-python = '>=3.7'\n",
             "[project]\nversion = '42'\nrequires-python = '>=3.7'\n",
-            pytest.param(
-                "[project]\nname='othername'\nrequires-python = '>=3.7'\n",
-                marks=pytest.mark.xfail(reason="abravalheri/validate-pyproject#28")
-            ),
+            "[project]\nname='othername'\nrequires-python = '>=3.7'\n",
         ],
     )
     def test_popular_config(self, tmp_path, pyproject_content, setup_attrs):
-- 
cgit v1.2.1


From 041090a9260adc4aa385ba5d34fccc6483d32d29 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 26 Mar 2022 16:10:24 +0000
Subject: Remove left-over debug statement

---
 setuptools/tests/config/test_pyprojecttoml.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/setuptools/tests/config/test_pyprojecttoml.py b/setuptools/tests/config/test_pyprojecttoml.py
index 8cf006a6..421445da 100644
--- a/setuptools/tests/config/test_pyprojecttoml.py
+++ b/setuptools/tests/config/test_pyprojecttoml.py
@@ -377,7 +377,6 @@ class TestSkipBadConfig:
 
         prev_name = dist.get_name()
         prev_deps = dist.install_requires
-        print(f"{dist=}, {prev_name=}, {prev_deps=}")
 
         with pytest.warns(_InvalidFile, match=r"DO NOT include.*\[project\].* table"):
             dist = apply_configuration(dist, pyproject)
-- 
cgit v1.2.1


From 07108bb1793b4049d32c28b381d520f28976bb20 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pablo=20C=C3=A1rdenas?= 
Date: Sat, 26 Mar 2022 14:33:27 -0500
Subject: Fix typo in quickstart section

The function should be between quotes like a string.

cli-name = mypkg.mymodule:some_func    =>    cli-name = "mypkg.mymodule:some_func"
---
 docs/userguide/quickstart.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst
index 5be1078a..c72db26b 100644
--- a/docs/userguide/quickstart.rst
+++ b/docs/userguide/quickstart.rst
@@ -222,7 +222,7 @@ The following configuration examples show how to accomplish this:
     .. code-block:: toml
 
        [project.scripts]
-       cli-name = mypkg.mymodule:some_func
+       cli-name = "mypkg.mymodule:some_func"
 
 When this project is installed, a ``cli-name`` executable will be created.
 ``cli-name`` will invoke the function ``some_func`` in the
-- 
cgit v1.2.1


From 61ff33e6a8b2c30c175b8444d788ba92e099f1c3 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 26 Mar 2022 19:49:15 +0000
Subject: Add news fragment

---
 changelog.d/3217.doc.rst | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/3217.doc.rst

diff --git a/changelog.d/3217.doc.rst b/changelog.d/3217.doc.rst
new file mode 100644
index 00000000..f044d1f0
--- /dev/null
+++ b/changelog.d/3217.doc.rst
@@ -0,0 +1 @@
+Fixed typo in pyproject.toml -- by :user:`pablo-cardenas`.
\ No newline at end of file
-- 
cgit v1.2.1


From d72b78c835d981317ca42ca3cfbcc4a2a0a86287 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sat, 26 Mar 2022 19:52:25 +0000
Subject: Improve news fragment

---
 changelog.d/3217.doc.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/changelog.d/3217.doc.rst b/changelog.d/3217.doc.rst
index f044d1f0..6cc3c969 100644
--- a/changelog.d/3217.doc.rst
+++ b/changelog.d/3217.doc.rst
@@ -1 +1 @@
-Fixed typo in pyproject.toml -- by :user:`pablo-cardenas`.
\ No newline at end of file
+Fixed typo in ``pyproject.toml`` example in Quickstart -- by :user:`pablo-cardenas`.
\ No newline at end of file
-- 
cgit v1.2.1


From 4e29d013f13dda7d9db7daaab011ab037af21f66 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 27 Mar 2022 00:21:30 +0000
Subject: Tests mixed pyproject metadata + config from setup.py

With emphasis on the ``dynamic`` behaviour
---
 setuptools/config/_apply_pyprojecttoml.py          | 31 ++++++++++++++
 .../tests/config/test_apply_pyprojecttoml.py       | 50 +++++++++++++++++++++-
 2 files changed, 79 insertions(+), 2 deletions(-)

diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py
index 5496502a..55eab26b 100644
--- a/setuptools/config/_apply_pyprojecttoml.py
+++ b/setuptools/config/_apply_pyprojecttoml.py
@@ -250,3 +250,34 @@ TOOL_TABLE_RENAMES = {"script_files": "scripts"}
 
 SETUPTOOLS_PATCHES = {"long_description_content_type", "project_urls",
                       "provides_extras", "license_file", "license_files"}
+
+
+class _WouldIgnoreField(UserWarning):
+    """Inform users that ``pyproject.toml`` would overwrite previously defined metadata.
+    !!\n\n
+    ##############################################
+    # field would be ignored by `pyproject.toml` #
+    ##############################################
+
+    `{field} = {value!r}` seems to be defined outside of `pyproject.toml`.
+    According to the spec (see the link bellow), however, setuptools CANNOT
+    consider this value unless {field!r} is listed as `dynamic`.
+
+    https://packaging.python.org/en/latest/specifications/declaring-project-metadata/
+
+    For the time being, `setuptools` will still consider the given value (as a
+    **transitional** measure), but please note that future releases of setuptools will
+    follow strictly the standard.
+
+    To prevent this warning, you can list {field!r} under `dynamic` or alternatively
+    remove the `[project]` table from your file and rely entirely on other means of
+    configuration.
+
+    \n\n!!
+    """
+
+    @classmethod
+    def message(cls, field, value):
+        from inspect import cleandoc
+        msg = "\n".join(cls.__doc__.splitlines()[1:])
+        return cleandoc(msg.format(field=field, value=value))
diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py
index 044f801c..42ec0f71 100644
--- a/setuptools/tests/config/test_apply_pyprojecttoml.py
+++ b/setuptools/tests/config/test_apply_pyprojecttoml.py
@@ -14,6 +14,7 @@ import setuptools  # noqa ensure monkey patch to metadata
 from setuptools.dist import Distribution
 from setuptools.config import setupcfg, pyprojecttoml
 from setuptools.config import expand
+from setuptools.config._apply_pyprojecttoml import _WouldIgnoreField
 
 
 EXAMPLES = (Path(__file__).parent / "setupcfg_examples.txt").read_text()
@@ -21,8 +22,8 @@ EXAMPLE_URLS = [x for x in EXAMPLES.splitlines() if not x.startswith("#")]
 DOWNLOAD_DIR = Path(__file__).parent / "downloads"
 
 
-def makedist(path):
-    return Distribution({"src_root": path})
+def makedist(path, **attrs):
+    return Distribution({"src_root": path, **attrs})
 
 
 @pytest.mark.parametrize("url", EXAMPLE_URLS)
@@ -205,6 +206,51 @@ def test_license_and_license_files(tmp_path):
     assert dist.metadata.license == "LicenseRef-Proprietary\n"
 
 
+class TestPresetField:
+    def pyproject(self, tmp_path, dynamic):
+        content = f"[project]\nname = 'proj'\ndynamic = {dynamic!r}\n"
+        if "version" not in dynamic:
+            content += "version = '42'\n"
+        file = tmp_path / "pyproject.toml"
+        file.write_text(content, encoding="utf-8")
+        return file
+
+    @pytest.mark.parametrize(
+        "attr, field, value",
+        [
+            ("install_requires", "dependencies", ["six"]),
+            ("classifiers", "classifiers", ["Private :: Classifier"]),
+        ]
+    )
+    def test_not_listed_in_dynamic(self, tmp_path, attr, field, value):
+        """For the time being we just warn if the user pre-set values (e.g. via
+        ``setup.py``) but do not include them in ``dynamic``.
+        """
+        pyproject = self.pyproject(tmp_path, [])
+        dist = makedist(tmp_path, **{attr: value})
+        msg = f"{field}.*seems to be defined outside of .pyproject.toml."
+        with pytest.warns(_WouldIgnoreField, match=msg):
+            dist = pyprojecttoml.apply_configuration(dist, pyproject)
+
+        # TODO: Once support for pyproject.toml config stabilizes attr should be None
+        dist_value = getattr(dist, attr, None) or getattr(dist.metadata, attr, object())
+        assert dist_value == value
+
+    @pytest.mark.parametrize(
+        "attr, field, value",
+        [
+            ("install_requires", "dependencies", ["six"]),
+            ("classifiers", "classifiers", ["Private :: Classifier"]),
+        ]
+    )
+    def test_listed_in_dynamic(self, tmp_path, attr, field, value):
+        pyproject = self.pyproject(tmp_path, [field])
+        dist = makedist(tmp_path, **{attr: value})
+        dist = pyprojecttoml.apply_configuration(dist, pyproject)
+        dist_value = getattr(dist, attr, None) or getattr(dist.metadata, attr, object())
+        assert dist_value == value
+
+
 # --- Auxiliary Functions ---
 
 
-- 
cgit v1.2.1


From d968977b4eac4064ae500d9c3e89cea1e3f769a3 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 27 Mar 2022 00:25:16 +0000
Subject: Warn if a project metadata is set outside of pyproject without
 dynamic

- PEP 621 requires the build backend to not backfill values without
  dynamic.

- Some users seem to been writing ``pyproject.toml`` with a "partial"
  ``[project]`` table even before setuptools added support for pyproject
  metadata. In several cases this table is incomplete and the real
  metadata lives either in ``setup.py`` or ``setup.cfg``.

To avoid ignoring metadata in these scenarios and resulting in failing
builds, the change implemented here adopts a more "forgiving" posture
and warns an informative message during the transition period.
---
 setuptools/config/_apply_pyprojecttoml.py | 99 +++++++++++++++++++++++++++----
 1 file changed, 89 insertions(+), 10 deletions(-)

diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py
index 55eab26b..203a5770 100644
--- a/setuptools/config/_apply_pyprojecttoml.py
+++ b/setuptools/config/_apply_pyprojecttoml.py
@@ -7,9 +7,10 @@ need to be processed before being applied.
 """
 import logging
 import os
+import warnings
 from collections.abc import Mapping
 from email.headerregistry import Address
-from functools import partial
+from functools import partial, reduce
 from itertools import chain
 from types import MappingProxyType
 from typing import (TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Tuple,
@@ -35,9 +36,29 @@ def apply(dist: "Distribution", config: dict, filename: _Path) -> "Distribution"
         return dist  # short-circuit unrelated pyproject.toml file
 
     root_dir = os.path.dirname(filename) or "."
-    tool_table = config.get("tool", {}).get("setuptools", {})
+
+    _apply_project_table(dist, config, root_dir)
+    _apply_tool_table(dist, config, filename)
+
+    current_directory = os.getcwd()
+    os.chdir(root_dir)
+    try:
+        dist._finalize_requires()
+        dist._finalize_license_files()
+    finally:
+        os.chdir(current_directory)
+
+    return dist
+
+
+def _apply_project_table(dist: "Distribution", config: dict, root_dir: _Path):
     project_table = config.get("project", {}).copy()
+    if not project_table:
+        return  # short-circuit
+
+    _handle_missing_dynamic(dist, project_table)
     _unify_entry_points(project_table)
+
     for field, value in project_table.items():
         norm_key = json_compatible_key(field)
         corresp = PYPROJECT_CORRESPONDENCE.get(norm_key, norm_key)
@@ -46,6 +67,12 @@ def apply(dist: "Distribution", config: dict, filename: _Path) -> "Distribution"
         else:
             _set_config(dist, corresp, value)
 
+
+def _apply_tool_table(dist: "Distribution", config: dict, filename: _Path):
+    tool_table = config.get("tool", {}).get("setuptools", {})
+    if not tool_table:
+        return  # short-circuit
+
     for field, value in tool_table.items():
         norm_key = json_compatible_key(field)
         norm_key = TOOL_TABLE_RENAMES.get(norm_key, norm_key)
@@ -53,15 +80,17 @@ def apply(dist: "Distribution", config: dict, filename: _Path) -> "Distribution"
 
     _copy_command_options(config, dist, filename)
 
-    current_directory = os.getcwd()
-    os.chdir(root_dir)
-    try:
-        dist._finalize_requires()
-        dist._finalize_license_files()
-    finally:
-        os.chdir(current_directory)
 
-    return dist
+def _handle_missing_dynamic(dist: "Distribution", project_table: dict):
+    """Be temporarily forgiving with ``dynamic`` fields not listed in ``dynamic``"""
+    # TODO: Set fields back to `None` once the feature stabilizes
+    dynamic = set(project_table.get("dynamic", []))
+    for field, getter in _PREVIOUSLY_DEFINED.items():
+        if not (field in project_table or field in dynamic):
+            value = getter(dist)
+            if value:
+                msg = _WouldIgnoreField.message(field, value)
+                warnings.warn(msg, _WouldIgnoreField)
 
 
 def json_compatible_key(key: str) -> str:
@@ -235,6 +264,39 @@ def _normalise_cmd_options(desc: List[Tuple[str, Optional[str], str]]) -> Set[st
     return {_normalise_cmd_option_key(fancy_option[0]) for fancy_option in desc}
 
 
+def _attrgetter(attr):
+    """
+    Similar to ``operator.attrgetter`` but returns None if ``attr`` is not found
+    >>> from types import SimpleNamespace
+    >>> obj = SimpleNamespace(a=42, b=SimpleNamespace(c=13))
+    >>> _attrgetter("a")(obj)
+    42
+    >>> _attrgetter("b.c")(obj)
+    13
+    >>> _attrgetter("d")(obj) is None
+    True
+    """
+    return partial(reduce, lambda acc, x: getattr(acc, x, None), attr.split("."))
+
+
+def _some_attrgetter(*items):
+    """
+    Return the first "truth-y" attribute or None
+    >>> from types import SimpleNamespace
+    >>> obj = SimpleNamespace(a=42, b=SimpleNamespace(c=13))
+    >>> _some_attrgetter("d", "a", "b.c")(obj)
+    42
+    >>> _some_attrgetter("d", "e", "b.c", "a")(obj)
+    13
+    >>> _some_attrgetter("d", "e", "f")(obj) is None
+    True
+    """
+    def _acessor(obj):
+        values = (_attrgetter(i)(obj) for i in items)
+        return next((i for i in values if i), None)
+    return _acessor
+
+
 PYPROJECT_CORRESPONDENCE: Dict[str, _Correspondence] = {
     "readme": _long_description,
     "license": _license,
@@ -251,6 +313,23 @@ TOOL_TABLE_RENAMES = {"script_files": "scripts"}
 SETUPTOOLS_PATCHES = {"long_description_content_type", "project_urls",
                       "provides_extras", "license_file", "license_files"}
 
+_PREVIOUSLY_DEFINED = {
+    "name": _attrgetter("metadata.name"),
+    "version": _attrgetter("metadata.version"),
+    "description": _attrgetter("metadata.description"),
+    "readme": _attrgetter("metadata.long_description"),
+    "requires-python": _some_attrgetter("python_requires", "metadata.python_requires"),
+    "license": _attrgetter("metadata.license"),
+    "authors": _some_attrgetter("metadata.author", "metadata.author_email"),
+    "maintainers": _some_attrgetter("metadata.maintainer", "metadata.maintainer_email"),
+    "keywords": _attrgetter("metadata.keywords"),
+    "classifiers": _attrgetter("metadata.classifiers"),
+    "urls": _attrgetter("metadata.project_urls"),
+    "entry-points": _attrgetter("entry_points"),
+    "dependencies": _some_attrgetter("_orig_install_requires", "install_requires"),
+    "optional-dependencies": _some_attrgetter("_orig_extras_require", "extras_require"),
+}
+
 
 class _WouldIgnoreField(UserWarning):
     """Inform users that ``pyproject.toml`` would overwrite previously defined metadata.
-- 
cgit v1.2.1


From c8ba27c4afb185ffddff6c754e3f091068e1b27a Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 27 Mar 2022 00:36:55 +0000
Subject: Restructure config.pyproject to consider "pre-set" dynamic values

Issues 3195 and 3204 surface the fact that setuptools may need to allow
dynamic values to be computed by the users in the ``setup.py`` file
(e.g. if they need to dynamically decide dependencies based on the host
machine in a way that is not supported by environment markers, such as
GPU presence).

The current implementation somehow already allows that by layering the
configs `setup.py` > `setup.cfg` > `pyproject.toml`. However this is
done without having in mind the limitations about `dynamic` imposed by
PEP 621.

The change implemented here tries to fix this problem.
---
 setuptools/config/pyprojecttoml.py | 315 +++++++++++++++++++------------------
 1 file changed, 165 insertions(+), 150 deletions(-)

diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index b8cd0c51..da1578d8 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Callable, Dict, Optional, Mapping, Union
 from setuptools.errors import FileError, OptionError
 
 from . import expand as _expand
-from ._apply_pyprojecttoml import apply
+from ._apply_pyprojecttoml import apply, _PREVIOUSLY_DEFINED
 
 if TYPE_CHECKING:
     from setuptools.dist import Distribution  # noqa
@@ -167,7 +167,7 @@ def _skip_bad_config(
 def expand_configuration(
     config: dict,
     root_dir: Optional[_Path] = None,
-    ignore_option_errors=False,
+    ignore_option_errors: bool = False,
     dist: Optional["Distribution"] = None,
 ) -> dict:
     """Given a configuration with unresolved fields (e.g. dynamic, cmdclass, ...)
@@ -184,38 +184,175 @@ def expand_configuration(
 
     :rtype: dict
     """
-    root_dir = root_dir or os.getcwd()
-    project_cfg = config.get("project", {})
-    setuptools_cfg = config.get("tool", {}).get("setuptools", {})
-    ignore = ignore_option_errors
-
-    _expand_packages(setuptools_cfg, root_dir, ignore)
-    _canonic_package_data(setuptools_cfg)
-    _canonic_package_data(setuptools_cfg, "exclude-package-data")
+    return _ConfigExpander(config, root_dir, ignore_option_errors, dist).expand()
 
-    # A distribution object is required for discovering the correct package_dir
-    dist = _ensure_dist(dist, project_cfg, root_dir)
 
-    with _EnsurePackagesDiscovered(dist, setuptools_cfg) as ensure_discovered:
-        package_dir = ensure_discovered.package_dir
-        process = partial(_process_field, ignore_option_errors=ignore)
+class _ConfigExpander:
+    def __init__(
+        self,
+        config: dict,
+        root_dir: Optional[_Path] = None,
+        ignore_option_errors: bool = False,
+        dist: Optional["Distribution"] = None,
+    ):
+        self.config = config
+        self.root_dir = root_dir or os.getcwd()
+        self.project_cfg = config.get("project", {})
+        self.dynamic = self.project_cfg.get("dynamic", [])
+        self.setuptools_cfg = config.get("tool", {}).get("setuptools", {})
+        self.dynamic_cfg = self.setuptools_cfg.get("dynamic", {})
+        self.ignore_option_errors = ignore_option_errors
+        self._dist = dist
+
+    def _ensure_dist(self) -> "Distribution":
+        from setuptools.dist import Distribution
+
+        attrs = {"src_root": self.root_dir, "name": self.project_cfg.get("name", None)}
+        return self._dist or Distribution(attrs)
+
+    def _process_field(self, container: dict, field: str, fn: Callable):
+        if field in container:
+            with _ignore_errors(self.ignore_option_errors):
+                container[field] = fn(container[field])
+
+    def _canonic_package_data(self, field="package-data"):
+        package_data = self.setuptools_cfg.get(field, {})
+        return _expand.canonic_package_data(package_data)
+
+    def expand(self):
+        self._expand_packages()
+        self._canonic_package_data()
+        self._canonic_package_data("exclude-package-data")
+
+        # A distribution object is required for discovering the correct package_dir
+        dist = self._ensure_dist()
+
+        with _EnsurePackagesDiscovered(dist, self.setuptools_cfg) as ensure_discovered:
+            package_dir = ensure_discovered.package_dir
+            self._expand_data_files()
+            self._expand_cmdclass(package_dir)
+            self._expand_all_dynamic(dist, package_dir)
+
+        return self.config
+
+    def _expand_packages(self):
+        packages = self.setuptools_cfg.get("packages")
+        if packages is None or isinstance(packages, (list, tuple)):
+            return
+
+        find = packages.get("find")
+        if isinstance(find, dict):
+            find["root_dir"] = self.root_dir
+            find["fill_package_dir"] = self.setuptools_cfg.setdefault("package-dir", {})
+            with _ignore_errors(self.ignore_option_errors):
+                self.setuptools_cfg["packages"] = _expand.find_packages(**find)
+
+    def _expand_data_files(self):
+        data_files = partial(_expand.canonic_data_files, root_dir=self.root_dir)
+        self._process_field(self.setuptools_cfg, "data-files", data_files)
+
+    def _expand_cmdclass(self, package_dir: Mapping[str, str]):
+        root_dir = self.root_dir
         cmdclass = partial(_expand.cmdclass, package_dir=package_dir, root_dir=root_dir)
-        data_files = partial(_expand.canonic_data_files, root_dir=root_dir)
+        self._process_field(self.setuptools_cfg, "cmdclass", cmdclass)
+
+    def _expand_all_dynamic(self, dist: "Distribution", package_dir: Mapping[str, str]):
+        special = (  # need special handling
+            "version",
+            "readme",
+            "entry-points",
+            "scripts",
+            "gui-scripts",
+            "classifiers",
+        )
+        obtained_dynamic = {
+            field: self._obtain(dist, field, package_dir)
+            for field in self.dynamic
+            if field not in special
+        }
+        obtained_dynamic.update(
+            self._obtain_entry_points(dist, package_dir) or {},
+            version=self._obtain_version(dist, package_dir),
+            readme=self._obtain_readme(dist),
+            classifiers=self._obtain_classifiers(dist),
+        )
+        # Preserve previous value if obtained value is None
+        self.project_cfg.update({k: v for k, v in obtained_dynamic.items() if v})
+
+    def _ensure_previously_set(self, dist: "Distribution", field: str):
+        previous = _PREVIOUSLY_DEFINED[field](dist)
+        if not previous and not self.ignore_option_errors:
+            msg = (
+                f"No configuration found for dynamic {field!r}. "
+                "Some fields need to be specified via `tool.setuptools.dynamic` "
+                "others must be specified via the equivalent attribute in `setup.py`."
+            )
+            raise OptionError(msg)
+
+    def _obtain(self, dist: "Distribution", field: str, package_dir: Mapping[str, str]):
+        if field in self.dynamic_cfg:
+            directive = self.dynamic_cfg[field]
+            with _ignore_errors(self.ignore_option_errors):
+                root_dir = self.root_dir
+                if "file" in directive:
+                    return _expand.read_files(directive["file"], root_dir)
+                if "attr" in directive:
+                    return _expand.read_attr(directive["attr"], package_dir, root_dir)
+        self._ensure_previously_set(dist, field)
+        return None
+
+    def _obtain_version(self, dist: "Distribution", package_dir: Mapping[str, str]):
+        # Since plugins can set version, let's silently skip if it cannot be obtained
+        if "version" in self.dynamic and "version" in self.dynamic_cfg:
+            return _expand.version(self._obtain(dist, "version", package_dir))
+        return None
+
+    def _obtain_readme(self, dist: "Distribution") -> Optional[Dict[str, str]]:
+        if "readme" in self.dynamic:
+            dynamic_cfg = self.dynamic_cfg
+            return {
+                "text": self._obtain(dist, "readme", {}),
+                "content-type": dynamic_cfg["readme"].get("content-type", "text/x-rst"),
+            }
+        return None
+
+    def _obtain_entry_points(
+        self, dist: "Distribution", package_dir: Mapping[str, str]
+    ) -> Optional[Dict[str, dict]]:
+        fields = ("entry-points", "scripts", "gui-scripts")
+        if not any(field in self.dynamic for field in fields):
+            return None
+
+        text = self._obtain(dist, "entry-points", package_dir)
+        if text is None:
+            return None
+
+        groups = _expand.entry_points(text)
+        expanded = {"entry-points": groups}
+        if "scripts" in self.dynamic and "console_scripts" in groups:
+            expanded["scripts"] = groups.pop("console_scripts")
+        if "gui-scripts" in self.dynamic and "gui_scripts" in groups:
+            expanded["gui-scripts"] = groups.pop("gui_scripts")
+        return expanded
+
+    def _obtain_classifiers(self, dist: "Distribution"):
+        if "classifiers" in self.dynamic:
+            value = self._obtain(dist, "classifiers", {})
+            if value:
+                return value.splitlines()
+        return None
 
-        process(setuptools_cfg, "data-files", data_files)
-        process(setuptools_cfg, "cmdclass", cmdclass)
-        _expand_all_dynamic(project_cfg, setuptools_cfg, package_dir, root_dir, ignore)
 
-    return config
-
-
-def _ensure_dist(
-    dist: Optional["Distribution"], project_cfg: dict, root_dir: _Path
-) -> "Distribution":
-    from setuptools.dist import Distribution
+@contextmanager
+def _ignore_errors(ignore_option_errors: bool):
+    if not ignore_option_errors:
+        yield
+        return
 
-    attrs = {"src_root": root_dir, "name": project_cfg.get("name", None)}
-    return dist or Distribution(attrs)
+    try:
+        yield
+    except Exception as ex:
+        _logger.debug(f"ignored error: {ex.__class__.__name__} - {ex}")
 
 
 class _EnsurePackagesDiscovered(_expand.EnsurePackagesDiscovered):
@@ -253,128 +390,6 @@ class _EnsurePackagesDiscovered(_expand.EnsurePackagesDiscovered):
         return super().__exit__(exc_type, exc_value, traceback)
 
 
-def _expand_all_dynamic(
-    project_cfg: dict,
-    setuptools_cfg: dict,
-    package_dir: Mapping[str, str],
-    root_dir: _Path,
-    ignore_option_errors: bool,
-):
-    ignore = ignore_option_errors
-    dynamic_cfg = setuptools_cfg.get("dynamic", {})
-    pkg_dir = package_dir
-    special = (
-        "readme",
-        "version",
-        "entry-points",
-        "scripts",
-        "gui-scripts",
-        "classifiers",
-    )
-    # readme, version and entry-points need special handling
-    dynamic = project_cfg.get("dynamic", [])
-    regular_dynamic = (x for x in dynamic if x not in special)
-
-    for field in regular_dynamic:
-        value = _expand_dynamic(dynamic_cfg, field, pkg_dir, root_dir, ignore)
-        project_cfg[field] = value
-
-    if "version" in dynamic and "version" in dynamic_cfg:
-        version = _expand_dynamic(dynamic_cfg, "version", pkg_dir, root_dir, ignore)
-        project_cfg["version"] = _expand.version(version)
-
-    if "readme" in dynamic:
-        project_cfg["readme"] = _expand_readme(dynamic_cfg, root_dir, ignore)
-
-    if "entry-points" in dynamic:
-        field = "entry-points"
-        value = _expand_dynamic(dynamic_cfg, field, pkg_dir, root_dir, ignore)
-        project_cfg.update(_expand_entry_points(value, dynamic))
-
-    if "classifiers" in dynamic:
-        value = _expand_dynamic(dynamic_cfg, "classifiers", pkg_dir, root_dir, ignore)
-        project_cfg["classifiers"] = (value or "").splitlines()
-
-
-def _expand_dynamic(
-    dynamic_cfg: dict,
-    field: str,
-    package_dir: Mapping[str, str],
-    root_dir: _Path,
-    ignore_option_errors: bool,
-):
-    if field in dynamic_cfg:
-        directive = dynamic_cfg[field]
-        with _ignore_errors(ignore_option_errors):
-            if "file" in directive:
-                return _expand.read_files(directive["file"], root_dir)
-            if "attr" in directive:
-                return _expand.read_attr(directive["attr"], package_dir, root_dir)
-    elif not ignore_option_errors:
-        msg = f"Impossible to expand dynamic value of {field!r}. "
-        msg += f"No configuration found for `tool.setuptools.dynamic.{field}`"
-        raise OptionError(msg)
-    return None
-
-
-def _expand_readme(
-    dynamic_cfg: dict, root_dir: _Path, ignore_option_errors: bool
-) -> Dict[str, str]:
-    ignore = ignore_option_errors
-    return {
-        "text": _expand_dynamic(dynamic_cfg, "readme", {}, root_dir, ignore),
-        "content-type": dynamic_cfg["readme"].get("content-type", "text/x-rst"),
-    }
-
-
-def _expand_entry_points(text: str, dynamic: set):
-    groups = _expand.entry_points(text)
-    expanded = {"entry-points": groups}
-    if "scripts" in dynamic and "console_scripts" in groups:
-        expanded["scripts"] = groups.pop("console_scripts")
-    if "gui-scripts" in dynamic and "gui_scripts" in groups:
-        expanded["gui-scripts"] = groups.pop("gui_scripts")
-    return expanded
-
-
-def _expand_packages(setuptools_cfg: dict, root_dir: _Path, ignore_option_errors=False):
-    packages = setuptools_cfg.get("packages")
-    if packages is None or isinstance(packages, (list, tuple)):
-        return
-
-    find = packages.get("find")
-    if isinstance(find, dict):
-        find["root_dir"] = root_dir
-        find["fill_package_dir"] = setuptools_cfg.setdefault("package-dir", {})
-        with _ignore_errors(ignore_option_errors):
-            setuptools_cfg["packages"] = _expand.find_packages(**find)
-
-
-def _process_field(
-    container: dict, field: str, fn: Callable, ignore_option_errors=False
-):
-    if field in container:
-        with _ignore_errors(ignore_option_errors):
-            container[field] = fn(container[field])
-
-
-def _canonic_package_data(setuptools_cfg, field="package-data"):
-    package_data = setuptools_cfg.get(field, {})
-    return _expand.canonic_package_data(package_data)
-
-
-@contextmanager
-def _ignore_errors(ignore_option_errors: bool):
-    if not ignore_option_errors:
-        yield
-        return
-
-    try:
-        yield
-    except Exception as ex:
-        _logger.debug(f"ignored error: {ex.__class__.__name__} - {ex}")
-
-
 class _ExperimentalProjectMetadata(UserWarning):
     """Explicitly inform users that `pyproject.toml` configuration is experimental"""
 
-- 
cgit v1.2.1


From dbf59885a09a1b7b23cba78b1c791cf4060b396d Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 27 Mar 2022 00:44:30 +0000
Subject: Adequate existing tests for the latest changes

---
 setuptools/tests/config/test_pyprojecttoml.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/setuptools/tests/config/test_pyprojecttoml.py b/setuptools/tests/config/test_pyprojecttoml.py
index 421445da..63ce7602 100644
--- a/setuptools/tests/config/test_pyprojecttoml.py
+++ b/setuptools/tests/config/test_pyprojecttoml.py
@@ -236,7 +236,7 @@ class TestClassifiers:
 
         pyproject = tmp_path / "pyproject.toml"
         pyproject.write_text(cleandoc(config))
-        with pytest.raises(OptionError, match="No configuration found"):
+        with pytest.raises(OptionError, match="No configuration .* .classifiers."):
             read_configuration(pyproject)
 
     def test_dynamic_without_file(self, tmp_path):
@@ -254,7 +254,7 @@ class TestClassifiers:
         pyproject.write_text(cleandoc(config))
         with pytest.warns(UserWarning, match="File .*classifiers.txt. cannot be found"):
             expanded = read_configuration(pyproject)
-        assert not expanded["project"]["classifiers"]
+        assert "classifiers" not in expanded["project"]
 
 
 @pytest.mark.parametrize(
-- 
cgit v1.2.1


From cd9e7ac6e2e9d62c71f823a1df6de8fb6d734141 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 27 Mar 2022 10:31:57 +0100
Subject: Consider missing edge case for tool.setuptools.dynamic in pyproject

---
 setuptools/config/_apply_pyprojecttoml.py | 1 -
 setuptools/config/pyprojecttoml.py        | 7 ++++++-
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py
index 203a5770..2a046a78 100644
--- a/setuptools/config/_apply_pyprojecttoml.py
+++ b/setuptools/config/_apply_pyprojecttoml.py
@@ -351,7 +351,6 @@ class _WouldIgnoreField(UserWarning):
     To prevent this warning, you can list {field!r} under `dynamic` or alternatively
     remove the `[project]` table from your file and rely entirely on other means of
     configuration.
-
     \n\n!!
     """
 
diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index da1578d8..d4e1460c 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -265,6 +265,7 @@ class _ConfigExpander:
             "gui-scripts",
             "classifiers",
         )
+        # `_obtain` functions are assumed to raise appropriate exceptions/warnings.
         obtained_dynamic = {
             field: self._obtain(dist, field, package_dir)
             for field in self.dynamic
@@ -276,7 +277,8 @@ class _ConfigExpander:
             readme=self._obtain_readme(dist),
             classifiers=self._obtain_classifiers(dist),
         )
-        # Preserve previous value if obtained value is None
+        # `None` indicates there is nothing in `tool.setuptools.dynamic` but the value
+        # might have already been set by setup.py/extensions, so avoid overwriting.
         self.project_cfg.update({k: v for k, v in obtained_dynamic.items() if v})
 
     def _ensure_previously_set(self, dist: "Distribution", field: str):
@@ -298,6 +300,9 @@ class _ConfigExpander:
                     return _expand.read_files(directive["file"], root_dir)
                 if "attr" in directive:
                     return _expand.read_attr(directive["attr"], package_dir, root_dir)
+                msg = f"invalid `tool.setuptools.dynamic.{field}`: {directive!r}"
+                raise ValueError(msg)
+            return None
         self._ensure_previously_set(dist, field)
         return None
 
-- 
cgit v1.2.1


From 93bae8213b21d23a6de2c40e5bb50ad723ad70a8 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 27 Mar 2022 11:12:48 +0100
Subject: Improve error/warning messages

---
 setuptools/config/_apply_pyprojecttoml.py           | 7 +++++--
 setuptools/config/pyprojecttoml.py                  | 8 ++++----
 setuptools/tests/config/test_apply_pyprojecttoml.py | 2 +-
 3 files changed, 10 insertions(+), 7 deletions(-)

diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py
index 2a046a78..421368af 100644
--- a/setuptools/config/_apply_pyprojecttoml.py
+++ b/setuptools/config/_apply_pyprojecttoml.py
@@ -332,13 +332,16 @@ _PREVIOUSLY_DEFINED = {
 
 
 class _WouldIgnoreField(UserWarning):
-    """Inform users that ``pyproject.toml`` would overwrite previously defined metadata.
+    """Inform users that ``pyproject.toml`` would overwrite previously defined metadata:
     !!\n\n
     ##############################################
     # field would be ignored by `pyproject.toml` #
     ##############################################
 
-    `{field} = {value!r}` seems to be defined outside of `pyproject.toml`.
+    The following seems to be defined outside of `pyproject.toml`:
+
+    `{field} = {value!r}`
+
     According to the spec (see the link bellow), however, setuptools CANNOT
     consider this value unless {field!r} is listed as `dynamic`.
 
diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index d4e1460c..a712a258 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -285,9 +285,9 @@ class _ConfigExpander:
         previous = _PREVIOUSLY_DEFINED[field](dist)
         if not previous and not self.ignore_option_errors:
             msg = (
-                f"No configuration found for dynamic {field!r}. "
-                "Some fields need to be specified via `tool.setuptools.dynamic` "
-                "others must be specified via the equivalent attribute in `setup.py`."
+                f"No configuration found for dynamic {field!r}.\n"
+                "Some dynamic fields need to be specified via `tool.setuptools.dynamic`"
+                "\nothers must be specified via the equivalent attribute in `setup.py`."
             )
             raise OptionError(msg)
 
@@ -400,7 +400,7 @@ class _ExperimentalProjectMetadata(UserWarning):
 
 
 class _InvalidFile(UserWarning):
-    """Inform users that the given `pyproject.toml` is experimental.
+    """Inform users that the given `pyproject.toml` is experimental:
     !!\n\n
     ############################
     # Invalid `pyproject.toml` #
diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py
index 42ec0f71..c09ff3e6 100644
--- a/setuptools/tests/config/test_apply_pyprojecttoml.py
+++ b/setuptools/tests/config/test_apply_pyprojecttoml.py
@@ -228,7 +228,7 @@ class TestPresetField:
         """
         pyproject = self.pyproject(tmp_path, [])
         dist = makedist(tmp_path, **{attr: value})
-        msg = f"{field}.*seems to be defined outside of .pyproject.toml."
+        msg = re.compile(f"defined outside of `pyproject.toml`:.*{field}", re.S)
         with pytest.warns(_WouldIgnoreField, match=msg):
             dist = pyprojecttoml.apply_configuration(dist, pyproject)
 
-- 
cgit v1.2.1


From 2538f017487fe6ed8827ac93ffca179b4f90377d Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 27 Mar 2022 12:06:17 +0100
Subject: Prepare to be strict in the future about entry-points in pyproject

---
 setuptools/config/_apply_pyprojecttoml.py     |  6 +--
 setuptools/config/pyprojecttoml.py            | 20 ++++++---
 setuptools/tests/config/test_pyprojecttoml.py | 64 ++++++++++++++++-----------
 3 files changed, 57 insertions(+), 33 deletions(-)

diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py
index 421368af..78a07273 100644
--- a/setuptools/config/_apply_pyprojecttoml.py
+++ b/setuptools/config/_apply_pyprojecttoml.py
@@ -334,9 +334,9 @@ _PREVIOUSLY_DEFINED = {
 class _WouldIgnoreField(UserWarning):
     """Inform users that ``pyproject.toml`` would overwrite previously defined metadata:
     !!\n\n
-    ##############################################
-    # field would be ignored by `pyproject.toml` #
-    ##############################################
+    ##########################################################################
+    # configuration would be ignored/result in error due to `pyproject.toml` #
+    ##########################################################################
 
     The following seems to be defined outside of `pyproject.toml`:
 
diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index a712a258..d2c6c9c5 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Callable, Dict, Optional, Mapping, Union
 from setuptools.errors import FileError, OptionError
 
 from . import expand as _expand
-from ._apply_pyprojecttoml import apply, _PREVIOUSLY_DEFINED
+from ._apply_pyprojecttoml import apply, _PREVIOUSLY_DEFINED, _WouldIgnoreField
 
 if TYPE_CHECKING:
     from setuptools.dist import Distribution  # noqa
@@ -334,10 +334,20 @@ class _ConfigExpander:
 
         groups = _expand.entry_points(text)
         expanded = {"entry-points": groups}
-        if "scripts" in self.dynamic and "console_scripts" in groups:
-            expanded["scripts"] = groups.pop("console_scripts")
-        if "gui-scripts" in self.dynamic and "gui_scripts" in groups:
-            expanded["gui-scripts"] = groups.pop("gui_scripts")
+
+        def _set_scripts(field: str, group: str):
+            if group in groups:
+                value = groups.pop(group)
+                if field not in self.dynamic:
+                    msg = _WouldIgnoreField.message(field, value)
+                    warnings.warn(msg, _WouldIgnoreField)
+                # TODO: Don't set field when support for pyproject.toml stabilizes
+                #       instead raise an error as specified in PEP 621
+                expanded[field] = value
+
+        _set_scripts("scripts", "console_scripts")
+        _set_scripts("gui-scripts", "gui_scripts")
+
         return expanded
 
     def _obtain_classifiers(self, dist: "Distribution"):
diff --git a/setuptools/tests/config/test_pyprojecttoml.py b/setuptools/tests/config/test_pyprojecttoml.py
index 63ce7602..4c237014 100644
--- a/setuptools/tests/config/test_pyprojecttoml.py
+++ b/setuptools/tests/config/test_pyprojecttoml.py
@@ -1,4 +1,5 @@
 import logging
+import re
 from configparser import ConfigParser
 from inspect import cleandoc
 
@@ -6,6 +7,7 @@ import pytest
 import tomli_w
 from path import Path as _Path
 
+from setuptools.config._apply_pyprojecttoml import _WouldIgnoreField
 from setuptools.config.pyprojecttoml import (
     read_configuration,
     expand_configuration,
@@ -171,31 +173,43 @@ ENTRY_POINTS = {
 }
 
 
-def test_expand_entry_point(tmp_path):
-    entry_points = ConfigParser()
-    entry_points.read_dict(ENTRY_POINTS)
-    with open(tmp_path / "entry-points.txt", "w") as f:
-        entry_points.write(f)
-
-    tool = {"setuptools": {"dynamic": {"entry-points": {"file": "entry-points.txt"}}}}
-    project = {"dynamic": ["scripts", "gui-scripts", "entry-points"]}
-    pyproject = {"project": project, "tool": tool}
-    expanded = expand_configuration(pyproject, tmp_path)
-    expanded_project = expanded["project"]
-    assert len(expanded_project["scripts"]) == 1
-    assert expanded_project["scripts"]["a"] == "mod.a:func"
-    assert len(expanded_project["gui-scripts"]) == 1
-    assert expanded_project["gui-scripts"]["b"] == "mod.b:func"
-    assert len(expanded_project["entry-points"]) == 1
-    assert expanded_project["entry-points"]["other"]["c"] == "mod.c:func [extra]"
-
-    project = {"dynamic": ["entry-points"]}
-    pyproject = {"project": project, "tool": tool}
-    expanded = expand_configuration(pyproject, tmp_path)
-    expanded_project = expanded["project"]
-    assert len(expanded_project["entry-points"]) == 3
-    assert "scripts" not in expanded_project
-    assert "gui-scripts" not in expanded_project
+class TestEntryPoints:
+    def write_entry_points(self, tmp_path):
+        entry_points = ConfigParser()
+        entry_points.read_dict(ENTRY_POINTS)
+        with open(tmp_path / "entry-points.txt", "w") as f:
+            entry_points.write(f)
+
+    def pyproject(self, dynamic=None):
+        project = {"dynamic": dynamic or ["scripts", "gui-scripts", "entry-points"]}
+        tool = {"dynamic": {"entry-points": {"file": "entry-points.txt"}}}
+        return {"project": project, "tool": {"setuptools": tool}}
+
+    def test_all_listed_in_dynamic(self, tmp_path):
+        self.write_entry_points(tmp_path)
+        expanded = expand_configuration(self.pyproject(), tmp_path)
+        expanded_project = expanded["project"]
+        assert len(expanded_project["scripts"]) == 1
+        assert expanded_project["scripts"]["a"] == "mod.a:func"
+        assert len(expanded_project["gui-scripts"]) == 1
+        assert expanded_project["gui-scripts"]["b"] == "mod.b:func"
+        assert len(expanded_project["entry-points"]) == 1
+        assert expanded_project["entry-points"]["other"]["c"] == "mod.c:func [extra]"
+
+    @pytest.mark.parametrize("missing_dynamic", ("scripts", "gui-scripts"))
+    def test_scripts_not_listed_in_dynamic(self, tmp_path, missing_dynamic):
+        self.write_entry_points(tmp_path)
+        dynamic = {"scripts", "gui-scripts", "entry-points"} - {missing_dynamic}
+
+        msg = f"defined outside of `pyproject.toml`:.*{missing_dynamic}"
+        with pytest.warns(_WouldIgnoreField, match=re.compile(msg, re.S)):
+            expanded = expand_configuration(self.pyproject(dynamic), tmp_path)
+
+        expanded_project = expanded["project"]
+        assert dynamic < set(expanded_project)
+        assert len(expanded_project["entry-points"]) == 1
+        # TODO: Test the following when pyproject.toml support stabilizes:
+        # >>> assert missing_dynamic not in expanded_project
 
 
 class TestClassifiers:
-- 
cgit v1.2.1


From 9dd078b89bf55a1c759577e5c9789dfe328d2776 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 27 Mar 2022 12:23:23 +0100
Subject: Add news fragment

---
 changelog.d/3218.change.rst | 8 ++++++++
 1 file changed, 8 insertions(+)
 create mode 100644 changelog.d/3218.change.rst

diff --git a/changelog.d/3218.change.rst b/changelog.d/3218.change.rst
new file mode 100644
index 00000000..9757943a
--- /dev/null
+++ b/changelog.d/3218.change.rst
@@ -0,0 +1,8 @@
+Prevented builds from erroring (**temporarily**) if the project specifies
+metadata via ``pyproject.toml``, but uses other files (e.g. ``setup.py``) to
+complement it, without setting ``dynamic`` properly.
+
+.. important::
+   This is a **transitional** behaviour.
+   Future releases of ``setuptools`` may simply ignore externally set metadata
+   not backed by ``dynamic`` or even halt the build with an error.
-- 
cgit v1.2.1


From b16cf407dcbbfab0df079e894819ea5dce166b48 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sun, 27 Mar 2022 10:20:24 -0400
Subject: =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins=20(delint).?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 distutils/command/build_scripts.py | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/distutils/command/build_scripts.py b/distutils/command/build_scripts.py
index e3312cf0..dbeef2dd 100644
--- a/distutils/command/build_scripts.py
+++ b/distutils/command/build_scripts.py
@@ -2,7 +2,8 @@
 
 Implements the Distutils 'build_scripts' command."""
 
-import os, re
+import os
+import re
 from stat import ST_MODE
 from distutils import sysconfig
 from distutils.core import Command
@@ -14,6 +15,7 @@ import tokenize
 # check if Python is called on the first line with this expression
 first_line_re = re.compile(b'^#!.*python[0-9.]*([ \t].*)?$')
 
+
 class build_scripts(Command):
 
     description = "\"build\" scripts (copy and fixup #! line)"
@@ -26,7 +28,6 @@ class build_scripts(Command):
 
     boolean_options = ['force']
 
-
     def initialize_options(self):
         self.build_dir = None
         self.scripts = None
@@ -49,7 +50,6 @@ class build_scripts(Command):
             return
         self.copy_scripts()
 
-
     def copy_scripts(self):
         r"""Copy each script listed in 'self.scripts'; if it's marked as a
         Python script in the Unix way (first line matches 'first_line_re',
@@ -101,8 +101,9 @@ class build_scripts(Command):
                     else:
                         executable = os.path.join(
                             sysconfig.get_config_var("BINDIR"),
-                           "python%s%s" % (sysconfig.get_config_var("VERSION"),
-                                           sysconfig.get_config_var("EXE")))
+                            "python%s%s" % (
+                                sysconfig.get_config_var("VERSION"),
+                                sysconfig.get_config_var("EXE")))
                     executable = os.fsencode(executable)
                     shebang = b"#!" + executable + post_interp + b"\n"
                     # Python parser starts to read a script using UTF-8 until
-- 
cgit v1.2.1


From 6736459f5bc024bd640ab564c7a5ee0b2d1c0416 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sun, 27 Mar 2022 10:28:33 -0400
Subject: Rewrite docstring for imperative voice and proper structure.

---
 distutils/command/build_scripts.py | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/distutils/command/build_scripts.py b/distutils/command/build_scripts.py
index dbeef2dd..64a472ae 100644
--- a/distutils/command/build_scripts.py
+++ b/distutils/command/build_scripts.py
@@ -51,10 +51,13 @@ class build_scripts(Command):
         self.copy_scripts()
 
     def copy_scripts(self):
-        r"""Copy each script listed in 'self.scripts'; if it's marked as a
-        Python script in the Unix way (first line matches 'first_line_re',
-        ie. starts with "\#!" and contains "python"), then adjust the first
-        line to refer to the current Python interpreter as we copy.
+        """
+        Copy each script listed in ``self.scripts``.
+
+        If a script is marked as a Python script (first line matches
+        'first_line_re', i.e. starts with ``#!`` and contains
+        "python"), then adjust in the copy the first line to refer to
+        the current Python interpreter.
         """
         self.mkpath(self.build_dir)
         outfiles = []
-- 
cgit v1.2.1


From beefbe746fd875ea75f6943f55ccca3b77b44674 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sun, 27 Mar 2022 10:31:08 -0400
Subject: Move 'updated_files' operation outside of if statement as it appears
 in both branches unconditionally.

---
 distutils/command/build_scripts.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/distutils/command/build_scripts.py b/distutils/command/build_scripts.py
index 64a472ae..cee65c64 100644
--- a/distutils/command/build_scripts.py
+++ b/distutils/command/build_scripts.py
@@ -94,10 +94,10 @@ class build_scripts(Command):
                     adjust = True
                     post_interp = match.group(1) or b''
 
+            updated_files.append(outfile)
             if adjust:
                 log.info("copying and adjusting %s -> %s", script,
                          self.build_dir)
-                updated_files.append(outfile)
                 if not self.dry_run:
                     if not sysconfig.python_build:
                         executable = self.executable
@@ -138,7 +138,6 @@ class build_scripts(Command):
             else:
                 if f:
                     f.close()
-                updated_files.append(outfile)
                 self.copy_file(script, outfile)
 
         if os.name == 'posix':
-- 
cgit v1.2.1


From fe5e02dd434e98137189406a710c8fa2bfa42e5c Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sun, 27 Mar 2022 10:44:25 -0400
Subject: Extract method for copying a file.

---
 distutils/command/build_scripts.py | 155 +++++++++++++++++++------------------
 1 file changed, 79 insertions(+), 76 deletions(-)

diff --git a/distutils/command/build_scripts.py b/distutils/command/build_scripts.py
index cee65c64..359b4765 100644
--- a/distutils/command/build_scripts.py
+++ b/distutils/command/build_scripts.py
@@ -63,82 +63,7 @@ class build_scripts(Command):
         outfiles = []
         updated_files = []
         for script in self.scripts:
-            adjust = False
-            script = convert_path(script)
-            outfile = os.path.join(self.build_dir, os.path.basename(script))
-            outfiles.append(outfile)
-
-            if not self.force and not newer(script, outfile):
-                log.debug("not copying %s (up-to-date)", script)
-                continue
-
-            # Always open the file, but ignore failures in dry-run mode --
-            # that way, we'll get accurate feedback if we can read the
-            # script.
-            try:
-                f = open(script, "rb")
-            except OSError:
-                if not self.dry_run:
-                    raise
-                f = None
-            else:
-                encoding, lines = tokenize.detect_encoding(f.readline)
-                f.seek(0)
-                first_line = f.readline()
-                if not first_line:
-                    self.warn("%s is an empty file (skipping)" % script)
-                    continue
-
-                match = first_line_re.match(first_line)
-                if match:
-                    adjust = True
-                    post_interp = match.group(1) or b''
-
-            updated_files.append(outfile)
-            if adjust:
-                log.info("copying and adjusting %s -> %s", script,
-                         self.build_dir)
-                if not self.dry_run:
-                    if not sysconfig.python_build:
-                        executable = self.executable
-                    else:
-                        executable = os.path.join(
-                            sysconfig.get_config_var("BINDIR"),
-                            "python%s%s" % (
-                                sysconfig.get_config_var("VERSION"),
-                                sysconfig.get_config_var("EXE")))
-                    executable = os.fsencode(executable)
-                    shebang = b"#!" + executable + post_interp + b"\n"
-                    # Python parser starts to read a script using UTF-8 until
-                    # it gets a #coding:xxx cookie. The shebang has to be the
-                    # first line of a file, the #coding:xxx cookie cannot be
-                    # written before. So the shebang has to be decodable from
-                    # UTF-8.
-                    try:
-                        shebang.decode('utf-8')
-                    except UnicodeDecodeError:
-                        raise ValueError(
-                            "The shebang ({!r}) is not decodable "
-                            "from utf-8".format(shebang))
-                    # If the script is encoded to a custom encoding (use a
-                    # #coding:xxx cookie), the shebang has to be decodable from
-                    # the script encoding too.
-                    try:
-                        shebang.decode(encoding)
-                    except UnicodeDecodeError:
-                        raise ValueError(
-                            "The shebang ({!r}) is not decodable "
-                            "from the script encoding ({})"
-                            .format(shebang, encoding))
-                    with open(outfile, "wb") as outf:
-                        outf.write(shebang)
-                        outf.writelines(f.readlines())
-                if f:
-                    f.close()
-            else:
-                if f:
-                    f.close()
-                self.copy_file(script, outfile)
+            self._copy_script(script, outfiles, updated_files)
 
         if os.name == 'posix':
             for file in outfiles:
@@ -153,3 +78,81 @@ class build_scripts(Command):
                         os.chmod(file, newmode)
         # XXX should we modify self.outfiles?
         return outfiles, updated_files
+
+    def _copy_script(self, script, outfiles, updated_files):
+        adjust = False
+        script = convert_path(script)
+        outfile = os.path.join(self.build_dir, os.path.basename(script))
+        outfiles.append(outfile)
+
+        if not self.force and not newer(script, outfile):
+            log.debug("not copying %s (up-to-date)", script)
+            return
+
+        # Always open the file, but ignore failures in dry-run mode --
+        # that way, we'll get accurate feedback if we can read the
+        # script.
+        try:
+            f = open(script, "rb")
+        except OSError:
+            if not self.dry_run:
+                raise
+            f = None
+        else:
+            encoding, lines = tokenize.detect_encoding(f.readline)
+            f.seek(0)
+            first_line = f.readline()
+            if not first_line:
+                self.warn("%s is an empty file (skipping)" % script)
+                return
+
+            match = first_line_re.match(first_line)
+            if match:
+                adjust = True
+                post_interp = match.group(1) or b''
+
+        updated_files.append(outfile)
+        if adjust:
+            log.info("copying and adjusting %s -> %s", script,
+                     self.build_dir)
+            if not self.dry_run:
+                if not sysconfig.python_build:
+                    executable = self.executable
+                else:
+                    executable = os.path.join(
+                        sysconfig.get_config_var("BINDIR"),
+                        "python%s%s" % (
+                            sysconfig.get_config_var("VERSION"),
+                            sysconfig.get_config_var("EXE")))
+                executable = os.fsencode(executable)
+                shebang = b"#!" + executable + post_interp + b"\n"
+                # Python parser starts to read a script using UTF-8 until
+                # it gets a #coding:xxx cookie. The shebang has to be the
+                # first line of a file, the #coding:xxx cookie cannot be
+                # written before. So the shebang has to be decodable from
+                # UTF-8.
+                try:
+                    shebang.decode('utf-8')
+                except UnicodeDecodeError:
+                    raise ValueError(
+                        "The shebang ({!r}) is not decodable "
+                        "from utf-8".format(shebang))
+                # If the script is encoded to a custom encoding (use a
+                # #coding:xxx cookie), the shebang has to be decodable from
+                # the script encoding too.
+                try:
+                    shebang.decode(encoding)
+                except UnicodeDecodeError:
+                    raise ValueError(
+                        "The shebang ({!r}) is not decodable "
+                        "from the script encoding ({})"
+                        .format(shebang, encoding))
+                with open(outfile, "wb") as outf:
+                    outf.write(shebang)
+                    outf.writelines(f.readlines())
+            if f:
+                f.close()
+        else:
+            if f:
+                f.close()
+            self.copy_file(script, outfile)
-- 
cgit v1.2.1


From d80e72007a0397efe2026173a93a50106145304d Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sun, 27 Mar 2022 10:50:49 -0400
Subject: Remove outfiles, unused.

---
 distutils/command/build_scripts.py | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/distutils/command/build_scripts.py b/distutils/command/build_scripts.py
index 359b4765..d717f300 100644
--- a/distutils/command/build_scripts.py
+++ b/distutils/command/build_scripts.py
@@ -33,7 +33,6 @@ class build_scripts(Command):
         self.scripts = None
         self.force = None
         self.executable = None
-        self.outfiles = None
 
     def finalize_options(self):
         self.set_undefined_options('build',
@@ -76,7 +75,7 @@ class build_scripts(Command):
                         log.info("changing mode of %s from %o to %o",
                                  file, oldmode, newmode)
                         os.chmod(file, newmode)
-        # XXX should we modify self.outfiles?
+
         return outfiles, updated_files
 
     def _copy_script(self, script, outfiles, updated_files):
-- 
cgit v1.2.1


From b760e946dc794f145b507a1512d7ff7138c06ae8 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 27 Mar 2022 13:15:21 +0100
Subject: Store install_requires and extras_require for future usage

---
 setuptools/dist.py | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/setuptools/dist.py b/setuptools/dist.py
index 865a19dd..67c988b1 100644
--- a/setuptools/dist.py
+++ b/setuptools/dist.py
@@ -468,6 +468,10 @@ class Distribution(_Distribution):
             },
         )
 
+        # Save the original dependencies before they are processed into the egg format
+        self._orig_extras_require = {}
+        self._orig_install_requires = []
+
         self.set_defaults = ConfigDiscovery(self)
 
         self._set_metadata_defaults(attrs)
@@ -540,6 +544,8 @@ class Distribution(_Distribution):
             self.metadata.python_requires = self.python_requires
 
         if getattr(self, 'extras_require', None):
+            # Save original before it is messed by _convert_extras_requirements
+            self._orig_extras_require = self._orig_extras_require or self.extras_require
             for extra in self.extras_require.keys():
                 # Since this gets called multiple times at points where the
                 # keys have become 'converted' extras, ensure that we are only
@@ -548,6 +554,10 @@ class Distribution(_Distribution):
                 if extra:
                     self.metadata.provides_extras.add(extra)
 
+        if getattr(self, 'install_requires', None) and not self._orig_install_requires:
+            # Save original before it is messed by _move_install_requirements_markers
+            self._orig_install_requires = self.install_requires
+
         self._convert_extras_requirements()
         self._move_install_requirements_markers()
 
-- 
cgit v1.2.1


From 91f9960726a7a73f1009ec3adeace04f4dd6c66c Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 27 Mar 2022 14:57:25 +0100
Subject: Make sure apply function remains private

---
 setuptools/config/pyprojecttoml.py | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index d2c6c9c5..0ee1b8f9 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -9,7 +9,8 @@ from typing import TYPE_CHECKING, Callable, Dict, Optional, Mapping, Union
 from setuptools.errors import FileError, OptionError
 
 from . import expand as _expand
-from ._apply_pyprojecttoml import apply, _PREVIOUSLY_DEFINED, _WouldIgnoreField
+from ._apply_pyprojecttoml import apply as _apply
+from ._apply_pyprojecttoml import _PREVIOUSLY_DEFINED, _WouldIgnoreField
 
 if TYPE_CHECKING:
     from setuptools.dist import Distribution  # noqa
@@ -44,13 +45,15 @@ def validate(config: dict, filepath: _Path):
 
 
 def apply_configuration(
-    dist: "Distribution", filepath: _Path, ignore_option_errors=False,
+    dist: "Distribution",
+    filepath: _Path,
+    ignore_option_errors=False,
 ) -> "Distribution":
     """Apply the configuration from a ``pyproject.toml`` file into an existing
     distribution object.
     """
     config = read_configuration(filepath, True, ignore_option_errors, dist)
-    return apply(dist, config, filepath)
+    return _apply(dist, config, filepath)
 
 
 def read_configuration(
-- 
cgit v1.2.1


From d0ee3e4944245db6b37cba2b3335dcacc2d3e6f6 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 27 Mar 2022 15:40:57 +0100
Subject: Ensure pyproject.toml does not break dynamic install_requires

---
 .../tests/config/test_apply_pyprojecttoml.py       | 27 ++++++++++++++++++++--
 1 file changed, 25 insertions(+), 2 deletions(-)

diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py
index c09ff3e6..a88bc1ec 100644
--- a/setuptools/tests/config/test_apply_pyprojecttoml.py
+++ b/setuptools/tests/config/test_apply_pyprojecttoml.py
@@ -15,6 +15,7 @@ from setuptools.dist import Distribution
 from setuptools.config import setupcfg, pyprojecttoml
 from setuptools.config import expand
 from setuptools.config._apply_pyprojecttoml import _WouldIgnoreField
+from setuptools.command.egg_info import write_requirements
 
 
 EXAMPLES = (Path(__file__).parent / "setupcfg_examples.txt").read_text()
@@ -207,12 +208,12 @@ def test_license_and_license_files(tmp_path):
 
 
 class TestPresetField:
-    def pyproject(self, tmp_path, dynamic):
+    def pyproject(self, tmp_path, dynamic, extra_content=""):
         content = f"[project]\nname = 'proj'\ndynamic = {dynamic!r}\n"
         if "version" not in dynamic:
             content += "version = '42'\n"
         file = tmp_path / "pyproject.toml"
-        file.write_text(content, encoding="utf-8")
+        file.write_text(content + extra_content, encoding="utf-8")
         return file
 
     @pytest.mark.parametrize(
@@ -250,6 +251,28 @@ class TestPresetField:
         dist_value = getattr(dist, attr, None) or getattr(dist.metadata, attr, object())
         assert dist_value == value
 
+    def test_optional_dependencies_dont_remove_env_markers(self, tmp_path):
+        """
+        Internally setuptools converts dependencies with markers to "extras".
+        If ``install_requires`` is given by ``setup.py``, we have to ensure that
+        applying ``optional-dependencies`` does not overwrite the mandatory
+        dependencies with markers (see #3204).
+        """
+        # If setuptools replace its internal mechanism that uses `requires.txt`
+        # this test has to be rewritten to adapt accordingly
+        extra = "\n[project.optional-dependencies]\nfoo = ['bar>1']\n"
+        pyproject = self.pyproject(tmp_path, ["dependencies"], extra)
+        install_req = ['importlib-resources (>=3.0.0) ; python_version < "3.7"']
+        dist = makedist(tmp_path, install_requires=install_req)
+        dist = pyprojecttoml.apply_configuration(dist, pyproject)
+        assert "foo" in dist.extras_require
+        assert ':python_version < "3.7"' in dist.extras_require
+        egg_info = dist.get_command_obj("egg_info")
+        write_requirements(egg_info, tmp_path, tmp_path / "requires.txt")
+        reqs = (tmp_path / "requires.txt").read_text(encoding="utf-8")
+        assert "importlib-resources" in reqs
+        assert "bar" in reqs
+
 
 # --- Auxiliary Functions ---
 
-- 
cgit v1.2.1


From 1a60a4f69979a4031faede2f792bb8f0eb63c01f Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 27 Mar 2022 14:59:52 +0100
Subject: Merge pre-set dependencies when applying pyproject

---
 setuptools/config/_apply_pyprojecttoml.py | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)

diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py
index 78a07273..5d34cdb7 100644
--- a/setuptools/config/_apply_pyprojecttoml.py
+++ b/setuptools/config/_apply_pyprojecttoml.py
@@ -194,6 +194,16 @@ def _python_requires(dist: "Distribution", val: dict, _root_dir):
     _set_config(dist, "python_requires", SpecifierSet(val))
 
 
+def _dependencies(dist: "Distribution", val: list, _root_dir):
+    existing = getattr(dist, "install_requires", [])
+    _set_config(dist, "install_requires", existing + val)
+
+
+def _optional_dependencies(dist: "Distribution", val: dict, _root_dir):
+    existing = getattr(dist, "extras_require", {})
+    _set_config(dist, "extras_require", {**existing, **val})
+
+
 def _unify_entry_points(project_table: dict):
     project = project_table
     entry_points = project.pop("entry-points", project.pop("entry_points", {}))
@@ -303,8 +313,8 @@ PYPROJECT_CORRESPONDENCE: Dict[str, _Correspondence] = {
     "authors": partial(_people, kind="author"),
     "maintainers": partial(_people, kind="maintainer"),
     "urls": _project_urls,
-    "dependencies": "install_requires",
-    "optional_dependencies": "extras_require",
+    "dependencies": _dependencies,
+    "optional_dependencies": _optional_dependencies,
     "requires_python": _python_requires,
 }
 
-- 
cgit v1.2.1


From 988d0646e7294f4b99485a9c38740f832cea89ea Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 27 Mar 2022 15:00:25 +0100
Subject: Small refactor

---
 setuptools/config/setupcfg.py | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/setuptools/config/setupcfg.py b/setuptools/config/setupcfg.py
index 5ecf6269..d485a8bb 100644
--- a/setuptools/config/setupcfg.py
+++ b/setuptools/config/setupcfg.py
@@ -70,7 +70,7 @@ def apply_configuration(dist: "Distribution", filepath: _Path) -> "Distribution"
 def _apply(
     dist: "Distribution", filepath: _Path,
     other_files: Iterable[_Path] = (),
-    ignore_option_errors: bool = False
+    ignore_option_errors: bool = False,
 ) -> Tuple["ConfigHandler", ...]:
     """Read configuration from ``filepath`` and applies to the ``dist`` object."""
     from setuptools.dist import _Distribution
@@ -677,9 +677,8 @@ class ConfigOptionsHandler(ConfigHandler["Distribution"]):
         :param dict section_options:
         """
         parse_list = partial(self._parse_list, separator=';')
-        self['extras_require'] = self._parse_section_to_dict(
-            section_options, parse_list
-        )
+        parsed = self._parse_section_to_dict(section_options, parse_list)
+        self['extras_require'] = parsed
 
     def parse_section_data_files(self, section_options):
         """Parses `data_files` configuration file section.
-- 
cgit v1.2.1


From 3f28fbc10f584da8555a6ea89155bc49ddcb18c9 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 27 Mar 2022 15:03:11 +0100
Subject: Preserve _tmp_extras_require as an ordered set

---
 setuptools/dist.py | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/setuptools/dist.py b/setuptools/dist.py
index 67c988b1..2aa532d2 100644
--- a/setuptools/dist.py
+++ b/setuptools/dist.py
@@ -471,6 +471,7 @@ class Distribution(_Distribution):
         # Save the original dependencies before they are processed into the egg format
         self._orig_extras_require = {}
         self._orig_install_requires = []
+        self._tmp_extras_require = defaultdict(ordered_set.OrderedSet)
 
         self.set_defaults = ConfigDiscovery(self)
 
@@ -568,7 +569,8 @@ class Distribution(_Distribution):
         `"extra:{marker}": ["barbazquux"]`.
         """
         spec_ext_reqs = getattr(self, 'extras_require', None) or {}
-        self._tmp_extras_require = defaultdict(list)
+        tmp = defaultdict(ordered_set.OrderedSet)
+        self._tmp_extras_require = getattr(self, '_tmp_extras_require', tmp)
         for section, v in spec_ext_reqs.items():
             # Do not strip empty sections.
             self._tmp_extras_require[section]
@@ -606,7 +608,8 @@ class Distribution(_Distribution):
         for r in complex_reqs:
             self._tmp_extras_require[':' + str(r.marker)].append(r)
         self.extras_require = dict(
-            (k, [str(r) for r in map(self._clean_req, v)])
+            # list(dict.fromkeys(...))  ensures a list of unique strings
+            (k, list(dict.fromkeys(str(r) for r in map(self._clean_req, v))))
             for k, v in self._tmp_extras_require.items()
         )
 
-- 
cgit v1.2.1


From 245b8686ace004f2827bfad542a57fe226d6765f Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 27 Mar 2022 15:20:20 +0100
Subject: Decrease verbosity of _install_setup_requires

---
 setuptools/__init__.py |  8 ++++++++
 setuptools/dist.py     | 15 ++++++++++-----
 2 files changed, 18 insertions(+), 5 deletions(-)

diff --git a/setuptools/__init__.py b/setuptools/__init__.py
index 502d2a2e..cff04323 100644
--- a/setuptools/__init__.py
+++ b/setuptools/__init__.py
@@ -58,6 +58,14 @@ def _install_setup_requires(attrs):
             # Prevent accidentally triggering discovery with incomplete set of attrs
             self.set_defaults._disable()
 
+        def _get_project_config_files(self, filenames=None):
+            """Ignore ``pyproject.toml``, they are not related to setup_requires"""
+            try:
+                cfg, toml = super()._split_standard_project_metadata(filenames)
+                return cfg, ()
+            except Exception:
+                return filenames, ()
+
         def finalize_options(self):
             """
             Disable finalize_options to avoid building the working set.
diff --git a/setuptools/dist.py b/setuptools/dist.py
index 2aa532d2..215c88e3 100644
--- a/setuptools/dist.py
+++ b/setuptools/dist.py
@@ -827,10 +827,8 @@ class Distribution(_Distribution):
             except ValueError as e:
                 raise DistutilsOptionError(e) from e
 
-    def parse_config_files(self, filenames=None, ignore_option_errors=False):
-        """Parses configuration files from various levels
-        and loads configuration.
-        """
+    def _get_project_config_files(self, filenames):
+        """Add default file and split between INI and TOML"""
         tomlfiles = []
         standard_project_metadata = Path(self.src_root or os.curdir, "pyproject.toml")
         if filenames is not None:
@@ -839,8 +837,15 @@ class Distribution(_Distribution):
             tomlfiles = list(parts[1])  # 2nd element => predicate is True
         elif standard_project_metadata.exists():
             tomlfiles = [standard_project_metadata]
+        return filenames, tomlfiles
+
+    def parse_config_files(self, filenames=None, ignore_option_errors=False):
+        """Parses configuration files from various levels
+        and loads configuration.
+        """
+        inifiles, tomlfiles = self._get_project_config_files(filenames)
 
-        self._parse_config_files(filenames=filenames)
+        self._parse_config_files(filenames=inifiles)
 
         setupcfg.parse_configuration(
             self, self.command_options, ignore_option_errors=ignore_option_errors
-- 
cgit v1.2.1


From f82f3689c93f97945a571aac30a244512eb98229 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 27 Mar 2022 15:53:14 +0100
Subject: Add news fragment

---
 changelog.d/3222.misc.rst | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 changelog.d/3222.misc.rst

diff --git a/changelog.d/3222.misc.rst b/changelog.d/3222.misc.rst
new file mode 100644
index 00000000..66f1489e
--- /dev/null
+++ b/changelog.d/3222.misc.rst
@@ -0,0 +1,2 @@
+Fixed missing requirements with environment markers when
+``optional-dependencies`` is set in ``pyproject.toml``.
-- 
cgit v1.2.1


From f91759e6b7e11af9ee23a28a324e8a67ffe897b2 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sun, 27 Mar 2022 11:01:09 -0400
Subject: Extract _change_modes and _change_mode functions.

---
 distutils/command/build_scripts.py | 31 ++++++++++++++++++++-----------
 1 file changed, 20 insertions(+), 11 deletions(-)

diff --git a/distutils/command/build_scripts.py b/distutils/command/build_scripts.py
index d717f300..07408efa 100644
--- a/distutils/command/build_scripts.py
+++ b/distutils/command/build_scripts.py
@@ -64,17 +64,7 @@ class build_scripts(Command):
         for script in self.scripts:
             self._copy_script(script, outfiles, updated_files)
 
-        if os.name == 'posix':
-            for file in outfiles:
-                if self.dry_run:
-                    log.info("changing mode of %s", file)
-                else:
-                    oldmode = os.stat(file)[ST_MODE] & 0o7777
-                    newmode = (oldmode | 0o555) & 0o7777
-                    if newmode != oldmode:
-                        log.info("changing mode of %s from %o to %o",
-                                 file, oldmode, newmode)
-                        os.chmod(file, newmode)
+        self._change_modes(outfiles)
 
         return outfiles, updated_files
 
@@ -155,3 +145,22 @@ class build_scripts(Command):
             if f:
                 f.close()
             self.copy_file(script, outfile)
+
+    def _change_modes(self, outfiles):
+        if os.name != 'posix':
+            return
+
+        for file in outfiles:
+            self._change_mode(file)
+
+    def _change_mode(self, file):
+        if self.dry_run:
+            log.info("changing mode of %s", file)
+            return
+
+        oldmode = os.stat(file)[ST_MODE] & 0o7777
+        newmode = (oldmode | 0o555) & 0o7777
+        if newmode != oldmode:
+            log.info("changing mode of %s from %o to %o",
+                     file, oldmode, newmode)
+            os.chmod(file, newmode)
-- 
cgit v1.2.1


From 2304d9992b74c3080955563cac24af0670db652b Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 27 Mar 2022 16:07:34 +0100
Subject: Fix incorrect PR number

---
 changelog.d/3222.misc.rst | 2 --
 changelog.d/3223.misc.rst | 2 ++
 2 files changed, 2 insertions(+), 2 deletions(-)
 delete mode 100644 changelog.d/3222.misc.rst
 create mode 100644 changelog.d/3223.misc.rst

diff --git a/changelog.d/3222.misc.rst b/changelog.d/3222.misc.rst
deleted file mode 100644
index 66f1489e..00000000
--- a/changelog.d/3222.misc.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Fixed missing requirements with environment markers when
-``optional-dependencies`` is set in ``pyproject.toml``.
diff --git a/changelog.d/3223.misc.rst b/changelog.d/3223.misc.rst
new file mode 100644
index 00000000..66f1489e
--- /dev/null
+++ b/changelog.d/3223.misc.rst
@@ -0,0 +1,2 @@
+Fixed missing requirements with environment markers when
+``optional-dependencies`` is set in ``pyproject.toml``.
-- 
cgit v1.2.1


From afaf3c099d745799ef6bc014f30ea417401e3baa Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sun, 27 Mar 2022 11:11:20 -0400
Subject: Rewrite the comment to match the implementation.

---
 distutils/command/build_scripts.py | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/distutils/command/build_scripts.py b/distutils/command/build_scripts.py
index 07408efa..36047dcc 100644
--- a/distutils/command/build_scripts.py
+++ b/distutils/command/build_scripts.py
@@ -78,9 +78,8 @@ class build_scripts(Command):
             log.debug("not copying %s (up-to-date)", script)
             return
 
-        # Always open the file, but ignore failures in dry-run mode --
-        # that way, we'll get accurate feedback if we can read the
-        # script.
+        # Always open the file, but ignore failures in dry-run mode
+        # in order to attempt to copy directly.
         try:
             f = open(script, "rb")
         except OSError:
-- 
cgit v1.2.1


From 6a7d01c0d0b1960b343db5bc120d668a9f58ce84 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sun, 27 Mar 2022 11:17:14 -0400
Subject: Use 'shebang_' for pattern and match.

---
 distutils/command/build_scripts.py | 14 ++++++--------
 1 file changed, 6 insertions(+), 8 deletions(-)

diff --git a/distutils/command/build_scripts.py b/distutils/command/build_scripts.py
index 36047dcc..d141a880 100644
--- a/distutils/command/build_scripts.py
+++ b/distutils/command/build_scripts.py
@@ -13,7 +13,7 @@ from distutils import log
 import tokenize
 
 # check if Python is called on the first line with this expression
-first_line_re = re.compile(b'^#!.*python[0-9.]*([ \t].*)?$')
+shebang_pattern = re.compile(b'^#!.*python[0-9.]*([ \t].*)?$')
 
 
 class build_scripts(Command):
@@ -54,7 +54,7 @@ class build_scripts(Command):
         Copy each script listed in ``self.scripts``.
 
         If a script is marked as a Python script (first line matches
-        'first_line_re', i.e. starts with ``#!`` and contains
+        'shebang_pattern', i.e. starts with ``#!`` and contains
         "python"), then adjust in the copy the first line to refer to
         the current Python interpreter.
         """
@@ -69,7 +69,7 @@ class build_scripts(Command):
         return outfiles, updated_files
 
     def _copy_script(self, script, outfiles, updated_files):
-        adjust = False
+        shebang_match = None
         script = convert_path(script)
         outfile = os.path.join(self.build_dir, os.path.basename(script))
         outfiles.append(outfile)
@@ -94,13 +94,10 @@ class build_scripts(Command):
                 self.warn("%s is an empty file (skipping)" % script)
                 return
 
-            match = first_line_re.match(first_line)
-            if match:
-                adjust = True
-                post_interp = match.group(1) or b''
+            shebang_match = shebang_pattern.match(first_line)
 
         updated_files.append(outfile)
-        if adjust:
+        if shebang_match:
             log.info("copying and adjusting %s -> %s", script,
                      self.build_dir)
             if not self.dry_run:
@@ -113,6 +110,7 @@ class build_scripts(Command):
                             sysconfig.get_config_var("VERSION"),
                             sysconfig.get_config_var("EXE")))
                 executable = os.fsencode(executable)
+                post_interp = shebang_match.group(1) or b''
                 shebang = b"#!" + executable + post_interp + b"\n"
                 # Python parser starts to read a script using UTF-8 until
                 # it gets a #coding:xxx cookie. The shebang has to be the
-- 
cgit v1.2.1


From 12edb8d575966b50afe6b2f89383bf804e99b310 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sun, 27 Mar 2022 11:33:14 -0400
Subject: Extract method to validate the shebang.

---
 distutils/command/build_scripts.py | 47 +++++++++++++++++++++-----------------
 1 file changed, 26 insertions(+), 21 deletions(-)

diff --git a/distutils/command/build_scripts.py b/distutils/command/build_scripts.py
index d141a880..94167d6c 100644
--- a/distutils/command/build_scripts.py
+++ b/distutils/command/build_scripts.py
@@ -112,27 +112,7 @@ class build_scripts(Command):
                 executable = os.fsencode(executable)
                 post_interp = shebang_match.group(1) or b''
                 shebang = b"#!" + executable + post_interp + b"\n"
-                # Python parser starts to read a script using UTF-8 until
-                # it gets a #coding:xxx cookie. The shebang has to be the
-                # first line of a file, the #coding:xxx cookie cannot be
-                # written before. So the shebang has to be decodable from
-                # UTF-8.
-                try:
-                    shebang.decode('utf-8')
-                except UnicodeDecodeError:
-                    raise ValueError(
-                        "The shebang ({!r}) is not decodable "
-                        "from utf-8".format(shebang))
-                # If the script is encoded to a custom encoding (use a
-                # #coding:xxx cookie), the shebang has to be decodable from
-                # the script encoding too.
-                try:
-                    shebang.decode(encoding)
-                except UnicodeDecodeError:
-                    raise ValueError(
-                        "The shebang ({!r}) is not decodable "
-                        "from the script encoding ({})"
-                        .format(shebang, encoding))
+                self._validate_shebang(shebang, encoding)
                 with open(outfile, "wb") as outf:
                     outf.write(shebang)
                     outf.writelines(f.readlines())
@@ -161,3 +141,28 @@ class build_scripts(Command):
             log.info("changing mode of %s from %o to %o",
                      file, oldmode, newmode)
             os.chmod(file, newmode)
+
+    @staticmethod
+    def _validate_shebang(shebang, encoding):
+        # Python parser starts to read a script using UTF-8 until
+        # it gets a #coding:xxx cookie. The shebang has to be the
+        # first line of a file, the #coding:xxx cookie cannot be
+        # written before. So the shebang has to be decodable from
+        # UTF-8.
+        try:
+            shebang.decode('utf-8')
+        except UnicodeDecodeError:
+            raise ValueError(
+                "The shebang ({!r}) is not decodable "
+                "from utf-8".format(shebang))
+
+        # If the script is encoded to a custom encoding (use a
+        # #coding:xxx cookie), the shebang has to be decodable from
+        # the script encoding too.
+        try:
+            shebang.decode(encoding)
+        except UnicodeDecodeError:
+            raise ValueError(
+                "The shebang ({!r}) is not decodable "
+                "from the script encoding ({})"
+                .format(shebang, encoding))
-- 
cgit v1.2.1


From 7038cf2a659509b76847e463a3d3f47927986e0d Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sun, 27 Mar 2022 11:44:07 -0400
Subject: Restore Setuptools compatibility.

---
 distutils/command/build_scripts.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/distutils/command/build_scripts.py b/distutils/command/build_scripts.py
index 94167d6c..cc4ca1db 100644
--- a/distutils/command/build_scripts.py
+++ b/distutils/command/build_scripts.py
@@ -15,6 +15,9 @@ import tokenize
 # check if Python is called on the first line with this expression
 shebang_pattern = re.compile(b'^#!.*python[0-9.]*([ \t].*)?$')
 
+# for Setuptools compatibility
+first_line_re = shebang_pattern
+
 
 class build_scripts(Command):
 
-- 
cgit v1.2.1


From e2f47dcfc2a8019254a7600c400062d5c392d944 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sun, 27 Mar 2022 12:02:25 -0400
Subject: In build_scripts, open scripts as text. Fixes pypa/distutils#124.

---
 distutils/command/build_scripts.py | 39 +++++++++++++++++++-------------------
 1 file changed, 19 insertions(+), 20 deletions(-)

diff --git a/distutils/command/build_scripts.py b/distutils/command/build_scripts.py
index cc4ca1db..e56511da 100644
--- a/distutils/command/build_scripts.py
+++ b/distutils/command/build_scripts.py
@@ -12,8 +12,10 @@ from distutils.util import convert_path
 from distutils import log
 import tokenize
 
-# check if Python is called on the first line with this expression
-shebang_pattern = re.compile(b'^#!.*python[0-9.]*([ \t].*)?$')
+shebang_pattern = re.compile('^#!.*python[0-9.]*([ \t].*)?$')
+"""
+Pattern matching a Python interpreter indicated in first line of a script.
+"""
 
 # for Setuptools compatibility
 first_line_re = shebang_pattern
@@ -84,14 +86,12 @@ class build_scripts(Command):
         # Always open the file, but ignore failures in dry-run mode
         # in order to attempt to copy directly.
         try:
-            f = open(script, "rb")
+            f = tokenize.open(script)
         except OSError:
             if not self.dry_run:
                 raise
             f = None
         else:
-            encoding, lines = tokenize.detect_encoding(f.readline)
-            f.seek(0)
             first_line = f.readline()
             if not first_line:
                 self.warn("%s is an empty file (skipping)" % script)
@@ -112,11 +112,10 @@ class build_scripts(Command):
                         "python%s%s" % (
                             sysconfig.get_config_var("VERSION"),
                             sysconfig.get_config_var("EXE")))
-                executable = os.fsencode(executable)
-                post_interp = shebang_match.group(1) or b''
-                shebang = b"#!" + executable + post_interp + b"\n"
-                self._validate_shebang(shebang, encoding)
-                with open(outfile, "wb") as outf:
+                post_interp = shebang_match.group(1) or ''
+                shebang = "#!" + executable + post_interp + "\n"
+                self._validate_shebang(shebang, f.encoding)
+                with open(outfile, "w", encoding=f.encoding) as outf:
                     outf.write(shebang)
                     outf.writelines(f.readlines())
             if f:
@@ -150,22 +149,22 @@ class build_scripts(Command):
         # Python parser starts to read a script using UTF-8 until
         # it gets a #coding:xxx cookie. The shebang has to be the
         # first line of a file, the #coding:xxx cookie cannot be
-        # written before. So the shebang has to be decodable from
+        # written before. So the shebang has to be encodable to
         # UTF-8.
         try:
-            shebang.decode('utf-8')
-        except UnicodeDecodeError:
+            shebang.encode('utf-8')
+        except UnicodeEncodeError:
             raise ValueError(
-                "The shebang ({!r}) is not decodable "
-                "from utf-8".format(shebang))
+                "The shebang ({!r}) is not encodable "
+                "to utf-8".format(shebang))
 
         # If the script is encoded to a custom encoding (use a
-        # #coding:xxx cookie), the shebang has to be decodable from
+        # #coding:xxx cookie), the shebang has to be encodable to
         # the script encoding too.
         try:
-            shebang.decode(encoding)
-        except UnicodeDecodeError:
+            shebang.encode(encoding)
+        except UnicodeEncodeError:
             raise ValueError(
-                "The shebang ({!r}) is not decodable "
-                "from the script encoding ({})"
+                "The shebang ({!r}) is not encodable "
+                "to the script encoding ({})"
                 .format(shebang, encoding))
-- 
cgit v1.2.1


From 603bb9852f3a6a53c97beaccc9f58dc47771a486 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 27 Mar 2022 16:57:26 +0100
Subject: Fix previous detection of empty arrays

---
 setuptools/config/_apply_pyprojecttoml.py           | 2 +-
 setuptools/config/pyprojecttoml.py                  | 5 +++--
 setuptools/tests/config/test_apply_pyprojecttoml.py | 8 +++++---
 3 files changed, 9 insertions(+), 6 deletions(-)

diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py
index 5d34cdb7..fce5c40e 100644
--- a/setuptools/config/_apply_pyprojecttoml.py
+++ b/setuptools/config/_apply_pyprojecttoml.py
@@ -303,7 +303,7 @@ def _some_attrgetter(*items):
     """
     def _acessor(obj):
         values = (_attrgetter(i)(obj) for i in items)
-        return next((i for i in values if i), None)
+        return next((i for i in values if i is not None), None)
     return _acessor
 
 
diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index 0ee1b8f9..e20d71d2 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -282,11 +282,12 @@ class _ConfigExpander:
         )
         # `None` indicates there is nothing in `tool.setuptools.dynamic` but the value
         # might have already been set by setup.py/extensions, so avoid overwriting.
-        self.project_cfg.update({k: v for k, v in obtained_dynamic.items() if v})
+        updates = {k: v for k, v in obtained_dynamic.items() if v is not None}
+        self.project_cfg.update(updates)
 
     def _ensure_previously_set(self, dist: "Distribution", field: str):
         previous = _PREVIOUSLY_DEFINED[field](dist)
-        if not previous and not self.ignore_option_errors:
+        if previous is None and not self.ignore_option_errors:
             msg = (
                 f"No configuration found for dynamic {field!r}.\n"
                 "Some dynamic fields need to be specified via `tool.setuptools.dynamic`"
diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py
index a88bc1ec..b8220963 100644
--- a/setuptools/tests/config/test_apply_pyprojecttoml.py
+++ b/setuptools/tests/config/test_apply_pyprojecttoml.py
@@ -14,7 +14,7 @@ import setuptools  # noqa ensure monkey patch to metadata
 from setuptools.dist import Distribution
 from setuptools.config import setupcfg, pyprojecttoml
 from setuptools.config import expand
-from setuptools.config._apply_pyprojecttoml import _WouldIgnoreField
+from setuptools.config._apply_pyprojecttoml import _WouldIgnoreField, _some_attrgetter
 from setuptools.command.egg_info import write_requirements
 
 
@@ -234,12 +234,14 @@ class TestPresetField:
             dist = pyprojecttoml.apply_configuration(dist, pyproject)
 
         # TODO: Once support for pyproject.toml config stabilizes attr should be None
-        dist_value = getattr(dist, attr, None) or getattr(dist.metadata, attr, object())
+        dist_value = _some_attrgetter(f"metadata.{attr}", attr)(dist)
         assert dist_value == value
 
     @pytest.mark.parametrize(
         "attr, field, value",
         [
+            ("install_requires", "dependencies", []),
+            ("extras_require", "optional-dependencies", {}),
             ("install_requires", "dependencies", ["six"]),
             ("classifiers", "classifiers", ["Private :: Classifier"]),
         ]
@@ -248,7 +250,7 @@ class TestPresetField:
         pyproject = self.pyproject(tmp_path, [field])
         dist = makedist(tmp_path, **{attr: value})
         dist = pyprojecttoml.apply_configuration(dist, pyproject)
-        dist_value = getattr(dist, attr, None) or getattr(dist.metadata, attr, object())
+        dist_value = _some_attrgetter(f"metadata.{attr}", attr)(dist)
         assert dist_value == value
 
     def test_optional_dependencies_dont_remove_env_markers(self, tmp_path):
-- 
cgit v1.2.1


From a0148a143a157b7458fa32e845bbce7b9bf6ea33 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sun, 27 Mar 2022 12:19:08 -0400
Subject: Update changelog

---
 changelog.d/3224.change.rst | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/3224.change.rst

diff --git a/changelog.d/3224.change.rst b/changelog.d/3224.change.rst
new file mode 100644
index 00000000..5b0b1724
--- /dev/null
+++ b/changelog.d/3224.change.rst
@@ -0,0 +1 @@
+Merge changes from pypa/distutils@e1d5c9b1f6
-- 
cgit v1.2.1


From c743883bcfbe8341dba3ae8659181b712d7339ec Mon Sep 17 00:00:00 2001
From: Isuru Fernando 
Date: Sun, 27 Mar 2022 12:08:41 -0500
Subject: Fix EXT_SUFFIX for windows py<3.8

---
 distutils/sysconfig.py            | 3 +++
 distutils/tests/test_sysconfig.py | 8 ++++++++
 2 files changed, 11 insertions(+)

diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py
index 9fad3835..ee50522c 100644
--- a/distutils/sysconfig.py
+++ b/distutils/sysconfig.py
@@ -449,6 +449,9 @@ def get_config_vars(*args):
     global _config_vars
     if _config_vars is None:
         _config_vars = sysconfig.get_config_vars().copy()
+        if os.name == 'nt':
+            # See https://github.com/pypa/distutils/issues/130
+            _config_vars['EXT_SUFFIX'] = _imp.extension_suffixes()[0]
 
     if args:
         vals = []
diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py
index 9de3cb70..66fb743e 100644
--- a/distutils/tests/test_sysconfig.py
+++ b/distutils/tests/test_sysconfig.py
@@ -299,6 +299,14 @@ class SysconfigTestCase(support.EnvironGuard, unittest.TestCase):
             result = sysconfig.parse_config_h(f)
         self.assertTrue(isinstance(result, dict))
 
+    @unittest.skipUnless(sys.platform == 'win32',
+                     'Testing windows pyd suffix')
+    @unittest.skipUnless(sys.implementation.name == 'cpython',
+                     'Need cpython for this test')
+    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")
+
 def test_suite():
     suite = unittest.TestSuite()
     suite.addTest(unittest.TestLoader().loadTestsFromTestCase(SysconfigTestCase))
-- 
cgit v1.2.1


From e3de684f3e8b065c642f7f0a821d13c830b980f4 Mon Sep 17 00:00:00 2001
From: Isuru Fernando 
Date: Sun, 27 Mar 2022 12:20:33 -0500
Subject: Fix SO too

---
 distutils/sysconfig.py | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py
index ee50522c..e2a395df 100644
--- a/distutils/sysconfig.py
+++ b/distutils/sysconfig.py
@@ -452,6 +452,11 @@ def get_config_vars(*args):
         if os.name == 'nt':
             # See https://github.com/pypa/distutils/issues/130
             _config_vars['EXT_SUFFIX'] = _imp.extension_suffixes()[0]
+            if not IS_PYPY:
+                # For backward compatibility, see issue19555
+                SO = _config_vars.get('EXT_SUFFIX')
+                if SO is not None:
+                    _config_vars['SO'] = SO
 
     if args:
         vals = []
-- 
cgit v1.2.1


From 74dc5363b3ef0c0d4f07d20e4d7d3aa91be159ae Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sun, 27 Mar 2022 14:44:20 -0400
Subject: Move EXT_SUFFIX support to _py39compat, to be removed after support
 for Python 3.9 is dropped. Fall back to default behavior on Python 3.10.
 Remove functionality for SO, which has been long deprecated and is untested.

---
 distutils/py39compat.py | 14 ++++++++++++++
 distutils/sysconfig.py  | 12 +++---------
 2 files changed, 17 insertions(+), 9 deletions(-)
 create mode 100644 distutils/py39compat.py

diff --git a/distutils/py39compat.py b/distutils/py39compat.py
new file mode 100644
index 00000000..6771f0ff
--- /dev/null
+++ b/distutils/py39compat.py
@@ -0,0 +1,14 @@
+import sys
+import platform
+
+
+def ext_suffix(vars):
+    """
+    Ensure vars contains 'EXT_SUFFIX'. pypa/distutils#130
+    """
+    if sys.version_info < (3, 10):
+        return vars
+    if platform.system() != 'Windows':
+        return vars
+    import _imp
+    vars.update(EXT_SUFFIX=_imp.extension_suffixes()[0])
diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py
index e2a395df..ef554768 100644
--- a/distutils/sysconfig.py
+++ b/distutils/sysconfig.py
@@ -16,6 +16,7 @@ import sys
 import sysconfig
 
 from .errors import DistutilsPlatformError
+from . import py39compat
 
 IS_PYPY = '__pypy__' in sys.builtin_module_names
 
@@ -448,15 +449,8 @@ def get_config_vars(*args):
     """
     global _config_vars
     if _config_vars is None:
-        _config_vars = sysconfig.get_config_vars().copy()
-        if os.name == 'nt':
-            # See https://github.com/pypa/distutils/issues/130
-            _config_vars['EXT_SUFFIX'] = _imp.extension_suffixes()[0]
-            if not IS_PYPY:
-                # For backward compatibility, see issue19555
-                SO = _config_vars.get('EXT_SUFFIX')
-                if SO is not None:
-                    _config_vars['SO'] = SO
+        _config_vars = py39compat.ext_suffix(
+            sysconfig.get_config_vars().copy())
 
     if args:
         vals = []
-- 
cgit v1.2.1


From 79dc3575b042bd991e2dbff7621a015fe02450d7 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sun, 27 Mar 2022 14:48:03 -0400
Subject: =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins=20(delint).?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 distutils/sysconfig.py | 31 ++++++++++++++++++++-----------
 1 file changed, 20 insertions(+), 11 deletions(-)

diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py
index 9fad3835..eaaf1d3d 100644
--- a/distutils/sysconfig.py
+++ b/distutils/sysconfig.py
@@ -9,7 +9,6 @@ Written by:   Fred L. Drake, Jr.
 Email:        
 """
 
-import _imp
 import os
 import re
 import sys
@@ -48,6 +47,7 @@ def _is_python_source_dir(d):
             return True
     return False
 
+
 _sys_home = getattr(sys, '_home', None)
 
 if os.name == 'nt':
@@ -59,11 +59,13 @@ if os.name == 'nt':
     project_base = _fix_pcbuild(project_base)
     _sys_home = _fix_pcbuild(_sys_home)
 
+
 def _python_build():
     if _sys_home:
         return _is_python_source_dir(_sys_home)
     return _is_python_source_dir(project_base)
 
+
 python_build = _python_build()
 
 
@@ -79,6 +81,7 @@ except AttributeError:
     # this attribute, which is fine.
     pass
 
+
 def get_python_version():
     """Return a string containing the major and minor Python version,
     leaving off the patchlevel.  Sample return values could be '1.5'
@@ -192,7 +195,6 @@ def get_python_lib(plat_specific=0, standard_lib=0, prefix=None):
             "on platform '%s'" % os.name)
 
 
-
 def customize_compiler(compiler):
     """Do any platform-specific customization of a CCompiler instance.
 
@@ -217,8 +219,9 @@ def customize_compiler(compiler):
                 _config_vars['CUSTOMIZED_OSX_COMPILER'] = 'True'
 
         (cc, cxx, cflags, ccshared, ldshared, shlib_suffix, ar, ar_flags) = \
-            get_config_vars('CC', 'CXX', 'CFLAGS',
-                            'CCSHARED', 'LDSHARED', 'SHLIB_SUFFIX', 'AR', 'ARFLAGS')
+            get_config_vars(
+                'CC', 'CXX', 'CFLAGS',
+                'CCSHARED', 'LDSHARED', 'SHLIB_SUFFIX', 'AR', 'ARFLAGS')
 
         if 'CC' in os.environ:
             newcc = os.environ['CC']
@@ -280,7 +283,6 @@ def get_config_h_filename():
         return sysconfig.get_config_h_filename()
 
 
-
 def get_makefile_filename():
     """Return full pathname of installed Makefile from the Python build."""
     return sysconfig.get_makefile_filename()
@@ -302,6 +304,7 @@ _variable_rx = re.compile(r"([a-zA-Z][a-zA-Z0-9_]+)\s*=\s*(.*)")
 _findvar1_rx = re.compile(r"\$\(([A-Za-z][A-Za-z0-9_]*)\)")
 _findvar2_rx = re.compile(r"\${([A-Za-z][A-Za-z0-9_]*)}")
 
+
 def parse_makefile(fn, g=None):
     """Parse a Makefile-style file.
 
@@ -310,7 +313,9 @@ def parse_makefile(fn, g=None):
     used instead of a new dictionary.
     """
     from distutils.text_file import TextFile
-    fp = TextFile(fn, strip_comments=1, skip_blanks=1, join_lines=1, errors="surrogateescape")
+    fp = TextFile(
+        fn, strip_comments=1, skip_blanks=1, join_lines=1,
+        errors="surrogateescape")
 
     if g is None:
         g = {}
@@ -319,7 +324,7 @@ def parse_makefile(fn, g=None):
 
     while True:
         line = fp.readline()
-        if line is None: # eof
+        if line is None:  # eof
             break
         m = _variable_rx.match(line)
         if m:
@@ -363,7 +368,8 @@ def parse_makefile(fn, g=None):
                     item = os.environ[n]
 
                 elif n in renamed_variables:
-                    if name.startswith('PY_') and name[3:] in renamed_variables:
+                    if name.startswith('PY_') and \
+                            name[3:] in renamed_variables:
                         item = ""
 
                     elif 'PY_' + n in notdone:
@@ -379,7 +385,8 @@ def parse_makefile(fn, g=None):
                     if "$" in after:
                         notdone[name] = value
                     else:
-                        try: value = int(value)
+                        try:
+                            value = int(value)
                         except ValueError:
                             done[name] = value.strip()
                         else:
@@ -387,7 +394,7 @@ def parse_makefile(fn, g=None):
                         del notdone[name]
 
                         if name.startswith('PY_') \
-                            and name[3:] in renamed_variables:
+                                and name[3:] in renamed_variables:
 
                             name = name[3:]
                             if name not in done:
@@ -458,6 +465,7 @@ def get_config_vars(*args):
     else:
         return _config_vars
 
+
 def get_config_var(name):
     """Return the value of a single variable using the dictionary
     returned by 'get_config_vars()'.  Equivalent to
@@ -465,5 +473,6 @@ def get_config_var(name):
     """
     if name == 'SO':
         import warnings
-        warnings.warn('SO is deprecated, use EXT_SUFFIX', DeprecationWarning, 2)
+        warnings.warn(
+            'SO is deprecated, use EXT_SUFFIX', DeprecationWarning, 2)
     return get_config_vars().get(name)
-- 
cgit v1.2.1


From 8f8d6555aea42186c866d8350738de57ff47eb21 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sun, 27 Mar 2022 14:50:54 -0400
Subject: Just modify the vars in place.

---
 distutils/py39compat.py | 4 ++--
 distutils/sysconfig.py  | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/distutils/py39compat.py b/distutils/py39compat.py
index 6771f0ff..f1dfba2c 100644
--- a/distutils/py39compat.py
+++ b/distutils/py39compat.py
@@ -7,8 +7,8 @@ def ext_suffix(vars):
     Ensure vars contains 'EXT_SUFFIX'. pypa/distutils#130
     """
     if sys.version_info < (3, 10):
-        return vars
+        return
     if platform.system() != 'Windows':
-        return vars
+        return
     import _imp
     vars.update(EXT_SUFFIX=_imp.extension_suffixes()[0])
diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py
index ef554768..205d64ce 100644
--- a/distutils/sysconfig.py
+++ b/distutils/sysconfig.py
@@ -449,8 +449,8 @@ def get_config_vars(*args):
     """
     global _config_vars
     if _config_vars is None:
-        _config_vars = py39compat.ext_suffix(
-            sysconfig.get_config_vars().copy())
+        _config_vars = sysconfig.get_config_vars().copy()
+        py39compat.ext_suffix(_config_vars)
 
     if args:
         vals = []
-- 
cgit v1.2.1


From 55da5cbe37477653ea68f9fbaf68b526b804116d Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sun, 27 Mar 2022 15:06:54 -0400
Subject: Restore expectation that SO matches EXT_SUFFIX with rationale.

---
 distutils/py39compat.py | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/distutils/py39compat.py b/distutils/py39compat.py
index f1dfba2c..0552db44 100644
--- a/distutils/py39compat.py
+++ b/distutils/py39compat.py
@@ -11,4 +11,11 @@ def ext_suffix(vars):
     if platform.system() != 'Windows':
         return
     import _imp
-    vars.update(EXT_SUFFIX=_imp.extension_suffixes()[0])
+    ext_suffix = _imp.extension_suffixes()[0]
+    vars.update(
+        EXT_SUFFIX=ext_suffix,
+        # sysconfig sets SO to match EXT_SUFFIX, so maintain
+        # that expectation.
+        # https://github.com/python/cpython/blob/785cc6770588de087d09e89a69110af2542be208/Lib/sysconfig.py#L671-L673
+        SO=ext_suffix,
+    )
-- 
cgit v1.2.1


From 8270cfa851b2cf42345639a3bd0466693dfdced2 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sun, 27 Mar 2022 15:10:08 -0400
Subject: Get the version logic correct.

---
 distutils/py39compat.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/distutils/py39compat.py b/distutils/py39compat.py
index 0552db44..d68cbce7 100644
--- a/distutils/py39compat.py
+++ b/distutils/py39compat.py
@@ -6,7 +6,7 @@ def ext_suffix(vars):
     """
     Ensure vars contains 'EXT_SUFFIX'. pypa/distutils#130
     """
-    if sys.version_info < (3, 10):
+    if sys.version_info > (3, 10):
         return
     if platform.system() != 'Windows':
         return
-- 
cgit v1.2.1


From 3ffef4f29e1fdbe4d619857d3ea10140452847bd Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 27 Mar 2022 20:16:28 +0100
Subject: Minor change on news fragments

---
 changelog.d/3215.change.1.rst | 7 +++++++
 changelog.d/3215.change.rst   | 7 -------
 changelog.d/3218.change.rst   | 6 +++---
 3 files changed, 10 insertions(+), 10 deletions(-)
 create mode 100644 changelog.d/3215.change.1.rst
 delete mode 100644 changelog.d/3215.change.rst

diff --git a/changelog.d/3215.change.1.rst b/changelog.d/3215.change.1.rst
new file mode 100644
index 00000000..a086799e
--- /dev/null
+++ b/changelog.d/3215.change.1.rst
@@ -0,0 +1,7 @@
+Ignored a subgroup of invalid ``pyproject.toml`` files that use the ``[project]``
+table to specify only ``requires-python`` (**transitional**).
+
+.. warning::
+   Please note that future releases of setuptools will halt the build process
+   if a ``pyproject.toml`` file that does not match doc:`the PyPA Specification
+   ` is given.
diff --git a/changelog.d/3215.change.rst b/changelog.d/3215.change.rst
deleted file mode 100644
index a086799e..00000000
--- a/changelog.d/3215.change.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-Ignored a subgroup of invalid ``pyproject.toml`` files that use the ``[project]``
-table to specify only ``requires-python`` (**transitional**).
-
-.. warning::
-   Please note that future releases of setuptools will halt the build process
-   if a ``pyproject.toml`` file that does not match doc:`the PyPA Specification
-   ` is given.
diff --git a/changelog.d/3218.change.rst b/changelog.d/3218.change.rst
index 9757943a..c02893e9 100644
--- a/changelog.d/3218.change.rst
+++ b/changelog.d/3218.change.rst
@@ -1,6 +1,6 @@
-Prevented builds from erroring (**temporarily**) if the project specifies
-metadata via ``pyproject.toml``, but uses other files (e.g. ``setup.py``) to
-complement it, without setting ``dynamic`` properly.
+Prevented builds from erroring if the project specifies metadata via
+``pyproject.toml``, but uses other files (e.g. ``setup.py``) to complement it,
+without setting ``dynamic`` properly.
 
 .. important::
    This is a **transitional** behaviour.
-- 
cgit v1.2.1


From 96629b70957a41b5a2d3a0856bff47828d3c09ba Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sun, 27 Mar 2022 15:15:44 -0400
Subject: Move compatibility concerns out of the function to do the adding.

---
 distutils/py39compat.py | 10 +++++-----
 distutils/sysconfig.py  |  2 +-
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/distutils/py39compat.py b/distutils/py39compat.py
index d68cbce7..9de95013 100644
--- a/distutils/py39compat.py
+++ b/distutils/py39compat.py
@@ -2,14 +2,10 @@ import sys
 import platform
 
 
-def ext_suffix(vars):
+def add_ext_suffix_39(vars):
     """
     Ensure vars contains 'EXT_SUFFIX'. pypa/distutils#130
     """
-    if sys.version_info > (3, 10):
-        return
-    if platform.system() != 'Windows':
-        return
     import _imp
     ext_suffix = _imp.extension_suffixes()[0]
     vars.update(
@@ -19,3 +15,7 @@ def ext_suffix(vars):
         # https://github.com/python/cpython/blob/785cc6770588de087d09e89a69110af2542be208/Lib/sysconfig.py#L671-L673
         SO=ext_suffix,
     )
+
+
+needs_ext_suffix = sys.version_info < (3, 10) and platform.system() == 'Windows'
+add_ext_suffix = add_ext_suffix_39 if needs_ext_suffix else lambda vars: None
diff --git a/distutils/sysconfig.py b/distutils/sysconfig.py
index 205d64ce..a76d43ce 100644
--- a/distutils/sysconfig.py
+++ b/distutils/sysconfig.py
@@ -450,7 +450,7 @@ def get_config_vars(*args):
     global _config_vars
     if _config_vars is None:
         _config_vars = sysconfig.get_config_vars().copy()
-        py39compat.ext_suffix(_config_vars)
+        py39compat.add_ext_suffix(_config_vars)
 
     if args:
         vals = []
-- 
cgit v1.2.1


From edb4d680d3c73b7b2184f0fd88e206306ec19537 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 27 Mar 2022 20:21:14 +0100
Subject: Fix invalid link on changelog

---
 CHANGES.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/CHANGES.rst b/CHANGES.rst
index 676cd15e..07a359d9 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -151,7 +151,7 @@ Changes
   in ``setup.cfg`` and ``pyproject.toml`` when ``package_dir`` is implicitly
   found via auto-discovery.
 * #3178: Postponed importing ``ctypes`` when hiding files on Windows.
-  This helps to prevent errors in systems that might not have `libffi` installed.
+  This helps to prevent errors in systems that might not have ``libffi`` installed.
 * #3179: Merge with pypa/distutils@267dbd25ac
 
 Documentation changes
-- 
cgit v1.2.1


From 0ec53b228800300992ba1c53c2f089a435d4970c Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Sun, 27 Mar 2022 20:22:18 +0100
Subject: =?UTF-8?q?Bump=20version:=2061.1.1=20=E2=86=92=2061.2.0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .bumpversion.cfg              |  2 +-
 CHANGES.rst                   | 34 ++++++++++++++++++++++++++++++++++
 changelog.d/3215.change.1.rst |  7 -------
 changelog.d/3215.change.2.rst |  1 -
 changelog.d/3217.doc.rst      |  1 -
 changelog.d/3218.change.rst   |  8 --------
 changelog.d/3223.misc.rst     |  2 --
 changelog.d/3224.change.rst   |  1 -
 setup.cfg                     |  2 +-
 9 files changed, 36 insertions(+), 22 deletions(-)
 delete mode 100644 changelog.d/3215.change.1.rst
 delete mode 100644 changelog.d/3215.change.2.rst
 delete mode 100644 changelog.d/3217.doc.rst
 delete mode 100644 changelog.d/3218.change.rst
 delete mode 100644 changelog.d/3223.misc.rst
 delete mode 100644 changelog.d/3224.change.rst

diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 70ba4d79..e8b7372c 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 61.1.1
+current_version = 61.2.0
 commit = True
 tag = True
 
diff --git a/CHANGES.rst b/CHANGES.rst
index 07a359d9..b4134436 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,3 +1,37 @@
+v61.2.0
+-------
+
+
+Changes
+^^^^^^^
+* #3215: Ignored a subgroup of invalid ``pyproject.toml`` files that use the ``[project]``
+  table to specify only ``requires-python`` (**transitional**).
+
+  .. warning::
+     Please note that future releases of setuptools will halt the build process
+     if a ``pyproject.toml`` file that does not match doc:`the PyPA Specification
+     ` is given.
+* #3215: Updated ``pyproject.toml`` validation, as generated by ``validate-pyproject==0.6.1``.
+* #3218: Prevented builds from erroring if the project specifies metadata via
+  ``pyproject.toml``, but uses other files (e.g. ``setup.py``) to complement it,
+  without setting ``dynamic`` properly.
+
+  .. important::
+     This is a **transitional** behaviour.
+     Future releases of ``setuptools`` may simply ignore externally set metadata
+     not backed by ``dynamic`` or even halt the build with an error.
+* #3224: Merge changes from pypa/distutils@e1d5c9b1f6
+
+Documentation changes
+^^^^^^^^^^^^^^^^^^^^^
+* #3217: Fixed typo in ``pyproject.toml`` example in Quickstart -- by :user:`pablo-cardenas`.
+
+Misc
+^^^^
+* #3223: Fixed missing requirements with environment markers when
+  ``optional-dependencies`` is set in ``pyproject.toml``.
+
+
 v61.1.1
 -------
 
diff --git a/changelog.d/3215.change.1.rst b/changelog.d/3215.change.1.rst
deleted file mode 100644
index a086799e..00000000
--- a/changelog.d/3215.change.1.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-Ignored a subgroup of invalid ``pyproject.toml`` files that use the ``[project]``
-table to specify only ``requires-python`` (**transitional**).
-
-.. warning::
-   Please note that future releases of setuptools will halt the build process
-   if a ``pyproject.toml`` file that does not match doc:`the PyPA Specification
-   ` is given.
diff --git a/changelog.d/3215.change.2.rst b/changelog.d/3215.change.2.rst
deleted file mode 100644
index b3a67f53..00000000
--- a/changelog.d/3215.change.2.rst
+++ /dev/null
@@ -1 +0,0 @@
-Updated ``pyproject.toml`` validation, as generated by ``validate-pyproject==0.6.1``.
diff --git a/changelog.d/3217.doc.rst b/changelog.d/3217.doc.rst
deleted file mode 100644
index 6cc3c969..00000000
--- a/changelog.d/3217.doc.rst
+++ /dev/null
@@ -1 +0,0 @@
-Fixed typo in ``pyproject.toml`` example in Quickstart -- by :user:`pablo-cardenas`.
\ No newline at end of file
diff --git a/changelog.d/3218.change.rst b/changelog.d/3218.change.rst
deleted file mode 100644
index c02893e9..00000000
--- a/changelog.d/3218.change.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-Prevented builds from erroring if the project specifies metadata via
-``pyproject.toml``, but uses other files (e.g. ``setup.py``) to complement it,
-without setting ``dynamic`` properly.
-
-.. important::
-   This is a **transitional** behaviour.
-   Future releases of ``setuptools`` may simply ignore externally set metadata
-   not backed by ``dynamic`` or even halt the build with an error.
diff --git a/changelog.d/3223.misc.rst b/changelog.d/3223.misc.rst
deleted file mode 100644
index 66f1489e..00000000
--- a/changelog.d/3223.misc.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Fixed missing requirements with environment markers when
-``optional-dependencies`` is set in ``pyproject.toml``.
diff --git a/changelog.d/3224.change.rst b/changelog.d/3224.change.rst
deleted file mode 100644
index 5b0b1724..00000000
--- a/changelog.d/3224.change.rst
+++ /dev/null
@@ -1 +0,0 @@
-Merge changes from pypa/distutils@e1d5c9b1f6
diff --git a/setup.cfg b/setup.cfg
index fa376efd..d23d2fd3 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = setuptools
-version = 61.1.1
+version = 61.2.0
 author = Python Packaging Authority
 author_email = distutils-sig@python.org
 description = Easily download, build, install, upgrade, and uninstall Python packages
-- 
cgit v1.2.1


From f2e73a418ec070340c711ae05315ad73c3982799 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sun, 27 Mar 2022 20:23:38 -0400
Subject: Emit warning after parsing. Fixes pypa/distutils#122.

---
 distutils/version.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/distutils/version.py b/distutils/version.py
index 35e181db..31f504e4 100644
--- a/distutils/version.py
+++ b/distutils/version.py
@@ -50,14 +50,14 @@ class Version:
     """
 
     def __init__ (self, vstring=None):
+        if vstring:
+            self.parse(vstring)
         warnings.warn(
             "distutils Version classes are deprecated. "
             "Use packaging.version instead.",
             DeprecationWarning,
             stacklevel=2,
         )
-        if vstring:
-            self.parse(vstring)
 
     def __repr__ (self):
         return "%s ('%s')" % (self.__class__.__name__, str(self))
-- 
cgit v1.2.1


From 2a233e54eba61e6dd6fc0c78f7f844430a758498 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sun, 27 Mar 2022 21:43:59 -0400
Subject: Disable installation of Setuptools in tox instead of GHA. Ref
 pypa/distutils#99.

---
 .github/workflows/main.yml | 4 ----
 tox.ini                    | 2 ++
 2 files changed, 2 insertions(+), 4 deletions(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 6fca2f69..12b049c6 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -6,10 +6,6 @@ concurrency:
   group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
   cancel-in-progress: true
 
-env:
-  # pypa/distutils#99
-  VIRTUALENV_NO_SETUPTOOLS: 1
-
 jobs:
   test:
     strategy:
diff --git a/tox.ini b/tox.ini
index 83d54b2f..2f285175 100644
--- a/tox.ini
+++ b/tox.ini
@@ -5,6 +5,8 @@ commands =
 	pytest {posargs}
 setenv =
     PYTHONPATH = {toxinidir}
+    # pypa/distutils#99
+    VIRTUALENV_NO_SETUPTOOLS = 1
 passenv =
     # workaround for tox-dev/tox#2382
     PROGRAMDATA
-- 
cgit v1.2.1


From 16c3f1f69f6ea005c24b056f33bcd75259d976e5 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 29 Mar 2022 03:00:32 +0100
Subject: Test dist_info creates similar dir to bdist_wheel

---
 setuptools/tests/test_dist_info.py | 70 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 70 insertions(+)

diff --git a/setuptools/tests/test_dist_info.py b/setuptools/tests/test_dist_info.py
index 29fbd09d..7f0e01cc 100644
--- a/setuptools/tests/test_dist_info.py
+++ b/setuptools/tests/test_dist_info.py
@@ -1,12 +1,21 @@
 """Test .dist-info style distributions.
 """
+import pathlib
+import subprocess
+import sys
+from functools import partial
+from unittest.mock import patch
 
 import pytest
 
 import pkg_resources
+from setuptools.archive_util import unpack_archive
 from .textwrap import DALS
 
 
+read = partial(pathlib.Path.read_text, encoding="utf-8")
+
+
 class TestDistInfo:
 
     metadata_base = DALS("""
@@ -72,3 +81,64 @@ class TestDistInfo:
                 pkg_resources.Requirement.parse('quux>=1.1;extra=="baz"'),
             ]
             assert d.extras == ['baz']
+
+
+class TestWheelCompatibility:
+    SETUPCFG = DALS("""
+    [metadata]
+    name = proj
+    version = 42
+
+    [options]
+    install_requires = foo>=12; sys_platform != "linux"
+
+    [options.extras_require]
+    test = pytest
+
+    [options.entry_points]
+    console_scripts =
+        executable-name = my_package.module:function
+    discover =
+        myproj = my_package.other_module:function
+    """)
+
+    FROZEN_TIME = "20220329"
+    EGG_INFO_OPTS = [
+        # Related: #3077 #2872
+        ("", ""),
+        (".post", "[egg_info]\ntag_build = post\n"),
+        (".post", "[egg_info]\ntag_build = .post\n"),
+        (f".post{FROZEN_TIME}", "[egg_info]\ntag_build = post\ntag_date = 1\n"),
+        (".dev", "[egg_info]\ntag_build = .dev\n"),
+        (f".dev{FROZEN_TIME}", "[egg_info]\ntag_build = .dev\ntag_date = 1\n"),
+        ("a1", "[egg_info]\ntag_build = .a1\n"),
+        ("+local", "[egg_info]\ntag_build = +local\n"),
+    ]
+
+    @pytest.mark.parametrize("suffix,cfg", EGG_INFO_OPTS)
+    @patch("setuptools.command.egg_info.time.strftime", FROZEN_TIME)
+    def test_dist_info_is_the_same_as_in_wheel(self, tmp_path, suffix, cfg):
+        config = self.SETUPCFG + cfg
+
+        for i in "dir_wheel", "dir_dist":
+            (tmp_path / i).mkdir()
+            (tmp_path / i / "setup.cfg").write_text(config, encoding="utf-8")
+
+        run_command("bdist_wheel", cwd=tmp_path / "dir_wheel")
+        wheel = next(tmp_path.glob("dir_wheel/dist/*.whl"))
+        unpack_archive(wheel, tmp_path / "unpack")
+        wheel_dist_info = next(tmp_path.glob("unpack/*.dist-info"))
+
+        run_command("dist_info", cwd=tmp_path / "dir_dist")
+        dist_info = next(tmp_path.glob("dir_dist/*.dist-info"))
+
+        assert dist_info.name == wheel_dist_info.name
+        assert dist_info.name.startswith(f"proj-42{suffix}")
+        for file in "METADATA", "entry_points.txt":
+            assert read(dist_info / file) == read(wheel_dist_info / file)
+
+
+def run_command(*cmd, **kwargs):
+    opts = {"stderr": subprocess.STDOUT, "text": True, **kwargs}
+    cmd = [sys.executable, "-c", "__import__('setuptools').setup()", *cmd]
+    return subprocess.check_output(cmd, **opts)
-- 
cgit v1.2.1


From 8bfc5f7164defc24386531e3f45cd223d4e275ba Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 29 Mar 2022 03:03:09 +0100
Subject: Capture expectation of invalid version warning

---
 setuptools/tests/test_dist_info.py | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/setuptools/tests/test_dist_info.py b/setuptools/tests/test_dist_info.py
index 7f0e01cc..1387fcd1 100644
--- a/setuptools/tests/test_dist_info.py
+++ b/setuptools/tests/test_dist_info.py
@@ -1,6 +1,7 @@
 """Test .dist-info style distributions.
 """
 import pathlib
+import re
 import subprocess
 import sys
 from functools import partial
@@ -82,6 +83,15 @@ class TestDistInfo:
             ]
             assert d.extras == ['baz']
 
+    def test_invalid_version(self, tmp_path):
+        config = "[metadata]\nname=proj\nversion=42\n[egg_info]\ntag_build=invalid!!!\n"
+        (tmp_path / "setup.cfg").write_text(config, encoding="utf-8")
+        msg = re.compile("invalid version", re.M | re.I)
+        output = run_command("dist_info", cwd=tmp_path)
+        assert msg.search(output)
+        dist_info = next(tmp_path.glob("*.dist-info"))
+        assert dist_info.name.startswith("proj-42")
+
 
 class TestWheelCompatibility:
     SETUPCFG = DALS("""
-- 
cgit v1.2.1


From cc93191764ed8b5de21369eec53aba32e692389c Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 29 Mar 2022 03:03:34 +0100
Subject: Fix duplicated version tags in egg_info

Previously egg_info was adding duplicated tags to the version string.
This was happening because of the version normalization.
When the version normalization was applied to the string the tag was
modified, then later egg_info could no longer recognize it before
applying.

The fix for this problem was to normalize the tag string before
applying.
---
 setuptools/command/egg_info.py | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py
index 63389654..ea47e519 100644
--- a/setuptools/command/egg_info.py
+++ b/setuptools/command/egg_info.py
@@ -140,13 +140,18 @@ class InfoCommon:
             else version + self.vtags
         )
 
-    def tags(self):
+    def _safe_tags(self, tags: str) -> str:
+        # To implement this we can rely on `safe_version` pretending to be version 0
+        # followed by tags. Then we simply discard the starting 0 (fake version number)
+        return safe_version(f"0{tags}")[1:]
+
+    def tags(self) -> str:
         version = ''
         if self.tag_build:
             version += self.tag_build
         if self.tag_date:
             version += time.strftime("-%Y%m%d")
-        return version
+        return self._safe_tags(version)
     vtags = property(tags)
 
 
-- 
cgit v1.2.1


From cabdd37db15e306060c1b5edcaeb242c218152f8 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 29 Mar 2022 03:37:04 +0100
Subject: Restore tags in egg_info but change the idempotency check

---
 setuptools/command/egg_info.py | 13 +++++++++----
 1 file changed, 9 insertions(+), 4 deletions(-)

diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py
index ea47e519..c37ab81f 100644
--- a/setuptools/command/egg_info.py
+++ b/setuptools/command/egg_info.py
@@ -136,14 +136,19 @@ class InfoCommon:
         in which case the version string already contains all tags.
         """
         return (
-            version if self.vtags and version.endswith(self.vtags)
+            version if self.vtags and self._already_tagged(version)
             else version + self.vtags
         )
 
-    def _safe_tags(self, tags: str) -> str:
+    def _already_tagged(self, version: str) -> bool:
+        # Depending on their format, tags may change with version normalization.
+        # So in addition the regular tags, we have to search for the normalized ones.
+        return version.endswith(self.vtags) or version.endswith(self._safe_tags())
+
+    def _safe_tags(self) -> str:
         # To implement this we can rely on `safe_version` pretending to be version 0
         # followed by tags. Then we simply discard the starting 0 (fake version number)
-        return safe_version(f"0{tags}")[1:]
+        return safe_version(f"0{self.vtags}")[1:]
 
     def tags(self) -> str:
         version = ''
@@ -151,7 +156,7 @@ class InfoCommon:
             version += self.tag_build
         if self.tag_date:
             version += time.strftime("-%Y%m%d")
-        return self._safe_tags(version)
+        return version
     vtags = property(tags)
 
 
-- 
cgit v1.2.1


From 4621b08512ab5c682191c13bf8810d7c200d7e34 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 29 Mar 2022 03:38:32 +0100
Subject: Change dist_info naming to use the same convention as bdist_wheel

---
 setuptools/command/dist_info.py | 34 +++++++++++++++++++++++++++++++++-
 1 file changed, 33 insertions(+), 1 deletion(-)

diff --git a/setuptools/command/dist_info.py b/setuptools/command/dist_info.py
index c45258fa..8b8509f3 100644
--- a/setuptools/command/dist_info.py
+++ b/setuptools/command/dist_info.py
@@ -4,9 +4,13 @@ As defined in the wheel specification
 """
 
 import os
+import re
+import warnings
+from inspect import cleandoc
 
 from distutils.core import Command
 from distutils import log
+from setuptools.extern import packaging
 
 
 class dist_info(Command):
@@ -29,8 +33,36 @@ class dist_info(Command):
         egg_info.egg_base = self.egg_base
         egg_info.finalize_options()
         egg_info.run()
-        dist_info_dir = egg_info.egg_info[:-len('.egg-info')] + '.dist-info'
+        name = _safe(self.distribution.get_name())
+        version = _version(self.distribution.get_version())
+        base = self.egg_base or os.curdir
+        dist_info_dir = os.path.join(base, f"{name}-{version}.dist-info")
         log.info("creating '{}'".format(os.path.abspath(dist_info_dir)))
 
         bdist_wheel = self.get_finalized_command('bdist_wheel')
         bdist_wheel.egg2dist(egg_info.egg_info, dist_info_dir)
+
+
+def _safe(component: str) -> str:
+    """Escape a component used to form a wheel name according to PEP 491"""
+    return re.sub(r"[^\w\d.]+", "_", component)
+
+
+def _version(version: str) -> str:
+    """Convert an arbitrary string to a version string."""
+    v = version.replace(' ', '.')
+    try:
+        return str(packaging.version.Version(v)).replace("-", "_")
+    except packaging.version.InvalidVersion:
+        msg = f"""!!\n\n
+        ###################
+        # Invalid version #
+        ###################
+        {version!r} is not valid according to PEP 440.\n
+        Please make sure specify a valid version for your package.
+        Also note that future releases of setuptools may halt the build process
+        if an invalid version is given.
+        \n\n!!
+        """
+        warnings.warn(cleandoc(msg))
+        return _safe(v).strip("_")
-- 
cgit v1.2.1


From 9360c61dcb52918967335754bf42d6100b987143 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 29 Mar 2022 03:46:39 +0100
Subject: Add comment explaining test

---
 setuptools/tests/test_dist_info.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/setuptools/tests/test_dist_info.py b/setuptools/tests/test_dist_info.py
index 1387fcd1..4c39ea88 100644
--- a/setuptools/tests/test_dist_info.py
+++ b/setuptools/tests/test_dist_info.py
@@ -94,6 +94,9 @@ class TestDistInfo:
 
 
 class TestWheelCompatibility:
+    """Make sure the .dist-info directory produced with the ``dist_info`` command
+    is the same as the one produced by ``bdist_wheel``.
+    """
     SETUPCFG = DALS("""
     [metadata]
     name = proj
-- 
cgit v1.2.1


From 8fdb0c171e2d0c9699d5df82fdf7e1891285e6ae Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 30 Mar 2022 15:28:14 +0100
Subject: Add news fragment

---
 changelog.d/3088.misc.rst | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/3088.misc.rst

diff --git a/changelog.d/3088.misc.rst b/changelog.d/3088.misc.rst
new file mode 100644
index 00000000..c507a824
--- /dev/null
+++ b/changelog.d/3088.misc.rst
@@ -0,0 +1 @@
+Fixed duplicated tag with the ``dist-info`` command.
-- 
cgit v1.2.1


From cc55da0c4afbd128cf58d1cd4862e30bfceba56d Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 30 Mar 2022 19:30:21 +0100
Subject: Separate vendoring script and code generator for pyproject
 validations

---
 setuptools/_vendor/vendored.txt   |  1 -
 tools/generate_validation_code.py | 32 ++++++++++++++++++++++++++++++++
 tools/vendored.py                 | 38 --------------------------------------
 tox.ini                           |  7 +++++++
 4 files changed, 39 insertions(+), 39 deletions(-)
 create mode 100644 tools/generate_validation_code.py

diff --git a/setuptools/_vendor/vendored.txt b/setuptools/_vendor/vendored.txt
index 798e2bab..b08b0d6f 100644
--- a/setuptools/_vendor/vendored.txt
+++ b/setuptools/_vendor/vendored.txt
@@ -11,4 +11,3 @@ typing_extensions==4.0.1
 # required for importlib_resources and _metadata on older Pythons
 zipp==3.7.0
 tomli==2.0.1
-# validate-pyproject[all]==0.6.1  # Special handling in tools/vendored, don't uncomment or remove
diff --git a/tools/generate_validation_code.py b/tools/generate_validation_code.py
new file mode 100644
index 00000000..5792110d
--- /dev/null
+++ b/tools/generate_validation_code.py
@@ -0,0 +1,32 @@
+import string
+import subprocess
+import sys
+from tempfile import TemporaryDirectory
+
+from pathlib import Path
+
+
+def generate_pyproject_validation(dest: Path):
+    """
+    Generates validation code for ``pyproject.toml`` based on JSON schemas and the
+    ``validate-pyproject`` library.
+    """
+    cmd = [
+        sys.executable,
+        "-m",
+        "validate_pyproject.vendoring",
+        f"--output-dir={dest}",
+        "--enable-plugins",
+        "setuptools",
+        "distutils",
+        "--very-verbose"
+    ]
+    subprocess.check_call(cmd)
+    print(f"Validation code generated at: {dest}")
+
+
+def main():
+    generate_pyproject_validation(Path("setuptools/config/_validate_pyproject"))
+
+
+__name__ == '__main__' and main()
diff --git a/tools/vendored.py b/tools/vendored.py
index dc1b0c07..cd15adbf 100644
--- a/tools/vendored.py
+++ b/tools/vendored.py
@@ -1,9 +1,6 @@
 import re
 import sys
-import string
 import subprocess
-import venv
-from tempfile import TemporaryDirectory
 
 from path import Path
 
@@ -140,7 +137,6 @@ def update_pkg_resources():
 def update_setuptools():
     vendor = Path('setuptools/_vendor')
     install(vendor)
-    install_validate_pyproject(vendor)
     rewrite_packaging(vendor / 'packaging', 'setuptools.extern')
     rewrite_jaraco_text(vendor / 'jaraco/text', 'setuptools.extern')
     rewrite_jaraco(vendor / 'jaraco', 'setuptools.extern')
@@ -150,38 +146,4 @@ def update_setuptools():
     rewrite_nspektr(vendor / "nspektr", 'setuptools.extern')
 
 
-def install_validate_pyproject(vendor):
-    """``validate-pyproject`` can be vendorized to remove all dependencies"""
-    req = next(
-        (x for x in (vendor / "vendored.txt").lines() if 'validate-pyproject' in x),
-        "validate-pyproject[all]"
-    )
-
-    pkg, _, _ = req.strip(string.whitespace + "#").partition("#")
-    pkg = pkg.strip()
-
-    opts = {}
-    if sys.version_info[:2] >= (3, 10):
-        opts["ignore_cleanup_errors"] = True
-
-    with TemporaryDirectory(**opts) as tmp:
-        env_builder = venv.EnvBuilder(with_pip=True)
-        env_builder.create(tmp)
-        context = env_builder.ensure_directories(tmp)
-        venv_python = getattr(context, 'env_exec_cmd', context.env_exe)
-
-        subprocess.check_call([venv_python, "-m", "pip", "install", pkg])
-        cmd = [
-            venv_python,
-            "-m",
-            "validate_pyproject.vendoring",
-            f"--output-dir={vendor / '_validate_pyproject' !s}",
-            "--enable-plugins",
-            "setuptools",
-            "distutils",
-            "--very-verbose"
-        ]
-        subprocess.check_call(cmd)
-
-
 __name__ == '__main__' and update_vendored()
diff --git a/tox.ini b/tox.ini
index 22c796ff..1b105d5d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -65,6 +65,13 @@ deps =
 commands =
 	python -m tools.vendored
 
+[testenv:generate-validation-code]
+skip_install = True
+deps =
+	validate-pyproject[all]==0.6.1
+commands =
+	python -m tools.generate_validation_code
+
 [testenv:release]
 skip_install = True
 deps =
-- 
cgit v1.2.1


From 0a5e992ea63b123982df60fdaec5bd2dce5e3248 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 30 Mar 2022 19:41:38 +0100
Subject: Move _validate_pyproject to config

---
 setuptools/_vendor/_validate_pyproject/NOTICE      |  439 ---------
 setuptools/_vendor/_validate_pyproject/__init__.py |   34 -
 .../_vendor/_validate_pyproject/error_reporting.py |  318 -------
 .../_validate_pyproject/extra_validations.py       |   36 -
 .../fastjsonschema_exceptions.py                   |   51 -
 .../fastjsonschema_validations.py                  | 1004 --------------------
 setuptools/_vendor/_validate_pyproject/formats.py  |  252 -----
 setuptools/config/_validate_pyproject/NOTICE       |  439 +++++++++
 setuptools/config/_validate_pyproject/__init__.py  |   34 +
 .../config/_validate_pyproject/error_reporting.py  |  318 +++++++
 .../_validate_pyproject/extra_validations.py       |   36 +
 .../fastjsonschema_exceptions.py                   |   51 +
 .../fastjsonschema_validations.py                  | 1004 ++++++++++++++++++++
 setuptools/config/_validate_pyproject/formats.py   |  252 +++++
 setuptools/config/pyprojecttoml.py                 |   14 +-
 setuptools/extern/__init__.py                      |    3 +-
 16 files changed, 2139 insertions(+), 2146 deletions(-)
 delete mode 100644 setuptools/_vendor/_validate_pyproject/NOTICE
 delete mode 100644 setuptools/_vendor/_validate_pyproject/__init__.py
 delete mode 100644 setuptools/_vendor/_validate_pyproject/error_reporting.py
 delete mode 100644 setuptools/_vendor/_validate_pyproject/extra_validations.py
 delete mode 100644 setuptools/_vendor/_validate_pyproject/fastjsonschema_exceptions.py
 delete mode 100644 setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py
 delete mode 100644 setuptools/_vendor/_validate_pyproject/formats.py
 create mode 100644 setuptools/config/_validate_pyproject/NOTICE
 create mode 100644 setuptools/config/_validate_pyproject/__init__.py
 create mode 100644 setuptools/config/_validate_pyproject/error_reporting.py
 create mode 100644 setuptools/config/_validate_pyproject/extra_validations.py
 create mode 100644 setuptools/config/_validate_pyproject/fastjsonschema_exceptions.py
 create mode 100644 setuptools/config/_validate_pyproject/fastjsonschema_validations.py
 create mode 100644 setuptools/config/_validate_pyproject/formats.py

diff --git a/setuptools/_vendor/_validate_pyproject/NOTICE b/setuptools/_vendor/_validate_pyproject/NOTICE
deleted file mode 100644
index 8ed8325e..00000000
--- a/setuptools/_vendor/_validate_pyproject/NOTICE
+++ /dev/null
@@ -1,439 +0,0 @@
-The code contained in this directory was automatically generated using the
-following command:
-
-    python -m validate_pyproject.vendoring --output-dir=setuptools/_vendor/_validate_pyproject --enable-plugins setuptools distutils --very-verbose
-
-Please avoid changing it manually.
-
-
-You can report issues or suggest changes directly to `validate-pyproject`
-(or to the relevant plugin repository)
-
-- https://github.com/abravalheri/validate-pyproject/issues
-
-
-***
-
-The following files include code from opensource projects
-(either as direct copies or modified versions):
-
-- `fastjsonschema_exceptions.py`:
-    - project: `fastjsonschema` - licensed under BSD-3-Clause
-      (https://github.com/horejsek/python-fastjsonschema)
-- `extra_validations.py` and `format.py`, `error_reporting.py`:
-    - project: `validate-pyproject` - licensed under MPL-2.0
-      (https://github.com/abravalheri/validate-pyproject)
-
-
-Additionally the following files are automatically generated by tools provided
-by the same projects:
-
-- `__init__.py`
-- `fastjsonschema_validations.py`
-
-The relevant copyright notes and licenses are included below.
-
-
-***
-
-`fastjsonschema`
-================
-
-Copyright (c) 2018, Michal Horejsek
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without modification,
-are permitted provided that the following conditions are met:
-
-  Redistributions of source code must retain the above copyright notice, this
-  list of conditions and the following disclaimer.
-
-  Redistributions in binary form must reproduce the above copyright notice, this
-  list of conditions and the following disclaimer in the documentation and/or
-  other materials provided with the distribution.
-
-  Neither the name of the {organization} nor the names of its
-  contributors may be used to endorse or promote products derived from
-  this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
-ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
-ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-
-
-***
-
-`validate-pyproject`
-====================
-
-Mozilla Public License, version 2.0
-
-1. Definitions
-
-1.1. "Contributor"
-
-     means each individual or legal entity that creates, contributes to the
-     creation of, or owns Covered Software.
-
-1.2. "Contributor Version"
-
-     means the combination of the Contributions of others (if any) used by a
-     Contributor and that particular Contributor's Contribution.
-
-1.3. "Contribution"
-
-     means Covered Software of a particular Contributor.
-
-1.4. "Covered Software"
-
-     means Source Code Form to which the initial Contributor has attached the
-     notice in Exhibit A, the Executable Form of such Source Code Form, and
-     Modifications of such Source Code Form, in each case including portions
-     thereof.
-
-1.5. "Incompatible With Secondary Licenses"
-     means
-
-     a. that the initial Contributor has attached the notice described in
-        Exhibit B to the Covered Software; or
-
-     b. that the Covered Software was made available under the terms of
-        version 1.1 or earlier of the License, but not also under the terms of
-        a Secondary License.
-
-1.6. "Executable Form"
-
-     means any form of the work other than Source Code Form.
-
-1.7. "Larger Work"
-
-     means a work that combines Covered Software with other material, in a
-     separate file or files, that is not Covered Software.
-
-1.8. "License"
-
-     means this document.
-
-1.9. "Licensable"
-
-     means having the right to grant, to the maximum extent possible, whether
-     at the time of the initial grant or subsequently, any and all of the
-     rights conveyed by this License.
-
-1.10. "Modifications"
-
-     means any of the following:
-
-     a. any file in Source Code Form that results from an addition to,
-        deletion from, or modification of the contents of Covered Software; or
-
-     b. any new file in Source Code Form that contains any Covered Software.
-
-1.11. "Patent Claims" of a Contributor
-
-      means any patent claim(s), including without limitation, method,
-      process, and apparatus claims, in any patent Licensable by such
-      Contributor that would be infringed, but for the grant of the License,
-      by the making, using, selling, offering for sale, having made, import,
-      or transfer of either its Contributions or its Contributor Version.
-
-1.12. "Secondary License"
-
-      means either the GNU General Public License, Version 2.0, the GNU Lesser
-      General Public License, Version 2.1, the GNU Affero General Public
-      License, Version 3.0, or any later versions of those licenses.
-
-1.13. "Source Code Form"
-
-      means the form of the work preferred for making modifications.
-
-1.14. "You" (or "Your")
-
-      means an individual or a legal entity exercising rights under this
-      License. For legal entities, "You" includes any entity that controls, is
-      controlled by, or is under common control with You. For purposes of this
-      definition, "control" means (a) the power, direct or indirect, to cause
-      the direction or management of such entity, whether by contract or
-      otherwise, or (b) ownership of more than fifty percent (50%) of the
-      outstanding shares or beneficial ownership of such entity.
-
-
-2. License Grants and Conditions
-
-2.1. Grants
-
-     Each Contributor hereby grants You a world-wide, royalty-free,
-     non-exclusive license:
-
-     a. under intellectual property rights (other than patent or trademark)
-        Licensable by such Contributor to use, reproduce, make available,
-        modify, display, perform, distribute, and otherwise exploit its
-        Contributions, either on an unmodified basis, with Modifications, or
-        as part of a Larger Work; and
-
-     b. under Patent Claims of such Contributor to make, use, sell, offer for
-        sale, have made, import, and otherwise transfer either its
-        Contributions or its Contributor Version.
-
-2.2. Effective Date
-
-     The licenses granted in Section 2.1 with respect to any Contribution
-     become effective for each Contribution on the date the Contributor first
-     distributes such Contribution.
-
-2.3. Limitations on Grant Scope
-
-     The licenses granted in this Section 2 are the only rights granted under
-     this License. No additional rights or licenses will be implied from the
-     distribution or licensing of Covered Software under this License.
-     Notwithstanding Section 2.1(b) above, no patent license is granted by a
-     Contributor:
-
-     a. for any code that a Contributor has removed from Covered Software; or
-
-     b. for infringements caused by: (i) Your and any other third party's
-        modifications of Covered Software, or (ii) the combination of its
-        Contributions with other software (except as part of its Contributor
-        Version); or
-
-     c. under Patent Claims infringed by Covered Software in the absence of
-        its Contributions.
-
-     This License does not grant any rights in the trademarks, service marks,
-     or logos of any Contributor (except as may be necessary to comply with
-     the notice requirements in Section 3.4).
-
-2.4. Subsequent Licenses
-
-     No Contributor makes additional grants as a result of Your choice to
-     distribute the Covered Software under a subsequent version of this
-     License (see Section 10.2) or under the terms of a Secondary License (if
-     permitted under the terms of Section 3.3).
-
-2.5. Representation
-
-     Each Contributor represents that the Contributor believes its
-     Contributions are its original creation(s) or it has sufficient rights to
-     grant the rights to its Contributions conveyed by this License.
-
-2.6. Fair Use
-
-     This License is not intended to limit any rights You have under
-     applicable copyright doctrines of fair use, fair dealing, or other
-     equivalents.
-
-2.7. Conditions
-
-     Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
-     Section 2.1.
-
-
-3. Responsibilities
-
-3.1. Distribution of Source Form
-
-     All distribution of Covered Software in Source Code Form, including any
-     Modifications that You create or to which You contribute, must be under
-     the terms of this License. You must inform recipients that the Source
-     Code Form of the Covered Software is governed by the terms of this
-     License, and how they can obtain a copy of this License. You may not
-     attempt to alter or restrict the recipients' rights in the Source Code
-     Form.
-
-3.2. Distribution of Executable Form
-
-     If You distribute Covered Software in Executable Form then:
-
-     a. such Covered Software must also be made available in Source Code Form,
-        as described in Section 3.1, and You must inform recipients of the
-        Executable Form how they can obtain a copy of such Source Code Form by
-        reasonable means in a timely manner, at a charge no more than the cost
-        of distribution to the recipient; and
-
-     b. You may distribute such Executable Form under the terms of this
-        License, or sublicense it under different terms, provided that the
-        license for the Executable Form does not attempt to limit or alter the
-        recipients' rights in the Source Code Form under this License.
-
-3.3. Distribution of a Larger Work
-
-     You may create and distribute a Larger Work under terms of Your choice,
-     provided that You also comply with the requirements of this License for
-     the Covered Software. If the Larger Work is a combination of Covered
-     Software with a work governed by one or more Secondary Licenses, and the
-     Covered Software is not Incompatible With Secondary Licenses, this
-     License permits You to additionally distribute such Covered Software
-     under the terms of such Secondary License(s), so that the recipient of
-     the Larger Work may, at their option, further distribute the Covered
-     Software under the terms of either this License or such Secondary
-     License(s).
-
-3.4. Notices
-
-     You may not remove or alter the substance of any license notices
-     (including copyright notices, patent notices, disclaimers of warranty, or
-     limitations of liability) contained within the Source Code Form of the
-     Covered Software, except that You may alter any license notices to the
-     extent required to remedy known factual inaccuracies.
-
-3.5. Application of Additional Terms
-
-     You may choose to offer, and to charge a fee for, warranty, support,
-     indemnity or liability obligations to one or more recipients of Covered
-     Software. However, You may do so only on Your own behalf, and not on
-     behalf of any Contributor. You must make it absolutely clear that any
-     such warranty, support, indemnity, or liability obligation is offered by
-     You alone, and You hereby agree to indemnify every Contributor for any
-     liability incurred by such Contributor as a result of warranty, support,
-     indemnity or liability terms You offer. You may include additional
-     disclaimers of warranty and limitations of liability specific to any
-     jurisdiction.
-
-4. Inability to Comply Due to Statute or Regulation
-
-   If it is impossible for You to comply with any of the terms of this License
-   with respect to some or all of the Covered Software due to statute,
-   judicial order, or regulation then You must: (a) comply with the terms of
-   this License to the maximum extent possible; and (b) describe the
-   limitations and the code they affect. Such description must be placed in a
-   text file included with all distributions of the Covered Software under
-   this License. Except to the extent prohibited by statute or regulation,
-   such description must be sufficiently detailed for a recipient of ordinary
-   skill to be able to understand it.
-
-5. Termination
-
-5.1. The rights granted under this License will terminate automatically if You
-     fail to comply with any of its terms. However, if You become compliant,
-     then the rights granted under this License from a particular Contributor
-     are reinstated (a) provisionally, unless and until such Contributor
-     explicitly and finally terminates Your grants, and (b) on an ongoing
-     basis, if such Contributor fails to notify You of the non-compliance by
-     some reasonable means prior to 60 days after You have come back into
-     compliance. Moreover, Your grants from a particular Contributor are
-     reinstated on an ongoing basis if such Contributor notifies You of the
-     non-compliance by some reasonable means, this is the first time You have
-     received notice of non-compliance with this License from such
-     Contributor, and You become compliant prior to 30 days after Your receipt
-     of the notice.
-
-5.2. If You initiate litigation against any entity by asserting a patent
-     infringement claim (excluding declaratory judgment actions,
-     counter-claims, and cross-claims) alleging that a Contributor Version
-     directly or indirectly infringes any patent, then the rights granted to
-     You by any and all Contributors for the Covered Software under Section
-     2.1 of this License shall terminate.
-
-5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
-     license agreements (excluding distributors and resellers) which have been
-     validly granted by You or Your distributors under this License prior to
-     termination shall survive termination.
-
-6. Disclaimer of Warranty
-
-   Covered Software is provided under this License on an "as is" basis,
-   without warranty of any kind, either expressed, implied, or statutory,
-   including, without limitation, warranties that the Covered Software is free
-   of defects, merchantable, fit for a particular purpose or non-infringing.
-   The entire risk as to the quality and performance of the Covered Software
-   is with You. Should any Covered Software prove defective in any respect,
-   You (not any Contributor) assume the cost of any necessary servicing,
-   repair, or correction. This disclaimer of warranty constitutes an essential
-   part of this License. No use of  any Covered Software is authorized under
-   this License except under this disclaimer.
-
-7. Limitation of Liability
-
-   Under no circumstances and under no legal theory, whether tort (including
-   negligence), contract, or otherwise, shall any Contributor, or anyone who
-   distributes Covered Software as permitted above, be liable to You for any
-   direct, indirect, special, incidental, or consequential damages of any
-   character including, without limitation, damages for lost profits, loss of
-   goodwill, work stoppage, computer failure or malfunction, or any and all
-   other commercial damages or losses, even if such party shall have been
-   informed of the possibility of such damages. This limitation of liability
-   shall not apply to liability for death or personal injury resulting from
-   such party's negligence to the extent applicable law prohibits such
-   limitation. Some jurisdictions do not allow the exclusion or limitation of
-   incidental or consequential damages, so this exclusion and limitation may
-   not apply to You.
-
-8. Litigation
-
-   Any litigation relating to this License may be brought only in the courts
-   of a jurisdiction where the defendant maintains its principal place of
-   business and such litigation shall be governed by laws of that
-   jurisdiction, without reference to its conflict-of-law provisions. Nothing
-   in this Section shall prevent a party's ability to bring cross-claims or
-   counter-claims.
-
-9. Miscellaneous
-
-   This License represents the complete agreement concerning the subject
-   matter hereof. If any provision of this License is held to be
-   unenforceable, such provision shall be reformed only to the extent
-   necessary to make it enforceable. Any law or regulation which provides that
-   the language of a contract shall be construed against the drafter shall not
-   be used to construe this License against a Contributor.
-
-
-10. Versions of the License
-
-10.1. New Versions
-
-      Mozilla Foundation is the license steward. Except as provided in Section
-      10.3, no one other than the license steward has the right to modify or
-      publish new versions of this License. Each version will be given a
-      distinguishing version number.
-
-10.2. Effect of New Versions
-
-      You may distribute the Covered Software under the terms of the version
-      of the License under which You originally received the Covered Software,
-      or under the terms of any subsequent version published by the license
-      steward.
-
-10.3. Modified Versions
-
-      If you create software not governed by this License, and you want to
-      create a new license for such software, you may create and use a
-      modified version of this License if you rename the license and remove
-      any references to the name of the license steward (except to note that
-      such modified license differs from this License).
-
-10.4. Distributing Source Code Form that is Incompatible With Secondary
-      Licenses If You choose to distribute Source Code Form that is
-      Incompatible With Secondary Licenses under the terms of this version of
-      the License, the notice described in Exhibit B of this License must be
-      attached.
-
-Exhibit A - Source Code Form License Notice
-
-      This Source Code Form is subject to the
-      terms of the Mozilla Public License, v.
-      2.0. If a copy of the MPL was not
-      distributed with this file, You can
-      obtain one at
-      https://mozilla.org/MPL/2.0/.
-
-If it is not possible or desirable to put the notice in a particular file,
-then You may include the notice in a location (such as a LICENSE file in a
-relevant directory) where a recipient would be likely to look for such a
-notice.
-
-You may add additional accurate notices of copyright ownership.
-
-Exhibit B - "Incompatible With Secondary Licenses" Notice
-
-      This Source Code Form is "Incompatible
-      With Secondary Licenses", as defined by
-      the Mozilla Public License, v. 2.0.
-
diff --git a/setuptools/_vendor/_validate_pyproject/__init__.py b/setuptools/_vendor/_validate_pyproject/__init__.py
deleted file mode 100644
index dbe6cb4c..00000000
--- a/setuptools/_vendor/_validate_pyproject/__init__.py
+++ /dev/null
@@ -1,34 +0,0 @@
-from functools import reduce
-from typing import Any, Callable, Dict
-
-from . import formats
-from .error_reporting import detailed_errors, ValidationError
-from .extra_validations import EXTRA_VALIDATIONS
-from .fastjsonschema_exceptions import JsonSchemaException, JsonSchemaValueException
-from .fastjsonschema_validations import validate as _validate
-
-__all__ = [
-    "validate",
-    "FORMAT_FUNCTIONS",
-    "EXTRA_VALIDATIONS",
-    "ValidationError",
-    "JsonSchemaException",
-    "JsonSchemaValueException",
-]
-
-
-FORMAT_FUNCTIONS: Dict[str, Callable[[str], bool]] = {
-    fn.__name__.replace("_", "-"): fn
-    for fn in formats.__dict__.values()
-    if callable(fn) and not fn.__name__.startswith("_")
-}
-
-
-def validate(data: Any) -> bool:
-    """Validate the given ``data`` object using JSON Schema
-    This function raises ``ValidationError`` if ``data`` is invalid.
-    """
-    with detailed_errors():
-        _validate(data, custom_formats=FORMAT_FUNCTIONS)
-    reduce(lambda acc, fn: fn(acc), EXTRA_VALIDATIONS, data)
-    return True
diff --git a/setuptools/_vendor/_validate_pyproject/error_reporting.py b/setuptools/_vendor/_validate_pyproject/error_reporting.py
deleted file mode 100644
index 3a4d4e9e..00000000
--- a/setuptools/_vendor/_validate_pyproject/error_reporting.py
+++ /dev/null
@@ -1,318 +0,0 @@
-import io
-import json
-import logging
-import os
-import re
-from contextlib import contextmanager
-from textwrap import indent, wrap
-from typing import Any, Dict, Iterator, List, Optional, Sequence, Union, cast
-
-from .fastjsonschema_exceptions import JsonSchemaValueException
-
-_logger = logging.getLogger(__name__)
-
-_MESSAGE_REPLACEMENTS = {
-    "must be named by propertyName definition": "keys must be named by",
-    "one of contains definition": "at least one item that matches",
-    " same as const definition:": "",
-    "only specified items": "only items matching the definition",
-}
-
-_SKIP_DETAILS = (
-    "must not be empty",
-    "is always invalid",
-    "must not be there",
-)
-
-_NEED_DETAILS = {"anyOf", "oneOf", "anyOf", "contains", "propertyNames", "not", "items"}
-
-_CAMEL_CASE_SPLITTER = re.compile(r"\W+|([A-Z][^A-Z\W]*)")
-_IDENTIFIER = re.compile(r"^[\w_]+$", re.I)
-
-_TOML_JARGON = {
-    "object": "table",
-    "property": "key",
-    "properties": "keys",
-    "property names": "keys",
-}
-
-
-class ValidationError(JsonSchemaValueException):
-    """Report violations of a given JSON schema.
-
-    This class extends :exc:`~fastjsonschema.JsonSchemaValueException`
-    by adding the following properties:
-
-    - ``summary``: an improved version of the ``JsonSchemaValueException`` error message
-      with only the necessary information)
-
-    - ``details``: more contextual information about the error like the failing schema
-      itself and the value that violates the schema.
-
-    Depending on the level of the verbosity of the ``logging`` configuration
-    the exception message will be only ``summary`` (default) or a combination of
-    ``summary`` and ``details`` (when the logging level is set to :obj:`logging.DEBUG`).
-    """
-
-    summary = ""
-    details = ""
-    _original_message = ""
-
-    @classmethod
-    def _from_jsonschema(cls, ex: JsonSchemaValueException):
-        formatter = _ErrorFormatting(ex)
-        obj = cls(str(formatter), ex.value, formatter.name, ex.definition, ex.rule)
-        debug_code = os.getenv("JSONSCHEMA_DEBUG_CODE_GENERATION", "false").lower()
-        if debug_code != "false":  # pragma: no cover
-            obj.__cause__, obj.__traceback__ = ex.__cause__, ex.__traceback__
-        obj._original_message = ex.message
-        obj.summary = formatter.summary
-        obj.details = formatter.details
-        return obj
-
-
-@contextmanager
-def detailed_errors():
-    try:
-        yield
-    except JsonSchemaValueException as ex:
-        raise ValidationError._from_jsonschema(ex) from None
-
-
-class _ErrorFormatting:
-    def __init__(self, ex: JsonSchemaValueException):
-        self.ex = ex
-        self.name = f"`{self._simplify_name(ex.name)}`"
-        self._original_message = self.ex.message.replace(ex.name, self.name)
-        self._summary = ""
-        self._details = ""
-
-    def __str__(self) -> str:
-        if _logger.getEffectiveLevel() <= logging.DEBUG and self.details:
-            return f"{self.summary}\n\n{self.details}"
-
-        return self.summary
-
-    @property
-    def summary(self) -> str:
-        if not self._summary:
-            self._summary = self._expand_summary()
-
-        return self._summary
-
-    @property
-    def details(self) -> str:
-        if not self._details:
-            self._details = self._expand_details()
-
-        return self._details
-
-    def _simplify_name(self, name):
-        x = len("data.")
-        return name[x:] if name.startswith("data.") else name
-
-    def _expand_summary(self):
-        msg = self._original_message
-
-        for bad, repl in _MESSAGE_REPLACEMENTS.items():
-            msg = msg.replace(bad, repl)
-
-        if any(substring in msg for substring in _SKIP_DETAILS):
-            return msg
-
-        schema = self.ex.rule_definition
-        if self.ex.rule in _NEED_DETAILS and schema:
-            summary = _SummaryWriter(_TOML_JARGON)
-            return f"{msg}:\n\n{indent(summary(schema), '    ')}"
-
-        return msg
-
-    def _expand_details(self) -> str:
-        optional = []
-        desc_lines = self.ex.definition.pop("$$description", [])
-        desc = self.ex.definition.pop("description", None) or " ".join(desc_lines)
-        if desc:
-            description = "\n".join(
-                wrap(
-                    desc,
-                    width=80,
-                    initial_indent="    ",
-                    subsequent_indent="    ",
-                    break_long_words=False,
-                )
-            )
-            optional.append(f"DESCRIPTION:\n{description}")
-        schema = json.dumps(self.ex.definition, indent=4)
-        value = json.dumps(self.ex.value, indent=4)
-        defaults = [
-            f"GIVEN VALUE:\n{indent(value, '    ')}",
-            f"OFFENDING RULE: {self.ex.rule!r}",
-            f"DEFINITION:\n{indent(schema, '    ')}",
-        ]
-        return "\n\n".join(optional + defaults)
-
-
-class _SummaryWriter:
-    _IGNORE = {"description", "default", "title", "examples"}
-
-    def __init__(self, jargon: Optional[Dict[str, str]] = None):
-        self.jargon: Dict[str, str] = jargon or {}
-        # Clarify confusing terms
-        self._terms = {
-            "anyOf": "at least one of the following",
-            "oneOf": "exactly one of the following",
-            "allOf": "all of the following",
-            "not": "(*NOT* the following)",
-            "prefixItems": f"{self._jargon('items')} (in order)",
-            "items": "items",
-            "contains": "contains at least one of",
-            "propertyNames": (
-                f"non-predefined acceptable {self._jargon('property names')}"
-            ),
-            "patternProperties": f"{self._jargon('properties')} named via pattern",
-            "const": "predefined value",
-            "enum": "one of",
-        }
-        # Attributes that indicate that the definition is easy and can be done
-        # inline (e.g. string and number)
-        self._guess_inline_defs = [
-            "enum",
-            "const",
-            "maxLength",
-            "minLength",
-            "pattern",
-            "format",
-            "minimum",
-            "maximum",
-            "exclusiveMinimum",
-            "exclusiveMaximum",
-            "multipleOf",
-        ]
-
-    def _jargon(self, term: Union[str, List[str]]) -> Union[str, List[str]]:
-        if isinstance(term, list):
-            return [self.jargon.get(t, t) for t in term]
-        return self.jargon.get(term, term)
-
-    def __call__(
-        self,
-        schema: Union[dict, List[dict]],
-        prefix: str = "",
-        *,
-        _path: Sequence[str] = (),
-    ) -> str:
-        if isinstance(schema, list):
-            return self._handle_list(schema, prefix, _path)
-
-        filtered = self._filter_unecessary(schema, _path)
-        simple = self._handle_simple_dict(filtered, _path)
-        if simple:
-            return f"{prefix}{simple}"
-
-        child_prefix = self._child_prefix(prefix, "  ")
-        item_prefix = self._child_prefix(prefix, "- ")
-        indent = len(prefix) * " "
-        with io.StringIO() as buffer:
-            for i, (key, value) in enumerate(filtered.items()):
-                child_path = [*_path, key]
-                line_prefix = prefix if i == 0 else indent
-                buffer.write(f"{line_prefix}{self._label(child_path)}:")
-                # ^  just the first item should receive the complete prefix
-                if isinstance(value, dict):
-                    filtered = self._filter_unecessary(value, child_path)
-                    simple = self._handle_simple_dict(filtered, child_path)
-                    buffer.write(
-                        f" {simple}"
-                        if simple
-                        else f"\n{self(value, child_prefix, _path=child_path)}"
-                    )
-                elif isinstance(value, list) and (
-                    key != "type" or self._is_property(child_path)
-                ):
-                    children = self._handle_list(value, item_prefix, child_path)
-                    sep = " " if children.startswith("[") else "\n"
-                    buffer.write(f"{sep}{children}")
-                else:
-                    buffer.write(f" {self._value(value, child_path)}\n")
-            return buffer.getvalue()
-
-    def _is_unecessary(self, path: Sequence[str]) -> bool:
-        if self._is_property(path) or not path:  # empty path => instruction @ root
-            return False
-        key = path[-1]
-        return any(key.startswith(k) for k in "$_") or key in self._IGNORE
-
-    def _filter_unecessary(self, schema: dict, path: Sequence[str]):
-        return {
-            key: value
-            for key, value in schema.items()
-            if not self._is_unecessary([*path, key])
-        }
-
-    def _handle_simple_dict(self, value: dict, path: Sequence[str]) -> Optional[str]:
-        inline = any(p in value for p in self._guess_inline_defs)
-        simple = not any(isinstance(v, (list, dict)) for v in value.values())
-        if inline or simple:
-            return f"{{{', '.join(self._inline_attrs(value, path))}}}\n"
-        return None
-
-    def _handle_list(
-        self, schemas: list, prefix: str = "", path: Sequence[str] = ()
-    ) -> str:
-        if self._is_unecessary(path):
-            return ""
-
-        repr_ = repr(schemas)
-        if all(not isinstance(e, (dict, list)) for e in schemas) and len(repr_) < 60:
-            return f"{repr_}\n"
-
-        item_prefix = self._child_prefix(prefix, "- ")
-        return "".join(
-            self(v, item_prefix, _path=[*path, f"[{i}]"]) for i, v in enumerate(schemas)
-        )
-
-    def _is_property(self, path: Sequence[str]):
-        """Check if the given path can correspond to an arbitrarily named property"""
-        counter = 0
-        for key in path[-2::-1]:
-            if key not in {"properties", "patternProperties"}:
-                break
-            counter += 1
-
-        # If the counter if even, the path correspond to a JSON Schema keyword
-        # otherwise it can be any arbitrary string naming a property
-        return counter % 2 == 1
-
-    def _label(self, path: Sequence[str]) -> str:
-        *parents, key = path
-        if not self._is_property(path):
-            norm_key = _separate_terms(key)
-            return self._terms.get(key) or " ".join(self._jargon(norm_key))
-
-        if parents[-1] == "patternProperties":
-            return f"(regex {key!r})"
-        return repr(key)  # property name
-
-    def _value(self, value: Any, path: Sequence[str]) -> str:
-        if path[-1] == "type" and not self._is_property(path):
-            type_ = self._jargon(value)
-            return (
-                f"[{', '.join(type_)}]" if isinstance(value, list) else cast(str, type_)
-            )
-        return repr(value)
-
-    def _inline_attrs(self, schema: dict, path: Sequence[str]) -> Iterator[str]:
-        for key, value in schema.items():
-            child_path = [*path, key]
-            yield f"{self._label(child_path)}: {self._value(value, child_path)}"
-
-    def _child_prefix(self, parent_prefix: str, child_prefix: str) -> str:
-        return len(parent_prefix) * " " + child_prefix
-
-
-def _separate_terms(word: str) -> List[str]:
-    """
-    >>> _separate_terms("FooBar-foo")
-    "foo bar foo"
-    """
-    return [w.lower() for w in _CAMEL_CASE_SPLITTER.split(word) if w]
diff --git a/setuptools/_vendor/_validate_pyproject/extra_validations.py b/setuptools/_vendor/_validate_pyproject/extra_validations.py
deleted file mode 100644
index 48c4e257..00000000
--- a/setuptools/_vendor/_validate_pyproject/extra_validations.py
+++ /dev/null
@@ -1,36 +0,0 @@
-"""The purpose of this module is implement PEP 621 validations that are
-difficult to express as a JSON Schema (or that are not supported by the current
-JSON Schema library).
-"""
-
-from typing import Mapping, TypeVar
-
-from .fastjsonschema_exceptions import JsonSchemaValueException
-
-T = TypeVar("T", bound=Mapping)
-
-
-class RedefiningStaticFieldAsDynamic(JsonSchemaValueException):
-    """According to PEP 621:
-
-    Build back-ends MUST raise an error if the metadata specifies a field
-    statically as well as being listed in dynamic.
-    """
-
-
-def validate_project_dynamic(pyproject: T) -> T:
-    project_table = pyproject.get("project", {})
-    dynamic = project_table.get("dynamic", [])
-
-    for field in dynamic:
-        if field in project_table:
-            msg = f"You cannot provide a value for `project.{field}` and "
-            msg += "list it under `project.dynamic` at the same time"
-            name = f"data.project.{field}"
-            value = {field: project_table[field], "...": " # ...", "dynamic": dynamic}
-            raise RedefiningStaticFieldAsDynamic(msg, value, name, rule="PEP 621")
-
-    return pyproject
-
-
-EXTRA_VALIDATIONS = (validate_project_dynamic,)
diff --git a/setuptools/_vendor/_validate_pyproject/fastjsonschema_exceptions.py b/setuptools/_vendor/_validate_pyproject/fastjsonschema_exceptions.py
deleted file mode 100644
index d2dddd6a..00000000
--- a/setuptools/_vendor/_validate_pyproject/fastjsonschema_exceptions.py
+++ /dev/null
@@ -1,51 +0,0 @@
-import re
-
-
-SPLIT_RE = re.compile(r'[\.\[\]]+')
-
-
-class JsonSchemaException(ValueError):
-    """
-    Base exception of ``fastjsonschema`` library.
-    """
-
-
-class JsonSchemaValueException(JsonSchemaException):
-    """
-    Exception raised by validation function. Available properties:
-
-     * ``message`` containing human-readable information what is wrong (e.g. ``data.property[index] must be smaller than or equal to 42``),
-     * invalid ``value`` (e.g. ``60``),
-     * ``name`` of a path in the data structure (e.g. ``data.property[index]``),
-     * ``path`` as an array in the data structure (e.g. ``['data', 'property', 'index']``),
-     * the whole ``definition`` which the ``value`` has to fulfil (e.g. ``{'type': 'number', 'maximum': 42}``),
-     * ``rule`` which the ``value`` is breaking (e.g. ``maximum``)
-     * and ``rule_definition`` (e.g. ``42``).
-
-    .. versionchanged:: 2.14.0
-        Added all extra properties.
-    """
-
-    def __init__(self, message, value=None, name=None, definition=None, rule=None):
-        super().__init__(message)
-        self.message = message
-        self.value = value
-        self.name = name
-        self.definition = definition
-        self.rule = rule
-
-    @property
-    def path(self):
-        return [item for item in SPLIT_RE.split(self.name) if item != '']
-
-    @property
-    def rule_definition(self):
-        if not self.rule or not self.definition:
-            return None
-        return self.definition.get(self.rule)
-
-
-class JsonSchemaDefinitionException(JsonSchemaException):
-    """
-    Exception raised by generator of validation function.
-    """
diff --git a/setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py b/setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py
deleted file mode 100644
index 3ad1edd0..00000000
--- a/setuptools/_vendor/_validate_pyproject/fastjsonschema_validations.py
+++ /dev/null
@@ -1,1004 +0,0 @@
-# noqa
-# type: ignore
-# flake8: noqa
-# pylint: skip-file
-# mypy: ignore-errors
-# yapf: disable
-# pylama:skip=1
-
-
-# *** PLEASE DO NOT MODIFY DIRECTLY: Automatically generated code *** 
-
-
-VERSION = "2.15.3"
-import re
-from .fastjsonschema_exceptions import JsonSchemaValueException
-
-
-REGEX_PATTERNS = {
-    '^.*$': re.compile('^.*$'),
-    '.+': re.compile('.+'),
-    '^.+$': re.compile('^.+$'),
-    'idn-email_re_pattern': re.compile('^[^@]+@[^@]+\\.[^@]+\\Z')
-}
-
-NoneType = type(None)
-
-def validate(data, custom_formats={}, name_prefix=None):
-    validate_https___packaging_python_org_en_latest_specifications_declaring_build_dependencies(data, custom_formats, (name_prefix or "data") + "")
-    return data
-
-def validate_https___packaging_python_org_en_latest_specifications_declaring_build_dependencies(data, custom_formats={}, name_prefix=None):
-    if not isinstance(data, (dict)):
-        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-build-dependencies/', 'title': 'Data structure for ``pyproject.toml`` files', '$$description': ['File format containing build-time configurations for the Python ecosystem. ', ':pep:`517` initially defined a build-system independent format for source trees', 'which was complemented by :pep:`518` to provide a way of specifying dependencies ', 'for building Python projects.', 'Please notice the ``project`` table (as initially defined in  :pep:`621`) is not included', 'in this schema and should be considered separately.'], 'type': 'object', 'additionalProperties': False, 'properties': {'build-system': {'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['dynamic'], 'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}}}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'required': ['version'], '$$description': ['version should be statically defined in the ``version`` field']}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, 'tool': {'type': 'object', 'properties': {'distutils': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://docs.python.org/3/install/', 'title': '``tool.distutils`` table', '$$description': ['Originally, ``distutils`` allowed developers to configure arguments for', '``setup.py`` scripts via `distutils configuration files', '`_.', '``tool.distutils`` subtables could be used with the same purpose', '(NOT CURRENTLY IMPLEMENTED).'], 'type': 'object', 'properties': {'global': {'type': 'object', 'description': 'Global options applied to all ``distutils`` commands'}}, 'patternProperties': {'.+': {'type': 'object'}}, '$comment': 'TODO: Is there a practical way of making this schema more specific?'}, 'setuptools': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$ref': '#/definitions/find-directive'}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might become standard with PEP 639).'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Probably ``project.license-files``?'}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'additionalProperties': False, 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'$ref': '#/definitions/attr-directive'}, {'$ref': '#/definitions/file-directive'}]}, 'classifiers': {'$ref': '#/definitions/file-directive'}, 'description': {'$ref': '#/definitions/file-directive'}, 'entry-points': {'$ref': '#/definitions/file-directive'}, 'readme': {'anyOf': [{'$ref': '#/definitions/file-directive'}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}}}}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['dynamic'], 'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}}}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'required': ['version'], '$$description': ['version should be statically defined in the ``version`` field']}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, rule='type')
-    data_is_dict = isinstance(data, dict)
-    if data_is_dict:
-        data_keys = set(data.keys())
-        if "build-system" in data_keys:
-            data_keys.remove("build-system")
-            data__buildsystem = data["build-system"]
-            if not isinstance(data__buildsystem, (dict)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".build-system must be object", value=data__buildsystem, name="" + (name_prefix or "data") + ".build-system", definition={'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, rule='type')
-            data__buildsystem_is_dict = isinstance(data__buildsystem, dict)
-            if data__buildsystem_is_dict:
-                data__buildsystem_len = len(data__buildsystem)
-                if not all(prop in data__buildsystem for prop in ['requires']):
-                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".build-system must contain ['requires'] properties", value=data__buildsystem, name="" + (name_prefix or "data") + ".build-system", definition={'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, rule='required')
-                data__buildsystem_keys = set(data__buildsystem.keys())
-                if "requires" in data__buildsystem_keys:
-                    data__buildsystem_keys.remove("requires")
-                    data__buildsystem__requires = data__buildsystem["requires"]
-                    if not isinstance(data__buildsystem__requires, (list, tuple)):
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".build-system.requires must be array", value=data__buildsystem__requires, name="" + (name_prefix or "data") + ".build-system.requires", definition={'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, rule='type')
-                    data__buildsystem__requires_is_list = isinstance(data__buildsystem__requires, (list, tuple))
-                    if data__buildsystem__requires_is_list:
-                        data__buildsystem__requires_len = len(data__buildsystem__requires)
-                        for data__buildsystem__requires_x, data__buildsystem__requires_item in enumerate(data__buildsystem__requires):
-                            if not isinstance(data__buildsystem__requires_item, (str)):
-                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".build-system.requires[{data__buildsystem__requires_x}]".format(**locals()) + " must be string", value=data__buildsystem__requires_item, name="" + (name_prefix or "data") + ".build-system.requires[{data__buildsystem__requires_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
-                if "build-backend" in data__buildsystem_keys:
-                    data__buildsystem_keys.remove("build-backend")
-                    data__buildsystem__buildbackend = data__buildsystem["build-backend"]
-                    if not isinstance(data__buildsystem__buildbackend, (str)):
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".build-system.build-backend must be string", value=data__buildsystem__buildbackend, name="" + (name_prefix or "data") + ".build-system.build-backend", definition={'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, rule='type')
-                    if isinstance(data__buildsystem__buildbackend, str):
-                        if not custom_formats["pep517-backend-reference"](data__buildsystem__buildbackend):
-                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".build-system.build-backend must be pep517-backend-reference", value=data__buildsystem__buildbackend, name="" + (name_prefix or "data") + ".build-system.build-backend", definition={'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, rule='format')
-                if "backend-path" in data__buildsystem_keys:
-                    data__buildsystem_keys.remove("backend-path")
-                    data__buildsystem__backendpath = data__buildsystem["backend-path"]
-                    if not isinstance(data__buildsystem__backendpath, (list, tuple)):
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".build-system.backend-path must be array", value=data__buildsystem__backendpath, name="" + (name_prefix or "data") + ".build-system.backend-path", definition={'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}, rule='type')
-                    data__buildsystem__backendpath_is_list = isinstance(data__buildsystem__backendpath, (list, tuple))
-                    if data__buildsystem__backendpath_is_list:
-                        data__buildsystem__backendpath_len = len(data__buildsystem__backendpath)
-                        for data__buildsystem__backendpath_x, data__buildsystem__backendpath_item in enumerate(data__buildsystem__backendpath):
-                            if not isinstance(data__buildsystem__backendpath_item, (str)):
-                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".build-system.backend-path[{data__buildsystem__backendpath_x}]".format(**locals()) + " must be string", value=data__buildsystem__backendpath_item, name="" + (name_prefix or "data") + ".build-system.backend-path[{data__buildsystem__backendpath_x}]".format(**locals()) + "", definition={'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}, rule='type')
-                if data__buildsystem_keys:
-                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".build-system must not contain "+str(data__buildsystem_keys)+" properties", value=data__buildsystem, name="" + (name_prefix or "data") + ".build-system", definition={'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, rule='additionalProperties')
-        if "project" in data_keys:
-            data_keys.remove("project")
-            data__project = data["project"]
-            validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata(data__project, custom_formats, (name_prefix or "data") + ".project")
-        if "tool" in data_keys:
-            data_keys.remove("tool")
-            data__tool = data["tool"]
-            if not isinstance(data__tool, (dict)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".tool must be object", value=data__tool, name="" + (name_prefix or "data") + ".tool", definition={'type': 'object', 'properties': {'distutils': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://docs.python.org/3/install/', 'title': '``tool.distutils`` table', '$$description': ['Originally, ``distutils`` allowed developers to configure arguments for', '``setup.py`` scripts via `distutils configuration files', '`_.', '``tool.distutils`` subtables could be used with the same purpose', '(NOT CURRENTLY IMPLEMENTED).'], 'type': 'object', 'properties': {'global': {'type': 'object', 'description': 'Global options applied to all ``distutils`` commands'}}, 'patternProperties': {'.+': {'type': 'object'}}, '$comment': 'TODO: Is there a practical way of making this schema more specific?'}, 'setuptools': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$ref': '#/definitions/find-directive'}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might become standard with PEP 639).'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Probably ``project.license-files``?'}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'additionalProperties': False, 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'$ref': '#/definitions/attr-directive'}, {'$ref': '#/definitions/file-directive'}]}, 'classifiers': {'$ref': '#/definitions/file-directive'}, 'description': {'$ref': '#/definitions/file-directive'}, 'entry-points': {'$ref': '#/definitions/file-directive'}, 'readme': {'anyOf': [{'$ref': '#/definitions/file-directive'}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}}}, rule='type')
-            data__tool_is_dict = isinstance(data__tool, dict)
-            if data__tool_is_dict:
-                data__tool_keys = set(data__tool.keys())
-                if "distutils" in data__tool_keys:
-                    data__tool_keys.remove("distutils")
-                    data__tool__distutils = data__tool["distutils"]
-                    validate_https___docs_python_org_3_install(data__tool__distutils, custom_formats, (name_prefix or "data") + ".tool.distutils")
-                if "setuptools" in data__tool_keys:
-                    data__tool_keys.remove("setuptools")
-                    data__tool__setuptools = data__tool["setuptools"]
-                    validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data__tool__setuptools, custom_formats, (name_prefix or "data") + ".tool.setuptools")
-        if data_keys:
-            raise JsonSchemaValueException("" + (name_prefix or "data") + " must not contain "+str(data_keys)+" properties", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-build-dependencies/', 'title': 'Data structure for ``pyproject.toml`` files', '$$description': ['File format containing build-time configurations for the Python ecosystem. ', ':pep:`517` initially defined a build-system independent format for source trees', 'which was complemented by :pep:`518` to provide a way of specifying dependencies ', 'for building Python projects.', 'Please notice the ``project`` table (as initially defined in  :pep:`621`) is not included', 'in this schema and should be considered separately.'], 'type': 'object', 'additionalProperties': False, 'properties': {'build-system': {'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['dynamic'], 'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}}}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'required': ['version'], '$$description': ['version should be statically defined in the ``version`` field']}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, 'tool': {'type': 'object', 'properties': {'distutils': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://docs.python.org/3/install/', 'title': '``tool.distutils`` table', '$$description': ['Originally, ``distutils`` allowed developers to configure arguments for', '``setup.py`` scripts via `distutils configuration files', '`_.', '``tool.distutils`` subtables could be used with the same purpose', '(NOT CURRENTLY IMPLEMENTED).'], 'type': 'object', 'properties': {'global': {'type': 'object', 'description': 'Global options applied to all ``distutils`` commands'}}, 'patternProperties': {'.+': {'type': 'object'}}, '$comment': 'TODO: Is there a practical way of making this schema more specific?'}, 'setuptools': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$ref': '#/definitions/find-directive'}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might become standard with PEP 639).'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Probably ``project.license-files``?'}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'additionalProperties': False, 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'$ref': '#/definitions/attr-directive'}, {'$ref': '#/definitions/file-directive'}]}, 'classifiers': {'$ref': '#/definitions/file-directive'}, 'description': {'$ref': '#/definitions/file-directive'}, 'entry-points': {'$ref': '#/definitions/file-directive'}, 'readme': {'anyOf': [{'$ref': '#/definitions/file-directive'}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}}}}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['dynamic'], 'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}}}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'required': ['version'], '$$description': ['version should be statically defined in the ``version`` field']}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, rule='additionalProperties')
-    return data
-
-def validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data, custom_formats={}, name_prefix=None):
-    if not isinstance(data, (dict)):
-        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might become standard with PEP 639).'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Probably ``project.license-files``?'}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'additionalProperties': False, 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}]}, 'classifiers': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'description': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'entry-points': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'readme': {'anyOf': [{'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}, rule='type')
-    data_is_dict = isinstance(data, dict)
-    if data_is_dict:
-        data_keys = set(data.keys())
-        if "platforms" in data_keys:
-            data_keys.remove("platforms")
-            data__platforms = data["platforms"]
-            if not isinstance(data__platforms, (list, tuple)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".platforms must be array", value=data__platforms, name="" + (name_prefix or "data") + ".platforms", definition={'type': 'array', 'items': {'type': 'string'}}, rule='type')
-            data__platforms_is_list = isinstance(data__platforms, (list, tuple))
-            if data__platforms_is_list:
-                data__platforms_len = len(data__platforms)
-                for data__platforms_x, data__platforms_item in enumerate(data__platforms):
-                    if not isinstance(data__platforms_item, (str)):
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".platforms[{data__platforms_x}]".format(**locals()) + " must be string", value=data__platforms_item, name="" + (name_prefix or "data") + ".platforms[{data__platforms_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
-        if "provides" in data_keys:
-            data_keys.remove("provides")
-            data__provides = data["provides"]
-            if not isinstance(data__provides, (list, tuple)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".provides must be array", value=data__provides, name="" + (name_prefix or "data") + ".provides", definition={'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, rule='type')
-            data__provides_is_list = isinstance(data__provides, (list, tuple))
-            if data__provides_is_list:
-                data__provides_len = len(data__provides)
-                for data__provides_x, data__provides_item in enumerate(data__provides):
-                    if not isinstance(data__provides_item, (str)):
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".provides[{data__provides_x}]".format(**locals()) + " must be string", value=data__provides_item, name="" + (name_prefix or "data") + ".provides[{data__provides_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'pep508-identifier'}, rule='type')
-                    if isinstance(data__provides_item, str):
-                        if not custom_formats["pep508-identifier"](data__provides_item):
-                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".provides[{data__provides_x}]".format(**locals()) + " must be pep508-identifier", value=data__provides_item, name="" + (name_prefix or "data") + ".provides[{data__provides_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'pep508-identifier'}, rule='format')
-        if "obsoletes" in data_keys:
-            data_keys.remove("obsoletes")
-            data__obsoletes = data["obsoletes"]
-            if not isinstance(data__obsoletes, (list, tuple)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".obsoletes must be array", value=data__obsoletes, name="" + (name_prefix or "data") + ".obsoletes", definition={'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, rule='type')
-            data__obsoletes_is_list = isinstance(data__obsoletes, (list, tuple))
-            if data__obsoletes_is_list:
-                data__obsoletes_len = len(data__obsoletes)
-                for data__obsoletes_x, data__obsoletes_item in enumerate(data__obsoletes):
-                    if not isinstance(data__obsoletes_item, (str)):
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".obsoletes[{data__obsoletes_x}]".format(**locals()) + " must be string", value=data__obsoletes_item, name="" + (name_prefix or "data") + ".obsoletes[{data__obsoletes_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'pep508-identifier'}, rule='type')
-                    if isinstance(data__obsoletes_item, str):
-                        if not custom_formats["pep508-identifier"](data__obsoletes_item):
-                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".obsoletes[{data__obsoletes_x}]".format(**locals()) + " must be pep508-identifier", value=data__obsoletes_item, name="" + (name_prefix or "data") + ".obsoletes[{data__obsoletes_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'pep508-identifier'}, rule='format')
-        if "zip-safe" in data_keys:
-            data_keys.remove("zip-safe")
-            data__zipsafe = data["zip-safe"]
-            if not isinstance(data__zipsafe, (bool)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".zip-safe must be boolean", value=data__zipsafe, name="" + (name_prefix or "data") + ".zip-safe", definition={'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, rule='type')
-        if "script-files" in data_keys:
-            data_keys.remove("script-files")
-            data__scriptfiles = data["script-files"]
-            if not isinstance(data__scriptfiles, (list, tuple)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".script-files must be array", value=data__scriptfiles, name="" + (name_prefix or "data") + ".script-files", definition={'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, rule='type')
-            data__scriptfiles_is_list = isinstance(data__scriptfiles, (list, tuple))
-            if data__scriptfiles_is_list:
-                data__scriptfiles_len = len(data__scriptfiles)
-                for data__scriptfiles_x, data__scriptfiles_item in enumerate(data__scriptfiles):
-                    if not isinstance(data__scriptfiles_item, (str)):
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".script-files[{data__scriptfiles_x}]".format(**locals()) + " must be string", value=data__scriptfiles_item, name="" + (name_prefix or "data") + ".script-files[{data__scriptfiles_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
-        if "eager-resources" in data_keys:
-            data_keys.remove("eager-resources")
-            data__eagerresources = data["eager-resources"]
-            if not isinstance(data__eagerresources, (list, tuple)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".eager-resources must be array", value=data__eagerresources, name="" + (name_prefix or "data") + ".eager-resources", definition={'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, rule='type')
-            data__eagerresources_is_list = isinstance(data__eagerresources, (list, tuple))
-            if data__eagerresources_is_list:
-                data__eagerresources_len = len(data__eagerresources)
-                for data__eagerresources_x, data__eagerresources_item in enumerate(data__eagerresources):
-                    if not isinstance(data__eagerresources_item, (str)):
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".eager-resources[{data__eagerresources_x}]".format(**locals()) + " must be string", value=data__eagerresources_item, name="" + (name_prefix or "data") + ".eager-resources[{data__eagerresources_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
-        if "packages" in data_keys:
-            data_keys.remove("packages")
-            data__packages = data["packages"]
-            data__packages_one_of_count1 = 0
-            if data__packages_one_of_count1 < 2:
-                try:
-                    if not isinstance(data__packages, (list, tuple)):
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".packages must be array", value=data__packages, name="" + (name_prefix or "data") + ".packages", definition={'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, rule='type')
-                    data__packages_is_list = isinstance(data__packages, (list, tuple))
-                    if data__packages_is_list:
-                        data__packages_len = len(data__packages)
-                        for data__packages_x, data__packages_item in enumerate(data__packages):
-                            if not isinstance(data__packages_item, (str)):
-                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".packages[{data__packages_x}]".format(**locals()) + " must be string", value=data__packages_item, name="" + (name_prefix or "data") + ".packages[{data__packages_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'python-module-name'}, rule='type')
-                            if isinstance(data__packages_item, str):
-                                if not custom_formats["python-module-name"](data__packages_item):
-                                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".packages[{data__packages_x}]".format(**locals()) + " must be python-module-name", value=data__packages_item, name="" + (name_prefix or "data") + ".packages[{data__packages_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'python-module-name'}, rule='format')
-                    data__packages_one_of_count1 += 1
-                except JsonSchemaValueException: pass
-            if data__packages_one_of_count1 < 2:
-                try:
-                    validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_find_directive(data__packages, custom_formats, (name_prefix or "data") + ".packages")
-                    data__packages_one_of_count1 += 1
-                except JsonSchemaValueException: pass
-            if data__packages_one_of_count1 != 1:
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".packages must be valid exactly by one definition" + (" (" + str(data__packages_one_of_count1) + " matches found)"), value=data__packages, name="" + (name_prefix or "data") + ".packages", definition={'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}]}, rule='oneOf')
-        if "package-dir" in data_keys:
-            data_keys.remove("package-dir")
-            data__packagedir = data["package-dir"]
-            if not isinstance(data__packagedir, (dict)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-dir must be object", value=data__packagedir, name="" + (name_prefix or "data") + ".package-dir", definition={'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, rule='type')
-            data__packagedir_is_dict = isinstance(data__packagedir, dict)
-            if data__packagedir_is_dict:
-                data__packagedir_keys = set(data__packagedir.keys())
-                for data__packagedir_key, data__packagedir_val in data__packagedir.items():
-                    if REGEX_PATTERNS['^.*$'].search(data__packagedir_key):
-                        if data__packagedir_key in data__packagedir_keys:
-                            data__packagedir_keys.remove(data__packagedir_key)
-                        if not isinstance(data__packagedir_val, (str)):
-                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-dir.{data__packagedir_key}".format(**locals()) + " must be string", value=data__packagedir_val, name="" + (name_prefix or "data") + ".package-dir.{data__packagedir_key}".format(**locals()) + "", definition={'type': 'string'}, rule='type')
-                if data__packagedir_keys:
-                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-dir must not contain "+str(data__packagedir_keys)+" properties", value=data__packagedir, name="" + (name_prefix or "data") + ".package-dir", definition={'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, rule='additionalProperties')
-                data__packagedir_len = len(data__packagedir)
-                if data__packagedir_len != 0:
-                    data__packagedir_property_names = True
-                    for data__packagedir_key in data__packagedir:
-                        try:
-                            data__packagedir_key_one_of_count2 = 0
-                            if data__packagedir_key_one_of_count2 < 2:
-                                try:
-                                    if isinstance(data__packagedir_key, str):
-                                        if not custom_formats["python-module-name"](data__packagedir_key):
-                                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-dir must be python-module-name", value=data__packagedir_key, name="" + (name_prefix or "data") + ".package-dir", definition={'format': 'python-module-name'}, rule='format')
-                                    data__packagedir_key_one_of_count2 += 1
-                                except JsonSchemaValueException: pass
-                            if data__packagedir_key_one_of_count2 < 2:
-                                try:
-                                    if data__packagedir_key != "":
-                                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-dir must be same as const definition: ", value=data__packagedir_key, name="" + (name_prefix or "data") + ".package-dir", definition={'const': ''}, rule='const')
-                                    data__packagedir_key_one_of_count2 += 1
-                                except JsonSchemaValueException: pass
-                            if data__packagedir_key_one_of_count2 != 1:
-                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-dir must be valid exactly by one definition" + (" (" + str(data__packagedir_key_one_of_count2) + " matches found)"), value=data__packagedir_key, name="" + (name_prefix or "data") + ".package-dir", definition={'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, rule='oneOf')
-                        except JsonSchemaValueException:
-                            data__packagedir_property_names = False
-                    if not data__packagedir_property_names:
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-dir must be named by propertyName definition", value=data__packagedir, name="" + (name_prefix or "data") + ".package-dir", definition={'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, rule='propertyNames')
-        if "package-data" in data_keys:
-            data_keys.remove("package-data")
-            data__packagedata = data["package-data"]
-            if not isinstance(data__packagedata, (dict)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-data must be object", value=data__packagedata, name="" + (name_prefix or "data") + ".package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='type')
-            data__packagedata_is_dict = isinstance(data__packagedata, dict)
-            if data__packagedata_is_dict:
-                data__packagedata_keys = set(data__packagedata.keys())
-                for data__packagedata_key, data__packagedata_val in data__packagedata.items():
-                    if REGEX_PATTERNS['^.*$'].search(data__packagedata_key):
-                        if data__packagedata_key in data__packagedata_keys:
-                            data__packagedata_keys.remove(data__packagedata_key)
-                        if not isinstance(data__packagedata_val, (list, tuple)):
-                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-data.{data__packagedata_key}".format(**locals()) + " must be array", value=data__packagedata_val, name="" + (name_prefix or "data") + ".package-data.{data__packagedata_key}".format(**locals()) + "", definition={'type': 'array', 'items': {'type': 'string'}}, rule='type')
-                        data__packagedata_val_is_list = isinstance(data__packagedata_val, (list, tuple))
-                        if data__packagedata_val_is_list:
-                            data__packagedata_val_len = len(data__packagedata_val)
-                            for data__packagedata_val_x, data__packagedata_val_item in enumerate(data__packagedata_val):
-                                if not isinstance(data__packagedata_val_item, (str)):
-                                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-data.{data__packagedata_key}[{data__packagedata_val_x}]".format(**locals()) + " must be string", value=data__packagedata_val_item, name="" + (name_prefix or "data") + ".package-data.{data__packagedata_key}[{data__packagedata_val_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
-                if data__packagedata_keys:
-                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-data must not contain "+str(data__packagedata_keys)+" properties", value=data__packagedata, name="" + (name_prefix or "data") + ".package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='additionalProperties')
-                data__packagedata_len = len(data__packagedata)
-                if data__packagedata_len != 0:
-                    data__packagedata_property_names = True
-                    for data__packagedata_key in data__packagedata:
-                        try:
-                            data__packagedata_key_one_of_count3 = 0
-                            if data__packagedata_key_one_of_count3 < 2:
-                                try:
-                                    if isinstance(data__packagedata_key, str):
-                                        if not custom_formats["python-module-name"](data__packagedata_key):
-                                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-data must be python-module-name", value=data__packagedata_key, name="" + (name_prefix or "data") + ".package-data", definition={'format': 'python-module-name'}, rule='format')
-                                    data__packagedata_key_one_of_count3 += 1
-                                except JsonSchemaValueException: pass
-                            if data__packagedata_key_one_of_count3 < 2:
-                                try:
-                                    if data__packagedata_key != "*":
-                                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-data must be same as const definition: *", value=data__packagedata_key, name="" + (name_prefix or "data") + ".package-data", definition={'const': '*'}, rule='const')
-                                    data__packagedata_key_one_of_count3 += 1
-                                except JsonSchemaValueException: pass
-                            if data__packagedata_key_one_of_count3 != 1:
-                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-data must be valid exactly by one definition" + (" (" + str(data__packagedata_key_one_of_count3) + " matches found)"), value=data__packagedata_key, name="" + (name_prefix or "data") + ".package-data", definition={'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, rule='oneOf')
-                        except JsonSchemaValueException:
-                            data__packagedata_property_names = False
-                    if not data__packagedata_property_names:
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-data must be named by propertyName definition", value=data__packagedata, name="" + (name_prefix or "data") + ".package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='propertyNames')
-        if "include-package-data" in data_keys:
-            data_keys.remove("include-package-data")
-            data__includepackagedata = data["include-package-data"]
-            if not isinstance(data__includepackagedata, (bool)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".include-package-data must be boolean", value=data__includepackagedata, name="" + (name_prefix or "data") + ".include-package-data", definition={'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, rule='type')
-        if "exclude-package-data" in data_keys:
-            data_keys.remove("exclude-package-data")
-            data__excludepackagedata = data["exclude-package-data"]
-            if not isinstance(data__excludepackagedata, (dict)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".exclude-package-data must be object", value=data__excludepackagedata, name="" + (name_prefix or "data") + ".exclude-package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='type')
-            data__excludepackagedata_is_dict = isinstance(data__excludepackagedata, dict)
-            if data__excludepackagedata_is_dict:
-                data__excludepackagedata_keys = set(data__excludepackagedata.keys())
-                for data__excludepackagedata_key, data__excludepackagedata_val in data__excludepackagedata.items():
-                    if REGEX_PATTERNS['^.*$'].search(data__excludepackagedata_key):
-                        if data__excludepackagedata_key in data__excludepackagedata_keys:
-                            data__excludepackagedata_keys.remove(data__excludepackagedata_key)
-                        if not isinstance(data__excludepackagedata_val, (list, tuple)):
-                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".exclude-package-data.{data__excludepackagedata_key}".format(**locals()) + " must be array", value=data__excludepackagedata_val, name="" + (name_prefix or "data") + ".exclude-package-data.{data__excludepackagedata_key}".format(**locals()) + "", definition={'type': 'array', 'items': {'type': 'string'}}, rule='type')
-                        data__excludepackagedata_val_is_list = isinstance(data__excludepackagedata_val, (list, tuple))
-                        if data__excludepackagedata_val_is_list:
-                            data__excludepackagedata_val_len = len(data__excludepackagedata_val)
-                            for data__excludepackagedata_val_x, data__excludepackagedata_val_item in enumerate(data__excludepackagedata_val):
-                                if not isinstance(data__excludepackagedata_val_item, (str)):
-                                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".exclude-package-data.{data__excludepackagedata_key}[{data__excludepackagedata_val_x}]".format(**locals()) + " must be string", value=data__excludepackagedata_val_item, name="" + (name_prefix or "data") + ".exclude-package-data.{data__excludepackagedata_key}[{data__excludepackagedata_val_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
-                if data__excludepackagedata_keys:
-                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".exclude-package-data must not contain "+str(data__excludepackagedata_keys)+" properties", value=data__excludepackagedata, name="" + (name_prefix or "data") + ".exclude-package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='additionalProperties')
-                data__excludepackagedata_len = len(data__excludepackagedata)
-                if data__excludepackagedata_len != 0:
-                    data__excludepackagedata_property_names = True
-                    for data__excludepackagedata_key in data__excludepackagedata:
-                        try:
-                            data__excludepackagedata_key_one_of_count4 = 0
-                            if data__excludepackagedata_key_one_of_count4 < 2:
-                                try:
-                                    if isinstance(data__excludepackagedata_key, str):
-                                        if not custom_formats["python-module-name"](data__excludepackagedata_key):
-                                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".exclude-package-data must be python-module-name", value=data__excludepackagedata_key, name="" + (name_prefix or "data") + ".exclude-package-data", definition={'format': 'python-module-name'}, rule='format')
-                                    data__excludepackagedata_key_one_of_count4 += 1
-                                except JsonSchemaValueException: pass
-                            if data__excludepackagedata_key_one_of_count4 < 2:
-                                try:
-                                    if data__excludepackagedata_key != "*":
-                                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".exclude-package-data must be same as const definition: *", value=data__excludepackagedata_key, name="" + (name_prefix or "data") + ".exclude-package-data", definition={'const': '*'}, rule='const')
-                                    data__excludepackagedata_key_one_of_count4 += 1
-                                except JsonSchemaValueException: pass
-                            if data__excludepackagedata_key_one_of_count4 != 1:
-                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".exclude-package-data must be valid exactly by one definition" + (" (" + str(data__excludepackagedata_key_one_of_count4) + " matches found)"), value=data__excludepackagedata_key, name="" + (name_prefix or "data") + ".exclude-package-data", definition={'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, rule='oneOf')
-                        except JsonSchemaValueException:
-                            data__excludepackagedata_property_names = False
-                    if not data__excludepackagedata_property_names:
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".exclude-package-data must be named by propertyName definition", value=data__excludepackagedata, name="" + (name_prefix or "data") + ".exclude-package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='propertyNames')
-        if "namespace-packages" in data_keys:
-            data_keys.remove("namespace-packages")
-            data__namespacepackages = data["namespace-packages"]
-            if not isinstance(data__namespacepackages, (list, tuple)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".namespace-packages must be array", value=data__namespacepackages, name="" + (name_prefix or "data") + ".namespace-packages", definition={'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, rule='type')
-            data__namespacepackages_is_list = isinstance(data__namespacepackages, (list, tuple))
-            if data__namespacepackages_is_list:
-                data__namespacepackages_len = len(data__namespacepackages)
-                for data__namespacepackages_x, data__namespacepackages_item in enumerate(data__namespacepackages):
-                    if not isinstance(data__namespacepackages_item, (str)):
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".namespace-packages[{data__namespacepackages_x}]".format(**locals()) + " must be string", value=data__namespacepackages_item, name="" + (name_prefix or "data") + ".namespace-packages[{data__namespacepackages_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'python-module-name'}, rule='type')
-                    if isinstance(data__namespacepackages_item, str):
-                        if not custom_formats["python-module-name"](data__namespacepackages_item):
-                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".namespace-packages[{data__namespacepackages_x}]".format(**locals()) + " must be python-module-name", value=data__namespacepackages_item, name="" + (name_prefix or "data") + ".namespace-packages[{data__namespacepackages_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'python-module-name'}, rule='format')
-        if "py-modules" in data_keys:
-            data_keys.remove("py-modules")
-            data__pymodules = data["py-modules"]
-            if not isinstance(data__pymodules, (list, tuple)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".py-modules must be array", value=data__pymodules, name="" + (name_prefix or "data") + ".py-modules", definition={'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, rule='type')
-            data__pymodules_is_list = isinstance(data__pymodules, (list, tuple))
-            if data__pymodules_is_list:
-                data__pymodules_len = len(data__pymodules)
-                for data__pymodules_x, data__pymodules_item in enumerate(data__pymodules):
-                    if not isinstance(data__pymodules_item, (str)):
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".py-modules[{data__pymodules_x}]".format(**locals()) + " must be string", value=data__pymodules_item, name="" + (name_prefix or "data") + ".py-modules[{data__pymodules_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'python-module-name'}, rule='type')
-                    if isinstance(data__pymodules_item, str):
-                        if not custom_formats["python-module-name"](data__pymodules_item):
-                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".py-modules[{data__pymodules_x}]".format(**locals()) + " must be python-module-name", value=data__pymodules_item, name="" + (name_prefix or "data") + ".py-modules[{data__pymodules_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'python-module-name'}, rule='format')
-        if "data-files" in data_keys:
-            data_keys.remove("data-files")
-            data__datafiles = data["data-files"]
-            if not isinstance(data__datafiles, (dict)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".data-files must be object", value=data__datafiles, name="" + (name_prefix or "data") + ".data-files", definition={'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='type')
-            data__datafiles_is_dict = isinstance(data__datafiles, dict)
-            if data__datafiles_is_dict:
-                data__datafiles_keys = set(data__datafiles.keys())
-                for data__datafiles_key, data__datafiles_val in data__datafiles.items():
-                    if REGEX_PATTERNS['^.*$'].search(data__datafiles_key):
-                        if data__datafiles_key in data__datafiles_keys:
-                            data__datafiles_keys.remove(data__datafiles_key)
-                        if not isinstance(data__datafiles_val, (list, tuple)):
-                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".data-files.{data__datafiles_key}".format(**locals()) + " must be array", value=data__datafiles_val, name="" + (name_prefix or "data") + ".data-files.{data__datafiles_key}".format(**locals()) + "", definition={'type': 'array', 'items': {'type': 'string'}}, rule='type')
-                        data__datafiles_val_is_list = isinstance(data__datafiles_val, (list, tuple))
-                        if data__datafiles_val_is_list:
-                            data__datafiles_val_len = len(data__datafiles_val)
-                            for data__datafiles_val_x, data__datafiles_val_item in enumerate(data__datafiles_val):
-                                if not isinstance(data__datafiles_val_item, (str)):
-                                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".data-files.{data__datafiles_key}[{data__datafiles_val_x}]".format(**locals()) + " must be string", value=data__datafiles_val_item, name="" + (name_prefix or "data") + ".data-files.{data__datafiles_key}[{data__datafiles_val_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
-        if "cmdclass" in data_keys:
-            data_keys.remove("cmdclass")
-            data__cmdclass = data["cmdclass"]
-            if not isinstance(data__cmdclass, (dict)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".cmdclass must be object", value=data__cmdclass, name="" + (name_prefix or "data") + ".cmdclass", definition={'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, rule='type')
-            data__cmdclass_is_dict = isinstance(data__cmdclass, dict)
-            if data__cmdclass_is_dict:
-                data__cmdclass_keys = set(data__cmdclass.keys())
-                for data__cmdclass_key, data__cmdclass_val in data__cmdclass.items():
-                    if REGEX_PATTERNS['^.*$'].search(data__cmdclass_key):
-                        if data__cmdclass_key in data__cmdclass_keys:
-                            data__cmdclass_keys.remove(data__cmdclass_key)
-                        if not isinstance(data__cmdclass_val, (str)):
-                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".cmdclass.{data__cmdclass_key}".format(**locals()) + " must be string", value=data__cmdclass_val, name="" + (name_prefix or "data") + ".cmdclass.{data__cmdclass_key}".format(**locals()) + "", definition={'type': 'string', 'format': 'python-qualified-identifier'}, rule='type')
-                        if isinstance(data__cmdclass_val, str):
-                            if not custom_formats["python-qualified-identifier"](data__cmdclass_val):
-                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".cmdclass.{data__cmdclass_key}".format(**locals()) + " must be python-qualified-identifier", value=data__cmdclass_val, name="" + (name_prefix or "data") + ".cmdclass.{data__cmdclass_key}".format(**locals()) + "", definition={'type': 'string', 'format': 'python-qualified-identifier'}, rule='format')
-        if "license-files" in data_keys:
-            data_keys.remove("license-files")
-            data__licensefiles = data["license-files"]
-            if not isinstance(data__licensefiles, (list, tuple)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".license-files must be array", value=data__licensefiles, name="" + (name_prefix or "data") + ".license-files", definition={'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might become standard with PEP 639).'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Probably ``project.license-files``?'}, rule='type')
-            data__licensefiles_is_list = isinstance(data__licensefiles, (list, tuple))
-            if data__licensefiles_is_list:
-                data__licensefiles_len = len(data__licensefiles)
-                for data__licensefiles_x, data__licensefiles_item in enumerate(data__licensefiles):
-                    if not isinstance(data__licensefiles_item, (str)):
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".license-files[{data__licensefiles_x}]".format(**locals()) + " must be string", value=data__licensefiles_item, name="" + (name_prefix or "data") + ".license-files[{data__licensefiles_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
-        else: data["license-files"] = ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*']
-        if "dynamic" in data_keys:
-            data_keys.remove("dynamic")
-            data__dynamic = data["dynamic"]
-            if not isinstance(data__dynamic, (dict)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic must be object", value=data__dynamic, name="" + (name_prefix or "data") + ".dynamic", definition={'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'additionalProperties': False, 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}]}, 'classifiers': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'description': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'entry-points': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'readme': {'anyOf': [{'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}}}, rule='type')
-            data__dynamic_is_dict = isinstance(data__dynamic, dict)
-            if data__dynamic_is_dict:
-                data__dynamic_keys = set(data__dynamic.keys())
-                if "version" in data__dynamic_keys:
-                    data__dynamic_keys.remove("version")
-                    data__dynamic__version = data__dynamic["version"]
-                    data__dynamic__version_one_of_count5 = 0
-                    if data__dynamic__version_one_of_count5 < 2:
-                        try:
-                            validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_attr_directive(data__dynamic__version, custom_formats, (name_prefix or "data") + ".dynamic.version")
-                            data__dynamic__version_one_of_count5 += 1
-                        except JsonSchemaValueException: pass
-                    if data__dynamic__version_one_of_count5 < 2:
-                        try:
-                            validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data__dynamic__version, custom_formats, (name_prefix or "data") + ".dynamic.version")
-                            data__dynamic__version_one_of_count5 += 1
-                        except JsonSchemaValueException: pass
-                    if data__dynamic__version_one_of_count5 != 1:
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic.version must be valid exactly by one definition" + (" (" + str(data__dynamic__version_one_of_count5) + " matches found)"), value=data__dynamic__version, name="" + (name_prefix or "data") + ".dynamic.version", definition={'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}]}, rule='oneOf')
-                if "classifiers" in data__dynamic_keys:
-                    data__dynamic_keys.remove("classifiers")
-                    data__dynamic__classifiers = data__dynamic["classifiers"]
-                    validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data__dynamic__classifiers, custom_formats, (name_prefix or "data") + ".dynamic.classifiers")
-                if "description" in data__dynamic_keys:
-                    data__dynamic_keys.remove("description")
-                    data__dynamic__description = data__dynamic["description"]
-                    validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data__dynamic__description, custom_formats, (name_prefix or "data") + ".dynamic.description")
-                if "entry-points" in data__dynamic_keys:
-                    data__dynamic_keys.remove("entry-points")
-                    data__dynamic__entrypoints = data__dynamic["entry-points"]
-                    validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data__dynamic__entrypoints, custom_formats, (name_prefix or "data") + ".dynamic.entry-points")
-                if "readme" in data__dynamic_keys:
-                    data__dynamic_keys.remove("readme")
-                    data__dynamic__readme = data__dynamic["readme"]
-                    data__dynamic__readme_any_of_count6 = 0
-                    if not data__dynamic__readme_any_of_count6:
-                        try:
-                            validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data__dynamic__readme, custom_formats, (name_prefix or "data") + ".dynamic.readme")
-                            data__dynamic__readme_any_of_count6 += 1
-                        except JsonSchemaValueException: pass
-                    if not data__dynamic__readme_any_of_count6:
-                        try:
-                            data__dynamic__readme_is_dict = isinstance(data__dynamic__readme, dict)
-                            if data__dynamic__readme_is_dict:
-                                data__dynamic__readme_keys = set(data__dynamic__readme.keys())
-                                if "content-type" in data__dynamic__readme_keys:
-                                    data__dynamic__readme_keys.remove("content-type")
-                                    data__dynamic__readme__contenttype = data__dynamic__readme["content-type"]
-                                    if not isinstance(data__dynamic__readme__contenttype, (str)):
-                                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic.readme.content-type must be string", value=data__dynamic__readme__contenttype, name="" + (name_prefix or "data") + ".dynamic.readme.content-type", definition={'type': 'string'}, rule='type')
-                            data__dynamic__readme_any_of_count6 += 1
-                        except JsonSchemaValueException: pass
-                    if not data__dynamic__readme_any_of_count6:
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic.readme cannot be validated by any definition", value=data__dynamic__readme, name="" + (name_prefix or "data") + ".dynamic.readme", definition={'anyOf': [{'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}, rule='anyOf')
-                    data__dynamic__readme_is_dict = isinstance(data__dynamic__readme, dict)
-                    if data__dynamic__readme_is_dict:
-                        data__dynamic__readme_len = len(data__dynamic__readme)
-                        if not all(prop in data__dynamic__readme for prop in ['file']):
-                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic.readme must contain ['file'] properties", value=data__dynamic__readme, name="" + (name_prefix or "data") + ".dynamic.readme", definition={'anyOf': [{'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}, rule='required')
-                if data__dynamic_keys:
-                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic must not contain "+str(data__dynamic_keys)+" properties", value=data__dynamic, name="" + (name_prefix or "data") + ".dynamic", definition={'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'additionalProperties': False, 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}]}, 'classifiers': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'description': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'entry-points': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'readme': {'anyOf': [{'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}}}, rule='additionalProperties')
-        if data_keys:
-            raise JsonSchemaValueException("" + (name_prefix or "data") + " must not contain "+str(data_keys)+" properties", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might become standard with PEP 639).'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Probably ``project.license-files``?'}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'additionalProperties': False, 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}]}, 'classifiers': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'description': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'entry-points': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'readme': {'anyOf': [{'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}, rule='additionalProperties')
-    return data
-
-def validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data, custom_formats={}, name_prefix=None):
-    if not isinstance(data, (dict)):
-        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, rule='type')
-    data_is_dict = isinstance(data, dict)
-    if data_is_dict:
-        data_len = len(data)
-        if not all(prop in data for prop in ['file']):
-            raise JsonSchemaValueException("" + (name_prefix or "data") + " must contain ['file'] properties", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, rule='required')
-        data_keys = set(data.keys())
-        if "file" in data_keys:
-            data_keys.remove("file")
-            data__file = data["file"]
-            data__file_one_of_count7 = 0
-            if data__file_one_of_count7 < 2:
-                try:
-                    if not isinstance(data__file, (str)):
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".file must be string", value=data__file, name="" + (name_prefix or "data") + ".file", definition={'type': 'string'}, rule='type')
-                    data__file_one_of_count7 += 1
-                except JsonSchemaValueException: pass
-            if data__file_one_of_count7 < 2:
-                try:
-                    if not isinstance(data__file, (list, tuple)):
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".file must be array", value=data__file, name="" + (name_prefix or "data") + ".file", definition={'type': 'array', 'items': {'type': 'string'}}, rule='type')
-                    data__file_is_list = isinstance(data__file, (list, tuple))
-                    if data__file_is_list:
-                        data__file_len = len(data__file)
-                        for data__file_x, data__file_item in enumerate(data__file):
-                            if not isinstance(data__file_item, (str)):
-                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".file[{data__file_x}]".format(**locals()) + " must be string", value=data__file_item, name="" + (name_prefix or "data") + ".file[{data__file_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
-                    data__file_one_of_count7 += 1
-                except JsonSchemaValueException: pass
-            if data__file_one_of_count7 != 1:
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".file must be valid exactly by one definition" + (" (" + str(data__file_one_of_count7) + " matches found)"), value=data__file, name="" + (name_prefix or "data") + ".file", definition={'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}, rule='oneOf')
-        if data_keys:
-            raise JsonSchemaValueException("" + (name_prefix or "data") + " must not contain "+str(data_keys)+" properties", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, rule='additionalProperties')
-    return data
-
-def validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_attr_directive(data, custom_formats={}, name_prefix=None):
-    if not isinstance(data, (dict)):
-        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, rule='type')
-    data_is_dict = isinstance(data, dict)
-    if data_is_dict:
-        data_len = len(data)
-        if not all(prop in data for prop in ['attr']):
-            raise JsonSchemaValueException("" + (name_prefix or "data") + " must contain ['attr'] properties", value=data, name="" + (name_prefix or "data") + "", definition={'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, rule='required')
-        data_keys = set(data.keys())
-        if "attr" in data_keys:
-            data_keys.remove("attr")
-            data__attr = data["attr"]
-            if not isinstance(data__attr, (str)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".attr must be string", value=data__attr, name="" + (name_prefix or "data") + ".attr", definition={'type': 'string'}, rule='type')
-        if data_keys:
-            raise JsonSchemaValueException("" + (name_prefix or "data") + " must not contain "+str(data_keys)+" properties", value=data, name="" + (name_prefix or "data") + "", definition={'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, rule='additionalProperties')
-    return data
-
-def validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_find_directive(data, custom_formats={}, name_prefix=None):
-    if not isinstance(data, (dict)):
-        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}, rule='type')
-    data_is_dict = isinstance(data, dict)
-    if data_is_dict:
-        data_keys = set(data.keys())
-        if "find" in data_keys:
-            data_keys.remove("find")
-            data__find = data["find"]
-            if not isinstance(data__find, (dict)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".find must be object", value=data__find, name="" + (name_prefix or "data") + ".find", definition={'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}, rule='type')
-            data__find_is_dict = isinstance(data__find, dict)
-            if data__find_is_dict:
-                data__find_keys = set(data__find.keys())
-                if "where" in data__find_keys:
-                    data__find_keys.remove("where")
-                    data__find__where = data__find["where"]
-                    if not isinstance(data__find__where, (list, tuple)):
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".find.where must be array", value=data__find__where, name="" + (name_prefix or "data") + ".find.where", definition={'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, rule='type')
-                    data__find__where_is_list = isinstance(data__find__where, (list, tuple))
-                    if data__find__where_is_list:
-                        data__find__where_len = len(data__find__where)
-                        for data__find__where_x, data__find__where_item in enumerate(data__find__where):
-                            if not isinstance(data__find__where_item, (str)):
-                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".find.where[{data__find__where_x}]".format(**locals()) + " must be string", value=data__find__where_item, name="" + (name_prefix or "data") + ".find.where[{data__find__where_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
-                if "exclude" in data__find_keys:
-                    data__find_keys.remove("exclude")
-                    data__find__exclude = data__find["exclude"]
-                    if not isinstance(data__find__exclude, (list, tuple)):
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".find.exclude must be array", value=data__find__exclude, name="" + (name_prefix or "data") + ".find.exclude", definition={'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, rule='type')
-                    data__find__exclude_is_list = isinstance(data__find__exclude, (list, tuple))
-                    if data__find__exclude_is_list:
-                        data__find__exclude_len = len(data__find__exclude)
-                        for data__find__exclude_x, data__find__exclude_item in enumerate(data__find__exclude):
-                            if not isinstance(data__find__exclude_item, (str)):
-                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".find.exclude[{data__find__exclude_x}]".format(**locals()) + " must be string", value=data__find__exclude_item, name="" + (name_prefix or "data") + ".find.exclude[{data__find__exclude_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
-                if "include" in data__find_keys:
-                    data__find_keys.remove("include")
-                    data__find__include = data__find["include"]
-                    if not isinstance(data__find__include, (list, tuple)):
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".find.include must be array", value=data__find__include, name="" + (name_prefix or "data") + ".find.include", definition={'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, rule='type')
-                    data__find__include_is_list = isinstance(data__find__include, (list, tuple))
-                    if data__find__include_is_list:
-                        data__find__include_len = len(data__find__include)
-                        for data__find__include_x, data__find__include_item in enumerate(data__find__include):
-                            if not isinstance(data__find__include_item, (str)):
-                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".find.include[{data__find__include_x}]".format(**locals()) + " must be string", value=data__find__include_item, name="" + (name_prefix or "data") + ".find.include[{data__find__include_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
-                if "namespaces" in data__find_keys:
-                    data__find_keys.remove("namespaces")
-                    data__find__namespaces = data__find["namespaces"]
-                    if not isinstance(data__find__namespaces, (bool)):
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".find.namespaces must be boolean", value=data__find__namespaces, name="" + (name_prefix or "data") + ".find.namespaces", definition={'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}, rule='type')
-                if data__find_keys:
-                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".find must not contain "+str(data__find_keys)+" properties", value=data__find, name="" + (name_prefix or "data") + ".find", definition={'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}, rule='additionalProperties')
-        if data_keys:
-            raise JsonSchemaValueException("" + (name_prefix or "data") + " must not contain "+str(data_keys)+" properties", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}, rule='additionalProperties')
-    return data
-
-def validate_https___docs_python_org_3_install(data, custom_formats={}, name_prefix=None):
-    if not isinstance(data, (dict)):
-        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://docs.python.org/3/install/', 'title': '``tool.distutils`` table', '$$description': ['Originally, ``distutils`` allowed developers to configure arguments for', '``setup.py`` scripts via `distutils configuration files', '`_.', '``tool.distutils`` subtables could be used with the same purpose', '(NOT CURRENTLY IMPLEMENTED).'], 'type': 'object', 'properties': {'global': {'type': 'object', 'description': 'Global options applied to all ``distutils`` commands'}}, 'patternProperties': {'.+': {'type': 'object'}}, '$comment': 'TODO: Is there a practical way of making this schema more specific?'}, rule='type')
-    data_is_dict = isinstance(data, dict)
-    if data_is_dict:
-        data_keys = set(data.keys())
-        if "global" in data_keys:
-            data_keys.remove("global")
-            data__global = data["global"]
-            if not isinstance(data__global, (dict)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".global must be object", value=data__global, name="" + (name_prefix or "data") + ".global", definition={'type': 'object', 'description': 'Global options applied to all ``distutils`` commands'}, rule='type')
-        for data_key, data_val in data.items():
-            if REGEX_PATTERNS['.+'].search(data_key):
-                if data_key in data_keys:
-                    data_keys.remove(data_key)
-                if not isinstance(data_val, (dict)):
-                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".{data_key}".format(**locals()) + " must be object", value=data_val, name="" + (name_prefix or "data") + ".{data_key}".format(**locals()) + "", definition={'type': 'object'}, rule='type')
-    return data
-
-def validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata(data, custom_formats={}, name_prefix=None):
-    if not isinstance(data, (dict)):
-        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'gui-scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['dynamic'], 'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}}}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'required': ['version'], '$$description': ['version should be statically defined in the ``version`` field']}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, rule='type')
-    data_is_dict = isinstance(data, dict)
-    if data_is_dict:
-        data_len = len(data)
-        if not all(prop in data for prop in ['name']):
-            raise JsonSchemaValueException("" + (name_prefix or "data") + " must contain ['name'] properties", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'gui-scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['dynamic'], 'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}}}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'required': ['version'], '$$description': ['version should be statically defined in the ``version`` field']}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, rule='required')
-        data_keys = set(data.keys())
-        if "name" in data_keys:
-            data_keys.remove("name")
-            data__name = data["name"]
-            if not isinstance(data__name, (str)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".name must be string", value=data__name, name="" + (name_prefix or "data") + ".name", definition={'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, rule='type')
-            if isinstance(data__name, str):
-                if not custom_formats["pep508-identifier"](data__name):
-                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".name must be pep508-identifier", value=data__name, name="" + (name_prefix or "data") + ".name", definition={'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, rule='format')
-        if "version" in data_keys:
-            data_keys.remove("version")
-            data__version = data["version"]
-            if not isinstance(data__version, (str)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".version must be string", value=data__version, name="" + (name_prefix or "data") + ".version", definition={'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, rule='type')
-            if isinstance(data__version, str):
-                if not custom_formats["pep440"](data__version):
-                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".version must be pep440", value=data__version, name="" + (name_prefix or "data") + ".version", definition={'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, rule='format')
-        if "description" in data_keys:
-            data_keys.remove("description")
-            data__description = data["description"]
-            if not isinstance(data__description, (str)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".description must be string", value=data__description, name="" + (name_prefix or "data") + ".description", definition={'type': 'string', '$$description': ['The `summary description of the project', '`_']}, rule='type')
-        if "readme" in data_keys:
-            data_keys.remove("readme")
-            data__readme = data["readme"]
-            data__readme_one_of_count8 = 0
-            if data__readme_one_of_count8 < 2:
-                try:
-                    if not isinstance(data__readme, (str)):
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme must be string", value=data__readme, name="" + (name_prefix or "data") + ".readme", definition={'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, rule='type')
-                    data__readme_one_of_count8 += 1
-                except JsonSchemaValueException: pass
-            if data__readme_one_of_count8 < 2:
-                try:
-                    if not isinstance(data__readme, (dict)):
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme must be object", value=data__readme, name="" + (name_prefix or "data") + ".readme", definition={'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}, rule='type')
-                    data__readme_any_of_count9 = 0
-                    if not data__readme_any_of_count9:
-                        try:
-                            data__readme_is_dict = isinstance(data__readme, dict)
-                            if data__readme_is_dict:
-                                data__readme_len = len(data__readme)
-                                if not all(prop in data__readme for prop in ['file']):
-                                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme must contain ['file'] properties", value=data__readme, name="" + (name_prefix or "data") + ".readme", definition={'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, rule='required')
-                                data__readme_keys = set(data__readme.keys())
-                                if "file" in data__readme_keys:
-                                    data__readme_keys.remove("file")
-                                    data__readme__file = data__readme["file"]
-                                    if not isinstance(data__readme__file, (str)):
-                                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme.file must be string", value=data__readme__file, name="" + (name_prefix or "data") + ".readme.file", definition={'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}, rule='type')
-                            data__readme_any_of_count9 += 1
-                        except JsonSchemaValueException: pass
-                    if not data__readme_any_of_count9:
-                        try:
-                            data__readme_is_dict = isinstance(data__readme, dict)
-                            if data__readme_is_dict:
-                                data__readme_len = len(data__readme)
-                                if not all(prop in data__readme for prop in ['text']):
-                                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme must contain ['text'] properties", value=data__readme, name="" + (name_prefix or "data") + ".readme", definition={'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}, rule='required')
-                                data__readme_keys = set(data__readme.keys())
-                                if "text" in data__readme_keys:
-                                    data__readme_keys.remove("text")
-                                    data__readme__text = data__readme["text"]
-                                    if not isinstance(data__readme__text, (str)):
-                                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme.text must be string", value=data__readme__text, name="" + (name_prefix or "data") + ".readme.text", definition={'type': 'string', 'description': 'Full text describing the project.'}, rule='type')
-                            data__readme_any_of_count9 += 1
-                        except JsonSchemaValueException: pass
-                    if not data__readme_any_of_count9:
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme cannot be validated by any definition", value=data__readme, name="" + (name_prefix or "data") + ".readme", definition={'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, rule='anyOf')
-                    data__readme_is_dict = isinstance(data__readme, dict)
-                    if data__readme_is_dict:
-                        data__readme_len = len(data__readme)
-                        if not all(prop in data__readme for prop in ['content-type']):
-                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme must contain ['content-type'] properties", value=data__readme, name="" + (name_prefix or "data") + ".readme", definition={'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}, rule='required')
-                        data__readme_keys = set(data__readme.keys())
-                        if "content-type" in data__readme_keys:
-                            data__readme_keys.remove("content-type")
-                            data__readme__contenttype = data__readme["content-type"]
-                            if not isinstance(data__readme__contenttype, (str)):
-                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme.content-type must be string", value=data__readme__contenttype, name="" + (name_prefix or "data") + ".readme.content-type", definition={'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}, rule='type')
-                    data__readme_one_of_count8 += 1
-                except JsonSchemaValueException: pass
-            if data__readme_one_of_count8 != 1:
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme must be valid exactly by one definition" + (" (" + str(data__readme_one_of_count8) + " matches found)"), value=data__readme, name="" + (name_prefix or "data") + ".readme", definition={'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, rule='oneOf')
-        if "requires-python" in data_keys:
-            data_keys.remove("requires-python")
-            data__requirespython = data["requires-python"]
-            if not isinstance(data__requirespython, (str)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".requires-python must be string", value=data__requirespython, name="" + (name_prefix or "data") + ".requires-python", definition={'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, rule='type')
-            if isinstance(data__requirespython, str):
-                if not custom_formats["pep508-versionspec"](data__requirespython):
-                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".requires-python must be pep508-versionspec", value=data__requirespython, name="" + (name_prefix or "data") + ".requires-python", definition={'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, rule='format')
-        if "license" in data_keys:
-            data_keys.remove("license")
-            data__license = data["license"]
-            data__license_one_of_count10 = 0
-            if data__license_one_of_count10 < 2:
-                try:
-                    data__license_is_dict = isinstance(data__license, dict)
-                    if data__license_is_dict:
-                        data__license_len = len(data__license)
-                        if not all(prop in data__license for prop in ['file']):
-                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".license must contain ['file'] properties", value=data__license, name="" + (name_prefix or "data") + ".license", definition={'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, rule='required')
-                        data__license_keys = set(data__license.keys())
-                        if "file" in data__license_keys:
-                            data__license_keys.remove("file")
-                            data__license__file = data__license["file"]
-                            if not isinstance(data__license__file, (str)):
-                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".license.file must be string", value=data__license__file, name="" + (name_prefix or "data") + ".license.file", definition={'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}, rule='type')
-                    data__license_one_of_count10 += 1
-                except JsonSchemaValueException: pass
-            if data__license_one_of_count10 < 2:
-                try:
-                    data__license_is_dict = isinstance(data__license, dict)
-                    if data__license_is_dict:
-                        data__license_len = len(data__license)
-                        if not all(prop in data__license for prop in ['text']):
-                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".license must contain ['text'] properties", value=data__license, name="" + (name_prefix or "data") + ".license", definition={'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}, rule='required')
-                        data__license_keys = set(data__license.keys())
-                        if "text" in data__license_keys:
-                            data__license_keys.remove("text")
-                            data__license__text = data__license["text"]
-                            if not isinstance(data__license__text, (str)):
-                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".license.text must be string", value=data__license__text, name="" + (name_prefix or "data") + ".license.text", definition={'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}, rule='type')
-                    data__license_one_of_count10 += 1
-                except JsonSchemaValueException: pass
-            if data__license_one_of_count10 != 1:
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".license must be valid exactly by one definition" + (" (" + str(data__license_one_of_count10) + " matches found)"), value=data__license, name="" + (name_prefix or "data") + ".license", definition={'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, rule='oneOf')
-        if "authors" in data_keys:
-            data_keys.remove("authors")
-            data__authors = data["authors"]
-            if not isinstance(data__authors, (list, tuple)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".authors must be array", value=data__authors, name="" + (name_prefix or "data") + ".authors", definition={'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, rule='type')
-            data__authors_is_list = isinstance(data__authors, (list, tuple))
-            if data__authors_is_list:
-                data__authors_len = len(data__authors)
-                for data__authors_x, data__authors_item in enumerate(data__authors):
-                    validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_author(data__authors_item, custom_formats, (name_prefix or "data") + ".authors[{data__authors_x}]")
-        if "maintainers" in data_keys:
-            data_keys.remove("maintainers")
-            data__maintainers = data["maintainers"]
-            if not isinstance(data__maintainers, (list, tuple)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".maintainers must be array", value=data__maintainers, name="" + (name_prefix or "data") + ".maintainers", definition={'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, rule='type')
-            data__maintainers_is_list = isinstance(data__maintainers, (list, tuple))
-            if data__maintainers_is_list:
-                data__maintainers_len = len(data__maintainers)
-                for data__maintainers_x, data__maintainers_item in enumerate(data__maintainers):
-                    validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_author(data__maintainers_item, custom_formats, (name_prefix or "data") + ".maintainers[{data__maintainers_x}]")
-        if "keywords" in data_keys:
-            data_keys.remove("keywords")
-            data__keywords = data["keywords"]
-            if not isinstance(data__keywords, (list, tuple)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".keywords must be array", value=data__keywords, name="" + (name_prefix or "data") + ".keywords", definition={'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, rule='type')
-            data__keywords_is_list = isinstance(data__keywords, (list, tuple))
-            if data__keywords_is_list:
-                data__keywords_len = len(data__keywords)
-                for data__keywords_x, data__keywords_item in enumerate(data__keywords):
-                    if not isinstance(data__keywords_item, (str)):
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".keywords[{data__keywords_x}]".format(**locals()) + " must be string", value=data__keywords_item, name="" + (name_prefix or "data") + ".keywords[{data__keywords_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
-        if "classifiers" in data_keys:
-            data_keys.remove("classifiers")
-            data__classifiers = data["classifiers"]
-            if not isinstance(data__classifiers, (list, tuple)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".classifiers must be array", value=data__classifiers, name="" + (name_prefix or "data") + ".classifiers", definition={'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, rule='type')
-            data__classifiers_is_list = isinstance(data__classifiers, (list, tuple))
-            if data__classifiers_is_list:
-                data__classifiers_len = len(data__classifiers)
-                for data__classifiers_x, data__classifiers_item in enumerate(data__classifiers):
-                    if not isinstance(data__classifiers_item, (str)):
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".classifiers[{data__classifiers_x}]".format(**locals()) + " must be string", value=data__classifiers_item, name="" + (name_prefix or "data") + ".classifiers[{data__classifiers_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, rule='type')
-                    if isinstance(data__classifiers_item, str):
-                        if not custom_formats["trove-classifier"](data__classifiers_item):
-                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".classifiers[{data__classifiers_x}]".format(**locals()) + " must be trove-classifier", value=data__classifiers_item, name="" + (name_prefix or "data") + ".classifiers[{data__classifiers_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, rule='format')
-        if "urls" in data_keys:
-            data_keys.remove("urls")
-            data__urls = data["urls"]
-            if not isinstance(data__urls, (dict)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".urls must be object", value=data__urls, name="" + (name_prefix or "data") + ".urls", definition={'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, rule='type')
-            data__urls_is_dict = isinstance(data__urls, dict)
-            if data__urls_is_dict:
-                data__urls_keys = set(data__urls.keys())
-                for data__urls_key, data__urls_val in data__urls.items():
-                    if REGEX_PATTERNS['^.+$'].search(data__urls_key):
-                        if data__urls_key in data__urls_keys:
-                            data__urls_keys.remove(data__urls_key)
-                        if not isinstance(data__urls_val, (str)):
-                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".urls.{data__urls_key}".format(**locals()) + " must be string", value=data__urls_val, name="" + (name_prefix or "data") + ".urls.{data__urls_key}".format(**locals()) + "", definition={'type': 'string', 'format': 'url'}, rule='type')
-                        if isinstance(data__urls_val, str):
-                            if not custom_formats["url"](data__urls_val):
-                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".urls.{data__urls_key}".format(**locals()) + " must be url", value=data__urls_val, name="" + (name_prefix or "data") + ".urls.{data__urls_key}".format(**locals()) + "", definition={'type': 'string', 'format': 'url'}, rule='format')
-                if data__urls_keys:
-                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".urls must not contain "+str(data__urls_keys)+" properties", value=data__urls, name="" + (name_prefix or "data") + ".urls", definition={'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, rule='additionalProperties')
-        if "scripts" in data_keys:
-            data_keys.remove("scripts")
-            data__scripts = data["scripts"]
-            validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_entry_point_group(data__scripts, custom_formats, (name_prefix or "data") + ".scripts")
-        if "gui-scripts" in data_keys:
-            data_keys.remove("gui-scripts")
-            data__guiscripts = data["gui-scripts"]
-            validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_entry_point_group(data__guiscripts, custom_formats, (name_prefix or "data") + ".gui-scripts")
-        if "entry-points" in data_keys:
-            data_keys.remove("entry-points")
-            data__entrypoints = data["entry-points"]
-            data__entrypoints_is_dict = isinstance(data__entrypoints, dict)
-            if data__entrypoints_is_dict:
-                data__entrypoints_keys = set(data__entrypoints.keys())
-                for data__entrypoints_key, data__entrypoints_val in data__entrypoints.items():
-                    if REGEX_PATTERNS['^.+$'].search(data__entrypoints_key):
-                        if data__entrypoints_key in data__entrypoints_keys:
-                            data__entrypoints_keys.remove(data__entrypoints_key)
-                        validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_entry_point_group(data__entrypoints_val, custom_formats, (name_prefix or "data") + ".entry-points.{data__entrypoints_key}")
-                if data__entrypoints_keys:
-                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".entry-points must not contain "+str(data__entrypoints_keys)+" properties", value=data__entrypoints, name="" + (name_prefix or "data") + ".entry-points", definition={'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}}}, rule='additionalProperties')
-                data__entrypoints_len = len(data__entrypoints)
-                if data__entrypoints_len != 0:
-                    data__entrypoints_property_names = True
-                    for data__entrypoints_key in data__entrypoints:
-                        try:
-                            if isinstance(data__entrypoints_key, str):
-                                if not custom_formats["python-entrypoint-group"](data__entrypoints_key):
-                                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".entry-points must be python-entrypoint-group", value=data__entrypoints_key, name="" + (name_prefix or "data") + ".entry-points", definition={'format': 'python-entrypoint-group'}, rule='format')
-                        except JsonSchemaValueException:
-                            data__entrypoints_property_names = False
-                    if not data__entrypoints_property_names:
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".entry-points must be named by propertyName definition", value=data__entrypoints, name="" + (name_prefix or "data") + ".entry-points", definition={'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}}}, rule='propertyNames')
-        if "dependencies" in data_keys:
-            data_keys.remove("dependencies")
-            data__dependencies = data["dependencies"]
-            if not isinstance(data__dependencies, (list, tuple)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".dependencies must be array", value=data__dependencies, name="" + (name_prefix or "data") + ".dependencies", definition={'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}, rule='type')
-            data__dependencies_is_list = isinstance(data__dependencies, (list, tuple))
-            if data__dependencies_is_list:
-                data__dependencies_len = len(data__dependencies)
-                for data__dependencies_x, data__dependencies_item in enumerate(data__dependencies):
-                    validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_dependency(data__dependencies_item, custom_formats, (name_prefix or "data") + ".dependencies[{data__dependencies_x}]")
-        if "optional-dependencies" in data_keys:
-            data_keys.remove("optional-dependencies")
-            data__optionaldependencies = data["optional-dependencies"]
-            if not isinstance(data__optionaldependencies, (dict)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".optional-dependencies must be object", value=data__optionaldependencies, name="" + (name_prefix or "data") + ".optional-dependencies", definition={'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, rule='type')
-            data__optionaldependencies_is_dict = isinstance(data__optionaldependencies, dict)
-            if data__optionaldependencies_is_dict:
-                data__optionaldependencies_keys = set(data__optionaldependencies.keys())
-                for data__optionaldependencies_key, data__optionaldependencies_val in data__optionaldependencies.items():
-                    if REGEX_PATTERNS['^.+$'].search(data__optionaldependencies_key):
-                        if data__optionaldependencies_key in data__optionaldependencies_keys:
-                            data__optionaldependencies_keys.remove(data__optionaldependencies_key)
-                        if not isinstance(data__optionaldependencies_val, (list, tuple)):
-                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".optional-dependencies.{data__optionaldependencies_key}".format(**locals()) + " must be array", value=data__optionaldependencies_val, name="" + (name_prefix or "data") + ".optional-dependencies.{data__optionaldependencies_key}".format(**locals()) + "", definition={'type': 'array', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}, rule='type')
-                        data__optionaldependencies_val_is_list = isinstance(data__optionaldependencies_val, (list, tuple))
-                        if data__optionaldependencies_val_is_list:
-                            data__optionaldependencies_val_len = len(data__optionaldependencies_val)
-                            for data__optionaldependencies_val_x, data__optionaldependencies_val_item in enumerate(data__optionaldependencies_val):
-                                validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_dependency(data__optionaldependencies_val_item, custom_formats, (name_prefix or "data") + ".optional-dependencies.{data__optionaldependencies_key}[{data__optionaldependencies_val_x}]")
-                if data__optionaldependencies_keys:
-                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".optional-dependencies must not contain "+str(data__optionaldependencies_keys)+" properties", value=data__optionaldependencies, name="" + (name_prefix or "data") + ".optional-dependencies", definition={'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, rule='additionalProperties')
-                data__optionaldependencies_len = len(data__optionaldependencies)
-                if data__optionaldependencies_len != 0:
-                    data__optionaldependencies_property_names = True
-                    for data__optionaldependencies_key in data__optionaldependencies:
-                        try:
-                            if isinstance(data__optionaldependencies_key, str):
-                                if not custom_formats["pep508-identifier"](data__optionaldependencies_key):
-                                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".optional-dependencies must be pep508-identifier", value=data__optionaldependencies_key, name="" + (name_prefix or "data") + ".optional-dependencies", definition={'format': 'pep508-identifier'}, rule='format')
-                        except JsonSchemaValueException:
-                            data__optionaldependencies_property_names = False
-                    if not data__optionaldependencies_property_names:
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".optional-dependencies must be named by propertyName definition", value=data__optionaldependencies, name="" + (name_prefix or "data") + ".optional-dependencies", definition={'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, rule='propertyNames')
-        if "dynamic" in data_keys:
-            data_keys.remove("dynamic")
-            data__dynamic = data["dynamic"]
-            if not isinstance(data__dynamic, (list, tuple)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic must be array", value=data__dynamic, name="" + (name_prefix or "data") + ".dynamic", definition={'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}, rule='type')
-            data__dynamic_is_list = isinstance(data__dynamic, (list, tuple))
-            if data__dynamic_is_list:
-                data__dynamic_len = len(data__dynamic)
-                for data__dynamic_x, data__dynamic_item in enumerate(data__dynamic):
-                    if data__dynamic_item not in ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']:
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic[{data__dynamic_x}]".format(**locals()) + " must be one of ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']", value=data__dynamic_item, name="" + (name_prefix or "data") + ".dynamic[{data__dynamic_x}]".format(**locals()) + "", definition={'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}, rule='enum')
-        if data_keys:
-            raise JsonSchemaValueException("" + (name_prefix or "data") + " must not contain "+str(data_keys)+" properties", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'gui-scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['dynamic'], 'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}}}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'required': ['version'], '$$description': ['version should be statically defined in the ``version`` field']}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, rule='additionalProperties')
-    try:
-        try:
-            data_is_dict = isinstance(data, dict)
-            if data_is_dict:
-                data_len = len(data)
-                if not all(prop in data for prop in ['dynamic']):
-                    raise JsonSchemaValueException("" + (name_prefix or "data") + " must contain ['dynamic'] properties", value=data, name="" + (name_prefix or "data") + "", definition={'required': ['dynamic'], 'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}}}, rule='required')
-                data_keys = set(data.keys())
-                if "dynamic" in data_keys:
-                    data_keys.remove("dynamic")
-                    data__dynamic = data["dynamic"]
-                    data__dynamic_is_list = isinstance(data__dynamic, (list, tuple))
-                    if data__dynamic_is_list:
-                        data__dynamic_contains = False
-                        for data__dynamic_key in data__dynamic:
-                            try:
-                                if data__dynamic_key != "version":
-                                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic must be same as const definition: version", value=data__dynamic_key, name="" + (name_prefix or "data") + ".dynamic", definition={'const': 'version'}, rule='const')
-                                data__dynamic_contains = True
-                                break
-                            except JsonSchemaValueException: pass
-                        if not data__dynamic_contains:
-                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic must contain one of contains definition", value=data__dynamic, name="" + (name_prefix or "data") + ".dynamic", definition={'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}, rule='contains')
-        except JsonSchemaValueException: pass
-        else:
-            raise JsonSchemaValueException("" + (name_prefix or "data") + " must NOT match a disallowed definition", value=data, name="" + (name_prefix or "data") + "", definition={'not': {'required': ['dynamic'], 'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}}}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, rule='not')
-    except JsonSchemaValueException:
-        pass
-    else:
-        data_is_dict = isinstance(data, dict)
-        if data_is_dict:
-            data_len = len(data)
-            if not all(prop in data for prop in ['version']):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + " must contain ['version'] properties", value=data, name="" + (name_prefix or "data") + "", definition={'required': ['version'], '$$description': ['version should be statically defined in the ``version`` field']}, rule='required')
-    return data
-
-def validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_dependency(data, custom_formats={}, name_prefix=None):
-    if not isinstance(data, (str)):
-        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be string", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}, rule='type')
-    if isinstance(data, str):
-        if not custom_formats["pep508"](data):
-            raise JsonSchemaValueException("" + (name_prefix or "data") + " must be pep508", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}, rule='format')
-    return data
-
-def validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_entry_point_group(data, custom_formats={}, name_prefix=None):
-    if not isinstance(data, (dict)):
-        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, rule='type')
-    data_is_dict = isinstance(data, dict)
-    if data_is_dict:
-        data_keys = set(data.keys())
-        for data_key, data_val in data.items():
-            if REGEX_PATTERNS['^.+$'].search(data_key):
-                if data_key in data_keys:
-                    data_keys.remove(data_key)
-                if not isinstance(data_val, (str)):
-                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".{data_key}".format(**locals()) + " must be string", value=data_val, name="" + (name_prefix or "data") + ".{data_key}".format(**locals()) + "", definition={'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}, rule='type')
-                if isinstance(data_val, str):
-                    if not custom_formats["python-entrypoint-reference"](data_val):
-                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".{data_key}".format(**locals()) + " must be python-entrypoint-reference", value=data_val, name="" + (name_prefix or "data") + ".{data_key}".format(**locals()) + "", definition={'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}, rule='format')
-        if data_keys:
-            raise JsonSchemaValueException("" + (name_prefix or "data") + " must not contain "+str(data_keys)+" properties", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, rule='additionalProperties')
-        data_len = len(data)
-        if data_len != 0:
-            data_property_names = True
-            for data_key in data:
-                try:
-                    if isinstance(data_key, str):
-                        if not custom_formats["python-entrypoint-name"](data_key):
-                            raise JsonSchemaValueException("" + (name_prefix or "data") + " must be python-entrypoint-name", value=data_key, name="" + (name_prefix or "data") + "", definition={'format': 'python-entrypoint-name'}, rule='format')
-                except JsonSchemaValueException:
-                    data_property_names = False
-            if not data_property_names:
-                raise JsonSchemaValueException("" + (name_prefix or "data") + " must be named by propertyName definition", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, rule='propertyNames')
-    return data
-
-def validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_author(data, custom_formats={}, name_prefix=None):
-    if not isinstance(data, (dict)):
-        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, rule='type')
-    data_is_dict = isinstance(data, dict)
-    if data_is_dict:
-        data_keys = set(data.keys())
-        if "name" in data_keys:
-            data_keys.remove("name")
-            data__name = data["name"]
-            if not isinstance(data__name, (str)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".name must be string", value=data__name, name="" + (name_prefix or "data") + ".name", definition={'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, rule='type')
-        if "email" in data_keys:
-            data_keys.remove("email")
-            data__email = data["email"]
-            if not isinstance(data__email, (str)):
-                raise JsonSchemaValueException("" + (name_prefix or "data") + ".email must be string", value=data__email, name="" + (name_prefix or "data") + ".email", definition={'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}, rule='type')
-            if isinstance(data__email, str):
-                if not REGEX_PATTERNS["idn-email_re_pattern"].match(data__email):
-                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".email must be idn-email", value=data__email, name="" + (name_prefix or "data") + ".email", definition={'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}, rule='format')
-    return data
\ No newline at end of file
diff --git a/setuptools/_vendor/_validate_pyproject/formats.py b/setuptools/_vendor/_validate_pyproject/formats.py
deleted file mode 100644
index a288eb5f..00000000
--- a/setuptools/_vendor/_validate_pyproject/formats.py
+++ /dev/null
@@ -1,252 +0,0 @@
-import logging
-import os
-import re
-import string
-import typing
-from itertools import chain as _chain
-
-_logger = logging.getLogger(__name__)
-
-# -------------------------------------------------------------------------------------
-# PEP 440
-
-VERSION_PATTERN = r"""
-    v?
-    (?:
-        (?:(?P[0-9]+)!)?                           # epoch
-        (?P[0-9]+(?:\.[0-9]+)*)                  # release segment
-        (?P
                                          # pre-release
-            [-_\.]?
-            (?P(a|b|c|rc|alpha|beta|pre|preview))
-            [-_\.]?
-            (?P[0-9]+)?
-        )?
-        (?P                                         # post release
-            (?:-(?P[0-9]+))
-            |
-            (?:
-                [-_\.]?
-                (?Ppost|rev|r)
-                [-_\.]?
-                (?P[0-9]+)?
-            )
-        )?
-        (?P                                          # dev release
-            [-_\.]?
-            (?Pdev)
-            [-_\.]?
-            (?P[0-9]+)?
-        )?
-    )
-    (?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
-"""
-
-VERSION_REGEX = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.X | re.I)
-
-
-def pep440(version: str) -> bool:
-    return VERSION_REGEX.match(version) is not None
-
-
-# -------------------------------------------------------------------------------------
-# PEP 508
-
-PEP508_IDENTIFIER_PATTERN = r"([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])"
-PEP508_IDENTIFIER_REGEX = re.compile(f"^{PEP508_IDENTIFIER_PATTERN}$", re.I)
-
-
-def pep508_identifier(name: str) -> bool:
-    return PEP508_IDENTIFIER_REGEX.match(name) is not None
-
-
-try:
-    try:
-        from packaging import requirements as _req
-    except ImportError:  # pragma: no cover
-        # let's try setuptools vendored version
-        from setuptools._vendor.packaging import requirements as _req  # type: ignore
-
-    def pep508(value: str) -> bool:
-        try:
-            _req.Requirement(value)
-            return True
-        except _req.InvalidRequirement:
-            return False
-
-except ImportError:  # pragma: no cover
-    _logger.warning(
-        "Could not find an installation of `packaging`. Requirements, dependencies and "
-        "versions might not be validated. "
-        "To enforce validation, please install `packaging`."
-    )
-
-    def pep508(value: str) -> bool:
-        return True
-
-
-def pep508_versionspec(value: str) -> bool:
-    """Expression that can be used to specify/lock versions (including ranges)"""
-    if any(c in value for c in (";", "]", "@")):
-        # In PEP 508:
-        # conditional markers, extras and URL specs are not included in the
-        # versionspec
-        return False
-    # Let's pretend we have a dependency called `requirement` with the given
-    # version spec, then we can re-use the pep508 function for validation:
-    return pep508(f"requirement{value}")
-
-
-# -------------------------------------------------------------------------------------
-# PEP 517
-
-
-def pep517_backend_reference(value: str) -> bool:
-    module, _, obj = value.partition(":")
-    identifiers = (i.strip() for i in _chain(module.split("."), obj.split(".")))
-    return all(python_identifier(i) for i in identifiers if i)
-
-
-# -------------------------------------------------------------------------------------
-# Classifiers - PEP 301
-
-
-def _download_classifiers() -> str:
-    import cgi
-    from urllib.request import urlopen
-
-    url = "https://pypi.org/pypi?:action=list_classifiers"
-    with urlopen(url) as response:
-        content_type = response.getheader("content-type", "text/plain")
-        encoding = cgi.parse_header(content_type)[1].get("charset", "utf-8")
-        return response.read().decode(encoding)
-
-
-class _TroveClassifier:
-    """The ``trove_classifiers`` package is the official way of validating classifiers,
-    however this package might not be always available.
-    As a workaround we can still download a list from PyPI.
-    We also don't want to be over strict about it, so simply skipping silently is an
-    option (classifiers will be validated anyway during the upload to PyPI).
-    """
-
-    def __init__(self):
-        self.downloaded: typing.Union[None, False, typing.Set[str]] = None
-        # None => not cached yet
-        # False => cache not available
-        self.__name__ = "trove_classifier"  # Emulate a public function
-
-    def __call__(self, value: str) -> bool:
-        if self.downloaded is False:
-            return True
-
-        if os.getenv("NO_NETWORK"):
-            self.downloaded = False
-            msg = (
-                "Install ``trove-classifiers`` to ensure proper validation. "
-                "Skipping download of classifiers list from PyPI (NO_NETWORK)."
-            )
-            _logger.debug(msg)
-            return True
-
-        if self.downloaded is None:
-            msg = (
-                "Install ``trove-classifiers`` to ensure proper validation. "
-                "Meanwhile a list of classifiers will be downloaded from PyPI."
-            )
-            _logger.debug(msg)
-            try:
-                self.downloaded = set(_download_classifiers().splitlines())
-            except Exception:
-                self.downloaded = False
-                _logger.debug("Problem with download, skipping validation")
-                return True
-
-        return value in self.downloaded or value.lower().startswith("private ::")
-
-
-try:
-    from trove_classifiers import classifiers as _trove_classifiers
-
-    def trove_classifier(value: str) -> bool:
-        return value in _trove_classifiers or value.lower().startswith("private ::")
-
-except ImportError:  # pragma: no cover
-    trove_classifier = _TroveClassifier()
-
-
-# -------------------------------------------------------------------------------------
-# Non-PEP related
-
-
-def url(value: str) -> bool:
-    from urllib.parse import urlparse
-
-    try:
-        parts = urlparse(value)
-        if not parts.scheme:
-            _logger.warning(
-                "For maximum compatibility please make sure to include a "
-                "`scheme` prefix in your URL (e.g. 'http://'). "
-                f"Given value: {value}"
-            )
-            if not (value.startswith("/") or value.startswith("\\") or "@" in value):
-                parts = urlparse(f"http://{value}")
-
-        return bool(parts.scheme and parts.netloc)
-    except Exception:
-        return False
-
-
-# https://packaging.python.org/specifications/entry-points/
-ENTRYPOINT_PATTERN = r"[^\[\s=]([^=]*[^\s=])?"
-ENTRYPOINT_REGEX = re.compile(f"^{ENTRYPOINT_PATTERN}$", re.I)
-RECOMMEDED_ENTRYPOINT_PATTERN = r"[\w.-]+"
-RECOMMEDED_ENTRYPOINT_REGEX = re.compile(f"^{RECOMMEDED_ENTRYPOINT_PATTERN}$", re.I)
-ENTRYPOINT_GROUP_PATTERN = r"\w+(\.\w+)*"
-ENTRYPOINT_GROUP_REGEX = re.compile(f"^{ENTRYPOINT_GROUP_PATTERN}$", re.I)
-
-
-def python_identifier(value: str) -> bool:
-    return value.isidentifier()
-
-
-def python_qualified_identifier(value: str) -> bool:
-    if value.startswith(".") or value.endswith("."):
-        return False
-    return all(python_identifier(m) for m in value.split("."))
-
-
-def python_module_name(value: str) -> bool:
-    return python_qualified_identifier(value)
-
-
-def python_entrypoint_group(value: str) -> bool:
-    return ENTRYPOINT_GROUP_REGEX.match(value) is not None
-
-
-def python_entrypoint_name(value: str) -> bool:
-    if not ENTRYPOINT_REGEX.match(value):
-        return False
-    if not RECOMMEDED_ENTRYPOINT_REGEX.match(value):
-        msg = f"Entry point `{value}` does not follow recommended pattern: "
-        msg += RECOMMEDED_ENTRYPOINT_PATTERN
-        _logger.warning(msg)
-    return True
-
-
-def python_entrypoint_reference(value: str) -> bool:
-    module, _, rest = value.partition(":")
-    if "[" in rest:
-        obj, _, extras_ = rest.partition("[")
-        if extras_.strip()[-1] != "]":
-            return False
-        extras = (x.strip() for x in extras_.strip(string.whitespace + "[]").split(","))
-        if not all(pep508_identifier(e) for e in extras):
-            return False
-        _logger.warning(f"`{value}` - using extras for entry points is not recommended")
-    else:
-        obj = rest
-
-    module_parts = module.split(".")
-    identifiers = _chain(module_parts, obj.split(".")) if rest else module_parts
-    return all(python_identifier(i.strip()) for i in identifiers)
diff --git a/setuptools/config/_validate_pyproject/NOTICE b/setuptools/config/_validate_pyproject/NOTICE
new file mode 100644
index 00000000..b426f7fd
--- /dev/null
+++ b/setuptools/config/_validate_pyproject/NOTICE
@@ -0,0 +1,439 @@
+The code contained in this directory was automatically generated using the
+following command:
+
+    python -m validate_pyproject.vendoring --output-dir=setuptools/config/_validate_pyproject --enable-plugins setuptools distutils --very-verbose
+
+Please avoid changing it manually.
+
+
+You can report issues or suggest changes directly to `validate-pyproject`
+(or to the relevant plugin repository)
+
+- https://github.com/abravalheri/validate-pyproject/issues
+
+
+***
+
+The following files include code from opensource projects
+(either as direct copies or modified versions):
+
+- `fastjsonschema_exceptions.py`:
+    - project: `fastjsonschema` - licensed under BSD-3-Clause
+      (https://github.com/horejsek/python-fastjsonschema)
+- `extra_validations.py` and `format.py`, `error_reporting.py`:
+    - project: `validate-pyproject` - licensed under MPL-2.0
+      (https://github.com/abravalheri/validate-pyproject)
+
+
+Additionally the following files are automatically generated by tools provided
+by the same projects:
+
+- `__init__.py`
+- `fastjsonschema_validations.py`
+
+The relevant copyright notes and licenses are included bellow.
+
+
+***
+
+`fastjsonschema`
+================
+
+Copyright (c) 2018, Michal Horejsek
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+  Redistributions of source code must retain the above copyright notice, this
+  list of conditions and the following disclaimer.
+
+  Redistributions in binary form must reproduce the above copyright notice, this
+  list of conditions and the following disclaimer in the documentation and/or
+  other materials provided with the distribution.
+
+  Neither the name of the {organization} nor the names of its
+  contributors may be used to endorse or promote products derived from
+  this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+
+***
+
+`validate-pyproject`
+====================
+
+Mozilla Public License, version 2.0
+
+1. Definitions
+
+1.1. "Contributor"
+
+     means each individual or legal entity that creates, contributes to the
+     creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+
+     means the combination of the Contributions of others (if any) used by a
+     Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+
+     means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+
+     means Source Code Form to which the initial Contributor has attached the
+     notice in Exhibit A, the Executable Form of such Source Code Form, and
+     Modifications of such Source Code Form, in each case including portions
+     thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+     means
+
+     a. that the initial Contributor has attached the notice described in
+        Exhibit B to the Covered Software; or
+
+     b. that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the terms of
+        a Secondary License.
+
+1.6. "Executable Form"
+
+     means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+
+     means a work that combines Covered Software with other material, in a
+     separate file or files, that is not Covered Software.
+
+1.8. "License"
+
+     means this document.
+
+1.9. "Licensable"
+
+     means having the right to grant, to the maximum extent possible, whether
+     at the time of the initial grant or subsequently, any and all of the
+     rights conveyed by this License.
+
+1.10. "Modifications"
+
+     means any of the following:
+
+     a. any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered Software; or
+
+     b. any new file in Source Code Form that contains any Covered Software.
+
+1.11. "Patent Claims" of a Contributor
+
+      means any patent claim(s), including without limitation, method,
+      process, and apparatus claims, in any patent Licensable by such
+      Contributor that would be infringed, but for the grant of the License,
+      by the making, using, selling, offering for sale, having made, import,
+      or transfer of either its Contributions or its Contributor Version.
+
+1.12. "Secondary License"
+
+      means either the GNU General Public License, Version 2.0, the GNU Lesser
+      General Public License, Version 2.1, the GNU Affero General Public
+      License, Version 3.0, or any later versions of those licenses.
+
+1.13. "Source Code Form"
+
+      means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+
+      means an individual or a legal entity exercising rights under this
+      License. For legal entities, "You" includes any entity that controls, is
+      controlled by, or is under common control with You. For purposes of this
+      definition, "control" means (a) the power, direct or indirect, to cause
+      the direction or management of such entity, whether by contract or
+      otherwise, or (b) ownership of more than fifty percent (50%) of the
+      outstanding shares or beneficial ownership of such entity.
+
+
+2. License Grants and Conditions
+
+2.1. Grants
+
+     Each Contributor hereby grants You a world-wide, royalty-free,
+     non-exclusive license:
+
+     a. under intellectual property rights (other than patent or trademark)
+        Licensable by such Contributor to use, reproduce, make available,
+        modify, display, perform, distribute, and otherwise exploit its
+        Contributions, either on an unmodified basis, with Modifications, or
+        as part of a Larger Work; and
+
+     b. under Patent Claims of such Contributor to make, use, sell, offer for
+        sale, have made, import, and otherwise transfer either its
+        Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+     The licenses granted in Section 2.1 with respect to any Contribution
+     become effective for each Contribution on the date the Contributor first
+     distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+     The licenses granted in this Section 2 are the only rights granted under
+     this License. No additional rights or licenses will be implied from the
+     distribution or licensing of Covered Software under this License.
+     Notwithstanding Section 2.1(b) above, no patent license is granted by a
+     Contributor:
+
+     a. for any code that a Contributor has removed from Covered Software; or
+
+     b. for infringements caused by: (i) Your and any other third party's
+        modifications of Covered Software, or (ii) the combination of its
+        Contributions with other software (except as part of its Contributor
+        Version); or
+
+     c. under Patent Claims infringed by Covered Software in the absence of
+        its Contributions.
+
+     This License does not grant any rights in the trademarks, service marks,
+     or logos of any Contributor (except as may be necessary to comply with
+     the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+     No Contributor makes additional grants as a result of Your choice to
+     distribute the Covered Software under a subsequent version of this
+     License (see Section 10.2) or under the terms of a Secondary License (if
+     permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+     Each Contributor represents that the Contributor believes its
+     Contributions are its original creation(s) or it has sufficient rights to
+     grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+     This License is not intended to limit any rights You have under
+     applicable copyright doctrines of fair use, fair dealing, or other
+     equivalents.
+
+2.7. Conditions
+
+     Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
+     Section 2.1.
+
+
+3. Responsibilities
+
+3.1. Distribution of Source Form
+
+     All distribution of Covered Software in Source Code Form, including any
+     Modifications that You create or to which You contribute, must be under
+     the terms of this License. You must inform recipients that the Source
+     Code Form of the Covered Software is governed by the terms of this
+     License, and how they can obtain a copy of this License. You may not
+     attempt to alter or restrict the recipients' rights in the Source Code
+     Form.
+
+3.2. Distribution of Executable Form
+
+     If You distribute Covered Software in Executable Form then:
+
+     a. such Covered Software must also be made available in Source Code Form,
+        as described in Section 3.1, and You must inform recipients of the
+        Executable Form how they can obtain a copy of such Source Code Form by
+        reasonable means in a timely manner, at a charge no more than the cost
+        of distribution to the recipient; and
+
+     b. You may distribute such Executable Form under the terms of this
+        License, or sublicense it under different terms, provided that the
+        license for the Executable Form does not attempt to limit or alter the
+        recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+     You may create and distribute a Larger Work under terms of Your choice,
+     provided that You also comply with the requirements of this License for
+     the Covered Software. If the Larger Work is a combination of Covered
+     Software with a work governed by one or more Secondary Licenses, and the
+     Covered Software is not Incompatible With Secondary Licenses, this
+     License permits You to additionally distribute such Covered Software
+     under the terms of such Secondary License(s), so that the recipient of
+     the Larger Work may, at their option, further distribute the Covered
+     Software under the terms of either this License or such Secondary
+     License(s).
+
+3.4. Notices
+
+     You may not remove or alter the substance of any license notices
+     (including copyright notices, patent notices, disclaimers of warranty, or
+     limitations of liability) contained within the Source Code Form of the
+     Covered Software, except that You may alter any license notices to the
+     extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+     You may choose to offer, and to charge a fee for, warranty, support,
+     indemnity or liability obligations to one or more recipients of Covered
+     Software. However, You may do so only on Your own behalf, and not on
+     behalf of any Contributor. You must make it absolutely clear that any
+     such warranty, support, indemnity, or liability obligation is offered by
+     You alone, and You hereby agree to indemnify every Contributor for any
+     liability incurred by such Contributor as a result of warranty, support,
+     indemnity or liability terms You offer. You may include additional
+     disclaimers of warranty and limitations of liability specific to any
+     jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+
+   If it is impossible for You to comply with any of the terms of this License
+   with respect to some or all of the Covered Software due to statute,
+   judicial order, or regulation then You must: (a) comply with the terms of
+   this License to the maximum extent possible; and (b) describe the
+   limitations and the code they affect. Such description must be placed in a
+   text file included with all distributions of the Covered Software under
+   this License. Except to the extent prohibited by statute or regulation,
+   such description must be sufficiently detailed for a recipient of ordinary
+   skill to be able to understand it.
+
+5. Termination
+
+5.1. The rights granted under this License will terminate automatically if You
+     fail to comply with any of its terms. However, if You become compliant,
+     then the rights granted under this License from a particular Contributor
+     are reinstated (a) provisionally, unless and until such Contributor
+     explicitly and finally terminates Your grants, and (b) on an ongoing
+     basis, if such Contributor fails to notify You of the non-compliance by
+     some reasonable means prior to 60 days after You have come back into
+     compliance. Moreover, Your grants from a particular Contributor are
+     reinstated on an ongoing basis if such Contributor notifies You of the
+     non-compliance by some reasonable means, this is the first time You have
+     received notice of non-compliance with this License from such
+     Contributor, and You become compliant prior to 30 days after Your receipt
+     of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+     infringement claim (excluding declaratory judgment actions,
+     counter-claims, and cross-claims) alleging that a Contributor Version
+     directly or indirectly infringes any patent, then the rights granted to
+     You by any and all Contributors for the Covered Software under Section
+     2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
+     license agreements (excluding distributors and resellers) which have been
+     validly granted by You or Your distributors under this License prior to
+     termination shall survive termination.
+
+6. Disclaimer of Warranty
+
+   Covered Software is provided under this License on an "as is" basis,
+   without warranty of any kind, either expressed, implied, or statutory,
+   including, without limitation, warranties that the Covered Software is free
+   of defects, merchantable, fit for a particular purpose or non-infringing.
+   The entire risk as to the quality and performance of the Covered Software
+   is with You. Should any Covered Software prove defective in any respect,
+   You (not any Contributor) assume the cost of any necessary servicing,
+   repair, or correction. This disclaimer of warranty constitutes an essential
+   part of this License. No use of  any Covered Software is authorized under
+   this License except under this disclaimer.
+
+7. Limitation of Liability
+
+   Under no circumstances and under no legal theory, whether tort (including
+   negligence), contract, or otherwise, shall any Contributor, or anyone who
+   distributes Covered Software as permitted above, be liable to You for any
+   direct, indirect, special, incidental, or consequential damages of any
+   character including, without limitation, damages for lost profits, loss of
+   goodwill, work stoppage, computer failure or malfunction, or any and all
+   other commercial damages or losses, even if such party shall have been
+   informed of the possibility of such damages. This limitation of liability
+   shall not apply to liability for death or personal injury resulting from
+   such party's negligence to the extent applicable law prohibits such
+   limitation. Some jurisdictions do not allow the exclusion or limitation of
+   incidental or consequential damages, so this exclusion and limitation may
+   not apply to You.
+
+8. Litigation
+
+   Any litigation relating to this License may be brought only in the courts
+   of a jurisdiction where the defendant maintains its principal place of
+   business and such litigation shall be governed by laws of that
+   jurisdiction, without reference to its conflict-of-law provisions. Nothing
+   in this Section shall prevent a party's ability to bring cross-claims or
+   counter-claims.
+
+9. Miscellaneous
+
+   This License represents the complete agreement concerning the subject
+   matter hereof. If any provision of this License is held to be
+   unenforceable, such provision shall be reformed only to the extent
+   necessary to make it enforceable. Any law or regulation which provides that
+   the language of a contract shall be construed against the drafter shall not
+   be used to construe this License against a Contributor.
+
+
+10. Versions of the License
+
+10.1. New Versions
+
+      Mozilla Foundation is the license steward. Except as provided in Section
+      10.3, no one other than the license steward has the right to modify or
+      publish new versions of this License. Each version will be given a
+      distinguishing version number.
+
+10.2. Effect of New Versions
+
+      You may distribute the Covered Software under the terms of the version
+      of the License under which You originally received the Covered Software,
+      or under the terms of any subsequent version published by the license
+      steward.
+
+10.3. Modified Versions
+
+      If you create software not governed by this License, and you want to
+      create a new license for such software, you may create and use a
+      modified version of this License if you rename the license and remove
+      any references to the name of the license steward (except to note that
+      such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+      Licenses If You choose to distribute Source Code Form that is
+      Incompatible With Secondary Licenses under the terms of this version of
+      the License, the notice described in Exhibit B of this License must be
+      attached.
+
+Exhibit A - Source Code Form License Notice
+
+      This Source Code Form is subject to the
+      terms of the Mozilla Public License, v.
+      2.0. If a copy of the MPL was not
+      distributed with this file, You can
+      obtain one at
+      https://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular file,
+then You may include the notice in a location (such as a LICENSE file in a
+relevant directory) where a recipient would be likely to look for such a
+notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+
+      This Source Code Form is "Incompatible
+      With Secondary Licenses", as defined by
+      the Mozilla Public License, v. 2.0.
+
diff --git a/setuptools/config/_validate_pyproject/__init__.py b/setuptools/config/_validate_pyproject/__init__.py
new file mode 100644
index 00000000..dbe6cb4c
--- /dev/null
+++ b/setuptools/config/_validate_pyproject/__init__.py
@@ -0,0 +1,34 @@
+from functools import reduce
+from typing import Any, Callable, Dict
+
+from . import formats
+from .error_reporting import detailed_errors, ValidationError
+from .extra_validations import EXTRA_VALIDATIONS
+from .fastjsonschema_exceptions import JsonSchemaException, JsonSchemaValueException
+from .fastjsonschema_validations import validate as _validate
+
+__all__ = [
+    "validate",
+    "FORMAT_FUNCTIONS",
+    "EXTRA_VALIDATIONS",
+    "ValidationError",
+    "JsonSchemaException",
+    "JsonSchemaValueException",
+]
+
+
+FORMAT_FUNCTIONS: Dict[str, Callable[[str], bool]] = {
+    fn.__name__.replace("_", "-"): fn
+    for fn in formats.__dict__.values()
+    if callable(fn) and not fn.__name__.startswith("_")
+}
+
+
+def validate(data: Any) -> bool:
+    """Validate the given ``data`` object using JSON Schema
+    This function raises ``ValidationError`` if ``data`` is invalid.
+    """
+    with detailed_errors():
+        _validate(data, custom_formats=FORMAT_FUNCTIONS)
+    reduce(lambda acc, fn: fn(acc), EXTRA_VALIDATIONS, data)
+    return True
diff --git a/setuptools/config/_validate_pyproject/error_reporting.py b/setuptools/config/_validate_pyproject/error_reporting.py
new file mode 100644
index 00000000..3a4d4e9e
--- /dev/null
+++ b/setuptools/config/_validate_pyproject/error_reporting.py
@@ -0,0 +1,318 @@
+import io
+import json
+import logging
+import os
+import re
+from contextlib import contextmanager
+from textwrap import indent, wrap
+from typing import Any, Dict, Iterator, List, Optional, Sequence, Union, cast
+
+from .fastjsonschema_exceptions import JsonSchemaValueException
+
+_logger = logging.getLogger(__name__)
+
+_MESSAGE_REPLACEMENTS = {
+    "must be named by propertyName definition": "keys must be named by",
+    "one of contains definition": "at least one item that matches",
+    " same as const definition:": "",
+    "only specified items": "only items matching the definition",
+}
+
+_SKIP_DETAILS = (
+    "must not be empty",
+    "is always invalid",
+    "must not be there",
+)
+
+_NEED_DETAILS = {"anyOf", "oneOf", "anyOf", "contains", "propertyNames", "not", "items"}
+
+_CAMEL_CASE_SPLITTER = re.compile(r"\W+|([A-Z][^A-Z\W]*)")
+_IDENTIFIER = re.compile(r"^[\w_]+$", re.I)
+
+_TOML_JARGON = {
+    "object": "table",
+    "property": "key",
+    "properties": "keys",
+    "property names": "keys",
+}
+
+
+class ValidationError(JsonSchemaValueException):
+    """Report violations of a given JSON schema.
+
+    This class extends :exc:`~fastjsonschema.JsonSchemaValueException`
+    by adding the following properties:
+
+    - ``summary``: an improved version of the ``JsonSchemaValueException`` error message
+      with only the necessary information)
+
+    - ``details``: more contextual information about the error like the failing schema
+      itself and the value that violates the schema.
+
+    Depending on the level of the verbosity of the ``logging`` configuration
+    the exception message will be only ``summary`` (default) or a combination of
+    ``summary`` and ``details`` (when the logging level is set to :obj:`logging.DEBUG`).
+    """
+
+    summary = ""
+    details = ""
+    _original_message = ""
+
+    @classmethod
+    def _from_jsonschema(cls, ex: JsonSchemaValueException):
+        formatter = _ErrorFormatting(ex)
+        obj = cls(str(formatter), ex.value, formatter.name, ex.definition, ex.rule)
+        debug_code = os.getenv("JSONSCHEMA_DEBUG_CODE_GENERATION", "false").lower()
+        if debug_code != "false":  # pragma: no cover
+            obj.__cause__, obj.__traceback__ = ex.__cause__, ex.__traceback__
+        obj._original_message = ex.message
+        obj.summary = formatter.summary
+        obj.details = formatter.details
+        return obj
+
+
+@contextmanager
+def detailed_errors():
+    try:
+        yield
+    except JsonSchemaValueException as ex:
+        raise ValidationError._from_jsonschema(ex) from None
+
+
+class _ErrorFormatting:
+    def __init__(self, ex: JsonSchemaValueException):
+        self.ex = ex
+        self.name = f"`{self._simplify_name(ex.name)}`"
+        self._original_message = self.ex.message.replace(ex.name, self.name)
+        self._summary = ""
+        self._details = ""
+
+    def __str__(self) -> str:
+        if _logger.getEffectiveLevel() <= logging.DEBUG and self.details:
+            return f"{self.summary}\n\n{self.details}"
+
+        return self.summary
+
+    @property
+    def summary(self) -> str:
+        if not self._summary:
+            self._summary = self._expand_summary()
+
+        return self._summary
+
+    @property
+    def details(self) -> str:
+        if not self._details:
+            self._details = self._expand_details()
+
+        return self._details
+
+    def _simplify_name(self, name):
+        x = len("data.")
+        return name[x:] if name.startswith("data.") else name
+
+    def _expand_summary(self):
+        msg = self._original_message
+
+        for bad, repl in _MESSAGE_REPLACEMENTS.items():
+            msg = msg.replace(bad, repl)
+
+        if any(substring in msg for substring in _SKIP_DETAILS):
+            return msg
+
+        schema = self.ex.rule_definition
+        if self.ex.rule in _NEED_DETAILS and schema:
+            summary = _SummaryWriter(_TOML_JARGON)
+            return f"{msg}:\n\n{indent(summary(schema), '    ')}"
+
+        return msg
+
+    def _expand_details(self) -> str:
+        optional = []
+        desc_lines = self.ex.definition.pop("$$description", [])
+        desc = self.ex.definition.pop("description", None) or " ".join(desc_lines)
+        if desc:
+            description = "\n".join(
+                wrap(
+                    desc,
+                    width=80,
+                    initial_indent="    ",
+                    subsequent_indent="    ",
+                    break_long_words=False,
+                )
+            )
+            optional.append(f"DESCRIPTION:\n{description}")
+        schema = json.dumps(self.ex.definition, indent=4)
+        value = json.dumps(self.ex.value, indent=4)
+        defaults = [
+            f"GIVEN VALUE:\n{indent(value, '    ')}",
+            f"OFFENDING RULE: {self.ex.rule!r}",
+            f"DEFINITION:\n{indent(schema, '    ')}",
+        ]
+        return "\n\n".join(optional + defaults)
+
+
+class _SummaryWriter:
+    _IGNORE = {"description", "default", "title", "examples"}
+
+    def __init__(self, jargon: Optional[Dict[str, str]] = None):
+        self.jargon: Dict[str, str] = jargon or {}
+        # Clarify confusing terms
+        self._terms = {
+            "anyOf": "at least one of the following",
+            "oneOf": "exactly one of the following",
+            "allOf": "all of the following",
+            "not": "(*NOT* the following)",
+            "prefixItems": f"{self._jargon('items')} (in order)",
+            "items": "items",
+            "contains": "contains at least one of",
+            "propertyNames": (
+                f"non-predefined acceptable {self._jargon('property names')}"
+            ),
+            "patternProperties": f"{self._jargon('properties')} named via pattern",
+            "const": "predefined value",
+            "enum": "one of",
+        }
+        # Attributes that indicate that the definition is easy and can be done
+        # inline (e.g. string and number)
+        self._guess_inline_defs = [
+            "enum",
+            "const",
+            "maxLength",
+            "minLength",
+            "pattern",
+            "format",
+            "minimum",
+            "maximum",
+            "exclusiveMinimum",
+            "exclusiveMaximum",
+            "multipleOf",
+        ]
+
+    def _jargon(self, term: Union[str, List[str]]) -> Union[str, List[str]]:
+        if isinstance(term, list):
+            return [self.jargon.get(t, t) for t in term]
+        return self.jargon.get(term, term)
+
+    def __call__(
+        self,
+        schema: Union[dict, List[dict]],
+        prefix: str = "",
+        *,
+        _path: Sequence[str] = (),
+    ) -> str:
+        if isinstance(schema, list):
+            return self._handle_list(schema, prefix, _path)
+
+        filtered = self._filter_unecessary(schema, _path)
+        simple = self._handle_simple_dict(filtered, _path)
+        if simple:
+            return f"{prefix}{simple}"
+
+        child_prefix = self._child_prefix(prefix, "  ")
+        item_prefix = self._child_prefix(prefix, "- ")
+        indent = len(prefix) * " "
+        with io.StringIO() as buffer:
+            for i, (key, value) in enumerate(filtered.items()):
+                child_path = [*_path, key]
+                line_prefix = prefix if i == 0 else indent
+                buffer.write(f"{line_prefix}{self._label(child_path)}:")
+                # ^  just the first item should receive the complete prefix
+                if isinstance(value, dict):
+                    filtered = self._filter_unecessary(value, child_path)
+                    simple = self._handle_simple_dict(filtered, child_path)
+                    buffer.write(
+                        f" {simple}"
+                        if simple
+                        else f"\n{self(value, child_prefix, _path=child_path)}"
+                    )
+                elif isinstance(value, list) and (
+                    key != "type" or self._is_property(child_path)
+                ):
+                    children = self._handle_list(value, item_prefix, child_path)
+                    sep = " " if children.startswith("[") else "\n"
+                    buffer.write(f"{sep}{children}")
+                else:
+                    buffer.write(f" {self._value(value, child_path)}\n")
+            return buffer.getvalue()
+
+    def _is_unecessary(self, path: Sequence[str]) -> bool:
+        if self._is_property(path) or not path:  # empty path => instruction @ root
+            return False
+        key = path[-1]
+        return any(key.startswith(k) for k in "$_") or key in self._IGNORE
+
+    def _filter_unecessary(self, schema: dict, path: Sequence[str]):
+        return {
+            key: value
+            for key, value in schema.items()
+            if not self._is_unecessary([*path, key])
+        }
+
+    def _handle_simple_dict(self, value: dict, path: Sequence[str]) -> Optional[str]:
+        inline = any(p in value for p in self._guess_inline_defs)
+        simple = not any(isinstance(v, (list, dict)) for v in value.values())
+        if inline or simple:
+            return f"{{{', '.join(self._inline_attrs(value, path))}}}\n"
+        return None
+
+    def _handle_list(
+        self, schemas: list, prefix: str = "", path: Sequence[str] = ()
+    ) -> str:
+        if self._is_unecessary(path):
+            return ""
+
+        repr_ = repr(schemas)
+        if all(not isinstance(e, (dict, list)) for e in schemas) and len(repr_) < 60:
+            return f"{repr_}\n"
+
+        item_prefix = self._child_prefix(prefix, "- ")
+        return "".join(
+            self(v, item_prefix, _path=[*path, f"[{i}]"]) for i, v in enumerate(schemas)
+        )
+
+    def _is_property(self, path: Sequence[str]):
+        """Check if the given path can correspond to an arbitrarily named property"""
+        counter = 0
+        for key in path[-2::-1]:
+            if key not in {"properties", "patternProperties"}:
+                break
+            counter += 1
+
+        # If the counter if even, the path correspond to a JSON Schema keyword
+        # otherwise it can be any arbitrary string naming a property
+        return counter % 2 == 1
+
+    def _label(self, path: Sequence[str]) -> str:
+        *parents, key = path
+        if not self._is_property(path):
+            norm_key = _separate_terms(key)
+            return self._terms.get(key) or " ".join(self._jargon(norm_key))
+
+        if parents[-1] == "patternProperties":
+            return f"(regex {key!r})"
+        return repr(key)  # property name
+
+    def _value(self, value: Any, path: Sequence[str]) -> str:
+        if path[-1] == "type" and not self._is_property(path):
+            type_ = self._jargon(value)
+            return (
+                f"[{', '.join(type_)}]" if isinstance(value, list) else cast(str, type_)
+            )
+        return repr(value)
+
+    def _inline_attrs(self, schema: dict, path: Sequence[str]) -> Iterator[str]:
+        for key, value in schema.items():
+            child_path = [*path, key]
+            yield f"{self._label(child_path)}: {self._value(value, child_path)}"
+
+    def _child_prefix(self, parent_prefix: str, child_prefix: str) -> str:
+        return len(parent_prefix) * " " + child_prefix
+
+
+def _separate_terms(word: str) -> List[str]:
+    """
+    >>> _separate_terms("FooBar-foo")
+    "foo bar foo"
+    """
+    return [w.lower() for w in _CAMEL_CASE_SPLITTER.split(word) if w]
diff --git a/setuptools/config/_validate_pyproject/extra_validations.py b/setuptools/config/_validate_pyproject/extra_validations.py
new file mode 100644
index 00000000..48c4e257
--- /dev/null
+++ b/setuptools/config/_validate_pyproject/extra_validations.py
@@ -0,0 +1,36 @@
+"""The purpose of this module is implement PEP 621 validations that are
+difficult to express as a JSON Schema (or that are not supported by the current
+JSON Schema library).
+"""
+
+from typing import Mapping, TypeVar
+
+from .fastjsonschema_exceptions import JsonSchemaValueException
+
+T = TypeVar("T", bound=Mapping)
+
+
+class RedefiningStaticFieldAsDynamic(JsonSchemaValueException):
+    """According to PEP 621:
+
+    Build back-ends MUST raise an error if the metadata specifies a field
+    statically as well as being listed in dynamic.
+    """
+
+
+def validate_project_dynamic(pyproject: T) -> T:
+    project_table = pyproject.get("project", {})
+    dynamic = project_table.get("dynamic", [])
+
+    for field in dynamic:
+        if field in project_table:
+            msg = f"You cannot provide a value for `project.{field}` and "
+            msg += "list it under `project.dynamic` at the same time"
+            name = f"data.project.{field}"
+            value = {field: project_table[field], "...": " # ...", "dynamic": dynamic}
+            raise RedefiningStaticFieldAsDynamic(msg, value, name, rule="PEP 621")
+
+    return pyproject
+
+
+EXTRA_VALIDATIONS = (validate_project_dynamic,)
diff --git a/setuptools/config/_validate_pyproject/fastjsonschema_exceptions.py b/setuptools/config/_validate_pyproject/fastjsonschema_exceptions.py
new file mode 100644
index 00000000..d2dddd6a
--- /dev/null
+++ b/setuptools/config/_validate_pyproject/fastjsonschema_exceptions.py
@@ -0,0 +1,51 @@
+import re
+
+
+SPLIT_RE = re.compile(r'[\.\[\]]+')
+
+
+class JsonSchemaException(ValueError):
+    """
+    Base exception of ``fastjsonschema`` library.
+    """
+
+
+class JsonSchemaValueException(JsonSchemaException):
+    """
+    Exception raised by validation function. Available properties:
+
+     * ``message`` containing human-readable information what is wrong (e.g. ``data.property[index] must be smaller than or equal to 42``),
+     * invalid ``value`` (e.g. ``60``),
+     * ``name`` of a path in the data structure (e.g. ``data.property[index]``),
+     * ``path`` as an array in the data structure (e.g. ``['data', 'property', 'index']``),
+     * the whole ``definition`` which the ``value`` has to fulfil (e.g. ``{'type': 'number', 'maximum': 42}``),
+     * ``rule`` which the ``value`` is breaking (e.g. ``maximum``)
+     * and ``rule_definition`` (e.g. ``42``).
+
+    .. versionchanged:: 2.14.0
+        Added all extra properties.
+    """
+
+    def __init__(self, message, value=None, name=None, definition=None, rule=None):
+        super().__init__(message)
+        self.message = message
+        self.value = value
+        self.name = name
+        self.definition = definition
+        self.rule = rule
+
+    @property
+    def path(self):
+        return [item for item in SPLIT_RE.split(self.name) if item != '']
+
+    @property
+    def rule_definition(self):
+        if not self.rule or not self.definition:
+            return None
+        return self.definition.get(self.rule)
+
+
+class JsonSchemaDefinitionException(JsonSchemaException):
+    """
+    Exception raised by generator of validation function.
+    """
diff --git a/setuptools/config/_validate_pyproject/fastjsonschema_validations.py b/setuptools/config/_validate_pyproject/fastjsonschema_validations.py
new file mode 100644
index 00000000..3ad1edd0
--- /dev/null
+++ b/setuptools/config/_validate_pyproject/fastjsonschema_validations.py
@@ -0,0 +1,1004 @@
+# noqa
+# type: ignore
+# flake8: noqa
+# pylint: skip-file
+# mypy: ignore-errors
+# yapf: disable
+# pylama:skip=1
+
+
+# *** PLEASE DO NOT MODIFY DIRECTLY: Automatically generated code *** 
+
+
+VERSION = "2.15.3"
+import re
+from .fastjsonschema_exceptions import JsonSchemaValueException
+
+
+REGEX_PATTERNS = {
+    '^.*$': re.compile('^.*$'),
+    '.+': re.compile('.+'),
+    '^.+$': re.compile('^.+$'),
+    'idn-email_re_pattern': re.compile('^[^@]+@[^@]+\\.[^@]+\\Z')
+}
+
+NoneType = type(None)
+
+def validate(data, custom_formats={}, name_prefix=None):
+    validate_https___packaging_python_org_en_latest_specifications_declaring_build_dependencies(data, custom_formats, (name_prefix or "data") + "")
+    return data
+
+def validate_https___packaging_python_org_en_latest_specifications_declaring_build_dependencies(data, custom_formats={}, name_prefix=None):
+    if not isinstance(data, (dict)):
+        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-build-dependencies/', 'title': 'Data structure for ``pyproject.toml`` files', '$$description': ['File format containing build-time configurations for the Python ecosystem. ', ':pep:`517` initially defined a build-system independent format for source trees', 'which was complemented by :pep:`518` to provide a way of specifying dependencies ', 'for building Python projects.', 'Please notice the ``project`` table (as initially defined in  :pep:`621`) is not included', 'in this schema and should be considered separately.'], 'type': 'object', 'additionalProperties': False, 'properties': {'build-system': {'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['dynamic'], 'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}}}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'required': ['version'], '$$description': ['version should be statically defined in the ``version`` field']}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, 'tool': {'type': 'object', 'properties': {'distutils': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://docs.python.org/3/install/', 'title': '``tool.distutils`` table', '$$description': ['Originally, ``distutils`` allowed developers to configure arguments for', '``setup.py`` scripts via `distutils configuration files', '`_.', '``tool.distutils`` subtables could be used with the same purpose', '(NOT CURRENTLY IMPLEMENTED).'], 'type': 'object', 'properties': {'global': {'type': 'object', 'description': 'Global options applied to all ``distutils`` commands'}}, 'patternProperties': {'.+': {'type': 'object'}}, '$comment': 'TODO: Is there a practical way of making this schema more specific?'}, 'setuptools': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$ref': '#/definitions/find-directive'}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might become standard with PEP 639).'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Probably ``project.license-files``?'}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'additionalProperties': False, 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'$ref': '#/definitions/attr-directive'}, {'$ref': '#/definitions/file-directive'}]}, 'classifiers': {'$ref': '#/definitions/file-directive'}, 'description': {'$ref': '#/definitions/file-directive'}, 'entry-points': {'$ref': '#/definitions/file-directive'}, 'readme': {'anyOf': [{'$ref': '#/definitions/file-directive'}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}}}}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['dynamic'], 'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}}}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'required': ['version'], '$$description': ['version should be statically defined in the ``version`` field']}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, rule='type')
+    data_is_dict = isinstance(data, dict)
+    if data_is_dict:
+        data_keys = set(data.keys())
+        if "build-system" in data_keys:
+            data_keys.remove("build-system")
+            data__buildsystem = data["build-system"]
+            if not isinstance(data__buildsystem, (dict)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".build-system must be object", value=data__buildsystem, name="" + (name_prefix or "data") + ".build-system", definition={'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, rule='type')
+            data__buildsystem_is_dict = isinstance(data__buildsystem, dict)
+            if data__buildsystem_is_dict:
+                data__buildsystem_len = len(data__buildsystem)
+                if not all(prop in data__buildsystem for prop in ['requires']):
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".build-system must contain ['requires'] properties", value=data__buildsystem, name="" + (name_prefix or "data") + ".build-system", definition={'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, rule='required')
+                data__buildsystem_keys = set(data__buildsystem.keys())
+                if "requires" in data__buildsystem_keys:
+                    data__buildsystem_keys.remove("requires")
+                    data__buildsystem__requires = data__buildsystem["requires"]
+                    if not isinstance(data__buildsystem__requires, (list, tuple)):
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".build-system.requires must be array", value=data__buildsystem__requires, name="" + (name_prefix or "data") + ".build-system.requires", definition={'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, rule='type')
+                    data__buildsystem__requires_is_list = isinstance(data__buildsystem__requires, (list, tuple))
+                    if data__buildsystem__requires_is_list:
+                        data__buildsystem__requires_len = len(data__buildsystem__requires)
+                        for data__buildsystem__requires_x, data__buildsystem__requires_item in enumerate(data__buildsystem__requires):
+                            if not isinstance(data__buildsystem__requires_item, (str)):
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".build-system.requires[{data__buildsystem__requires_x}]".format(**locals()) + " must be string", value=data__buildsystem__requires_item, name="" + (name_prefix or "data") + ".build-system.requires[{data__buildsystem__requires_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
+                if "build-backend" in data__buildsystem_keys:
+                    data__buildsystem_keys.remove("build-backend")
+                    data__buildsystem__buildbackend = data__buildsystem["build-backend"]
+                    if not isinstance(data__buildsystem__buildbackend, (str)):
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".build-system.build-backend must be string", value=data__buildsystem__buildbackend, name="" + (name_prefix or "data") + ".build-system.build-backend", definition={'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, rule='type')
+                    if isinstance(data__buildsystem__buildbackend, str):
+                        if not custom_formats["pep517-backend-reference"](data__buildsystem__buildbackend):
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".build-system.build-backend must be pep517-backend-reference", value=data__buildsystem__buildbackend, name="" + (name_prefix or "data") + ".build-system.build-backend", definition={'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, rule='format')
+                if "backend-path" in data__buildsystem_keys:
+                    data__buildsystem_keys.remove("backend-path")
+                    data__buildsystem__backendpath = data__buildsystem["backend-path"]
+                    if not isinstance(data__buildsystem__backendpath, (list, tuple)):
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".build-system.backend-path must be array", value=data__buildsystem__backendpath, name="" + (name_prefix or "data") + ".build-system.backend-path", definition={'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}, rule='type')
+                    data__buildsystem__backendpath_is_list = isinstance(data__buildsystem__backendpath, (list, tuple))
+                    if data__buildsystem__backendpath_is_list:
+                        data__buildsystem__backendpath_len = len(data__buildsystem__backendpath)
+                        for data__buildsystem__backendpath_x, data__buildsystem__backendpath_item in enumerate(data__buildsystem__backendpath):
+                            if not isinstance(data__buildsystem__backendpath_item, (str)):
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".build-system.backend-path[{data__buildsystem__backendpath_x}]".format(**locals()) + " must be string", value=data__buildsystem__backendpath_item, name="" + (name_prefix or "data") + ".build-system.backend-path[{data__buildsystem__backendpath_x}]".format(**locals()) + "", definition={'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}, rule='type')
+                if data__buildsystem_keys:
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".build-system must not contain "+str(data__buildsystem_keys)+" properties", value=data__buildsystem, name="" + (name_prefix or "data") + ".build-system", definition={'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, rule='additionalProperties')
+        if "project" in data_keys:
+            data_keys.remove("project")
+            data__project = data["project"]
+            validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata(data__project, custom_formats, (name_prefix or "data") + ".project")
+        if "tool" in data_keys:
+            data_keys.remove("tool")
+            data__tool = data["tool"]
+            if not isinstance(data__tool, (dict)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".tool must be object", value=data__tool, name="" + (name_prefix or "data") + ".tool", definition={'type': 'object', 'properties': {'distutils': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://docs.python.org/3/install/', 'title': '``tool.distutils`` table', '$$description': ['Originally, ``distutils`` allowed developers to configure arguments for', '``setup.py`` scripts via `distutils configuration files', '`_.', '``tool.distutils`` subtables could be used with the same purpose', '(NOT CURRENTLY IMPLEMENTED).'], 'type': 'object', 'properties': {'global': {'type': 'object', 'description': 'Global options applied to all ``distutils`` commands'}}, 'patternProperties': {'.+': {'type': 'object'}}, '$comment': 'TODO: Is there a practical way of making this schema more specific?'}, 'setuptools': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$ref': '#/definitions/find-directive'}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might become standard with PEP 639).'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Probably ``project.license-files``?'}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'additionalProperties': False, 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'$ref': '#/definitions/attr-directive'}, {'$ref': '#/definitions/file-directive'}]}, 'classifiers': {'$ref': '#/definitions/file-directive'}, 'description': {'$ref': '#/definitions/file-directive'}, 'entry-points': {'$ref': '#/definitions/file-directive'}, 'readme': {'anyOf': [{'$ref': '#/definitions/file-directive'}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}}}, rule='type')
+            data__tool_is_dict = isinstance(data__tool, dict)
+            if data__tool_is_dict:
+                data__tool_keys = set(data__tool.keys())
+                if "distutils" in data__tool_keys:
+                    data__tool_keys.remove("distutils")
+                    data__tool__distutils = data__tool["distutils"]
+                    validate_https___docs_python_org_3_install(data__tool__distutils, custom_formats, (name_prefix or "data") + ".tool.distutils")
+                if "setuptools" in data__tool_keys:
+                    data__tool_keys.remove("setuptools")
+                    data__tool__setuptools = data__tool["setuptools"]
+                    validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data__tool__setuptools, custom_formats, (name_prefix or "data") + ".tool.setuptools")
+        if data_keys:
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must not contain "+str(data_keys)+" properties", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-build-dependencies/', 'title': 'Data structure for ``pyproject.toml`` files', '$$description': ['File format containing build-time configurations for the Python ecosystem. ', ':pep:`517` initially defined a build-system independent format for source trees', 'which was complemented by :pep:`518` to provide a way of specifying dependencies ', 'for building Python projects.', 'Please notice the ``project`` table (as initially defined in  :pep:`621`) is not included', 'in this schema and should be considered separately.'], 'type': 'object', 'additionalProperties': False, 'properties': {'build-system': {'type': 'object', 'description': 'Table used to store build-related data', 'additionalProperties': False, 'properties': {'requires': {'type': 'array', '$$description': ['List of dependencies in the :pep:`508` format required to execute the build', 'system. Please notice that the resulting dependency graph', '**MUST NOT contain cycles**'], 'items': {'type': 'string'}}, 'build-backend': {'type': 'string', 'description': 'Python object that will be used to perform the build according to :pep:`517`', 'format': 'pep517-backend-reference'}, 'backend-path': {'type': 'array', '$$description': ['List of directories to be prepended to ``sys.path`` when loading the', 'back-end, and running its hooks'], 'items': {'type': 'string', '$comment': 'Should be a path (TODO: enforce it with format?)'}}}, 'required': ['requires']}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['dynamic'], 'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}}}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'required': ['version'], '$$description': ['version should be statically defined in the ``version`` field']}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, 'tool': {'type': 'object', 'properties': {'distutils': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://docs.python.org/3/install/', 'title': '``tool.distutils`` table', '$$description': ['Originally, ``distutils`` allowed developers to configure arguments for', '``setup.py`` scripts via `distutils configuration files', '`_.', '``tool.distutils`` subtables could be used with the same purpose', '(NOT CURRENTLY IMPLEMENTED).'], 'type': 'object', 'properties': {'global': {'type': 'object', 'description': 'Global options applied to all ``distutils`` commands'}}, 'patternProperties': {'.+': {'type': 'object'}}, '$comment': 'TODO: Is there a practical way of making this schema more specific?'}, 'setuptools': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$ref': '#/definitions/find-directive'}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might become standard with PEP 639).'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Probably ``project.license-files``?'}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'additionalProperties': False, 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'$ref': '#/definitions/attr-directive'}, {'$ref': '#/definitions/file-directive'}]}, 'classifiers': {'$ref': '#/definitions/file-directive'}, 'description': {'$ref': '#/definitions/file-directive'}, 'entry-points': {'$ref': '#/definitions/file-directive'}, 'readme': {'anyOf': [{'$ref': '#/definitions/file-directive'}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}}}}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$ref': '#/definitions/author'}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create command-line wrappers for the given', '`entry points `_.']}, 'gui-scripts': {'$ref': '#/definitions/entry-point-group', '$$description': ['Instruct the installer to create GUI wrappers for the given', '`entry points `_.', 'The difference between ``scripts`` and ``gui-scripts`` is only relevant in', 'Windows.']}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$ref': '#/definitions/entry-point-group'}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$ref': '#/definitions/dependency'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$ref': '#/definitions/dependency'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['dynamic'], 'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}}}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'required': ['version'], '$$description': ['version should be statically defined in the ``version`` field']}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, rule='additionalProperties')
+    return data
+
+def validate_https___setuptools_pypa_io_en_latest_references_keywords_html(data, custom_formats={}, name_prefix=None):
+    if not isinstance(data, (dict)):
+        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might become standard with PEP 639).'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Probably ``project.license-files``?'}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'additionalProperties': False, 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}]}, 'classifiers': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'description': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'entry-points': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'readme': {'anyOf': [{'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}, rule='type')
+    data_is_dict = isinstance(data, dict)
+    if data_is_dict:
+        data_keys = set(data.keys())
+        if "platforms" in data_keys:
+            data_keys.remove("platforms")
+            data__platforms = data["platforms"]
+            if not isinstance(data__platforms, (list, tuple)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".platforms must be array", value=data__platforms, name="" + (name_prefix or "data") + ".platforms", definition={'type': 'array', 'items': {'type': 'string'}}, rule='type')
+            data__platforms_is_list = isinstance(data__platforms, (list, tuple))
+            if data__platforms_is_list:
+                data__platforms_len = len(data__platforms)
+                for data__platforms_x, data__platforms_item in enumerate(data__platforms):
+                    if not isinstance(data__platforms_item, (str)):
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".platforms[{data__platforms_x}]".format(**locals()) + " must be string", value=data__platforms_item, name="" + (name_prefix or "data") + ".platforms[{data__platforms_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
+        if "provides" in data_keys:
+            data_keys.remove("provides")
+            data__provides = data["provides"]
+            if not isinstance(data__provides, (list, tuple)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".provides must be array", value=data__provides, name="" + (name_prefix or "data") + ".provides", definition={'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, rule='type')
+            data__provides_is_list = isinstance(data__provides, (list, tuple))
+            if data__provides_is_list:
+                data__provides_len = len(data__provides)
+                for data__provides_x, data__provides_item in enumerate(data__provides):
+                    if not isinstance(data__provides_item, (str)):
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".provides[{data__provides_x}]".format(**locals()) + " must be string", value=data__provides_item, name="" + (name_prefix or "data") + ".provides[{data__provides_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'pep508-identifier'}, rule='type')
+                    if isinstance(data__provides_item, str):
+                        if not custom_formats["pep508-identifier"](data__provides_item):
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".provides[{data__provides_x}]".format(**locals()) + " must be pep508-identifier", value=data__provides_item, name="" + (name_prefix or "data") + ".provides[{data__provides_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'pep508-identifier'}, rule='format')
+        if "obsoletes" in data_keys:
+            data_keys.remove("obsoletes")
+            data__obsoletes = data["obsoletes"]
+            if not isinstance(data__obsoletes, (list, tuple)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".obsoletes must be array", value=data__obsoletes, name="" + (name_prefix or "data") + ".obsoletes", definition={'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, rule='type')
+            data__obsoletes_is_list = isinstance(data__obsoletes, (list, tuple))
+            if data__obsoletes_is_list:
+                data__obsoletes_len = len(data__obsoletes)
+                for data__obsoletes_x, data__obsoletes_item in enumerate(data__obsoletes):
+                    if not isinstance(data__obsoletes_item, (str)):
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".obsoletes[{data__obsoletes_x}]".format(**locals()) + " must be string", value=data__obsoletes_item, name="" + (name_prefix or "data") + ".obsoletes[{data__obsoletes_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'pep508-identifier'}, rule='type')
+                    if isinstance(data__obsoletes_item, str):
+                        if not custom_formats["pep508-identifier"](data__obsoletes_item):
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".obsoletes[{data__obsoletes_x}]".format(**locals()) + " must be pep508-identifier", value=data__obsoletes_item, name="" + (name_prefix or "data") + ".obsoletes[{data__obsoletes_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'pep508-identifier'}, rule='format')
+        if "zip-safe" in data_keys:
+            data_keys.remove("zip-safe")
+            data__zipsafe = data["zip-safe"]
+            if not isinstance(data__zipsafe, (bool)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".zip-safe must be boolean", value=data__zipsafe, name="" + (name_prefix or "data") + ".zip-safe", definition={'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, rule='type')
+        if "script-files" in data_keys:
+            data_keys.remove("script-files")
+            data__scriptfiles = data["script-files"]
+            if not isinstance(data__scriptfiles, (list, tuple)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".script-files must be array", value=data__scriptfiles, name="" + (name_prefix or "data") + ".script-files", definition={'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, rule='type')
+            data__scriptfiles_is_list = isinstance(data__scriptfiles, (list, tuple))
+            if data__scriptfiles_is_list:
+                data__scriptfiles_len = len(data__scriptfiles)
+                for data__scriptfiles_x, data__scriptfiles_item in enumerate(data__scriptfiles):
+                    if not isinstance(data__scriptfiles_item, (str)):
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".script-files[{data__scriptfiles_x}]".format(**locals()) + " must be string", value=data__scriptfiles_item, name="" + (name_prefix or "data") + ".script-files[{data__scriptfiles_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
+        if "eager-resources" in data_keys:
+            data_keys.remove("eager-resources")
+            data__eagerresources = data["eager-resources"]
+            if not isinstance(data__eagerresources, (list, tuple)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".eager-resources must be array", value=data__eagerresources, name="" + (name_prefix or "data") + ".eager-resources", definition={'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, rule='type')
+            data__eagerresources_is_list = isinstance(data__eagerresources, (list, tuple))
+            if data__eagerresources_is_list:
+                data__eagerresources_len = len(data__eagerresources)
+                for data__eagerresources_x, data__eagerresources_item in enumerate(data__eagerresources):
+                    if not isinstance(data__eagerresources_item, (str)):
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".eager-resources[{data__eagerresources_x}]".format(**locals()) + " must be string", value=data__eagerresources_item, name="" + (name_prefix or "data") + ".eager-resources[{data__eagerresources_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
+        if "packages" in data_keys:
+            data_keys.remove("packages")
+            data__packages = data["packages"]
+            data__packages_one_of_count1 = 0
+            if data__packages_one_of_count1 < 2:
+                try:
+                    if not isinstance(data__packages, (list, tuple)):
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".packages must be array", value=data__packages, name="" + (name_prefix or "data") + ".packages", definition={'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, rule='type')
+                    data__packages_is_list = isinstance(data__packages, (list, tuple))
+                    if data__packages_is_list:
+                        data__packages_len = len(data__packages)
+                        for data__packages_x, data__packages_item in enumerate(data__packages):
+                            if not isinstance(data__packages_item, (str)):
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".packages[{data__packages_x}]".format(**locals()) + " must be string", value=data__packages_item, name="" + (name_prefix or "data") + ".packages[{data__packages_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'python-module-name'}, rule='type')
+                            if isinstance(data__packages_item, str):
+                                if not custom_formats["python-module-name"](data__packages_item):
+                                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".packages[{data__packages_x}]".format(**locals()) + " must be python-module-name", value=data__packages_item, name="" + (name_prefix or "data") + ".packages[{data__packages_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'python-module-name'}, rule='format')
+                    data__packages_one_of_count1 += 1
+                except JsonSchemaValueException: pass
+            if data__packages_one_of_count1 < 2:
+                try:
+                    validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_find_directive(data__packages, custom_formats, (name_prefix or "data") + ".packages")
+                    data__packages_one_of_count1 += 1
+                except JsonSchemaValueException: pass
+            if data__packages_one_of_count1 != 1:
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".packages must be valid exactly by one definition" + (" (" + str(data__packages_one_of_count1) + " matches found)"), value=data__packages, name="" + (name_prefix or "data") + ".packages", definition={'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}]}, rule='oneOf')
+        if "package-dir" in data_keys:
+            data_keys.remove("package-dir")
+            data__packagedir = data["package-dir"]
+            if not isinstance(data__packagedir, (dict)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-dir must be object", value=data__packagedir, name="" + (name_prefix or "data") + ".package-dir", definition={'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, rule='type')
+            data__packagedir_is_dict = isinstance(data__packagedir, dict)
+            if data__packagedir_is_dict:
+                data__packagedir_keys = set(data__packagedir.keys())
+                for data__packagedir_key, data__packagedir_val in data__packagedir.items():
+                    if REGEX_PATTERNS['^.*$'].search(data__packagedir_key):
+                        if data__packagedir_key in data__packagedir_keys:
+                            data__packagedir_keys.remove(data__packagedir_key)
+                        if not isinstance(data__packagedir_val, (str)):
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-dir.{data__packagedir_key}".format(**locals()) + " must be string", value=data__packagedir_val, name="" + (name_prefix or "data") + ".package-dir.{data__packagedir_key}".format(**locals()) + "", definition={'type': 'string'}, rule='type')
+                if data__packagedir_keys:
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-dir must not contain "+str(data__packagedir_keys)+" properties", value=data__packagedir, name="" + (name_prefix or "data") + ".package-dir", definition={'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, rule='additionalProperties')
+                data__packagedir_len = len(data__packagedir)
+                if data__packagedir_len != 0:
+                    data__packagedir_property_names = True
+                    for data__packagedir_key in data__packagedir:
+                        try:
+                            data__packagedir_key_one_of_count2 = 0
+                            if data__packagedir_key_one_of_count2 < 2:
+                                try:
+                                    if isinstance(data__packagedir_key, str):
+                                        if not custom_formats["python-module-name"](data__packagedir_key):
+                                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-dir must be python-module-name", value=data__packagedir_key, name="" + (name_prefix or "data") + ".package-dir", definition={'format': 'python-module-name'}, rule='format')
+                                    data__packagedir_key_one_of_count2 += 1
+                                except JsonSchemaValueException: pass
+                            if data__packagedir_key_one_of_count2 < 2:
+                                try:
+                                    if data__packagedir_key != "":
+                                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-dir must be same as const definition: ", value=data__packagedir_key, name="" + (name_prefix or "data") + ".package-dir", definition={'const': ''}, rule='const')
+                                    data__packagedir_key_one_of_count2 += 1
+                                except JsonSchemaValueException: pass
+                            if data__packagedir_key_one_of_count2 != 1:
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-dir must be valid exactly by one definition" + (" (" + str(data__packagedir_key_one_of_count2) + " matches found)"), value=data__packagedir_key, name="" + (name_prefix or "data") + ".package-dir", definition={'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, rule='oneOf')
+                        except JsonSchemaValueException:
+                            data__packagedir_property_names = False
+                    if not data__packagedir_property_names:
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-dir must be named by propertyName definition", value=data__packagedir, name="" + (name_prefix or "data") + ".package-dir", definition={'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, rule='propertyNames')
+        if "package-data" in data_keys:
+            data_keys.remove("package-data")
+            data__packagedata = data["package-data"]
+            if not isinstance(data__packagedata, (dict)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-data must be object", value=data__packagedata, name="" + (name_prefix or "data") + ".package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='type')
+            data__packagedata_is_dict = isinstance(data__packagedata, dict)
+            if data__packagedata_is_dict:
+                data__packagedata_keys = set(data__packagedata.keys())
+                for data__packagedata_key, data__packagedata_val in data__packagedata.items():
+                    if REGEX_PATTERNS['^.*$'].search(data__packagedata_key):
+                        if data__packagedata_key in data__packagedata_keys:
+                            data__packagedata_keys.remove(data__packagedata_key)
+                        if not isinstance(data__packagedata_val, (list, tuple)):
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-data.{data__packagedata_key}".format(**locals()) + " must be array", value=data__packagedata_val, name="" + (name_prefix or "data") + ".package-data.{data__packagedata_key}".format(**locals()) + "", definition={'type': 'array', 'items': {'type': 'string'}}, rule='type')
+                        data__packagedata_val_is_list = isinstance(data__packagedata_val, (list, tuple))
+                        if data__packagedata_val_is_list:
+                            data__packagedata_val_len = len(data__packagedata_val)
+                            for data__packagedata_val_x, data__packagedata_val_item in enumerate(data__packagedata_val):
+                                if not isinstance(data__packagedata_val_item, (str)):
+                                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-data.{data__packagedata_key}[{data__packagedata_val_x}]".format(**locals()) + " must be string", value=data__packagedata_val_item, name="" + (name_prefix or "data") + ".package-data.{data__packagedata_key}[{data__packagedata_val_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
+                if data__packagedata_keys:
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-data must not contain "+str(data__packagedata_keys)+" properties", value=data__packagedata, name="" + (name_prefix or "data") + ".package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='additionalProperties')
+                data__packagedata_len = len(data__packagedata)
+                if data__packagedata_len != 0:
+                    data__packagedata_property_names = True
+                    for data__packagedata_key in data__packagedata:
+                        try:
+                            data__packagedata_key_one_of_count3 = 0
+                            if data__packagedata_key_one_of_count3 < 2:
+                                try:
+                                    if isinstance(data__packagedata_key, str):
+                                        if not custom_formats["python-module-name"](data__packagedata_key):
+                                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-data must be python-module-name", value=data__packagedata_key, name="" + (name_prefix or "data") + ".package-data", definition={'format': 'python-module-name'}, rule='format')
+                                    data__packagedata_key_one_of_count3 += 1
+                                except JsonSchemaValueException: pass
+                            if data__packagedata_key_one_of_count3 < 2:
+                                try:
+                                    if data__packagedata_key != "*":
+                                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-data must be same as const definition: *", value=data__packagedata_key, name="" + (name_prefix or "data") + ".package-data", definition={'const': '*'}, rule='const')
+                                    data__packagedata_key_one_of_count3 += 1
+                                except JsonSchemaValueException: pass
+                            if data__packagedata_key_one_of_count3 != 1:
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-data must be valid exactly by one definition" + (" (" + str(data__packagedata_key_one_of_count3) + " matches found)"), value=data__packagedata_key, name="" + (name_prefix or "data") + ".package-data", definition={'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, rule='oneOf')
+                        except JsonSchemaValueException:
+                            data__packagedata_property_names = False
+                    if not data__packagedata_property_names:
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-data must be named by propertyName definition", value=data__packagedata, name="" + (name_prefix or "data") + ".package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='propertyNames')
+        if "include-package-data" in data_keys:
+            data_keys.remove("include-package-data")
+            data__includepackagedata = data["include-package-data"]
+            if not isinstance(data__includepackagedata, (bool)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".include-package-data must be boolean", value=data__includepackagedata, name="" + (name_prefix or "data") + ".include-package-data", definition={'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, rule='type')
+        if "exclude-package-data" in data_keys:
+            data_keys.remove("exclude-package-data")
+            data__excludepackagedata = data["exclude-package-data"]
+            if not isinstance(data__excludepackagedata, (dict)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".exclude-package-data must be object", value=data__excludepackagedata, name="" + (name_prefix or "data") + ".exclude-package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='type')
+            data__excludepackagedata_is_dict = isinstance(data__excludepackagedata, dict)
+            if data__excludepackagedata_is_dict:
+                data__excludepackagedata_keys = set(data__excludepackagedata.keys())
+                for data__excludepackagedata_key, data__excludepackagedata_val in data__excludepackagedata.items():
+                    if REGEX_PATTERNS['^.*$'].search(data__excludepackagedata_key):
+                        if data__excludepackagedata_key in data__excludepackagedata_keys:
+                            data__excludepackagedata_keys.remove(data__excludepackagedata_key)
+                        if not isinstance(data__excludepackagedata_val, (list, tuple)):
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".exclude-package-data.{data__excludepackagedata_key}".format(**locals()) + " must be array", value=data__excludepackagedata_val, name="" + (name_prefix or "data") + ".exclude-package-data.{data__excludepackagedata_key}".format(**locals()) + "", definition={'type': 'array', 'items': {'type': 'string'}}, rule='type')
+                        data__excludepackagedata_val_is_list = isinstance(data__excludepackagedata_val, (list, tuple))
+                        if data__excludepackagedata_val_is_list:
+                            data__excludepackagedata_val_len = len(data__excludepackagedata_val)
+                            for data__excludepackagedata_val_x, data__excludepackagedata_val_item in enumerate(data__excludepackagedata_val):
+                                if not isinstance(data__excludepackagedata_val_item, (str)):
+                                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".exclude-package-data.{data__excludepackagedata_key}[{data__excludepackagedata_val_x}]".format(**locals()) + " must be string", value=data__excludepackagedata_val_item, name="" + (name_prefix or "data") + ".exclude-package-data.{data__excludepackagedata_key}[{data__excludepackagedata_val_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
+                if data__excludepackagedata_keys:
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".exclude-package-data must not contain "+str(data__excludepackagedata_keys)+" properties", value=data__excludepackagedata, name="" + (name_prefix or "data") + ".exclude-package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='additionalProperties')
+                data__excludepackagedata_len = len(data__excludepackagedata)
+                if data__excludepackagedata_len != 0:
+                    data__excludepackagedata_property_names = True
+                    for data__excludepackagedata_key in data__excludepackagedata:
+                        try:
+                            data__excludepackagedata_key_one_of_count4 = 0
+                            if data__excludepackagedata_key_one_of_count4 < 2:
+                                try:
+                                    if isinstance(data__excludepackagedata_key, str):
+                                        if not custom_formats["python-module-name"](data__excludepackagedata_key):
+                                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".exclude-package-data must be python-module-name", value=data__excludepackagedata_key, name="" + (name_prefix or "data") + ".exclude-package-data", definition={'format': 'python-module-name'}, rule='format')
+                                    data__excludepackagedata_key_one_of_count4 += 1
+                                except JsonSchemaValueException: pass
+                            if data__excludepackagedata_key_one_of_count4 < 2:
+                                try:
+                                    if data__excludepackagedata_key != "*":
+                                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".exclude-package-data must be same as const definition: *", value=data__excludepackagedata_key, name="" + (name_prefix or "data") + ".exclude-package-data", definition={'const': '*'}, rule='const')
+                                    data__excludepackagedata_key_one_of_count4 += 1
+                                except JsonSchemaValueException: pass
+                            if data__excludepackagedata_key_one_of_count4 != 1:
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".exclude-package-data must be valid exactly by one definition" + (" (" + str(data__excludepackagedata_key_one_of_count4) + " matches found)"), value=data__excludepackagedata_key, name="" + (name_prefix or "data") + ".exclude-package-data", definition={'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, rule='oneOf')
+                        except JsonSchemaValueException:
+                            data__excludepackagedata_property_names = False
+                    if not data__excludepackagedata_property_names:
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".exclude-package-data must be named by propertyName definition", value=data__excludepackagedata, name="" + (name_prefix or "data") + ".exclude-package-data", definition={'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='propertyNames')
+        if "namespace-packages" in data_keys:
+            data_keys.remove("namespace-packages")
+            data__namespacepackages = data["namespace-packages"]
+            if not isinstance(data__namespacepackages, (list, tuple)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".namespace-packages must be array", value=data__namespacepackages, name="" + (name_prefix or "data") + ".namespace-packages", definition={'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, rule='type')
+            data__namespacepackages_is_list = isinstance(data__namespacepackages, (list, tuple))
+            if data__namespacepackages_is_list:
+                data__namespacepackages_len = len(data__namespacepackages)
+                for data__namespacepackages_x, data__namespacepackages_item in enumerate(data__namespacepackages):
+                    if not isinstance(data__namespacepackages_item, (str)):
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".namespace-packages[{data__namespacepackages_x}]".format(**locals()) + " must be string", value=data__namespacepackages_item, name="" + (name_prefix or "data") + ".namespace-packages[{data__namespacepackages_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'python-module-name'}, rule='type')
+                    if isinstance(data__namespacepackages_item, str):
+                        if not custom_formats["python-module-name"](data__namespacepackages_item):
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".namespace-packages[{data__namespacepackages_x}]".format(**locals()) + " must be python-module-name", value=data__namespacepackages_item, name="" + (name_prefix or "data") + ".namespace-packages[{data__namespacepackages_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'python-module-name'}, rule='format')
+        if "py-modules" in data_keys:
+            data_keys.remove("py-modules")
+            data__pymodules = data["py-modules"]
+            if not isinstance(data__pymodules, (list, tuple)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".py-modules must be array", value=data__pymodules, name="" + (name_prefix or "data") + ".py-modules", definition={'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, rule='type')
+            data__pymodules_is_list = isinstance(data__pymodules, (list, tuple))
+            if data__pymodules_is_list:
+                data__pymodules_len = len(data__pymodules)
+                for data__pymodules_x, data__pymodules_item in enumerate(data__pymodules):
+                    if not isinstance(data__pymodules_item, (str)):
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".py-modules[{data__pymodules_x}]".format(**locals()) + " must be string", value=data__pymodules_item, name="" + (name_prefix or "data") + ".py-modules[{data__pymodules_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'python-module-name'}, rule='type')
+                    if isinstance(data__pymodules_item, str):
+                        if not custom_formats["python-module-name"](data__pymodules_item):
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".py-modules[{data__pymodules_x}]".format(**locals()) + " must be python-module-name", value=data__pymodules_item, name="" + (name_prefix or "data") + ".py-modules[{data__pymodules_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'python-module-name'}, rule='format')
+        if "data-files" in data_keys:
+            data_keys.remove("data-files")
+            data__datafiles = data["data-files"]
+            if not isinstance(data__datafiles, (dict)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".data-files must be object", value=data__datafiles, name="" + (name_prefix or "data") + ".data-files", definition={'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, rule='type')
+            data__datafiles_is_dict = isinstance(data__datafiles, dict)
+            if data__datafiles_is_dict:
+                data__datafiles_keys = set(data__datafiles.keys())
+                for data__datafiles_key, data__datafiles_val in data__datafiles.items():
+                    if REGEX_PATTERNS['^.*$'].search(data__datafiles_key):
+                        if data__datafiles_key in data__datafiles_keys:
+                            data__datafiles_keys.remove(data__datafiles_key)
+                        if not isinstance(data__datafiles_val, (list, tuple)):
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".data-files.{data__datafiles_key}".format(**locals()) + " must be array", value=data__datafiles_val, name="" + (name_prefix or "data") + ".data-files.{data__datafiles_key}".format(**locals()) + "", definition={'type': 'array', 'items': {'type': 'string'}}, rule='type')
+                        data__datafiles_val_is_list = isinstance(data__datafiles_val, (list, tuple))
+                        if data__datafiles_val_is_list:
+                            data__datafiles_val_len = len(data__datafiles_val)
+                            for data__datafiles_val_x, data__datafiles_val_item in enumerate(data__datafiles_val):
+                                if not isinstance(data__datafiles_val_item, (str)):
+                                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".data-files.{data__datafiles_key}[{data__datafiles_val_x}]".format(**locals()) + " must be string", value=data__datafiles_val_item, name="" + (name_prefix or "data") + ".data-files.{data__datafiles_key}[{data__datafiles_val_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
+        if "cmdclass" in data_keys:
+            data_keys.remove("cmdclass")
+            data__cmdclass = data["cmdclass"]
+            if not isinstance(data__cmdclass, (dict)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".cmdclass must be object", value=data__cmdclass, name="" + (name_prefix or "data") + ".cmdclass", definition={'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, rule='type')
+            data__cmdclass_is_dict = isinstance(data__cmdclass, dict)
+            if data__cmdclass_is_dict:
+                data__cmdclass_keys = set(data__cmdclass.keys())
+                for data__cmdclass_key, data__cmdclass_val in data__cmdclass.items():
+                    if REGEX_PATTERNS['^.*$'].search(data__cmdclass_key):
+                        if data__cmdclass_key in data__cmdclass_keys:
+                            data__cmdclass_keys.remove(data__cmdclass_key)
+                        if not isinstance(data__cmdclass_val, (str)):
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".cmdclass.{data__cmdclass_key}".format(**locals()) + " must be string", value=data__cmdclass_val, name="" + (name_prefix or "data") + ".cmdclass.{data__cmdclass_key}".format(**locals()) + "", definition={'type': 'string', 'format': 'python-qualified-identifier'}, rule='type')
+                        if isinstance(data__cmdclass_val, str):
+                            if not custom_formats["python-qualified-identifier"](data__cmdclass_val):
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".cmdclass.{data__cmdclass_key}".format(**locals()) + " must be python-qualified-identifier", value=data__cmdclass_val, name="" + (name_prefix or "data") + ".cmdclass.{data__cmdclass_key}".format(**locals()) + "", definition={'type': 'string', 'format': 'python-qualified-identifier'}, rule='format')
+        if "license-files" in data_keys:
+            data_keys.remove("license-files")
+            data__licensefiles = data["license-files"]
+            if not isinstance(data__licensefiles, (list, tuple)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".license-files must be array", value=data__licensefiles, name="" + (name_prefix or "data") + ".license-files", definition={'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might become standard with PEP 639).'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Probably ``project.license-files``?'}, rule='type')
+            data__licensefiles_is_list = isinstance(data__licensefiles, (list, tuple))
+            if data__licensefiles_is_list:
+                data__licensefiles_len = len(data__licensefiles)
+                for data__licensefiles_x, data__licensefiles_item in enumerate(data__licensefiles):
+                    if not isinstance(data__licensefiles_item, (str)):
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".license-files[{data__licensefiles_x}]".format(**locals()) + " must be string", value=data__licensefiles_item, name="" + (name_prefix or "data") + ".license-files[{data__licensefiles_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
+        else: data["license-files"] = ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*']
+        if "dynamic" in data_keys:
+            data_keys.remove("dynamic")
+            data__dynamic = data["dynamic"]
+            if not isinstance(data__dynamic, (dict)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic must be object", value=data__dynamic, name="" + (name_prefix or "data") + ".dynamic", definition={'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'additionalProperties': False, 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}]}, 'classifiers': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'description': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'entry-points': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'readme': {'anyOf': [{'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}}}, rule='type')
+            data__dynamic_is_dict = isinstance(data__dynamic, dict)
+            if data__dynamic_is_dict:
+                data__dynamic_keys = set(data__dynamic.keys())
+                if "version" in data__dynamic_keys:
+                    data__dynamic_keys.remove("version")
+                    data__dynamic__version = data__dynamic["version"]
+                    data__dynamic__version_one_of_count5 = 0
+                    if data__dynamic__version_one_of_count5 < 2:
+                        try:
+                            validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_attr_directive(data__dynamic__version, custom_formats, (name_prefix or "data") + ".dynamic.version")
+                            data__dynamic__version_one_of_count5 += 1
+                        except JsonSchemaValueException: pass
+                    if data__dynamic__version_one_of_count5 < 2:
+                        try:
+                            validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data__dynamic__version, custom_formats, (name_prefix or "data") + ".dynamic.version")
+                            data__dynamic__version_one_of_count5 += 1
+                        except JsonSchemaValueException: pass
+                    if data__dynamic__version_one_of_count5 != 1:
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic.version must be valid exactly by one definition" + (" (" + str(data__dynamic__version_one_of_count5) + " matches found)"), value=data__dynamic__version, name="" + (name_prefix or "data") + ".dynamic.version", definition={'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}]}, rule='oneOf')
+                if "classifiers" in data__dynamic_keys:
+                    data__dynamic_keys.remove("classifiers")
+                    data__dynamic__classifiers = data__dynamic["classifiers"]
+                    validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data__dynamic__classifiers, custom_formats, (name_prefix or "data") + ".dynamic.classifiers")
+                if "description" in data__dynamic_keys:
+                    data__dynamic_keys.remove("description")
+                    data__dynamic__description = data__dynamic["description"]
+                    validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data__dynamic__description, custom_formats, (name_prefix or "data") + ".dynamic.description")
+                if "entry-points" in data__dynamic_keys:
+                    data__dynamic_keys.remove("entry-points")
+                    data__dynamic__entrypoints = data__dynamic["entry-points"]
+                    validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data__dynamic__entrypoints, custom_formats, (name_prefix or "data") + ".dynamic.entry-points")
+                if "readme" in data__dynamic_keys:
+                    data__dynamic_keys.remove("readme")
+                    data__dynamic__readme = data__dynamic["readme"]
+                    data__dynamic__readme_any_of_count6 = 0
+                    if not data__dynamic__readme_any_of_count6:
+                        try:
+                            validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data__dynamic__readme, custom_formats, (name_prefix or "data") + ".dynamic.readme")
+                            data__dynamic__readme_any_of_count6 += 1
+                        except JsonSchemaValueException: pass
+                    if not data__dynamic__readme_any_of_count6:
+                        try:
+                            data__dynamic__readme_is_dict = isinstance(data__dynamic__readme, dict)
+                            if data__dynamic__readme_is_dict:
+                                data__dynamic__readme_keys = set(data__dynamic__readme.keys())
+                                if "content-type" in data__dynamic__readme_keys:
+                                    data__dynamic__readme_keys.remove("content-type")
+                                    data__dynamic__readme__contenttype = data__dynamic__readme["content-type"]
+                                    if not isinstance(data__dynamic__readme__contenttype, (str)):
+                                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic.readme.content-type must be string", value=data__dynamic__readme__contenttype, name="" + (name_prefix or "data") + ".dynamic.readme.content-type", definition={'type': 'string'}, rule='type')
+                            data__dynamic__readme_any_of_count6 += 1
+                        except JsonSchemaValueException: pass
+                    if not data__dynamic__readme_any_of_count6:
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic.readme cannot be validated by any definition", value=data__dynamic__readme, name="" + (name_prefix or "data") + ".dynamic.readme", definition={'anyOf': [{'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}, rule='anyOf')
+                    data__dynamic__readme_is_dict = isinstance(data__dynamic__readme, dict)
+                    if data__dynamic__readme_is_dict:
+                        data__dynamic__readme_len = len(data__dynamic__readme)
+                        if not all(prop in data__dynamic__readme for prop in ['file']):
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic.readme must contain ['file'] properties", value=data__dynamic__readme, name="" + (name_prefix or "data") + ".dynamic.readme", definition={'anyOf': [{'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}, rule='required')
+                if data__dynamic_keys:
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic must not contain "+str(data__dynamic_keys)+" properties", value=data__dynamic, name="" + (name_prefix or "data") + ".dynamic", definition={'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'additionalProperties': False, 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}]}, 'classifiers': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'description': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'entry-points': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'readme': {'anyOf': [{'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}}}, rule='additionalProperties')
+        if data_keys:
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must not contain "+str(data_keys)+" properties", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://setuptools.pypa.io/en/latest/references/keywords.html', 'title': '``tool.setuptools`` table', '$$description': ['Please notice for the time being the ``setuptools`` project does not specify', 'a way of configuring builds via ``pyproject.toml``.', 'Therefore this schema should be taken just as a *"thought experiment"* on how', 'this *might be done*, by following the principles established in', '`ini2toml `_.', 'It considers only ``setuptools`` `parameters', '`_', 'that can currently be configured via ``setup.cfg`` and are not covered by :pep:`621`', 'but intentionally excludes ``dependency_links`` and ``setup_requires``.', 'NOTE: ``scripts`` was renamed to ``script-files`` to avoid confusion with', 'entry-point based scripts (defined in :pep:`621`).'], 'type': 'object', 'additionalProperties': False, 'properties': {'platforms': {'type': 'array', 'items': {'type': 'string'}}, 'provides': {'$$description': ['Package and virtual package names contained within this package', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'obsoletes': {'$$description': ['Packages which this package renders obsolete', '**(not supported by pip)**'], 'type': 'array', 'items': {'type': 'string', 'format': 'pep508-identifier'}}, 'zip-safe': {'description': 'Whether the project can be safely installed and run from a zip file.', 'type': 'boolean'}, 'script-files': {'description': 'Legacy way of defining scripts (entry-points are preferred).', 'type': 'array', 'items': {'type': 'string'}, '$comment': 'TODO: is this field deprecated/should be removed?'}, 'eager-resources': {'$$description': ['Resources that should be extracted together, if any of them is needed,', 'or if any C extensions included in the project are imported.'], 'type': 'array', 'items': {'type': 'string'}}, 'packages': {'$$description': ['Packages that should be included in the distribution.', 'It can be given either as a list of package identifiers', 'or as a ``dict``-like structure with a single key ``find``', 'which corresponds to a dynamic call to', '``setuptools.config.expand.find_packages`` function.', 'The ``find`` key is associated with a nested ``dict``-like structure that can', 'contain ``where``, ``include``, ``exclude`` and ``namespaces`` keys,', 'mimicking the keyword arguments of the associated function.'], 'oneOf': [{'title': 'Array of Python package identifiers', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}}, {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}]}, 'package-dir': {'$$description': [':class:`dict`-like structure mapping from package names to directories where their', 'code can be found.', 'The empty string (as key) means that all packages are contained inside', 'the given directory will be included in the distribution.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': ''}]}, 'patternProperties': {'^.*$': {'type': 'string'}}}, 'package-data': {'$$description': ['Mapping from package names to lists of glob patterns.', 'Usually this option is not needed when using ``include-package-data = true``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'include-package-data': {'$$description': ['Automatically include any data files inside the package directories', 'that are specified by ``MANIFEST.in``', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'boolean'}, 'exclude-package-data': {'$$description': ['Mapping from package names to lists of glob patterns that should be excluded', 'For more information on how to include data files, check ``setuptools`` `docs', '`_.'], 'type': 'object', 'additionalProperties': False, 'propertyNames': {'oneOf': [{'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'data-files': {'$$description': ['**DEPRECATED**: dict-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', "Please notice this don't work with wheels. See `data files support", '`_'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'cmdclass': {'$$description': ['Mapping of distutils-style command names to ``setuptools.Command`` subclasses', 'which in turn should be represented by strings with a qualified class name', '(i.e., "dotted" form with module), e.g.::\n\n', '    cmdclass = {mycmd = "pkg.subpkg.module.CommandClass"}\n\n', 'The command class should be a directly defined at the top-level of the', 'containing module (no class nesting).'], 'type': 'object', 'patternProperties': {'^.*$': {'type': 'string', 'format': 'python-qualified-identifier'}}}, 'license-files': {'type': 'array', 'items': {'type': 'string'}, '$$description': ['PROVISIONAL: List of glob patterns for all license files being distributed.', '(might become standard with PEP 639).'], 'default': ['LICEN[CS]E*', ' COPYING*', ' NOTICE*', 'AUTHORS*'], '$comment': 'TODO: revise if PEP 639 is accepted. Probably ``project.license-files``?'}, 'dynamic': {'type': 'object', 'description': 'Instructions for loading :pep:`621`-related metadata dynamically', 'additionalProperties': False, 'properties': {'version': {'$$description': ['A version dynamically loaded via either the ``attr:`` or ``file:``', 'directives. Please make sure the given file or attribute respects :pep:`440`.'], 'oneOf': [{'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}]}, 'classifiers': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'description': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'entry-points': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'readme': {'anyOf': [{'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, {'properties': {'content-type': {'type': 'string'}}}], 'required': ['file']}}}}, 'definitions': {'file-directive': {'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, 'attr-directive': {'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, 'find-directive': {'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}}}, rule='additionalProperties')
+    return data
+
+def validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_file_directive(data, custom_formats={}, name_prefix=None):
+    if not isinstance(data, (dict)):
+        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, rule='type')
+    data_is_dict = isinstance(data, dict)
+    if data_is_dict:
+        data_len = len(data)
+        if not all(prop in data for prop in ['file']):
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must contain ['file'] properties", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, rule='required')
+        data_keys = set(data.keys())
+        if "file" in data_keys:
+            data_keys.remove("file")
+            data__file = data["file"]
+            data__file_one_of_count7 = 0
+            if data__file_one_of_count7 < 2:
+                try:
+                    if not isinstance(data__file, (str)):
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".file must be string", value=data__file, name="" + (name_prefix or "data") + ".file", definition={'type': 'string'}, rule='type')
+                    data__file_one_of_count7 += 1
+                except JsonSchemaValueException: pass
+            if data__file_one_of_count7 < 2:
+                try:
+                    if not isinstance(data__file, (list, tuple)):
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".file must be array", value=data__file, name="" + (name_prefix or "data") + ".file", definition={'type': 'array', 'items': {'type': 'string'}}, rule='type')
+                    data__file_is_list = isinstance(data__file, (list, tuple))
+                    if data__file_is_list:
+                        data__file_len = len(data__file)
+                        for data__file_x, data__file_item in enumerate(data__file):
+                            if not isinstance(data__file_item, (str)):
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".file[{data__file_x}]".format(**locals()) + " must be string", value=data__file_item, name="" + (name_prefix or "data") + ".file[{data__file_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
+                    data__file_one_of_count7 += 1
+                except JsonSchemaValueException: pass
+            if data__file_one_of_count7 != 1:
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".file must be valid exactly by one definition" + (" (" + str(data__file_one_of_count7) + " matches found)"), value=data__file, name="" + (name_prefix or "data") + ".file", definition={'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}, rule='oneOf')
+        if data_keys:
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must not contain "+str(data_keys)+" properties", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/file-directive', 'title': "'file:' directive", 'description': 'Value is read from a file (or list of files and then concatenated)', 'type': 'object', 'additionalProperties': False, 'properties': {'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'required': ['file']}, rule='additionalProperties')
+    return data
+
+def validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_attr_directive(data, custom_formats={}, name_prefix=None):
+    if not isinstance(data, (dict)):
+        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, rule='type')
+    data_is_dict = isinstance(data, dict)
+    if data_is_dict:
+        data_len = len(data)
+        if not all(prop in data for prop in ['attr']):
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must contain ['attr'] properties", value=data, name="" + (name_prefix or "data") + "", definition={'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, rule='required')
+        data_keys = set(data.keys())
+        if "attr" in data_keys:
+            data_keys.remove("attr")
+            data__attr = data["attr"]
+            if not isinstance(data__attr, (str)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".attr must be string", value=data__attr, name="" + (name_prefix or "data") + ".attr", definition={'type': 'string'}, rule='type')
+        if data_keys:
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must not contain "+str(data_keys)+" properties", value=data, name="" + (name_prefix or "data") + "", definition={'title': "'attr:' directive", '$id': '#/definitions/attr-directive', '$$description': ['Value is read from a module attribute. Supports callables and iterables;', 'unsupported types are cast via ``str()``'], 'type': 'object', 'additionalProperties': False, 'properties': {'attr': {'type': 'string'}}, 'required': ['attr']}, rule='additionalProperties')
+    return data
+
+def validate_https___setuptools_pypa_io_en_latest_references_keywords_html__definitions_find_directive(data, custom_formats={}, name_prefix=None):
+    if not isinstance(data, (dict)):
+        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}, rule='type')
+    data_is_dict = isinstance(data, dict)
+    if data_is_dict:
+        data_keys = set(data.keys())
+        if "find" in data_keys:
+            data_keys.remove("find")
+            data__find = data["find"]
+            if not isinstance(data__find, (dict)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".find must be object", value=data__find, name="" + (name_prefix or "data") + ".find", definition={'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}, rule='type')
+            data__find_is_dict = isinstance(data__find, dict)
+            if data__find_is_dict:
+                data__find_keys = set(data__find.keys())
+                if "where" in data__find_keys:
+                    data__find_keys.remove("where")
+                    data__find__where = data__find["where"]
+                    if not isinstance(data__find__where, (list, tuple)):
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".find.where must be array", value=data__find__where, name="" + (name_prefix or "data") + ".find.where", definition={'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, rule='type')
+                    data__find__where_is_list = isinstance(data__find__where, (list, tuple))
+                    if data__find__where_is_list:
+                        data__find__where_len = len(data__find__where)
+                        for data__find__where_x, data__find__where_item in enumerate(data__find__where):
+                            if not isinstance(data__find__where_item, (str)):
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".find.where[{data__find__where_x}]".format(**locals()) + " must be string", value=data__find__where_item, name="" + (name_prefix or "data") + ".find.where[{data__find__where_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
+                if "exclude" in data__find_keys:
+                    data__find_keys.remove("exclude")
+                    data__find__exclude = data__find["exclude"]
+                    if not isinstance(data__find__exclude, (list, tuple)):
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".find.exclude must be array", value=data__find__exclude, name="" + (name_prefix or "data") + ".find.exclude", definition={'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, rule='type')
+                    data__find__exclude_is_list = isinstance(data__find__exclude, (list, tuple))
+                    if data__find__exclude_is_list:
+                        data__find__exclude_len = len(data__find__exclude)
+                        for data__find__exclude_x, data__find__exclude_item in enumerate(data__find__exclude):
+                            if not isinstance(data__find__exclude_item, (str)):
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".find.exclude[{data__find__exclude_x}]".format(**locals()) + " must be string", value=data__find__exclude_item, name="" + (name_prefix or "data") + ".find.exclude[{data__find__exclude_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
+                if "include" in data__find_keys:
+                    data__find_keys.remove("include")
+                    data__find__include = data__find["include"]
+                    if not isinstance(data__find__include, (list, tuple)):
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".find.include must be array", value=data__find__include, name="" + (name_prefix or "data") + ".find.include", definition={'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, rule='type')
+                    data__find__include_is_list = isinstance(data__find__include, (list, tuple))
+                    if data__find__include_is_list:
+                        data__find__include_len = len(data__find__include)
+                        for data__find__include_x, data__find__include_item in enumerate(data__find__include):
+                            if not isinstance(data__find__include_item, (str)):
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".find.include[{data__find__include_x}]".format(**locals()) + " must be string", value=data__find__include_item, name="" + (name_prefix or "data") + ".find.include[{data__find__include_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
+                if "namespaces" in data__find_keys:
+                    data__find_keys.remove("namespaces")
+                    data__find__namespaces = data__find["namespaces"]
+                    if not isinstance(data__find__namespaces, (bool)):
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".find.namespaces must be boolean", value=data__find__namespaces, name="" + (name_prefix or "data") + ".find.namespaces", definition={'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}, rule='type')
+                if data__find_keys:
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".find must not contain "+str(data__find_keys)+" properties", value=data__find, name="" + (name_prefix or "data") + ".find", definition={'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}, rule='additionalProperties')
+        if data_keys:
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must not contain "+str(data_keys)+" properties", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/find-directive', 'title': "'find:' directive", 'type': 'object', 'additionalProperties': False, 'properties': {'find': {'type': 'object', '$$description': ['Dynamic `package discovery', '`_.'], 'additionalProperties': False, 'properties': {'where': {'description': 'Directories to be searched for packages (Unix-style relative path)', 'type': 'array', 'items': {'type': 'string'}}, 'exclude': {'type': 'array', '$$description': ['Exclude packages that match the values listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'include': {'type': 'array', '$$description': ['Restrict the found packages to just the ones listed in this field.', "Can container shell-style wildcards (e.g. ``'pkg.*'``)"], 'items': {'type': 'string'}}, 'namespaces': {'type': 'boolean', '$$description': ['When ``True``, directories without a ``__init__.py`` file will also', 'be scanned for :pep:`420`-style implicit namespaces']}}}}}, rule='additionalProperties')
+    return data
+
+def validate_https___docs_python_org_3_install(data, custom_formats={}, name_prefix=None):
+    if not isinstance(data, (dict)):
+        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://docs.python.org/3/install/', 'title': '``tool.distutils`` table', '$$description': ['Originally, ``distutils`` allowed developers to configure arguments for', '``setup.py`` scripts via `distutils configuration files', '`_.', '``tool.distutils`` subtables could be used with the same purpose', '(NOT CURRENTLY IMPLEMENTED).'], 'type': 'object', 'properties': {'global': {'type': 'object', 'description': 'Global options applied to all ``distutils`` commands'}}, 'patternProperties': {'.+': {'type': 'object'}}, '$comment': 'TODO: Is there a practical way of making this schema more specific?'}, rule='type')
+    data_is_dict = isinstance(data, dict)
+    if data_is_dict:
+        data_keys = set(data.keys())
+        if "global" in data_keys:
+            data_keys.remove("global")
+            data__global = data["global"]
+            if not isinstance(data__global, (dict)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".global must be object", value=data__global, name="" + (name_prefix or "data") + ".global", definition={'type': 'object', 'description': 'Global options applied to all ``distutils`` commands'}, rule='type')
+        for data_key, data_val in data.items():
+            if REGEX_PATTERNS['.+'].search(data_key):
+                if data_key in data_keys:
+                    data_keys.remove(data_key)
+                if not isinstance(data_val, (dict)):
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".{data_key}".format(**locals()) + " must be object", value=data_val, name="" + (name_prefix or "data") + ".{data_key}".format(**locals()) + "", definition={'type': 'object'}, rule='type')
+    return data
+
+def validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata(data, custom_formats={}, name_prefix=None):
+    if not isinstance(data, (dict)):
+        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'gui-scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['dynamic'], 'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}}}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'required': ['version'], '$$description': ['version should be statically defined in the ``version`` field']}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, rule='type')
+    data_is_dict = isinstance(data, dict)
+    if data_is_dict:
+        data_len = len(data)
+        if not all(prop in data for prop in ['name']):
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must contain ['name'] properties", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'gui-scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['dynamic'], 'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}}}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'required': ['version'], '$$description': ['version should be statically defined in the ``version`` field']}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, rule='required')
+        data_keys = set(data.keys())
+        if "name" in data_keys:
+            data_keys.remove("name")
+            data__name = data["name"]
+            if not isinstance(data__name, (str)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".name must be string", value=data__name, name="" + (name_prefix or "data") + ".name", definition={'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, rule='type')
+            if isinstance(data__name, str):
+                if not custom_formats["pep508-identifier"](data__name):
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".name must be pep508-identifier", value=data__name, name="" + (name_prefix or "data") + ".name", definition={'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, rule='format')
+        if "version" in data_keys:
+            data_keys.remove("version")
+            data__version = data["version"]
+            if not isinstance(data__version, (str)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".version must be string", value=data__version, name="" + (name_prefix or "data") + ".version", definition={'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, rule='type')
+            if isinstance(data__version, str):
+                if not custom_formats["pep440"](data__version):
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".version must be pep440", value=data__version, name="" + (name_prefix or "data") + ".version", definition={'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, rule='format')
+        if "description" in data_keys:
+            data_keys.remove("description")
+            data__description = data["description"]
+            if not isinstance(data__description, (str)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".description must be string", value=data__description, name="" + (name_prefix or "data") + ".description", definition={'type': 'string', '$$description': ['The `summary description of the project', '`_']}, rule='type')
+        if "readme" in data_keys:
+            data_keys.remove("readme")
+            data__readme = data["readme"]
+            data__readme_one_of_count8 = 0
+            if data__readme_one_of_count8 < 2:
+                try:
+                    if not isinstance(data__readme, (str)):
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme must be string", value=data__readme, name="" + (name_prefix or "data") + ".readme", definition={'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, rule='type')
+                    data__readme_one_of_count8 += 1
+                except JsonSchemaValueException: pass
+            if data__readme_one_of_count8 < 2:
+                try:
+                    if not isinstance(data__readme, (dict)):
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme must be object", value=data__readme, name="" + (name_prefix or "data") + ".readme", definition={'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}, rule='type')
+                    data__readme_any_of_count9 = 0
+                    if not data__readme_any_of_count9:
+                        try:
+                            data__readme_is_dict = isinstance(data__readme, dict)
+                            if data__readme_is_dict:
+                                data__readme_len = len(data__readme)
+                                if not all(prop in data__readme for prop in ['file']):
+                                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme must contain ['file'] properties", value=data__readme, name="" + (name_prefix or "data") + ".readme", definition={'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, rule='required')
+                                data__readme_keys = set(data__readme.keys())
+                                if "file" in data__readme_keys:
+                                    data__readme_keys.remove("file")
+                                    data__readme__file = data__readme["file"]
+                                    if not isinstance(data__readme__file, (str)):
+                                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme.file must be string", value=data__readme__file, name="" + (name_prefix or "data") + ".readme.file", definition={'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}, rule='type')
+                            data__readme_any_of_count9 += 1
+                        except JsonSchemaValueException: pass
+                    if not data__readme_any_of_count9:
+                        try:
+                            data__readme_is_dict = isinstance(data__readme, dict)
+                            if data__readme_is_dict:
+                                data__readme_len = len(data__readme)
+                                if not all(prop in data__readme for prop in ['text']):
+                                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme must contain ['text'] properties", value=data__readme, name="" + (name_prefix or "data") + ".readme", definition={'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}, rule='required')
+                                data__readme_keys = set(data__readme.keys())
+                                if "text" in data__readme_keys:
+                                    data__readme_keys.remove("text")
+                                    data__readme__text = data__readme["text"]
+                                    if not isinstance(data__readme__text, (str)):
+                                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme.text must be string", value=data__readme__text, name="" + (name_prefix or "data") + ".readme.text", definition={'type': 'string', 'description': 'Full text describing the project.'}, rule='type')
+                            data__readme_any_of_count9 += 1
+                        except JsonSchemaValueException: pass
+                    if not data__readme_any_of_count9:
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme cannot be validated by any definition", value=data__readme, name="" + (name_prefix or "data") + ".readme", definition={'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, rule='anyOf')
+                    data__readme_is_dict = isinstance(data__readme, dict)
+                    if data__readme_is_dict:
+                        data__readme_len = len(data__readme)
+                        if not all(prop in data__readme for prop in ['content-type']):
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme must contain ['content-type'] properties", value=data__readme, name="" + (name_prefix or "data") + ".readme", definition={'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}, rule='required')
+                        data__readme_keys = set(data__readme.keys())
+                        if "content-type" in data__readme_keys:
+                            data__readme_keys.remove("content-type")
+                            data__readme__contenttype = data__readme["content-type"]
+                            if not isinstance(data__readme__contenttype, (str)):
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme.content-type must be string", value=data__readme__contenttype, name="" + (name_prefix or "data") + ".readme.content-type", definition={'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}, rule='type')
+                    data__readme_one_of_count8 += 1
+                except JsonSchemaValueException: pass
+            if data__readme_one_of_count8 != 1:
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme must be valid exactly by one definition" + (" (" + str(data__readme_one_of_count8) + " matches found)"), value=data__readme, name="" + (name_prefix or "data") + ".readme", definition={'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, rule='oneOf')
+        if "requires-python" in data_keys:
+            data_keys.remove("requires-python")
+            data__requirespython = data["requires-python"]
+            if not isinstance(data__requirespython, (str)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".requires-python must be string", value=data__requirespython, name="" + (name_prefix or "data") + ".requires-python", definition={'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, rule='type')
+            if isinstance(data__requirespython, str):
+                if not custom_formats["pep508-versionspec"](data__requirespython):
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".requires-python must be pep508-versionspec", value=data__requirespython, name="" + (name_prefix or "data") + ".requires-python", definition={'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, rule='format')
+        if "license" in data_keys:
+            data_keys.remove("license")
+            data__license = data["license"]
+            data__license_one_of_count10 = 0
+            if data__license_one_of_count10 < 2:
+                try:
+                    data__license_is_dict = isinstance(data__license, dict)
+                    if data__license_is_dict:
+                        data__license_len = len(data__license)
+                        if not all(prop in data__license for prop in ['file']):
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".license must contain ['file'] properties", value=data__license, name="" + (name_prefix or "data") + ".license", definition={'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, rule='required')
+                        data__license_keys = set(data__license.keys())
+                        if "file" in data__license_keys:
+                            data__license_keys.remove("file")
+                            data__license__file = data__license["file"]
+                            if not isinstance(data__license__file, (str)):
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".license.file must be string", value=data__license__file, name="" + (name_prefix or "data") + ".license.file", definition={'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}, rule='type')
+                    data__license_one_of_count10 += 1
+                except JsonSchemaValueException: pass
+            if data__license_one_of_count10 < 2:
+                try:
+                    data__license_is_dict = isinstance(data__license, dict)
+                    if data__license_is_dict:
+                        data__license_len = len(data__license)
+                        if not all(prop in data__license for prop in ['text']):
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".license must contain ['text'] properties", value=data__license, name="" + (name_prefix or "data") + ".license", definition={'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}, rule='required')
+                        data__license_keys = set(data__license.keys())
+                        if "text" in data__license_keys:
+                            data__license_keys.remove("text")
+                            data__license__text = data__license["text"]
+                            if not isinstance(data__license__text, (str)):
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".license.text must be string", value=data__license__text, name="" + (name_prefix or "data") + ".license.text", definition={'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}, rule='type')
+                    data__license_one_of_count10 += 1
+                except JsonSchemaValueException: pass
+            if data__license_one_of_count10 != 1:
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".license must be valid exactly by one definition" + (" (" + str(data__license_one_of_count10) + " matches found)"), value=data__license, name="" + (name_prefix or "data") + ".license", definition={'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, rule='oneOf')
+        if "authors" in data_keys:
+            data_keys.remove("authors")
+            data__authors = data["authors"]
+            if not isinstance(data__authors, (list, tuple)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".authors must be array", value=data__authors, name="" + (name_prefix or "data") + ".authors", definition={'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, rule='type')
+            data__authors_is_list = isinstance(data__authors, (list, tuple))
+            if data__authors_is_list:
+                data__authors_len = len(data__authors)
+                for data__authors_x, data__authors_item in enumerate(data__authors):
+                    validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_author(data__authors_item, custom_formats, (name_prefix or "data") + ".authors[{data__authors_x}]")
+        if "maintainers" in data_keys:
+            data_keys.remove("maintainers")
+            data__maintainers = data["maintainers"]
+            if not isinstance(data__maintainers, (list, tuple)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".maintainers must be array", value=data__maintainers, name="" + (name_prefix or "data") + ".maintainers", definition={'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, rule='type')
+            data__maintainers_is_list = isinstance(data__maintainers, (list, tuple))
+            if data__maintainers_is_list:
+                data__maintainers_len = len(data__maintainers)
+                for data__maintainers_x, data__maintainers_item in enumerate(data__maintainers):
+                    validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_author(data__maintainers_item, custom_formats, (name_prefix or "data") + ".maintainers[{data__maintainers_x}]")
+        if "keywords" in data_keys:
+            data_keys.remove("keywords")
+            data__keywords = data["keywords"]
+            if not isinstance(data__keywords, (list, tuple)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".keywords must be array", value=data__keywords, name="" + (name_prefix or "data") + ".keywords", definition={'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, rule='type')
+            data__keywords_is_list = isinstance(data__keywords, (list, tuple))
+            if data__keywords_is_list:
+                data__keywords_len = len(data__keywords)
+                for data__keywords_x, data__keywords_item in enumerate(data__keywords):
+                    if not isinstance(data__keywords_item, (str)):
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".keywords[{data__keywords_x}]".format(**locals()) + " must be string", value=data__keywords_item, name="" + (name_prefix or "data") + ".keywords[{data__keywords_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type')
+        if "classifiers" in data_keys:
+            data_keys.remove("classifiers")
+            data__classifiers = data["classifiers"]
+            if not isinstance(data__classifiers, (list, tuple)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".classifiers must be array", value=data__classifiers, name="" + (name_prefix or "data") + ".classifiers", definition={'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, rule='type')
+            data__classifiers_is_list = isinstance(data__classifiers, (list, tuple))
+            if data__classifiers_is_list:
+                data__classifiers_len = len(data__classifiers)
+                for data__classifiers_x, data__classifiers_item in enumerate(data__classifiers):
+                    if not isinstance(data__classifiers_item, (str)):
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".classifiers[{data__classifiers_x}]".format(**locals()) + " must be string", value=data__classifiers_item, name="" + (name_prefix or "data") + ".classifiers[{data__classifiers_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, rule='type')
+                    if isinstance(data__classifiers_item, str):
+                        if not custom_formats["trove-classifier"](data__classifiers_item):
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".classifiers[{data__classifiers_x}]".format(**locals()) + " must be trove-classifier", value=data__classifiers_item, name="" + (name_prefix or "data") + ".classifiers[{data__classifiers_x}]".format(**locals()) + "", definition={'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, rule='format')
+        if "urls" in data_keys:
+            data_keys.remove("urls")
+            data__urls = data["urls"]
+            if not isinstance(data__urls, (dict)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".urls must be object", value=data__urls, name="" + (name_prefix or "data") + ".urls", definition={'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, rule='type')
+            data__urls_is_dict = isinstance(data__urls, dict)
+            if data__urls_is_dict:
+                data__urls_keys = set(data__urls.keys())
+                for data__urls_key, data__urls_val in data__urls.items():
+                    if REGEX_PATTERNS['^.+$'].search(data__urls_key):
+                        if data__urls_key in data__urls_keys:
+                            data__urls_keys.remove(data__urls_key)
+                        if not isinstance(data__urls_val, (str)):
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".urls.{data__urls_key}".format(**locals()) + " must be string", value=data__urls_val, name="" + (name_prefix or "data") + ".urls.{data__urls_key}".format(**locals()) + "", definition={'type': 'string', 'format': 'url'}, rule='type')
+                        if isinstance(data__urls_val, str):
+                            if not custom_formats["url"](data__urls_val):
+                                raise JsonSchemaValueException("" + (name_prefix or "data") + ".urls.{data__urls_key}".format(**locals()) + " must be url", value=data__urls_val, name="" + (name_prefix or "data") + ".urls.{data__urls_key}".format(**locals()) + "", definition={'type': 'string', 'format': 'url'}, rule='format')
+                if data__urls_keys:
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".urls must not contain "+str(data__urls_keys)+" properties", value=data__urls, name="" + (name_prefix or "data") + ".urls", definition={'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, rule='additionalProperties')
+        if "scripts" in data_keys:
+            data_keys.remove("scripts")
+            data__scripts = data["scripts"]
+            validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_entry_point_group(data__scripts, custom_formats, (name_prefix or "data") + ".scripts")
+        if "gui-scripts" in data_keys:
+            data_keys.remove("gui-scripts")
+            data__guiscripts = data["gui-scripts"]
+            validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_entry_point_group(data__guiscripts, custom_formats, (name_prefix or "data") + ".gui-scripts")
+        if "entry-points" in data_keys:
+            data_keys.remove("entry-points")
+            data__entrypoints = data["entry-points"]
+            data__entrypoints_is_dict = isinstance(data__entrypoints, dict)
+            if data__entrypoints_is_dict:
+                data__entrypoints_keys = set(data__entrypoints.keys())
+                for data__entrypoints_key, data__entrypoints_val in data__entrypoints.items():
+                    if REGEX_PATTERNS['^.+$'].search(data__entrypoints_key):
+                        if data__entrypoints_key in data__entrypoints_keys:
+                            data__entrypoints_keys.remove(data__entrypoints_key)
+                        validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_entry_point_group(data__entrypoints_val, custom_formats, (name_prefix or "data") + ".entry-points.{data__entrypoints_key}")
+                if data__entrypoints_keys:
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".entry-points must not contain "+str(data__entrypoints_keys)+" properties", value=data__entrypoints, name="" + (name_prefix or "data") + ".entry-points", definition={'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}}}, rule='additionalProperties')
+                data__entrypoints_len = len(data__entrypoints)
+                if data__entrypoints_len != 0:
+                    data__entrypoints_property_names = True
+                    for data__entrypoints_key in data__entrypoints:
+                        try:
+                            if isinstance(data__entrypoints_key, str):
+                                if not custom_formats["python-entrypoint-group"](data__entrypoints_key):
+                                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".entry-points must be python-entrypoint-group", value=data__entrypoints_key, name="" + (name_prefix or "data") + ".entry-points", definition={'format': 'python-entrypoint-group'}, rule='format')
+                        except JsonSchemaValueException:
+                            data__entrypoints_property_names = False
+                    if not data__entrypoints_property_names:
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".entry-points must be named by propertyName definition", value=data__entrypoints, name="" + (name_prefix or "data") + ".entry-points", definition={'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}}}, rule='propertyNames')
+        if "dependencies" in data_keys:
+            data_keys.remove("dependencies")
+            data__dependencies = data["dependencies"]
+            if not isinstance(data__dependencies, (list, tuple)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".dependencies must be array", value=data__dependencies, name="" + (name_prefix or "data") + ".dependencies", definition={'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}, rule='type')
+            data__dependencies_is_list = isinstance(data__dependencies, (list, tuple))
+            if data__dependencies_is_list:
+                data__dependencies_len = len(data__dependencies)
+                for data__dependencies_x, data__dependencies_item in enumerate(data__dependencies):
+                    validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_dependency(data__dependencies_item, custom_formats, (name_prefix or "data") + ".dependencies[{data__dependencies_x}]")
+        if "optional-dependencies" in data_keys:
+            data_keys.remove("optional-dependencies")
+            data__optionaldependencies = data["optional-dependencies"]
+            if not isinstance(data__optionaldependencies, (dict)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".optional-dependencies must be object", value=data__optionaldependencies, name="" + (name_prefix or "data") + ".optional-dependencies", definition={'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, rule='type')
+            data__optionaldependencies_is_dict = isinstance(data__optionaldependencies, dict)
+            if data__optionaldependencies_is_dict:
+                data__optionaldependencies_keys = set(data__optionaldependencies.keys())
+                for data__optionaldependencies_key, data__optionaldependencies_val in data__optionaldependencies.items():
+                    if REGEX_PATTERNS['^.+$'].search(data__optionaldependencies_key):
+                        if data__optionaldependencies_key in data__optionaldependencies_keys:
+                            data__optionaldependencies_keys.remove(data__optionaldependencies_key)
+                        if not isinstance(data__optionaldependencies_val, (list, tuple)):
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".optional-dependencies.{data__optionaldependencies_key}".format(**locals()) + " must be array", value=data__optionaldependencies_val, name="" + (name_prefix or "data") + ".optional-dependencies.{data__optionaldependencies_key}".format(**locals()) + "", definition={'type': 'array', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}, rule='type')
+                        data__optionaldependencies_val_is_list = isinstance(data__optionaldependencies_val, (list, tuple))
+                        if data__optionaldependencies_val_is_list:
+                            data__optionaldependencies_val_len = len(data__optionaldependencies_val)
+                            for data__optionaldependencies_val_x, data__optionaldependencies_val_item in enumerate(data__optionaldependencies_val):
+                                validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_dependency(data__optionaldependencies_val_item, custom_formats, (name_prefix or "data") + ".optional-dependencies.{data__optionaldependencies_key}[{data__optionaldependencies_val_x}]")
+                if data__optionaldependencies_keys:
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".optional-dependencies must not contain "+str(data__optionaldependencies_keys)+" properties", value=data__optionaldependencies, name="" + (name_prefix or "data") + ".optional-dependencies", definition={'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, rule='additionalProperties')
+                data__optionaldependencies_len = len(data__optionaldependencies)
+                if data__optionaldependencies_len != 0:
+                    data__optionaldependencies_property_names = True
+                    for data__optionaldependencies_key in data__optionaldependencies:
+                        try:
+                            if isinstance(data__optionaldependencies_key, str):
+                                if not custom_formats["pep508-identifier"](data__optionaldependencies_key):
+                                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".optional-dependencies must be pep508-identifier", value=data__optionaldependencies_key, name="" + (name_prefix or "data") + ".optional-dependencies", definition={'format': 'pep508-identifier'}, rule='format')
+                        except JsonSchemaValueException:
+                            data__optionaldependencies_property_names = False
+                    if not data__optionaldependencies_property_names:
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".optional-dependencies must be named by propertyName definition", value=data__optionaldependencies, name="" + (name_prefix or "data") + ".optional-dependencies", definition={'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, rule='propertyNames')
+        if "dynamic" in data_keys:
+            data_keys.remove("dynamic")
+            data__dynamic = data["dynamic"]
+            if not isinstance(data__dynamic, (list, tuple)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic must be array", value=data__dynamic, name="" + (name_prefix or "data") + ".dynamic", definition={'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}, rule='type')
+            data__dynamic_is_list = isinstance(data__dynamic, (list, tuple))
+            if data__dynamic_is_list:
+                data__dynamic_len = len(data__dynamic)
+                for data__dynamic_x, data__dynamic_item in enumerate(data__dynamic):
+                    if data__dynamic_item not in ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']:
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic[{data__dynamic_x}]".format(**locals()) + " must be one of ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']", value=data__dynamic_item, name="" + (name_prefix or "data") + ".dynamic[{data__dynamic_x}]".format(**locals()) + "", definition={'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}, rule='enum')
+        if data_keys:
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must not contain "+str(data_keys)+" properties", value=data, name="" + (name_prefix or "data") + "", definition={'$schema': 'http://json-schema.org/draft-07/schema', '$id': 'https://packaging.python.org/en/latest/specifications/declaring-project-metadata/', 'title': 'Package metadata stored in the ``project`` table', '$$description': ['Data structure for the **project** table inside ``pyproject.toml``', '(as initially defined in :pep:`621`)'], 'type': 'object', 'properties': {'name': {'type': 'string', 'description': 'The name (primary identifier) of the project. MUST be statically defined.', 'format': 'pep508-identifier'}, 'version': {'type': 'string', 'description': 'The version of the project as supported by :pep:`440`.', 'format': 'pep440'}, 'description': {'type': 'string', '$$description': ['The `summary description of the project', '`_']}, 'readme': {'$$description': ['`Full/detailed description of the project in the form of a README', '`_', "with meaning similar to the one defined in `core metadata's Description", '`_'], 'oneOf': [{'type': 'string', '$$description': ['Relative path to a text file (UTF-8) containing the full description', 'of the project. If the file path ends in case-insensitive ``.md`` or', '``.rst`` suffixes, then the content-type is respectively', '``text/markdown`` or ``text/x-rst``']}, {'type': 'object', 'allOf': [{'anyOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to a text file containing the full description', 'of the project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', 'description': 'Full text describing the project.'}}, 'required': ['text']}]}, {'properties': {'content-type': {'type': 'string', '$$description': ['Content-type (:rfc:`1341`) of the full description', '(e.g. ``text/markdown``). The ``charset`` parameter is assumed', 'UTF-8 when not present.'], '$comment': 'TODO: add regex pattern or format?'}}, 'required': ['content-type']}]}]}, 'requires-python': {'type': 'string', 'format': 'pep508-versionspec', '$$description': ['`The Python version requirements of the project', '`_.']}, 'license': {'description': '`Project license `_.', 'oneOf': [{'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'authors': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'authors' of the project.", 'The exact meaning is open to interpretation (e.g. original or primary authors,', 'current maintainers, or owners of the package).']}, 'maintainers': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, '$$description': ["The people or organizations considered to be the 'maintainers' of the project.", 'Similarly to ``authors``, the exact meaning is open to interpretation.']}, 'keywords': {'type': 'array', 'items': {'type': 'string'}, 'description': 'List of keywords to assist searching for the distribution in a larger catalog.'}, 'classifiers': {'type': 'array', 'items': {'type': 'string', 'format': 'trove-classifier', 'description': '`PyPI classifier `_.'}, '$$description': ['`Trove classifiers `_', 'which apply to the project.']}, 'urls': {'type': 'object', 'description': 'URLs associated with the project in the form ``label => value``.', 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', 'format': 'url'}}}, 'scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'gui-scripts': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'entry-points': {'$$description': ['Instruct the installer to expose the given modules/functions via', '``entry-point`` discovery mechanism (useful for plugins).', 'More information available in the `Python packaging guide', '`_.'], 'propertyNames': {'format': 'python-entrypoint-group'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}}}, 'dependencies': {'type': 'array', 'description': 'Project (mandatory) dependencies.', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}, 'optional-dependencies': {'type': 'object', 'description': 'Optional dependency for the project', 'propertyNames': {'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'array', 'items': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}}, 'dynamic': {'type': 'array', '$$description': ['Specifies which fields are intentionally unspecified and expected to be', 'dynamically provided by build tools'], 'items': {'enum': ['version', 'description', 'readme', 'requires-python', 'license', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'if': {'not': {'required': ['dynamic'], 'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}}}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, 'then': {'required': ['version'], '$$description': ['version should be statically defined in the ``version`` field']}, 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, 'entry-point-group': {'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, 'dependency': {'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}}}, rule='additionalProperties')
+    try:
+        try:
+            data_is_dict = isinstance(data, dict)
+            if data_is_dict:
+                data_len = len(data)
+                if not all(prop in data for prop in ['dynamic']):
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + " must contain ['dynamic'] properties", value=data, name="" + (name_prefix or "data") + "", definition={'required': ['dynamic'], 'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}}}, rule='required')
+                data_keys = set(data.keys())
+                if "dynamic" in data_keys:
+                    data_keys.remove("dynamic")
+                    data__dynamic = data["dynamic"]
+                    data__dynamic_is_list = isinstance(data__dynamic, (list, tuple))
+                    if data__dynamic_is_list:
+                        data__dynamic_contains = False
+                        for data__dynamic_key in data__dynamic:
+                            try:
+                                if data__dynamic_key != "version":
+                                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic must be same as const definition: version", value=data__dynamic_key, name="" + (name_prefix or "data") + ".dynamic", definition={'const': 'version'}, rule='const')
+                                data__dynamic_contains = True
+                                break
+                            except JsonSchemaValueException: pass
+                        if not data__dynamic_contains:
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic must contain one of contains definition", value=data__dynamic, name="" + (name_prefix or "data") + ".dynamic", definition={'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}, rule='contains')
+        except JsonSchemaValueException: pass
+        else:
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must NOT match a disallowed definition", value=data, name="" + (name_prefix or "data") + "", definition={'not': {'required': ['dynamic'], 'properties': {'dynamic': {'contains': {'const': 'version'}, '$$description': ['version is listed in ``dynamic``']}}}, '$$comment': ['According to :pep:`621`:', '    If the core metadata specification lists a field as "Required", then', '    the metadata MUST specify the field statically or list it in dynamic', 'In turn, `core metadata`_ defines:', '    The required fields are: Metadata-Version, Name, Version.', '    All the other fields are optional.', 'Since ``Metadata-Version`` is defined by the build back-end, ``name`` and', '``version`` are the only mandatory information in ``pyproject.toml``.', '.. _core metadata: https://packaging.python.org/specifications/core-metadata/']}, rule='not')
+    except JsonSchemaValueException:
+        pass
+    else:
+        data_is_dict = isinstance(data, dict)
+        if data_is_dict:
+            data_len = len(data)
+            if not all(prop in data for prop in ['version']):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + " must contain ['version'] properties", value=data, name="" + (name_prefix or "data") + "", definition={'required': ['version'], '$$description': ['version should be statically defined in the ``version`` field']}, rule='required')
+    return data
+
+def validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_dependency(data, custom_formats={}, name_prefix=None):
+    if not isinstance(data, (str)):
+        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be string", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}, rule='type')
+    if isinstance(data, str):
+        if not custom_formats["pep508"](data):
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must be pep508", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/dependency', 'title': 'Dependency', 'type': 'string', 'description': 'Project dependency specification according to PEP 508', 'format': 'pep508'}, rule='format')
+    return data
+
+def validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_entry_point_group(data, custom_formats={}, name_prefix=None):
+    if not isinstance(data, (dict)):
+        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, rule='type')
+    data_is_dict = isinstance(data, dict)
+    if data_is_dict:
+        data_keys = set(data.keys())
+        for data_key, data_val in data.items():
+            if REGEX_PATTERNS['^.+$'].search(data_key):
+                if data_key in data_keys:
+                    data_keys.remove(data_key)
+                if not isinstance(data_val, (str)):
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".{data_key}".format(**locals()) + " must be string", value=data_val, name="" + (name_prefix or "data") + ".{data_key}".format(**locals()) + "", definition={'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}, rule='type')
+                if isinstance(data_val, str):
+                    if not custom_formats["python-entrypoint-reference"](data_val):
+                        raise JsonSchemaValueException("" + (name_prefix or "data") + ".{data_key}".format(**locals()) + " must be python-entrypoint-reference", value=data_val, name="" + (name_prefix or "data") + ".{data_key}".format(**locals()) + "", definition={'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}, rule='format')
+        if data_keys:
+            raise JsonSchemaValueException("" + (name_prefix or "data") + " must not contain "+str(data_keys)+" properties", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, rule='additionalProperties')
+        data_len = len(data)
+        if data_len != 0:
+            data_property_names = True
+            for data_key in data:
+                try:
+                    if isinstance(data_key, str):
+                        if not custom_formats["python-entrypoint-name"](data_key):
+                            raise JsonSchemaValueException("" + (name_prefix or "data") + " must be python-entrypoint-name", value=data_key, name="" + (name_prefix or "data") + "", definition={'format': 'python-entrypoint-name'}, rule='format')
+                except JsonSchemaValueException:
+                    data_property_names = False
+            if not data_property_names:
+                raise JsonSchemaValueException("" + (name_prefix or "data") + " must be named by propertyName definition", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/entry-point-group', 'title': 'Entry-points', 'type': 'object', '$$description': ['Entry-points are grouped together to indicate what sort of capabilities they', 'provide.', 'See the `packaging guides', '`_', 'and `setuptools docs', '`_', 'for more information.'], 'propertyNames': {'format': 'python-entrypoint-name'}, 'additionalProperties': False, 'patternProperties': {'^.+$': {'type': 'string', '$$description': ['Reference to a Python object. It is either in the form', '``importable.module``, or ``importable.module:object.attr``.'], 'format': 'python-entrypoint-reference', '$comment': 'https://packaging.python.org/specifications/entry-points/'}}}, rule='propertyNames')
+    return data
+
+def validate_https___packaging_python_org_en_latest_specifications_declaring_project_metadata___definitions_author(data, custom_formats={}, name_prefix=None):
+    if not isinstance(data, (dict)):
+        raise JsonSchemaValueException("" + (name_prefix or "data") + " must be object", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://www.python.org/dev/peps/pep-0621/#authors-maintainers', 'type': 'object', 'properties': {'name': {'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, 'email': {'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}}}, rule='type')
+    data_is_dict = isinstance(data, dict)
+    if data_is_dict:
+        data_keys = set(data.keys())
+        if "name" in data_keys:
+            data_keys.remove("name")
+            data__name = data["name"]
+            if not isinstance(data__name, (str)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".name must be string", value=data__name, name="" + (name_prefix or "data") + ".name", definition={'type': 'string', '$$description': ['MUST be a valid email name, i.e. whatever can be put as a name, before an', 'email, in :rfc:`822`.']}, rule='type')
+        if "email" in data_keys:
+            data_keys.remove("email")
+            data__email = data["email"]
+            if not isinstance(data__email, (str)):
+                raise JsonSchemaValueException("" + (name_prefix or "data") + ".email must be string", value=data__email, name="" + (name_prefix or "data") + ".email", definition={'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}, rule='type')
+            if isinstance(data__email, str):
+                if not REGEX_PATTERNS["idn-email_re_pattern"].match(data__email):
+                    raise JsonSchemaValueException("" + (name_prefix or "data") + ".email must be idn-email", value=data__email, name="" + (name_prefix or "data") + ".email", definition={'type': 'string', 'format': 'idn-email', 'description': 'MUST be a valid email address'}, rule='format')
+    return data
\ No newline at end of file
diff --git a/setuptools/config/_validate_pyproject/formats.py b/setuptools/config/_validate_pyproject/formats.py
new file mode 100644
index 00000000..a288eb5f
--- /dev/null
+++ b/setuptools/config/_validate_pyproject/formats.py
@@ -0,0 +1,252 @@
+import logging
+import os
+import re
+import string
+import typing
+from itertools import chain as _chain
+
+_logger = logging.getLogger(__name__)
+
+# -------------------------------------------------------------------------------------
+# PEP 440
+
+VERSION_PATTERN = r"""
+    v?
+    (?:
+        (?:(?P[0-9]+)!)?                           # epoch
+        (?P[0-9]+(?:\.[0-9]+)*)                  # release segment
+        (?P
                                          # pre-release
+            [-_\.]?
+            (?P(a|b|c|rc|alpha|beta|pre|preview))
+            [-_\.]?
+            (?P[0-9]+)?
+        )?
+        (?P                                         # post release
+            (?:-(?P[0-9]+))
+            |
+            (?:
+                [-_\.]?
+                (?Ppost|rev|r)
+                [-_\.]?
+                (?P[0-9]+)?
+            )
+        )?
+        (?P                                          # dev release
+            [-_\.]?
+            (?Pdev)
+            [-_\.]?
+            (?P[0-9]+)?
+        )?
+    )
+    (?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
+"""
+
+VERSION_REGEX = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.X | re.I)
+
+
+def pep440(version: str) -> bool:
+    return VERSION_REGEX.match(version) is not None
+
+
+# -------------------------------------------------------------------------------------
+# PEP 508
+
+PEP508_IDENTIFIER_PATTERN = r"([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])"
+PEP508_IDENTIFIER_REGEX = re.compile(f"^{PEP508_IDENTIFIER_PATTERN}$", re.I)
+
+
+def pep508_identifier(name: str) -> bool:
+    return PEP508_IDENTIFIER_REGEX.match(name) is not None
+
+
+try:
+    try:
+        from packaging import requirements as _req
+    except ImportError:  # pragma: no cover
+        # let's try setuptools vendored version
+        from setuptools._vendor.packaging import requirements as _req  # type: ignore
+
+    def pep508(value: str) -> bool:
+        try:
+            _req.Requirement(value)
+            return True
+        except _req.InvalidRequirement:
+            return False
+
+except ImportError:  # pragma: no cover
+    _logger.warning(
+        "Could not find an installation of `packaging`. Requirements, dependencies and "
+        "versions might not be validated. "
+        "To enforce validation, please install `packaging`."
+    )
+
+    def pep508(value: str) -> bool:
+        return True
+
+
+def pep508_versionspec(value: str) -> bool:
+    """Expression that can be used to specify/lock versions (including ranges)"""
+    if any(c in value for c in (";", "]", "@")):
+        # In PEP 508:
+        # conditional markers, extras and URL specs are not included in the
+        # versionspec
+        return False
+    # Let's pretend we have a dependency called `requirement` with the given
+    # version spec, then we can re-use the pep508 function for validation:
+    return pep508(f"requirement{value}")
+
+
+# -------------------------------------------------------------------------------------
+# PEP 517
+
+
+def pep517_backend_reference(value: str) -> bool:
+    module, _, obj = value.partition(":")
+    identifiers = (i.strip() for i in _chain(module.split("."), obj.split(".")))
+    return all(python_identifier(i) for i in identifiers if i)
+
+
+# -------------------------------------------------------------------------------------
+# Classifiers - PEP 301
+
+
+def _download_classifiers() -> str:
+    import cgi
+    from urllib.request import urlopen
+
+    url = "https://pypi.org/pypi?:action=list_classifiers"
+    with urlopen(url) as response:
+        content_type = response.getheader("content-type", "text/plain")
+        encoding = cgi.parse_header(content_type)[1].get("charset", "utf-8")
+        return response.read().decode(encoding)
+
+
+class _TroveClassifier:
+    """The ``trove_classifiers`` package is the official way of validating classifiers,
+    however this package might not be always available.
+    As a workaround we can still download a list from PyPI.
+    We also don't want to be over strict about it, so simply skipping silently is an
+    option (classifiers will be validated anyway during the upload to PyPI).
+    """
+
+    def __init__(self):
+        self.downloaded: typing.Union[None, False, typing.Set[str]] = None
+        # None => not cached yet
+        # False => cache not available
+        self.__name__ = "trove_classifier"  # Emulate a public function
+
+    def __call__(self, value: str) -> bool:
+        if self.downloaded is False:
+            return True
+
+        if os.getenv("NO_NETWORK"):
+            self.downloaded = False
+            msg = (
+                "Install ``trove-classifiers`` to ensure proper validation. "
+                "Skipping download of classifiers list from PyPI (NO_NETWORK)."
+            )
+            _logger.debug(msg)
+            return True
+
+        if self.downloaded is None:
+            msg = (
+                "Install ``trove-classifiers`` to ensure proper validation. "
+                "Meanwhile a list of classifiers will be downloaded from PyPI."
+            )
+            _logger.debug(msg)
+            try:
+                self.downloaded = set(_download_classifiers().splitlines())
+            except Exception:
+                self.downloaded = False
+                _logger.debug("Problem with download, skipping validation")
+                return True
+
+        return value in self.downloaded or value.lower().startswith("private ::")
+
+
+try:
+    from trove_classifiers import classifiers as _trove_classifiers
+
+    def trove_classifier(value: str) -> bool:
+        return value in _trove_classifiers or value.lower().startswith("private ::")
+
+except ImportError:  # pragma: no cover
+    trove_classifier = _TroveClassifier()
+
+
+# -------------------------------------------------------------------------------------
+# Non-PEP related
+
+
+def url(value: str) -> bool:
+    from urllib.parse import urlparse
+
+    try:
+        parts = urlparse(value)
+        if not parts.scheme:
+            _logger.warning(
+                "For maximum compatibility please make sure to include a "
+                "`scheme` prefix in your URL (e.g. 'http://'). "
+                f"Given value: {value}"
+            )
+            if not (value.startswith("/") or value.startswith("\\") or "@" in value):
+                parts = urlparse(f"http://{value}")
+
+        return bool(parts.scheme and parts.netloc)
+    except Exception:
+        return False
+
+
+# https://packaging.python.org/specifications/entry-points/
+ENTRYPOINT_PATTERN = r"[^\[\s=]([^=]*[^\s=])?"
+ENTRYPOINT_REGEX = re.compile(f"^{ENTRYPOINT_PATTERN}$", re.I)
+RECOMMEDED_ENTRYPOINT_PATTERN = r"[\w.-]+"
+RECOMMEDED_ENTRYPOINT_REGEX = re.compile(f"^{RECOMMEDED_ENTRYPOINT_PATTERN}$", re.I)
+ENTRYPOINT_GROUP_PATTERN = r"\w+(\.\w+)*"
+ENTRYPOINT_GROUP_REGEX = re.compile(f"^{ENTRYPOINT_GROUP_PATTERN}$", re.I)
+
+
+def python_identifier(value: str) -> bool:
+    return value.isidentifier()
+
+
+def python_qualified_identifier(value: str) -> bool:
+    if value.startswith(".") or value.endswith("."):
+        return False
+    return all(python_identifier(m) for m in value.split("."))
+
+
+def python_module_name(value: str) -> bool:
+    return python_qualified_identifier(value)
+
+
+def python_entrypoint_group(value: str) -> bool:
+    return ENTRYPOINT_GROUP_REGEX.match(value) is not None
+
+
+def python_entrypoint_name(value: str) -> bool:
+    if not ENTRYPOINT_REGEX.match(value):
+        return False
+    if not RECOMMEDED_ENTRYPOINT_REGEX.match(value):
+        msg = f"Entry point `{value}` does not follow recommended pattern: "
+        msg += RECOMMEDED_ENTRYPOINT_PATTERN
+        _logger.warning(msg)
+    return True
+
+
+def python_entrypoint_reference(value: str) -> bool:
+    module, _, rest = value.partition(":")
+    if "[" in rest:
+        obj, _, extras_ = rest.partition("[")
+        if extras_.strip()[-1] != "]":
+            return False
+        extras = (x.strip() for x in extras_.strip(string.whitespace + "[]").split(","))
+        if not all(pep508_identifier(e) for e in extras):
+            return False
+        _logger.warning(f"`{value}` - using extras for entry points is not recommended")
+    else:
+        obj = rest
+
+    module_parts = module.split(".")
+    identifiers = _chain(module_parts, obj.split(".")) if rest else module_parts
+    return all(python_identifier(i.strip()) for i in identifiers)
diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index e20d71d2..9666ca18 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -26,18 +26,12 @@ def load_file(filepath: _Path) -> dict:
         return tomli.load(file)
 
 
-def validate(config: dict, filepath: _Path):
-    from setuptools.extern._validate_pyproject import validate as _validate
+def validate(config: dict, filepath: _Path) -> bool:
+    from . import _validate_pyproject as validator
 
     try:
-        return _validate(config)
-    except Exception as ex:
-        if ex.__class__.__name__ != "ValidationError":
-            # Workaround for the fact that `extern` can duplicate imports
-            ex_cls = ex.__class__.__name__
-            error = ValueError(f"invalid pyproject.toml config: {ex_cls} - {ex}")
-            raise error from None
-
+        return validator._validate(config)
+    except validator.ValidationError as ex:
         _logger.error(f"configuration error: {ex.summary}")  # type: ignore
         _logger.debug(ex.details)  # type: ignore
         error = ValueError(f"invalid pyproject.toml config: {ex.name}")  # type: ignore
diff --git a/setuptools/extern/__init__.py b/setuptools/extern/__init__.py
index f09b7faa..192e55f6 100644
--- a/setuptools/extern/__init__.py
+++ b/setuptools/extern/__init__.py
@@ -71,7 +71,6 @@ class VendorImporter:
 
 names = (
     'packaging', 'pyparsing', 'ordered_set', 'more_itertools', 'importlib_metadata',
-    'zipp', 'importlib_resources', 'jaraco', 'typing_extensions', 'nspektr',
-    'tomli', '_validate_pyproject',
+    'zipp', 'importlib_resources', 'jaraco', 'typing_extensions', 'nspektr', 'tomli',
 )
 VendorImporter(__name__, names, 'setuptools._vendor').install()
-- 
cgit v1.2.1


From d007b8963b1e2fc1b0e5961f54e555206c2d8e96 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 30 Mar 2022 19:42:01 +0100
Subject: Ignore coverage in automatically generated code

---
 .coveragerc | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.coveragerc b/.coveragerc
index 6a34e662..77c10700 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -2,6 +2,7 @@
 omit =
 	# leading `*/` for pytest-dev/pytest-cov#456
 	*/.tox/*
+	*/_validate_pyproject/*  # generated code, tested in _validate_pyproject
 
 [report]
 show_missing = True
-- 
cgit v1.2.1


From 40e95967bd1a4976cacad342b52c52b344790ba9 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 30 Mar 2022 19:43:16 +0100
Subject: Update version of validate-pyproject

---
 setuptools/config/_validate_pyproject/NOTICE     | 4 ++--
 setuptools/config/_validate_pyproject/formats.py | 2 +-
 tox.ini                                          | 2 +-
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/setuptools/config/_validate_pyproject/NOTICE b/setuptools/config/_validate_pyproject/NOTICE
index b426f7fd..286d2908 100644
--- a/setuptools/config/_validate_pyproject/NOTICE
+++ b/setuptools/config/_validate_pyproject/NOTICE
@@ -1,7 +1,7 @@
 The code contained in this directory was automatically generated using the
 following command:
 
-    python -m validate_pyproject.vendoring --output-dir=setuptools/config/_validate_pyproject --enable-plugins setuptools distutils --very-verbose
+    python -m validate_pyproject.pre_compile --output-dir=setuptools/config/_validate_pyproject --enable-plugins setuptools distutils --very-verbose
 
 Please avoid changing it manually.
 
@@ -31,7 +31,7 @@ by the same projects:
 - `__init__.py`
 - `fastjsonschema_validations.py`
 
-The relevant copyright notes and licenses are included bellow.
+The relevant copyright notes and licenses are included below.
 
 
 ***
diff --git a/setuptools/config/_validate_pyproject/formats.py b/setuptools/config/_validate_pyproject/formats.py
index a288eb5f..4f23d98a 100644
--- a/setuptools/config/_validate_pyproject/formats.py
+++ b/setuptools/config/_validate_pyproject/formats.py
@@ -139,7 +139,7 @@ class _TroveClassifier:
         if self.downloaded is False:
             return True
 
-        if os.getenv("NO_NETWORK"):
+        if os.getenv("NO_NETWORK") or os.getenv("VALIDATE_PYPROJECT_NO_NETWORK"):
             self.downloaded = False
             msg = (
                 "Install ``trove-classifiers`` to ensure proper validation. "
diff --git a/tox.ini b/tox.ini
index 1b105d5d..e3dd03fb 100644
--- a/tox.ini
+++ b/tox.ini
@@ -68,7 +68,7 @@ commands =
 [testenv:generate-validation-code]
 skip_install = True
 deps =
-	validate-pyproject[all]==0.6.1
+	validate-pyproject[all]==0.7
 commands =
 	python -m tools.generate_validation_code
 
-- 
cgit v1.2.1


From 5d4fbb320f4ed67ba875ba3afcfe414a80240dc0 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 30 Mar 2022 20:25:31 +0100
Subject: Fix flake8 errors

---
 .flake8                           | 1 +
 conftest.py                       | 1 +
 tools/generate_validation_code.py | 2 --
 3 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/.flake8 b/.flake8
index dd3cc206..9a5f2615 100644
--- a/.flake8
+++ b/.flake8
@@ -8,6 +8,7 @@ extend-exclude =
 	build
 	setuptools/_vendor
 	setuptools/_distutils
+	setuptools/config/_validate_pyproject/fastjsonschema_*
 	pkg_resources/_vendor
 
 extend-ignore =
diff --git a/conftest.py b/conftest.py
index 723e5b43..2271ec3e 100644
--- a/conftest.py
+++ b/conftest.py
@@ -32,6 +32,7 @@ collect_ignore = [
     'pkg_resources/tests/data',
     'setuptools/_vendor',
     'pkg_resources/_vendor',
+    'setuptools/config/_validate_pyproject',
 ]
 
 
diff --git a/tools/generate_validation_code.py b/tools/generate_validation_code.py
index 5792110d..201d1b70 100644
--- a/tools/generate_validation_code.py
+++ b/tools/generate_validation_code.py
@@ -1,7 +1,5 @@
-import string
 import subprocess
 import sys
-from tempfile import TemporaryDirectory
 
 from pathlib import Path
 
-- 
cgit v1.2.1


From 85c815d97500c8ab7efb8ed41b525b3927daf70c Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 30 Mar 2022 20:26:27 +0100
Subject: Fix unintentional mistake in config/pyproject

---
 setuptools/config/pyprojecttoml.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index 9666ca18..2481b63a 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -30,7 +30,7 @@ def validate(config: dict, filepath: _Path) -> bool:
     from . import _validate_pyproject as validator
 
     try:
-        return validator._validate(config)
+        return validator.validate(config)
     except validator.ValidationError as ex:
         _logger.error(f"configuration error: {ex.summary}")  # type: ignore
         _logger.debug(ex.details)  # type: ignore
-- 
cgit v1.2.1


From 7a66ab24766002c8dff8bb0d8a315c23a3fbc9fd Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 30 Mar 2022 21:17:46 +0100
Subject: Update validate-pyproject to v0.7.1

---
 setuptools/config/_validate_pyproject/error_reporting.py | 2 +-
 setuptools/config/_validate_pyproject/formats.py         | 7 ++++++-
 tox.ini                                                  | 2 +-
 3 files changed, 8 insertions(+), 3 deletions(-)

diff --git a/setuptools/config/_validate_pyproject/error_reporting.py b/setuptools/config/_validate_pyproject/error_reporting.py
index 3a4d4e9e..f78e4838 100644
--- a/setuptools/config/_validate_pyproject/error_reporting.py
+++ b/setuptools/config/_validate_pyproject/error_reporting.py
@@ -313,6 +313,6 @@ class _SummaryWriter:
 def _separate_terms(word: str) -> List[str]:
     """
     >>> _separate_terms("FooBar-foo")
-    "foo bar foo"
+    ['foo', 'bar', 'foo']
     """
     return [w.lower() for w in _CAMEL_CASE_SPLITTER.split(word) if w]
diff --git a/setuptools/config/_validate_pyproject/formats.py b/setuptools/config/_validate_pyproject/formats.py
index 4f23d98a..f41fce38 100644
--- a/setuptools/config/_validate_pyproject/formats.py
+++ b/setuptools/config/_validate_pyproject/formats.py
@@ -131,12 +131,17 @@ class _TroveClassifier:
 
     def __init__(self):
         self.downloaded: typing.Union[None, False, typing.Set[str]] = None
+        self._skip_download = False
         # None => not cached yet
         # False => cache not available
         self.__name__ = "trove_classifier"  # Emulate a public function
 
+    def _disable_download(self):
+        # This is a private API. Only setuptools has the consent of using it.
+        self._skip_download = True
+
     def __call__(self, value: str) -> bool:
-        if self.downloaded is False:
+        if self.downloaded is False or self._skip_download is True:
             return True
 
         if os.getenv("NO_NETWORK") or os.getenv("VALIDATE_PYPROJECT_NO_NETWORK"):
diff --git a/tox.ini b/tox.ini
index e3dd03fb..973f3763 100644
--- a/tox.ini
+++ b/tox.ini
@@ -68,7 +68,7 @@ commands =
 [testenv:generate-validation-code]
 skip_install = True
 deps =
-	validate-pyproject[all]==0.7
+	validate-pyproject[all]==0.7.1
 commands =
 	python -m tools.generate_validation_code
 
-- 
cgit v1.2.1


From 93d8b0d917e805360649ebfdae9c223494943faa Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 30 Mar 2022 21:18:23 +0100
Subject: Disable automatic download of trove classifiers by default

This helps to improve reproducibility.
See #abravalheri/validate-pyproject#31.
---
 setuptools/config/pyprojecttoml.py | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index 2481b63a..d4024956 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -29,6 +29,11 @@ def load_file(filepath: _Path) -> dict:
 def validate(config: dict, filepath: _Path) -> bool:
     from . import _validate_pyproject as validator
 
+    trove_classifier = validator.FORMAT_FUNCTIONS.get("trove-classifier")
+    if hasattr(trove_classifier, "_disable_download"):
+        # Improve reproducibility by default. See issue 31 for validate-pyproject.
+        trove_classifier._disable_download()  # type: ignore
+
     try:
         return validator.validate(config)
     except validator.ValidationError as ex:
-- 
cgit v1.2.1


From d1a6ca76d89661f61a0ad523a8d3674ad6801e33 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 30 Mar 2022 21:42:44 +0100
Subject: Add news fragment

---
 changelog.d/3229.change.rst | 1 +
 changelog.d/3229.misc.1.rst | 1 +
 changelog.d/3229.misc.2.rst | 3 +++
 3 files changed, 5 insertions(+)
 create mode 100644 changelog.d/3229.change.rst
 create mode 100644 changelog.d/3229.misc.1.rst
 create mode 100644 changelog.d/3229.misc.2.rst

diff --git a/changelog.d/3229.change.rst b/changelog.d/3229.change.rst
new file mode 100644
index 00000000..d414b753
--- /dev/null
+++ b/changelog.d/3229.change.rst
@@ -0,0 +1 @@
+Disabled automatic download of ``trove-classifiers`` to facilitate reproducibility.
diff --git a/changelog.d/3229.misc.1.rst b/changelog.d/3229.misc.1.rst
new file mode 100644
index 00000000..a905c45a
--- /dev/null
+++ b/changelog.d/3229.misc.1.rst
@@ -0,0 +1 @@
+Updated ``pyproject.toml`` validation via ``validate-pyproject`` v0.7.1.
diff --git a/changelog.d/3229.misc.2.rst b/changelog.d/3229.misc.2.rst
new file mode 100644
index 00000000..0f740033
--- /dev/null
+++ b/changelog.d/3229.misc.2.rst
@@ -0,0 +1,3 @@
+New internal tool made available for updating the code responsible for
+the validation of ``pyproject.toml``.
+This tool can be executed via ``tox -e generate-validation-code``.
-- 
cgit v1.2.1


From 207354be8a9c98a20209fba35de4e808ecd60b5f Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 30 Mar 2022 21:43:06 +0100
Subject: Update .coveragerc

---
 .coveragerc | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.coveragerc b/.coveragerc
index 77c10700..3153808d 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -2,7 +2,7 @@
 omit =
 	# leading `*/` for pytest-dev/pytest-cov#456
 	*/.tox/*
-	*/_validate_pyproject/*  # generated code, tested in _validate_pyproject
+	*/_validate_pyproject/*  # generated code, tested in `validate-pyproject`
 
 [report]
 show_missing = True
-- 
cgit v1.2.1


From c8ad9737a86f13d47b543cb4a0af859948fd2043 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 30 Mar 2022 22:14:04 +0100
Subject: Test with different package names

---
 setuptools/tests/test_dist_info.py | 23 ++++++++++++-----------
 1 file changed, 12 insertions(+), 11 deletions(-)

diff --git a/setuptools/tests/test_dist_info.py b/setuptools/tests/test_dist_info.py
index 4c39ea88..0fcff17a 100644
--- a/setuptools/tests/test_dist_info.py
+++ b/setuptools/tests/test_dist_info.py
@@ -5,7 +5,6 @@ import re
 import subprocess
 import sys
 from functools import partial
-from unittest.mock import patch
 
 import pytest
 
@@ -99,8 +98,8 @@ class TestWheelCompatibility:
     """
     SETUPCFG = DALS("""
     [metadata]
-    name = proj
-    version = 42
+    name = {name}
+    version = {version}
 
     [options]
     install_requires = foo>=12; sys_platform != "linux"
@@ -115,23 +114,25 @@ class TestWheelCompatibility:
         myproj = my_package.other_module:function
     """)
 
-    FROZEN_TIME = "20220329"
     EGG_INFO_OPTS = [
         # Related: #3077 #2872
         ("", ""),
         (".post", "[egg_info]\ntag_build = post\n"),
         (".post", "[egg_info]\ntag_build = .post\n"),
-        (f".post{FROZEN_TIME}", "[egg_info]\ntag_build = post\ntag_date = 1\n"),
+        (".post", "[egg_info]\ntag_build = post\ntag_date = 1\n"),
         (".dev", "[egg_info]\ntag_build = .dev\n"),
-        (f".dev{FROZEN_TIME}", "[egg_info]\ntag_build = .dev\ntag_date = 1\n"),
+        (".dev", "[egg_info]\ntag_build = .dev\ntag_date = 1\n"),
         ("a1", "[egg_info]\ntag_build = .a1\n"),
         ("+local", "[egg_info]\ntag_build = +local\n"),
     ]
 
-    @pytest.mark.parametrize("suffix,cfg", EGG_INFO_OPTS)
-    @patch("setuptools.command.egg_info.time.strftime", FROZEN_TIME)
-    def test_dist_info_is_the_same_as_in_wheel(self, tmp_path, suffix, cfg):
-        config = self.SETUPCFG + cfg
+    @pytest.mark.parametrize("name", "my-proj my_proj my.proj My.Proj".split())
+    @pytest.mark.parametrize("version", ["0.42.13"])
+    @pytest.mark.parametrize("suffix, cfg", EGG_INFO_OPTS)
+    def test_dist_info_is_the_same_as_in_wheel(
+        self, name, version, tmp_path, suffix, cfg
+    ):
+        config = self.SETUPCFG.format(name=name, version=version) + cfg
 
         for i in "dir_wheel", "dir_dist":
             (tmp_path / i).mkdir()
@@ -146,7 +147,7 @@ class TestWheelCompatibility:
         dist_info = next(tmp_path.glob("dir_dist/*.dist-info"))
 
         assert dist_info.name == wheel_dist_info.name
-        assert dist_info.name.startswith(f"proj-42{suffix}")
+        assert dist_info.name.startswith(f"{name.replace('-', '_')}-{version}{suffix}")
         for file in "METADATA", "entry_points.txt":
             assert read(dist_info / file) == read(wheel_dist_info / file)
 
-- 
cgit v1.2.1


From b1dca400264fb4c1471d4040fd63d5c76ed38a83 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Wed, 30 Mar 2022 22:19:23 +0100
Subject: Fix reference to issue number

---
 setuptools/tests/test_dist_info.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setuptools/tests/test_dist_info.py b/setuptools/tests/test_dist_info.py
index 0fcff17a..813ef51d 100644
--- a/setuptools/tests/test_dist_info.py
+++ b/setuptools/tests/test_dist_info.py
@@ -115,7 +115,7 @@ class TestWheelCompatibility:
     """)
 
     EGG_INFO_OPTS = [
-        # Related: #3077 #2872
+        # Related: #3088 #2872
         ("", ""),
         (".post", "[egg_info]\ntag_build = post\n"),
         (".post", "[egg_info]\ntag_build = .post\n"),
-- 
cgit v1.2.1


From f54ec144abf25bb80a2c7b894a8a5a25035c4ac9 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 31 Mar 2022 18:46:51 +0100
Subject: =?UTF-8?q?Bump=20version:=2061.2.0=20=E2=86=92=2061.3.0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .bumpversion.cfg            |  2 +-
 CHANGES.rst                 | 16 ++++++++++++++++
 changelog.d/3229.change.rst |  1 -
 changelog.d/3229.misc.1.rst |  1 -
 changelog.d/3229.misc.2.rst |  3 ---
 setup.cfg                   |  2 +-
 6 files changed, 18 insertions(+), 7 deletions(-)
 delete mode 100644 changelog.d/3229.change.rst
 delete mode 100644 changelog.d/3229.misc.1.rst
 delete mode 100644 changelog.d/3229.misc.2.rst

diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index e8b7372c..b800edd1 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 61.2.0
+current_version = 61.3.0
 commit = True
 tag = True
 
diff --git a/CHANGES.rst b/CHANGES.rst
index b4134436..97e075e5 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,3 +1,19 @@
+v61.3.0
+-------
+
+
+Changes
+^^^^^^^
+* #3229: Disabled automatic download of ``trove-classifiers`` to facilitate reproducibility.
+
+Misc
+^^^^
+* #3229: Updated ``pyproject.toml`` validation via ``validate-pyproject`` v0.7.1.
+* #3229: New internal tool made available for updating the code responsible for
+  the validation of ``pyproject.toml``.
+  This tool can be executed via ``tox -e generate-validation-code``.
+
+
 v61.2.0
 -------
 
diff --git a/changelog.d/3229.change.rst b/changelog.d/3229.change.rst
deleted file mode 100644
index d414b753..00000000
--- a/changelog.d/3229.change.rst
+++ /dev/null
@@ -1 +0,0 @@
-Disabled automatic download of ``trove-classifiers`` to facilitate reproducibility.
diff --git a/changelog.d/3229.misc.1.rst b/changelog.d/3229.misc.1.rst
deleted file mode 100644
index a905c45a..00000000
--- a/changelog.d/3229.misc.1.rst
+++ /dev/null
@@ -1 +0,0 @@
-Updated ``pyproject.toml`` validation via ``validate-pyproject`` v0.7.1.
diff --git a/changelog.d/3229.misc.2.rst b/changelog.d/3229.misc.2.rst
deleted file mode 100644
index 0f740033..00000000
--- a/changelog.d/3229.misc.2.rst
+++ /dev/null
@@ -1,3 +0,0 @@
-New internal tool made available for updating the code responsible for
-the validation of ``pyproject.toml``.
-This tool can be executed via ``tox -e generate-validation-code``.
diff --git a/setup.cfg b/setup.cfg
index d23d2fd3..1a6b27f5 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = setuptools
-version = 61.2.0
+version = 61.3.0
 author = Python Packaging Authority
 author_email = distutils-sig@python.org
 description = Easily download, build, install, upgrade, and uninstall Python packages
-- 
cgit v1.2.1


From d829e0b57a8c884229885e6c9bddf36314c70a68 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 1 Apr 2022 17:50:20 +0100
Subject: Add metatest to make sure auxiliary file is properly packaged

---
 setuptools/tests/config/test_apply_pyprojecttoml.py | 17 ++++++++++++++++-
 1 file changed, 16 insertions(+), 1 deletion(-)

diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py
index b8220963..ec9f602d 100644
--- a/setuptools/tests/config/test_apply_pyprojecttoml.py
+++ b/setuptools/tests/config/test_apply_pyprojecttoml.py
@@ -3,9 +3,11 @@ applying a similar configuration from setup.cfg
 """
 import io
 import re
+import tarfile
 from pathlib import Path
 from urllib.request import urlopen
 from unittest.mock import Mock
+from zipfile import ZipFile
 
 import pytest
 from ini2toml.api import Translator
@@ -18,7 +20,8 @@ from setuptools.config._apply_pyprojecttoml import _WouldIgnoreField, _some_attr
 from setuptools.command.egg_info import write_requirements
 
 
-EXAMPLES = (Path(__file__).parent / "setupcfg_examples.txt").read_text()
+EXAMPLES_FILE = "setupcfg_examples.txt"
+EXAMPLES = (Path(__file__).parent / EXAMPLES_FILE).read_text()
 EXAMPLE_URLS = [x for x in EXAMPLES.splitlines() if not x.startswith("#")]
 DOWNLOAD_DIR = Path(__file__).parent / "downloads"
 
@@ -276,6 +279,18 @@ class TestPresetField:
         assert "bar" in reqs
 
 
+class TestMeta:
+    def test_example_file_in_sdist(self, setuptools_sdist):
+        """Meta test to ensure tests can run from sdist"""
+        with tarfile.open(setuptools_sdist) as tar:
+            assert any(name.endswith(EXAMPLES_FILE) for name in tar.getnames())
+
+    def test_example_file_not_in_wheel(self, setuptools_wheel):
+        """Meta test to ensure auxiliary test files are not in wheel"""
+        with ZipFile(setuptools_wheel) as zipfile:
+            assert not any(name.endswith(EXAMPLES_FILE) for name in zipfile.namelist())
+
+
 # --- Auxiliary Functions ---
 
 
-- 
cgit v1.2.1


From 91cacdea697953ad3dd861a8573121e7d125906c Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 1 Apr 2022 18:39:34 +0100
Subject: Fix missing file in manifest

---
 MANIFEST.in | 1 +
 1 file changed, 1 insertion(+)

diff --git a/MANIFEST.in b/MANIFEST.in
index 3e8f09de..ac3308ed 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -15,3 +15,4 @@ include launcher.c
 include msvc-build-launcher.cmd
 include pytest.ini
 include tox.ini
+include setuptools/tests/config/setupcfg_examples.txt
-- 
cgit v1.2.1


From c0f7966dca4a58b382e0023a8531fd13111dbe34 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 1 Apr 2022 18:40:08 +0100
Subject: Split download helpers to their own file

---
 setuptools/tests/config/downloads/__init__.py      | 47 ++++++++++++++++++++++
 .../tests/config/test_apply_pyprojecttoml.py       | 35 +++-------------
 2 files changed, 52 insertions(+), 30 deletions(-)
 create mode 100644 setuptools/tests/config/downloads/__init__.py

diff --git a/setuptools/tests/config/downloads/__init__.py b/setuptools/tests/config/downloads/__init__.py
new file mode 100644
index 00000000..2a10f260
--- /dev/null
+++ b/setuptools/tests/config/downloads/__init__.py
@@ -0,0 +1,47 @@
+import io
+import re
+from pathlib import Path
+from urllib.request import urlopen
+
+__all__ = ["DOWNLOAD_DIR", "retrieve_file", "output_file", "urls_from_file"]
+
+
+NAME_REMOVE = ("http://", "https://", "github.com/", "/raw/")
+DOWNLOAD_DIR = Path(__file__).parent
+
+
+def output_file(url: str, download_dir: Path = DOWNLOAD_DIR):
+    file_name = url.strip()
+    for part in NAME_REMOVE:
+        file_name = file_name.replace(part, '').strip().strip('/:').strip()
+    return Path(download_dir, re.sub(r"[^\-_\.\w\d]+", "_", file_name))
+
+
+def retrieve_file(url: str, download_dir: Path = DOWNLOAD_DIR):
+    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)
+    return path
+
+
+def urls_from_file(list_file: Path):
+    """``list_file`` should be a text file where each line corresponds to a URL to
+    download.
+    """
+    print(f"file: {list_file}")
+    content = list_file.read_text(encoding="utf-8")
+    return [url for url in content.splitlines() if not url.startswith("#")]
+
+
+def download(url: str, dest: Path):
+    with urlopen(url) as f:
+        data = f.read()
+
+    with open(dest, "wb") as f:
+        f.write(data)
+
+    assert Path(dest).exists()
diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py
index ec9f602d..44c2e36d 100644
--- a/setuptools/tests/config/test_apply_pyprojecttoml.py
+++ b/setuptools/tests/config/test_apply_pyprojecttoml.py
@@ -19,23 +19,23 @@ from setuptools.config import expand
 from setuptools.config._apply_pyprojecttoml import _WouldIgnoreField, _some_attrgetter
 from setuptools.command.egg_info import write_requirements
 
+from .downloads import retrieve_file, urls_from_file
 
+
+HERE = Path(__file__).parent
 EXAMPLES_FILE = "setupcfg_examples.txt"
-EXAMPLES = (Path(__file__).parent / EXAMPLES_FILE).read_text()
-EXAMPLE_URLS = [x for x in EXAMPLES.splitlines() if not x.startswith("#")]
-DOWNLOAD_DIR = Path(__file__).parent / "downloads"
 
 
 def makedist(path, **attrs):
     return Distribution({"src_root": path, **attrs})
 
 
-@pytest.mark.parametrize("url", EXAMPLE_URLS)
+@pytest.mark.parametrize("url", urls_from_file(HERE / EXAMPLES_FILE))
 @pytest.mark.filterwarnings("ignore")
 @pytest.mark.uses_network
 def test_apply_pyproject_equivalent_to_setupcfg(url, monkeypatch, tmp_path):
     monkeypatch.setattr(expand, "read_attr", Mock(return_value="0.0.1"))
-    setupcfg_example = retrieve_file(url, DOWNLOAD_DIR)
+    setupcfg_example = retrieve_file(url)
     pyproject_example = Path(tmp_path, "pyproject.toml")
     toml_config = Translator().translate(setupcfg_example.read_text(), "setup.cfg")
     pyproject_example.write_text(toml_config)
@@ -294,31 +294,6 @@ class TestMeta:
 # --- Auxiliary Functions ---
 
 
-NAME_REMOVE = ("http://", "https://", "github.com/", "/raw/")
-
-
-def retrieve_file(url, download_dir):
-    file_name = url.strip()
-    for part in NAME_REMOVE:
-        file_name = file_name.replace(part, '').strip().strip('/:').strip()
-    file_name = re.sub(r"[^\-_\.\w\d]+", "_", file_name)
-    path = Path(download_dir, file_name)
-    if not path.exists():
-        download_dir.mkdir(exist_ok=True, parents=True)
-        download(url, path)
-    return path
-
-
-def download(url, dest):
-    with urlopen(url) as f:
-        data = f.read()
-
-    with open(dest, "wb") as f:
-        f.write(data)
-
-    assert Path(dest).exists()
-
-
 def core_metadata(dist) -> str:
     with io.StringIO() as buffer:
         dist.metadata.write_pkg_file(buffer)
-- 
cgit v1.2.1


From 3d752cb7bc6019f2fd85cdf8b6635728a5f8c5f5 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 1 Apr 2022 18:40:37 +0100
Subject: Add script that allow users to preload examples for offline testing

---
 setuptools/tests/config/downloads/.gitignore |  2 ++
 setuptools/tests/config/downloads/preload.py | 18 ++++++++++++++++++
 2 files changed, 20 insertions(+)
 create mode 100644 setuptools/tests/config/downloads/preload.py

diff --git a/setuptools/tests/config/downloads/.gitignore b/setuptools/tests/config/downloads/.gitignore
index d6b7ef32..df3779fc 100644
--- a/setuptools/tests/config/downloads/.gitignore
+++ b/setuptools/tests/config/downloads/.gitignore
@@ -1,2 +1,4 @@
 *
 !.gitignore
+!__init__.py
+!preload.py
diff --git a/setuptools/tests/config/downloads/preload.py b/setuptools/tests/config/downloads/preload.py
new file mode 100644
index 00000000..64b3f1c8
--- /dev/null
+++ b/setuptools/tests/config/downloads/preload.py
@@ -0,0 +1,18 @@
+"""This file can be used to preload files needed for testing.
+
+For example you can use::
+
+    cd setuptools/tests/config
+    python -m downloads.preload setupcfg_examples.txt
+
+to make sure the `setup.cfg` examples are downloaded before starting the tests.
+"""
+import sys
+from pathlib import Path
+
+from . import retrieve_file, urls_from_file
+
+
+if __name__ == "__main__":
+    urls = urls_from_file(Path(sys.argv[1]))
+    list(map(retrieve_file, urls))
-- 
cgit v1.2.1


From bcd4414e64df0893b292bb02eb8909fb49c22dec Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 1 Apr 2022 18:45:25 +0100
Subject: Add news fragment

---
 changelog.d/3233.misc.1.rst | 1 +
 changelog.d/3233.misc.2.rst | 3 +++
 2 files changed, 4 insertions(+)
 create mode 100644 changelog.d/3233.misc.1.rst
 create mode 100644 changelog.d/3233.misc.2.rst

diff --git a/changelog.d/3233.misc.1.rst b/changelog.d/3233.misc.1.rst
new file mode 100644
index 00000000..f518f357
--- /dev/null
+++ b/changelog.d/3233.misc.1.rst
@@ -0,0 +1 @@
+Included missing test file ``setupcfg_examples.txt`` in ``sdist``.
diff --git a/changelog.d/3233.misc.2.rst b/changelog.d/3233.misc.2.rst
new file mode 100644
index 00000000..dce30965
--- /dev/null
+++ b/changelog.d/3233.misc.2.rst
@@ -0,0 +1,3 @@
+Added script that allows developers to download ``setupcfg_examples.txt`` prior to
+running tests. By caching these files it should be possible to run the test suite
+offline.
-- 
cgit v1.2.1


From 6819fd261ca9571832733449e03dc095da4b79a9 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 1 Apr 2022 18:48:42 +0100
Subject: Fix flake8 problems

---
 setuptools/tests/config/downloads/__init__.py       | 1 -
 setuptools/tests/config/test_apply_pyprojecttoml.py | 1 -
 2 files changed, 2 deletions(-)

diff --git a/setuptools/tests/config/downloads/__init__.py b/setuptools/tests/config/downloads/__init__.py
index 2a10f260..9a6013f4 100644
--- a/setuptools/tests/config/downloads/__init__.py
+++ b/setuptools/tests/config/downloads/__init__.py
@@ -1,4 +1,3 @@
-import io
 import re
 from pathlib import Path
 from urllib.request import urlopen
diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py
index 44c2e36d..15f2fe21 100644
--- a/setuptools/tests/config/test_apply_pyprojecttoml.py
+++ b/setuptools/tests/config/test_apply_pyprojecttoml.py
@@ -5,7 +5,6 @@ import io
 import re
 import tarfile
 from pathlib import Path
-from urllib.request import urlopen
 from unittest.mock import Mock
 from zipfile import ZipFile
 
-- 
cgit v1.2.1


From 4b8b573b5ef238fd12f2eb29afeaeadeb8c9a57a Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 1 Apr 2022 19:01:08 +0100
Subject: Add comments with instructions for developers

---
 setuptools/tests/config/downloads/__init__.py       | 5 +++++
 setuptools/tests/config/test_apply_pyprojecttoml.py | 2 ++
 2 files changed, 7 insertions(+)

diff --git a/setuptools/tests/config/downloads/__init__.py b/setuptools/tests/config/downloads/__init__.py
index 9a6013f4..de43cffb 100644
--- a/setuptools/tests/config/downloads/__init__.py
+++ b/setuptools/tests/config/downloads/__init__.py
@@ -9,6 +9,11 @@ NAME_REMOVE = ("http://", "https://", "github.com/", "/raw/")
 DOWNLOAD_DIR = Path(__file__).parent
 
 
+# ----------------------------------------------------------------------
+# Please update ./preload.py accordingly when modifying this file
+# ----------------------------------------------------------------------
+
+
 def output_file(url: str, download_dir: Path = DOWNLOAD_DIR):
     file_name = url.strip()
     for part in NAME_REMOVE:
diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py
index 15f2fe21..045d7f40 100644
--- a/setuptools/tests/config/test_apply_pyprojecttoml.py
+++ b/setuptools/tests/config/test_apply_pyprojecttoml.py
@@ -1,5 +1,7 @@
 """Make sure that applying the configuration from pyproject.toml is equivalent to
 applying a similar configuration from setup.cfg
+
+To run these tests offline, please have a look on ``./downloads/preload.py``
 """
 import io
 import re
-- 
cgit v1.2.1


From b58bdcad0513a4a2ff2c70b1b152fd565960a34f Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 1 Apr 2022 22:37:20 +0100
Subject: =?UTF-8?q?Bump=20version:=2061.3.0=20=E2=86=92=2061.3.1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .bumpversion.cfg            |  2 +-
 CHANGES.rst                 | 12 ++++++++++++
 changelog.d/3233.misc.1.rst |  1 -
 changelog.d/3233.misc.2.rst |  3 ---
 setup.cfg                   |  2 +-
 5 files changed, 14 insertions(+), 6 deletions(-)
 delete mode 100644 changelog.d/3233.misc.1.rst
 delete mode 100644 changelog.d/3233.misc.2.rst

diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index b800edd1..87fa5350 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 61.3.0
+current_version = 61.3.1
 commit = True
 tag = True
 
diff --git a/CHANGES.rst b/CHANGES.rst
index 97e075e5..590f7766 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,3 +1,15 @@
+v61.3.1
+-------
+
+
+Misc
+^^^^
+* #3233: Included missing test file ``setupcfg_examples.txt`` in ``sdist``.
+* #3233: Added script that allows developers to download ``setupcfg_examples.txt`` prior to
+  running tests. By caching these files it should be possible to run the test suite
+  offline.
+
+
 v61.3.0
 -------
 
diff --git a/changelog.d/3233.misc.1.rst b/changelog.d/3233.misc.1.rst
deleted file mode 100644
index f518f357..00000000
--- a/changelog.d/3233.misc.1.rst
+++ /dev/null
@@ -1 +0,0 @@
-Included missing test file ``setupcfg_examples.txt`` in ``sdist``.
diff --git a/changelog.d/3233.misc.2.rst b/changelog.d/3233.misc.2.rst
deleted file mode 100644
index dce30965..00000000
--- a/changelog.d/3233.misc.2.rst
+++ /dev/null
@@ -1,3 +0,0 @@
-Added script that allows developers to download ``setupcfg_examples.txt`` prior to
-running tests. By caching these files it should be possible to run the test suite
-offline.
diff --git a/setup.cfg b/setup.cfg
index 1a6b27f5..4f84656d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = setuptools
-version = 61.3.0
+version = 61.3.1
 author = Python Packaging Authority
 author_email = distutils-sig@python.org
 description = Easily download, build, install, upgrade, and uninstall Python packages
-- 
cgit v1.2.1


From 437b890ea8151c7d82fe03d08927232eb75a03fc Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 4 Apr 2022 01:54:10 +0100
Subject: Fix error with test_easy_install

---
 setuptools/tests/test_easy_install.py | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py
index 85f528db..53a81f2d 100644
--- a/setuptools/tests/test_easy_install.py
+++ b/setuptools/tests/test_easy_install.py
@@ -1184,16 +1184,19 @@ def test_editable_user_and_build_isolation(setup_context, monkeypatch, tmp_path)
     # it will `makedirs("/home/user/.pyenv/versions/3.9.10 /home/user/.pyenv/versions/3.9.10/lib /home/user/.pyenv/versions/3.9.10/lib/python3.9 /home/user/.pyenv/versions/3.9.10/lib/python3.9/lib-dynload")``  # noqa: E501
     # 2. We are going to force `site` to update site.USER_BASE and site.USER_SITE
     #    To point inside our new home
-    monkeypatch.setenv('HOME', str(tmp_path / 'home'))
+    monkeypatch.setenv('HOME', str(tmp_path / '.home'))
     monkeypatch.setattr('site.USER_BASE', None)
     monkeypatch.setattr('site.USER_SITE', None)
     user_site = Path(site.getusersitepackages())
     user_site.mkdir(parents=True, exist_ok=True)
 
-    sys_prefix = (tmp_path / 'sys_prefix')
+    sys_prefix = (tmp_path / '.sys_prefix')
     sys_prefix.mkdir(parents=True, exist_ok=True)
     monkeypatch.setattr('sys.prefix', str(sys_prefix))
 
+    setup_script = "__import__('setuptools').setup(name='aproj', version=42)\n"
+    (tmp_path / "setup.py").write_text(setup_script, encoding="utf-8")
+
     # == Sanity check ==
     assert list(sys_prefix.glob("*")) == []
     assert list(user_site.glob("*")) == []
@@ -1208,4 +1211,4 @@ def test_editable_user_and_build_isolation(setup_context, monkeypatch, tmp_path)
     installed = {f.name for f in user_site.glob("*")}
     # sometimes easy-install.pth is created and sometimes not
     installed = installed - {"easy-install.pth"}
-    assert installed == {'UNKNOWN.egg-link'}
+    assert installed == {'aproj.egg-link'}
-- 
cgit v1.2.1


From 9cb8a419796f29068f0a34086e104a60979eaa52 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 4 Apr 2022 08:06:52 +0100
Subject: Be explicit about packages

---
 setuptools/tests/test_easy_install.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py
index 53a81f2d..dfe8b911 100644
--- a/setuptools/tests/test_easy_install.py
+++ b/setuptools/tests/test_easy_install.py
@@ -1194,7 +1194,9 @@ def test_editable_user_and_build_isolation(setup_context, monkeypatch, tmp_path)
     sys_prefix.mkdir(parents=True, exist_ok=True)
     monkeypatch.setattr('sys.prefix', str(sys_prefix))
 
-    setup_script = "__import__('setuptools').setup(name='aproj', version=42)\n"
+    setup_script = (
+        "__import__('setuptools').setup(name='aproj', version=42, packages=[])\n"
+    )
     (tmp_path / "setup.py").write_text(setup_script, encoding="utf-8")
 
     # == Sanity check ==
-- 
cgit v1.2.1


From ec62173700ea7121e8f3d0707e3af20a6b60e92b Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 4 Apr 2022 08:07:11 +0100
Subject: Attempt to fix problems on windows

---
 setuptools/tests/test_easy_install.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py
index dfe8b911..726f9fda 100644
--- a/setuptools/tests/test_easy_install.py
+++ b/setuptools/tests/test_easy_install.py
@@ -1185,6 +1185,8 @@ def test_editable_user_and_build_isolation(setup_context, monkeypatch, tmp_path)
     # 2. We are going to force `site` to update site.USER_BASE and site.USER_SITE
     #    To point inside our new home
     monkeypatch.setenv('HOME', str(tmp_path / '.home'))
+    monkeypatch.setenv('USERPROFILE', str(tmp_path / '.home'))
+    monkeypatch.setenv('APPDATA', str(tmp_path / '.home'))
     monkeypatch.setattr('site.USER_BASE', None)
     monkeypatch.setattr('site.USER_SITE', None)
     user_site = Path(site.getusersitepackages())
-- 
cgit v1.2.1


From ba7dc9eaa88131e7eab502a43c8e552de18b5319 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 4 Apr 2022 01:25:09 +0100
Subject: Add test for dynamic readme from setup.py args

---
 setuptools/tests/config/test_pyprojecttoml.py | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/setuptools/tests/config/test_pyprojecttoml.py b/setuptools/tests/config/test_pyprojecttoml.py
index 4c237014..200312b5 100644
--- a/setuptools/tests/config/test_pyprojecttoml.py
+++ b/setuptools/tests/config/test_pyprojecttoml.py
@@ -253,6 +253,20 @@ class TestClassifiers:
         with pytest.raises(OptionError, match="No configuration .* .classifiers."):
             read_configuration(pyproject)
 
+    def test_dynamic_readme_from_setup_script_args(self, tmp_path):
+        config = """
+        [project]
+        name = "myproj"
+        version = '42'
+        dynamic = ["readme"]
+        """
+        pyproject = tmp_path / "pyproject.toml"
+        pyproject.write_text(cleandoc(config))
+        dist = Distribution(attrs={"long_description": "42"})
+        # No error should occur because of missing `readme`
+        dist = apply_configuration(dist, pyproject)
+        assert dist.metadata.long_description == "42"
+
     def test_dynamic_without_file(self, tmp_path):
         config = """
         [project]
-- 
cgit v1.2.1


From 0a836a3c2c8cb2f3a7b418f2c476b4b499cabdd1 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 4 Apr 2022 01:25:20 +0100
Subject: Fix dynamic readme

---
 setuptools/config/pyprojecttoml.py | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py
index d4024956..be812142 100644
--- a/setuptools/config/pyprojecttoml.py
+++ b/setuptools/config/pyprojecttoml.py
@@ -316,12 +316,17 @@ class _ConfigExpander:
         return None
 
     def _obtain_readme(self, dist: "Distribution") -> Optional[Dict[str, str]]:
-        if "readme" in self.dynamic:
-            dynamic_cfg = self.dynamic_cfg
+        if "readme" not in self.dynamic:
+            return None
+
+        dynamic_cfg = self.dynamic_cfg
+        if "readme" in dynamic_cfg:
             return {
                 "text": self._obtain(dist, "readme", {}),
                 "content-type": dynamic_cfg["readme"].get("content-type", "text/x-rst"),
             }
+
+        self._ensure_previously_set(dist, "readme")
         return None
 
     def _obtain_entry_points(
-- 
cgit v1.2.1


From 5c33096f68828bc2afe8490174fa6ccf8a70931d Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 4 Apr 2022 01:30:43 +0100
Subject: Add news fragment

---
 changelog.d/3244.misc.rst | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 changelog.d/3244.misc.rst

diff --git a/changelog.d/3244.misc.rst b/changelog.d/3244.misc.rst
new file mode 100644
index 00000000..b3fa121a
--- /dev/null
+++ b/changelog.d/3244.misc.rst
@@ -0,0 +1,2 @@
+Fixed problem preventing ``readme`` specified as dynamic in ``pyproject.toml``
+from being dynamically specified in ``setup.py``.
-- 
cgit v1.2.1


From 760255d17c68937c6ff7e98169d9dc61f29cadf9 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 4 Apr 2022 09:48:38 +0100
Subject: Rename news fragment file

---
 changelog.d/3244.misc.rst | 2 --
 changelog.d/3247.misc.rst | 2 ++
 2 files changed, 2 insertions(+), 2 deletions(-)
 delete mode 100644 changelog.d/3244.misc.rst
 create mode 100644 changelog.d/3247.misc.rst

diff --git a/changelog.d/3244.misc.rst b/changelog.d/3244.misc.rst
deleted file mode 100644
index b3fa121a..00000000
--- a/changelog.d/3244.misc.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Fixed problem preventing ``readme`` specified as dynamic in ``pyproject.toml``
-from being dynamically specified in ``setup.py``.
diff --git a/changelog.d/3247.misc.rst b/changelog.d/3247.misc.rst
new file mode 100644
index 00000000..b3fa121a
--- /dev/null
+++ b/changelog.d/3247.misc.rst
@@ -0,0 +1,2 @@
+Fixed problem preventing ``readme`` specified as dynamic in ``pyproject.toml``
+from being dynamically specified in ``setup.py``.
-- 
cgit v1.2.1


From b686a319a2938019039c73aecba714970f9d6f74 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 4 Apr 2022 11:27:45 +0100
Subject: =?UTF-8?q?Bump=20version:=2061.3.1=20=E2=86=92=2062.0.0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .bumpversion.cfg              |  2 +-
 CHANGES.rst                   | 20 ++++++++++++++++++++
 changelog.d/3088.misc.rst     |  1 -
 changelog.d/3151.breaking.rst |  1 -
 changelog.d/3153.change.rst   |  1 -
 changelog.d/3167.change.rst   |  1 -
 changelog.d/3247.misc.rst     |  2 --
 setup.cfg                     |  2 +-
 8 files changed, 22 insertions(+), 8 deletions(-)
 delete mode 100644 changelog.d/3088.misc.rst
 delete mode 100644 changelog.d/3151.breaking.rst
 delete mode 100644 changelog.d/3153.change.rst
 delete mode 100644 changelog.d/3167.change.rst
 delete mode 100644 changelog.d/3247.misc.rst

diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 87fa5350..5c2f2e45 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 61.3.1
+current_version = 62.0.0
 commit = True
 tag = True
 
diff --git a/CHANGES.rst b/CHANGES.rst
index 590f7766..126457be 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,3 +1,23 @@
+v62.0.0
+-------
+
+
+Breaking Changes
+^^^^^^^^^^^^^^^^
+* #3151: Made ``setup.py develop --user`` install to the user site packages directory even if it is disabled in the current interpreter.
+
+Changes
+^^^^^^^
+* #3153: When resolving requirements use both canonical and normalized names -- by :user:`ldaniluk`
+* #3167: Honor unix file mode in ZipFile when installing wheel via ``install_as_egg`` -- by :user:`delijati`
+
+Misc
+^^^^
+* #3088: Fixed duplicated tag with the ``dist-info`` command.
+* #3247: Fixed problem preventing ``readme`` specified as dynamic in ``pyproject.toml``
+  from being dynamically specified in ``setup.py``.
+
+
 v61.3.1
 -------
 
diff --git a/changelog.d/3088.misc.rst b/changelog.d/3088.misc.rst
deleted file mode 100644
index c507a824..00000000
--- a/changelog.d/3088.misc.rst
+++ /dev/null
@@ -1 +0,0 @@
-Fixed duplicated tag with the ``dist-info`` command.
diff --git a/changelog.d/3151.breaking.rst b/changelog.d/3151.breaking.rst
deleted file mode 100644
index 73f7c1a8..00000000
--- a/changelog.d/3151.breaking.rst
+++ /dev/null
@@ -1 +0,0 @@
-Made ``setup.py develop --user`` install to the user site packages directory even if it is disabled in the current interpreter.
diff --git a/changelog.d/3153.change.rst b/changelog.d/3153.change.rst
deleted file mode 100644
index d7e0755b..00000000
--- a/changelog.d/3153.change.rst
+++ /dev/null
@@ -1 +0,0 @@
-When resolving requirements use both canonical and normalized names -- by :user:`ldaniluk`
diff --git a/changelog.d/3167.change.rst b/changelog.d/3167.change.rst
deleted file mode 100644
index 5f44bec4..00000000
--- a/changelog.d/3167.change.rst
+++ /dev/null
@@ -1 +0,0 @@
-Honor unix file mode in ZipFile when installing wheel via ``install_as_egg`` -- by :user:`delijati`
diff --git a/changelog.d/3247.misc.rst b/changelog.d/3247.misc.rst
deleted file mode 100644
index b3fa121a..00000000
--- a/changelog.d/3247.misc.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Fixed problem preventing ``readme`` specified as dynamic in ``pyproject.toml``
-from being dynamically specified in ``setup.py``.
diff --git a/setup.cfg b/setup.cfg
index 4f84656d..78c088a1 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = setuptools
-version = 61.3.1
+version = 62.0.0
 author = Python Packaging Authority
 author_email = distutils-sig@python.org
 description = Easily download, build, install, upgrade, and uninstall Python packages
-- 
cgit v1.2.1


From 00b4fb1aef3feb77f9db0cd05bfeb02d1fa1cf75 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Mon, 4 Apr 2022 20:42:07 +0100
Subject: Simplify auto-discovered package_dir

If the directory follows a src-layout-ish, try harder to
make `package_dir` in the form `{"": "src"}`.

This might be later important for PEP 660 (e.g. when composing pth
files or symlinking the toplevel packages).
---
 setuptools/config/expand.py            | 27 +++++++++++++++++++++++++--
 setuptools/tests/config/test_expand.py | 32 +++++++++++++++++++++++++++++++-
 2 files changed, 56 insertions(+), 3 deletions(-)

diff --git a/setuptools/config/expand.py b/setuptools/config/expand.py
index ff9b2c9b..156d7473 100644
--- a/setuptools/config/expand.py
+++ b/setuptools/config/expand.py
@@ -312,8 +312,12 @@ def find_packages(
     where = kwargs.pop('where', ['.'])
     packages: List[str] = []
     fill_package_dir = {} if fill_package_dir is None else fill_package_dir
+    find = list(unique_everseen(always_iterable(where)))
 
-    for path in unique_everseen(always_iterable(where)):
+    if len(find) == 1 and all(not _same_path(find[0], x) for x in (".", root_dir)):
+        fill_package_dir.setdefault("", find[0])
+
+    for path in find:
         package_path = _nest_path(root_dir, path)
         pkgs = PackageFinder.find(package_path, **kwargs)
         packages.extend(pkgs)
@@ -326,8 +330,27 @@ def find_packages(
     return packages
 
 
+def _same_path(p1: _Path, p2: _Path) -> bool:
+    """Differs from os.path.samefile because it does not require paths to exist.
+    Purely string based (no comparison between i-nodes).
+    >>> _same_path("a/b", "./a/b")
+    True
+    >>> _same_path("a/b", "a/./b")
+    True
+    >>> _same_path("a/b", "././a/b")
+    True
+    >>> _same_path("a/b", "./a/b/c/..")
+    True
+    >>> _same_path("a/b", "../a/b/c")
+    False
+    >>> _same_path("a", "a/b")
+    False
+    """
+    return os.path.normpath(p1) == os.path.normpath(p2)
+
+
 def _nest_path(parent: _Path, path: _Path) -> str:
-    path = parent if path == "." else os.path.join(parent, path)
+    path = parent if path in {".", ""} else os.path.join(parent, path)
     return os.path.normpath(path)
 
 
diff --git a/setuptools/tests/config/test_expand.py b/setuptools/tests/config/test_expand.py
index 3a59edbb..15053c8f 100644
--- a/setuptools/tests/config/test_expand.py
+++ b/setuptools/tests/config/test_expand.py
@@ -130,7 +130,7 @@ def test_resolve_class(tmp_path, package_dir, file, module, return_value):
         ({}, {"pkg", "other", "dir1", "dir1.dir2"}),  # default value for `namespaces`
     ]
 )
-def test_find_packages(tmp_path, monkeypatch, args, pkgs):
+def test_find_packages(tmp_path, args, pkgs):
     files = {
         "pkg/__init__.py",
         "other/__init__.py",
@@ -153,3 +153,33 @@ def test_find_packages(tmp_path, monkeypatch, args, pkgs):
     ]
 
     assert set(expand.find_packages(where=where, **args)) == pkgs
+
+
+@pytest.mark.parametrize(
+    "files, where, expected_package_dir",
+    [
+        (["pkg1/__init__.py", "pkg1/other.py"], ["."], {}),
+        (["pkg1/__init__.py", "pkg2/__init__.py"], ["."], {}),
+        (["src/pkg1/__init__.py", "src/pkg1/other.py"], ["src"], {"": "src"}),
+        (["src/pkg1/__init__.py", "src/pkg2/__init__.py"], ["src"], {"": "src"}),
+        (
+            ["src1/pkg1/__init__.py", "src2/pkg2/__init__.py"],
+            ["src1", "src2"],
+            {"pkg1": "src1/pkg1", "pkg2": "src2/pkg2"},
+        ),
+        (
+            ["src/pkg1/__init__.py", "pkg2/__init__.py"],
+            ["src", "."],
+            {"pkg1": "src/pkg1"},
+        ),
+    ],
+)
+def test_fill_package_dir(tmp_path, files, where, expected_package_dir):
+    write_files({k: "" for k in files}, tmp_path)
+    pkg_dir = {}
+    kwargs = {"root_dir": tmp_path, "fill_package_dir": pkg_dir, "namespaces": False}
+    pkgs = expand.find_packages(where=where, **kwargs)
+    assert set(pkg_dir.items()) == set(expected_package_dir.items())
+    for pkg in pkgs:
+        pkg_path = find_package_path(pkg, pkg_dir, tmp_path)
+        assert os.path.exists(pkg_path)
-- 
cgit v1.2.1


From f565df599f5d513bfde355f111bd84e426325f9b Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 5 Apr 2022 10:49:29 +0100
Subject: Add news fragment

---
 changelog.d/3249.misc.rst | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/3249.misc.rst

diff --git a/changelog.d/3249.misc.rst b/changelog.d/3249.misc.rst
new file mode 100644
index 00000000..3ef85049
--- /dev/null
+++ b/changelog.d/3249.misc.rst
@@ -0,0 +1 @@
+Simplified ``package_dir`` obtained via auto-discovery.
-- 
cgit v1.2.1


From dc6b21b040e31e036c613e8ea88e33a3aee3401d Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Tue, 5 Apr 2022 10:51:16 +0100
Subject: Rename variable

---
 setuptools/config/expand.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/setuptools/config/expand.py b/setuptools/config/expand.py
index 156d7473..da55d4ee 100644
--- a/setuptools/config/expand.py
+++ b/setuptools/config/expand.py
@@ -312,12 +312,12 @@ def find_packages(
     where = kwargs.pop('where', ['.'])
     packages: List[str] = []
     fill_package_dir = {} if fill_package_dir is None else fill_package_dir
-    find = list(unique_everseen(always_iterable(where)))
+    search = list(unique_everseen(always_iterable(where)))
 
-    if len(find) == 1 and all(not _same_path(find[0], x) for x in (".", root_dir)):
-        fill_package_dir.setdefault("", find[0])
+    if len(search) == 1 and all(not _same_path(search[0], x) for x in (".", root_dir)):
+        fill_package_dir.setdefault("", search[0])
 
-    for path in find:
+    for path in search:
         package_path = _nest_path(root_dir, path)
         pkgs = PackageFinder.find(package_path, **kwargs)
         packages.extend(pkgs)
-- 
cgit v1.2.1


From f4af5afbbf1c7d26139e5348d2202d0ab923c9ac Mon Sep 17 00:00:00 2001
From: Chuck McCallum 
Date: Tue, 5 Apr 2022 13:58:19 -0400
Subject: Small wording tweaks for readability

---
 docs/userguide/dependency_management.rst | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/docs/userguide/dependency_management.rst b/docs/userguide/dependency_management.rst
index 279f794d..d15b45cb 100644
--- a/docs/userguide/dependency_management.rst
+++ b/docs/userguide/dependency_management.rst
@@ -43,7 +43,7 @@ other two types of dependency keyword, this one is specified in your
 Declaring required dependency
 =============================
 This is where a package declares its core dependencies, without which it won't
-be able to run. ``setuptools`` support automatically download and install
+be able to run. ``setuptools`` supports automatically downloading and installing
 these dependencies when the package is installed. Although there is more
 finesse to it, let's start with a simple example.
 
@@ -90,7 +90,7 @@ that verify the availability of the specified dependencies at runtime.
 
 Platform specific dependencies
 ------------------------------
-Setuptools offer the capability to evaluate certain conditions before blindly
+Setuptools offers the capability to evaluate certain conditions before blindly
 installing everything listed in ``install_requires``. This is great for platform
 specific dependencies. For example, the ``enum`` package was added in Python
 3.4, therefore, package that depends on it can elect to install it only when
@@ -250,9 +250,9 @@ distributions, if the package's dependencies aren't already installed:
 Optional dependencies
 =====================
 Setuptools allows you to declare dependencies that only get installed under
-specific circumstances. These dependencies are specified with ``extras_require``
+specific circumstances. These dependencies are specified with the ``extras_require``
 keyword and are only installed if another package depends on it (either
-directly or indirectly) This makes it convenient to declare dependencies for
+directly or indirectly). This makes it convenient to declare dependencies for
 ancillary functions such as "tests" and "docs".
 
 .. note::
-- 
cgit v1.2.1


From 1c23f5e1e4b18b50081cbabb2dea22bf345f5894 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= 
Date: Sat, 9 Apr 2022 13:13:23 +0200
Subject: Use cache_tag in default build_platlib dir

Use `sys.implementation.cache_tag` instead of Python version to create
the default directory for `build_platlib`.  This guarantees that
the directories used by CPython and PyPy are distinct.  Prior to
the change, both CPython and PyPy would use e.g. `lib.linux-x86_64-3.9`.
With the change, they are going to use `lib.linux-x86_64-cpython39`
and `lib.linux-x86_64-pypy39` respectively.
---
 distutils/command/build.py    | 3 ++-
 distutils/tests/test_build.py | 6 +++---
 2 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/distutils/command/build.py b/distutils/command/build.py
index 4355a632..9606b81a 100644
--- a/distutils/command/build.py
+++ b/distutils/command/build.py
@@ -81,7 +81,8 @@ class build(Command):
                             "--plat-name only supported on Windows (try "
                             "using './configure --help' on your platform)")
 
-        plat_specifier = ".%s-%d.%d" % (self.plat_name, *sys.version_info[:2])
+        plat_specifier = ".%s-%s" % (self.plat_name,
+                                     sys.implementation.cache_tag)
 
         # Make it so Python 2.x and Python 2.x with --with-pydebug don't
         # share the same build directories. Doing so confuses the build
diff --git a/distutils/tests/test_build.py b/distutils/tests/test_build.py
index 83a9e4f4..93724419 100644
--- a/distutils/tests/test_build.py
+++ b/distutils/tests/test_build.py
@@ -24,10 +24,10 @@ class BuildTestCase(support.TempdirManager,
         wanted = os.path.join(cmd.build_base, 'lib')
         self.assertEqual(cmd.build_purelib, wanted)
 
-        # build_platlib is 'build/lib.platform-x.x[-pydebug]'
+        # build_platlib is 'build/lib.platform-cache_tag[-pydebug]'
         # examples:
-        #   build/lib.macosx-10.3-i386-2.7
-        plat_spec = '.%s-%d.%d' % (cmd.plat_name, *sys.version_info[:2])
+        #   build/lib.macosx-10.3-i386-cpython39
+        plat_spec = '.%s-%s' % (cmd.plat_name, sys.implementation.cache_tag)
         if hasattr(sys, 'gettotalrefcount'):
             self.assertTrue(cmd.build_platlib.endswith('-pydebug'))
             plat_spec += '-pydebug'
-- 
cgit v1.2.1


From c76269b48153a2abd5fe1a88d0305172cb30fb3c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= 
Date: Sat, 9 Apr 2022 13:20:08 +0200
Subject: Skip test_get_makefile_filename on non-CPython

The Makefile is specific to CPython and does not exist e.g. on PyPy
installs.  Skip the test appropriately.
---
 distutils/tests/test_sysconfig.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/distutils/tests/test_sysconfig.py b/distutils/tests/test_sysconfig.py
index 66fb743e..e671f9e0 100644
--- a/distutils/tests/test_sysconfig.py
+++ b/distutils/tests/test_sysconfig.py
@@ -40,6 +40,8 @@ class SysconfigTestCase(support.EnvironGuard, unittest.TestCase):
 
     @unittest.skipIf(sys.platform == 'win32',
                      'Makefile only exists on Unix like systems')
+    @unittest.skipIf(sys.implementation.name != 'cpython',
+                     'Makefile only exists in CPython')
     def test_get_makefile_filename(self):
         makefile = sysconfig.get_makefile_filename()
         self.assertTrue(os.path.isfile(makefile), makefile)
-- 
cgit v1.2.1


From 12fd59da027bc546ceab0d3ae413320b0e187905 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= 
Date: Sat, 9 Apr 2022 13:28:52 +0200
Subject: Update test_home_installation_scheme for pypy install paths

---
 distutils/tests/test_install.py | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/distutils/tests/test_install.py b/distutils/tests/test_install.py
index 5dbc06b0..8cf24545 100644
--- a/distutils/tests/test_install.py
+++ b/distutils/tests/test_install.py
@@ -56,14 +56,17 @@ class InstallTestCase(support.TempdirManager,
             expected = os.path.normpath(expected)
             self.assertEqual(got, expected)
 
-        libdir = os.path.join(destination, "lib", "python")
+        impl_name = sys.implementation.name
+        if impl_name == "cpython":
+            impl_name = "python"
+        libdir = os.path.join(destination, "lib", impl_name)
         check_path(cmd.install_lib, libdir)
         _platlibdir = getattr(sys, "platlibdir", "lib")
-        platlibdir = os.path.join(destination, _platlibdir, "python")
+        platlibdir = os.path.join(destination, _platlibdir, impl_name)
         check_path(cmd.install_platlib, platlibdir)
         check_path(cmd.install_purelib, libdir)
         check_path(cmd.install_headers,
-                   os.path.join(destination, "include", "python", "foopkg"))
+                   os.path.join(destination, "include", impl_name, "foopkg"))
         check_path(cmd.install_scripts, os.path.join(destination, "bin"))
         check_path(cmd.install_data, destination)
 
-- 
cgit v1.2.1


From 9d86bf2e0583e6265d2952dfb8cb8a2c8281068b Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sun, 10 Apr 2022 12:40:44 -0400
Subject: Refactor as simple replace. If a full string substitution proves to
 be necessary, let's create a mapping.

---
 distutils/tests/test_install.py | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/distutils/tests/test_install.py b/distutils/tests/test_install.py
index 8cf24545..3aef9e43 100644
--- a/distutils/tests/test_install.py
+++ b/distutils/tests/test_install.py
@@ -56,9 +56,7 @@ class InstallTestCase(support.TempdirManager,
             expected = os.path.normpath(expected)
             self.assertEqual(got, expected)
 
-        impl_name = sys.implementation.name
-        if impl_name == "cpython":
-            impl_name = "python"
+        impl_name = sys.implementation.name.replace("cpython", "python")
         libdir = os.path.join(destination, "lib", impl_name)
         check_path(cmd.install_lib, libdir)
         _platlibdir = getattr(sys, "platlibdir", "lib")
-- 
cgit v1.2.1


From 08c89e38dc191ca0dd9e05f62fd132868af87640 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sun, 10 Apr 2022 12:45:58 -0400
Subject: Update changelog.

---
 changelog.d/3258.change.rst | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/3258.change.rst

diff --git a/changelog.d/3258.change.rst b/changelog.d/3258.change.rst
new file mode 100644
index 00000000..3fcf0935
--- /dev/null
+++ b/changelog.d/3258.change.rst
@@ -0,0 +1 @@
+Merge pypa/distutils@5229dad46b.
-- 
cgit v1.2.1


From 5bd3e98e2641fd526fc9f1f61e5f6700dfa895ae Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Sun, 10 Apr 2022 15:46:46 -0400
Subject: =?UTF-8?q?Bump=20version:=2062.0.0=20=E2=86=92=2062.1.0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .bumpversion.cfg            |  2 +-
 CHANGES.rst                 | 13 +++++++++++++
 changelog.d/3249.misc.rst   |  1 -
 changelog.d/3258.change.rst |  1 -
 setup.cfg                   |  2 +-
 5 files changed, 15 insertions(+), 4 deletions(-)
 delete mode 100644 changelog.d/3249.misc.rst
 delete mode 100644 changelog.d/3258.change.rst

diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index 5c2f2e45..1125d38d 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 62.0.0
+current_version = 62.1.0
 commit = True
 tag = True
 
diff --git a/CHANGES.rst b/CHANGES.rst
index 126457be..5061ecb9 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,3 +1,16 @@
+v62.1.0
+-------
+
+
+Changes
+^^^^^^^
+* #3258: Merge pypa/distutils@5229dad46b.
+
+Misc
+^^^^
+* #3249: Simplified ``package_dir`` obtained via auto-discovery.
+
+
 v62.0.0
 -------
 
diff --git a/changelog.d/3249.misc.rst b/changelog.d/3249.misc.rst
deleted file mode 100644
index 3ef85049..00000000
--- a/changelog.d/3249.misc.rst
+++ /dev/null
@@ -1 +0,0 @@
-Simplified ``package_dir`` obtained via auto-discovery.
diff --git a/changelog.d/3258.change.rst b/changelog.d/3258.change.rst
deleted file mode 100644
index 3fcf0935..00000000
--- a/changelog.d/3258.change.rst
+++ /dev/null
@@ -1 +0,0 @@
-Merge pypa/distutils@5229dad46b.
diff --git a/setup.cfg b/setup.cfg
index 78c088a1..4b386243 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = setuptools
-version = 62.0.0
+version = 62.1.0
 author = Python Packaging Authority
 author_email = distutils-sig@python.org
 description = Easily download, build, install, upgrade, and uninstall Python packages
-- 
cgit v1.2.1


From a4a7527a61cb81d761c4b117d799f483a833faba Mon Sep 17 00:00:00 2001
From: Vladimir Berlev <1783633+tegoo@users.noreply.github.com>
Date: Thu, 14 Apr 2022 16:54:55 +0200
Subject: Fix typo in docs

---
 docs/userguide/quickstart.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst
index c72db26b..2f778521 100644
--- a/docs/userguide/quickstart.rst
+++ b/docs/userguide/quickstart.rst
@@ -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
-- 
cgit v1.2.1


From 2fab368305769553b58e94ec4f6ae6d4e93f039a Mon Sep 17 00:00:00 2001
From: Markus Bong <2Fake1987@gmail.com>
Date: Thu, 21 Apr 2022 15:08:02 +0200
Subject: fix typo

---
 docs/build_meta.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/build_meta.rst b/docs/build_meta.rst
index cb372721..57aea986 100644
--- a/docs/build_meta.rst
+++ b/docs/build_meta.rst
@@ -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
-- 
cgit v1.2.1


From a73fb963896019bbe6216c877d3ec03e797dd056 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 21 Apr 2022 18:03:07 +0100
Subject: Cache downloaded files used during tests for setuptools.config

Recently Github Actions started to fail with `HTTP Error 429: Too Many Requests`.
A solution for this problem is to add some caching.
---
 .github/workflows/main.yml | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index c680fb36..e2197aad 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -39,6 +39,20 @@ 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',
+              '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
-- 
cgit v1.2.1


From c055902693210dad605c6cdbea4d2adc0e08730e Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 21 Apr 2022 18:11:43 +0100
Subject: Fix YAML error

---
 .github/workflows/main.yml | 9 +++------
 1 file changed, 3 insertions(+), 6 deletions(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index e2197aad..4275bbde 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -43,12 +43,9 @@ jobs:
         id: cache
         with:
           path: setuptools/tests/config/downloads/*.cfg
-          key: ${{
-            hashFiles(
-              'setuptools/tests/config/setupcfg_examples.txt',
-              'setuptools/tests/config/downloads/*.py'
-            )
-          }}
+          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
-- 
cgit v1.2.1


From 1286d38db9a7846ebaf9d0a8b87ea1a51a5cbf78 Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Thu, 21 Apr 2022 18:23:19 +0100
Subject: Try to rescue the download backing off a few seconds

---
 setuptools/tests/config/downloads/__init__.py | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)

diff --git a/setuptools/tests/config/downloads/__init__.py b/setuptools/tests/config/downloads/__init__.py
index de43cffb..9fb9b14b 100644
--- a/setuptools/tests/config/downloads/__init__.py
+++ b/setuptools/tests/config/downloads/__init__.py
@@ -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
 
 
-- 
cgit v1.2.1


From a18d7fdc254d67a90dac3044f6947998b062adb8 Mon Sep 17 00:00:00 2001
From: Binjian 
Date: Fri, 22 Apr 2022 17:21:15 +0800
Subject: Update package_discovery.rst

double "can be"
---
 docs/userguide/package_discovery.rst | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/userguide/package_discovery.rst b/docs/userguide/package_discovery.rst
index ee8e9836..38119bc6 100644
--- a/docs/userguide/package_discovery.rst
+++ b/docs/userguide/package_discovery.rst
@@ -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
-- 
cgit v1.2.1


From f9ca838aa9659f5f4358f5df7b71350afe66d8ce Mon Sep 17 00:00:00 2001
From: Anderson Bravalheri 
Date: Fri, 22 Apr 2022 12:07:58 +0100
Subject: Add news fragment

---
 changelog.d/3282.misc.rst | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/3282.misc.rst

diff --git a/changelog.d/3282.misc.rst b/changelog.d/3282.misc.rst
new file mode 100644
index 00000000..e7fbec76
--- /dev/null
+++ b/changelog.d/3282.misc.rst
@@ -0,0 +1 @@
+Added CI cache for ``setup.cfg`` examples used when testing ``setuptools.config``.
-- 
cgit v1.2.1


From 1cfa27c05bd6753c7a7c5fa4cb498c85ce088392 Mon Sep 17 00:00:00 2001
From: wim glenn 
Date: Fri, 29 Apr 2022 22:34:27 -0500
Subject: do not backfill Project-URL: homepage into Home-page: field (causes
 duplicates on PyPI).  prevent "UNKNOWN" vals from appearing in summary,
 license, platform.  prevent an extra newline getting added in long
 description

---
 setuptools/config/_apply_pyprojecttoml.py          | 16 +--------------
 setuptools/dist.py                                 | 22 +++++++++++++++------
 .../tests/config/test_apply_pyprojecttoml.py       | 23 ++++++++++++++--------
 3 files changed, 32 insertions(+), 29 deletions(-)

diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py
index fce5c40e..a580b63f 100644
--- a/setuptools/config/_apply_pyprojecttoml.py
+++ b/setuptools/config/_apply_pyprojecttoml.py
@@ -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):
diff --git a/setuptools/dist.py b/setuptools/dist.py
index 215c88e3..5507167d 100644
--- a/setuptools/dist.py
+++ b/setuptools/dist.py
@@ -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
diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py
index 045d7f40..4f541697 100644
--- a/setuptools/tests/config/test_apply_pyprojecttoml.py
+++ b/setuptools/tests/config/test_apply_pyprojecttoml.py
@@ -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)
-- 
cgit v1.2.1


From e009a87b5578cb16099b697ba8395c8f6bdd70f3 Mon Sep 17 00:00:00 2001
From: "Jason R. Coombs" 
Date: Mon, 9 May 2022 09:33:37 -0400
Subject: Update changelog. Ref #3299.

---
 changelog.d/3299.change.rst | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 changelog.d/3299.change.rst

diff --git a/changelog.d/3299.change.rst b/changelog.d/3299.change.rst
new file mode 100644
index 00000000..c84d7f0f
--- /dev/null
+++ b/changelog.d/3299.change.rst
@@ -0,0 +1 @@
+Optional metadata fields are now truly optional.
-- 
cgit v1.2.1