From ef9b8e5c5eec50853c4cd2ceeccbf5f963172560 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 3 Nov 2024 16:42:22 +0100 Subject: [PATCH 01/36] Store license-files in licenses subfolder --- newsfragments/4728.feature.rst | 1 + setuptools/command/bdist_wheel.py | 6 ++-- setuptools/dist.py | 7 +++-- setuptools/tests/test_bdist_wheel.py | 46 ++++++++++++++++++++++++++-- setuptools/tests/test_build_meta.py | 7 +++-- setuptools/tests/test_egg_info.py | 20 ++++++++++++ 6 files changed, 78 insertions(+), 9 deletions(-) create mode 100644 newsfragments/4728.feature.rst diff --git a/newsfragments/4728.feature.rst b/newsfragments/4728.feature.rst new file mode 100644 index 0000000000..61906656c0 --- /dev/null +++ b/newsfragments/4728.feature.rst @@ -0,0 +1 @@ +Store ``License-File``s in ``.dist-info/licenses`` subfolder and added support for recursive globs for ``license_files`` (`PEP 639 `_). -- by :user:`cdce8p` diff --git a/setuptools/command/bdist_wheel.py b/setuptools/command/bdist_wheel.py index bcd176f98e..73a028269e 100644 --- a/setuptools/command/bdist_wheel.py +++ b/setuptools/command/bdist_wheel.py @@ -590,9 +590,11 @@ def adios(p: str) -> None: metadata_path = os.path.join(distinfo_path, "METADATA") shutil.copy(pkginfo_path, metadata_path) + licenses_folder_path = os.path.join(distinfo_path, "licenses") for license_path in self.license_paths: - filename = os.path.basename(license_path) - shutil.copy(license_path, os.path.join(distinfo_path, filename)) + dist_info_license_path = os.path.join(licenses_folder_path, license_path) + os.makedirs(os.path.dirname(dist_info_license_path), exist_ok=True) + shutil.copy(license_path, dist_info_license_path) adios(egginfo_path) diff --git a/setuptools/dist.py b/setuptools/dist.py index 0249651267..962da7c34b 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -418,7 +418,10 @@ def _finalize_license_files(self) -> None: patterns = ['LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*'] self.metadata.license_files = list( - unique_everseen(self._expand_patterns(patterns)) + map( + lambda path: path.replace("\\", "/"), + unique_everseen(self._expand_patterns(patterns)), + ) ) @staticmethod @@ -432,7 +435,7 @@ def _expand_patterns(patterns): return ( path for pattern in patterns - for path in sorted(iglob(pattern)) + for path in sorted(iglob(pattern, recursive=True)) if not path.endswith('~') and os.path.isfile(path) ) diff --git a/setuptools/tests/test_bdist_wheel.py b/setuptools/tests/test_bdist_wheel.py index d51dfbeb6d..0f2e6ce136 100644 --- a/setuptools/tests/test_bdist_wheel.py +++ b/setuptools/tests/test_bdist_wheel.py @@ -172,6 +172,20 @@ ), "README.rst": "UTF-8 描述 説明", }, + "licenses-dist": { + "setup.cfg": cleandoc( + """ + [metadata] + name = licenses-dist + version = 1.0 + license_files = **/LICENSE + """ + ), + "LICENSE": "", + "src": { + "vendor": {"LICENSE": ""}, + }, + }, } @@ -238,6 +252,11 @@ def dummy_dist(tmp_path_factory): return mkexample(tmp_path_factory, "dummy-dist") +@pytest.fixture +def licenses_dist(tmp_path_factory): + return mkexample(tmp_path_factory, "licenses-dist") + + def test_no_scripts(wheel_paths): """Make sure entry point scripts are not generated.""" path = next(path for path in wheel_paths if "complex_dist" in path) @@ -297,7 +316,8 @@ def test_licenses_default(dummy_dist, monkeypatch, tmp_path): bdist_wheel_cmd(bdist_dir=str(tmp_path)).run() with ZipFile("dist/dummy_dist-1.0-py3-none-any.whl") as wf: license_files = { - "dummy_dist-1.0.dist-info/" + fname for fname in DEFAULT_LICENSE_FILES + "dummy_dist-1.0.dist-info/licenses/" + fname + for fname in DEFAULT_LICENSE_FILES } assert set(wf.namelist()) == DEFAULT_FILES | license_files @@ -311,7 +331,7 @@ def test_licenses_deprecated(dummy_dist, monkeypatch, tmp_path): bdist_wheel_cmd(bdist_dir=str(tmp_path)).run() with ZipFile("dist/dummy_dist-1.0-py3-none-any.whl") as wf: - license_files = {"dummy_dist-1.0.dist-info/DUMMYFILE"} + license_files = {"dummy_dist-1.0.dist-info/licenses/licenses/DUMMYFILE"} assert set(wf.namelist()) == DEFAULT_FILES | license_files @@ -334,9 +354,29 @@ def test_licenses_override(dummy_dist, monkeypatch, tmp_path, config_file, confi bdist_wheel_cmd(bdist_dir=str(tmp_path)).run() with ZipFile("dist/dummy_dist-1.0-py3-none-any.whl") as wf: license_files = { - "dummy_dist-1.0.dist-info/" + fname for fname in {"DUMMYFILE", "LICENSE"} + "dummy_dist-1.0.dist-info/licenses/" + fname + for fname in {"licenses/DUMMYFILE", "LICENSE"} } assert set(wf.namelist()) == DEFAULT_FILES | license_files + metadata = wf.read("dummy_dist-1.0.dist-info/METADATA").decode("utf8") + assert "License-File: licenses/DUMMYFILE" in metadata + assert "License-File: LICENSE" in metadata + + +def test_licenses_preserve_folder_structure(licenses_dist, monkeypatch, tmp_path): + monkeypatch.chdir(licenses_dist) + bdist_wheel_cmd(bdist_dir=str(tmp_path)).run() + print(os.listdir("dist")) + with ZipFile("dist/licenses_dist-1.0-py3-none-any.whl") as wf: + default_files = {name.replace("dummy_", "licenses_") for name in DEFAULT_FILES} + license_files = { + "licenses_dist-1.0.dist-info/licenses/LICENSE", + "licenses_dist-1.0.dist-info/licenses/src/vendor/LICENSE", + } + assert set(wf.namelist()) == default_files | license_files + metadata = wf.read("licenses_dist-1.0.dist-info/METADATA").decode("utf8") + assert "License-File: src/vendor/LICENSE" in metadata + assert "License-File: LICENSE" in metadata def test_licenses_disabled(dummy_dist, monkeypatch, tmp_path): diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index 121f409057..b26fd2f5b0 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -393,7 +393,9 @@ def test_build_with_pyproject_config(self, tmpdir, setup_script): 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") + license = str( + zipfile.read("foo-0.1.dist-info/licenses/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"} == { @@ -426,7 +428,7 @@ def test_build_with_pyproject_config(self, tmpdir, setup_script): "foo/cli.py", "foo/data.txt", # include_package_data defaults to True "foo/py.typed", # include type information by default - "foo-0.1.dist-info/LICENSE.txt", + "foo-0.1.dist-info/licenses/LICENSE.txt", "foo-0.1.dist-info/METADATA", "foo-0.1.dist-info/WHEEL", "foo-0.1.dist-info/entry_points.txt", @@ -438,6 +440,7 @@ def test_build_with_pyproject_config(self, tmpdir, setup_script): for line in ( "Summary: This is a Python package", "License: MIT", + "License-File: LICENSE.txt", "Classifier: Intended Audience :: Developers", "Requires-Dist: appdirs", "Requires-Dist: " + str(Requirement('tomli>=1 ; extra == "all"')), diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index 8879ec58ce..9756d7c519 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -815,6 +815,22 @@ def test_setup_cfg_license_file(self, tmpdir_cwd, env, files, license_in_sources [], id="files_only_added_once", ), + pytest.param( + { + 'setup.cfg': DALS( + """ + [metadata] + license_files = **/LICENSE + """ + ), + 'LICENSE': "ABC license", + 'LICENSE-OTHER': "Don't include", + 'vendor': {'LICENSE': "Vendor license"}, + }, + ['LICENSE', 'vendor/LICENSE'], + ['LICENSE-OTHER'], + id="recursive_glob", + ), ], ) def test_setup_cfg_license_files( @@ -1032,12 +1048,14 @@ def test_license_file_attr_pkg_info(self, tmpdir_cwd, env): license_files = NOTICE* LICENSE* + **/LICENSE """ ), "LICENSE-ABC": "ABC license", "LICENSE-XYZ": "XYZ license", "NOTICE": "included", "IGNORE": "not include", + "vendor": {'LICENSE': "Vendor license"}, }) environment.run_setup_py( @@ -1053,9 +1071,11 @@ def test_license_file_attr_pkg_info(self, tmpdir_cwd, env): # Only 'NOTICE', LICENSE-ABC', and 'LICENSE-XYZ' should have been matched # Also assert that order from license_files is keeped + assert len(license_file_lines) == 4 assert "License-File: NOTICE" == license_file_lines[0] assert "License-File: LICENSE-ABC" in license_file_lines[1:] assert "License-File: LICENSE-XYZ" in license_file_lines[1:] + assert "License-File: vendor/LICENSE" in license_file_lines[3] def test_metadata_version(self, tmpdir_cwd, env): """Make sure latest metadata version is used by default.""" From 02e1062cf6bcdffdae7e945fa5e5d04e9b6d2974 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Wed, 6 Nov 2024 12:09:27 +0100 Subject: [PATCH 02/36] Update validate-pyproject to 0.23.0 --- newsfragments/4734.misc.rst | 1 + setuptools/config/_validate_pyproject/NOTICE | 2 +- .../_validate_pyproject/extra_validations.py | 32 +- .../fastjsonschema_validations.py | 351 +++++++++++------- .../config/_validate_pyproject/formats.py | 33 +- tox.ini | 2 +- 6 files changed, 286 insertions(+), 135 deletions(-) create mode 100644 newsfragments/4734.misc.rst diff --git a/newsfragments/4734.misc.rst b/newsfragments/4734.misc.rst new file mode 100644 index 0000000000..3b3f2c94f3 --- /dev/null +++ b/newsfragments/4734.misc.rst @@ -0,0 +1 @@ +Updated ``pyproject.toml`` validation via ``validate-pyproject`` v0.23.0. diff --git a/setuptools/config/_validate_pyproject/NOTICE b/setuptools/config/_validate_pyproject/NOTICE index 74e8821fc8..ac5464d88c 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.pre_compile --output-dir=setuptools/config/_validate_pyproject --enable-plugins setuptools distutils --very-verbose -t distutils=setuptools/config/distutils.schema.json -t setuptools=setuptools/config/setuptools.schema.json + python -m validate_pyproject.pre_compile --output-dir=setuptools/config/_validate_pyproject --enable-plugins setuptools distutils --very-verbose -t setuptools=setuptools/config/setuptools.schema.json -t distutils=setuptools/config/distutils.schema.json Please avoid changing it manually. diff --git a/setuptools/config/_validate_pyproject/extra_validations.py b/setuptools/config/_validate_pyproject/extra_validations.py index c4ffe651dd..789411d0ff 100644 --- a/setuptools/config/_validate_pyproject/extra_validations.py +++ b/setuptools/config/_validate_pyproject/extra_validations.py @@ -24,6 +24,13 @@ class RedefiningStaticFieldAsDynamic(ValidationError): ) +class IncludedDependencyGroupMustExist(ValidationError): + _DESC = """An included dependency group must exist and must not be cyclic. + """ + __doc__ = _DESC + _URL = "https://peps.python.org/pep-0735/" + + def validate_project_dynamic(pyproject: T) -> T: project_table = pyproject.get("project", {}) dynamic = project_table.get("dynamic", []) @@ -49,4 +56,27 @@ def validate_project_dynamic(pyproject: T) -> T: return pyproject -EXTRA_VALIDATIONS = (validate_project_dynamic,) +def validate_include_depenency(pyproject: T) -> T: + dependency_groups = pyproject.get("dependency-groups", {}) + for key, value in dependency_groups.items(): + for each in value: + if ( + isinstance(each, dict) + and (include_group := each.get("include-group")) + and include_group not in dependency_groups + ): + raise IncludedDependencyGroupMustExist( + message=f"The included dependency group {include_group} doesn't exist", + value=each, + name=f"data.dependency_groups.{key}", + definition={ + "description": cleandoc(IncludedDependencyGroupMustExist._DESC), + "see": IncludedDependencyGroupMustExist._URL, + }, + rule="PEP 735", + ) + # TODO: check for `include-group` cycles (can be conditional to graphlib) + return pyproject + + +EXTRA_VALIDATIONS = (validate_project_dynamic, validate_include_depenency) diff --git a/setuptools/config/_validate_pyproject/fastjsonschema_validations.py b/setuptools/config/_validate_pyproject/fastjsonschema_validations.py index 42e7aa5e33..c69368a83f 100644 --- a/setuptools/config/_validate_pyproject/fastjsonschema_validations.py +++ b/setuptools/config/_validate_pyproject/fastjsonschema_validations.py @@ -17,6 +17,7 @@ REGEX_PATTERNS = { + '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])$': re.compile('^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])\\Z'), '^.*$': re.compile('^.*$'), '.+': re.compile('.+'), '^.+$': re.compile('^.+$'), @@ -31,7 +32,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/pyproject-toml/', '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://peps.python.org/pep-0621/#authors-maintainers', 'type': 'object', 'additionalProperties': False, '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://setuptools.pypa.io/en/latest/deprecated/distutils/configfile.html', 'title': '``tool.distutils`` table', '$$description': ['**EXPERIMENTAL** (NOT OFFICIALLY SUPPORTED): Use ``tool.distutils``', 'subtables to configure arguments for ``distutils`` commands.', 'Originally, ``distutils`` allowed developers to configure arguments for', '``setup.py`` commands via `distutils configuration files', '`_.', 'See also `the old Python docs _`.'], '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/userguide/pyproject_config.html', 'title': '``tool.setuptools`` table', '$$description': ['``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 some of the `Keywords', '`_', 'used by the ``setup.py`` file, and can be set via the ``tool.setuptools`` table.', 'It considers only ``setuptools`` `parameters', '`_', 'that are not covered by :pep:`621`; and intentionally excludes ``dependency_links``', 'and ``setup_requires`` (incompatible with modern workflows/standards).'], '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.', '**OBSOLETE**: only relevant for ``pkg_resources``, ``easy_install`` and', '``setup.py install`` in the context of ``eggs`` (**DEPRECATED**).'], 'type': 'boolean'}, 'script-files': {'$$description': ['Legacy way of defining scripts (entry-points are preferred).', 'Equivalent to the ``script`` keyword in ``setup.py``', '(it was renamed to avoid confusion with entry-point based ``project.scripts``', 'defined in :pep:`621`).', '**DISCOURAGED**: generic script wrappers are tricky and may not work properly.', 'Whenever possible, please use ``project.scripts`` instead.'], '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.', '**OBSOLETE**: only relevant for ``pkg_resources``, ``easy_install`` and', '``setup.py install`` in the context of ``eggs`` (**DEPRECATED**).'], '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': {'$ref': '#/definitions/package-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': {'anyOf': [{'const': ''}, {'$ref': '#/definitions/package-name'}]}, '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': {'anyOf': [{'type': 'string', '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': {'anyOf': [{'type': 'string', 'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name-relaxed'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html', 'description': '**DEPRECATED**: use implicit namespaces instead (:pep:`420`).'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name-relaxed'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'ext-modules': {'description': 'Extension modules to be compiled by setuptools', 'type': 'array', 'items': {'$ref': '#/definitions/ext-module'}}, 'data-files': {'$$description': ['``dict``-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', '**DISCOURAGED**: please notice this might not work as expected with wheels.', 'Whenever possible, consider using data files inside the package directories', '(or create a new namespace package that only contains data files).', '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.', '(likely to become standard with :pep:`639`).', "By 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`.', 'Also ensure to set ``project.dynamic`` accordingly.'], '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'}, 'dependencies': {'$ref': '#/definitions/file-directive-for-dependencies'}, 'optional-dependencies': {'type': 'object', 'propertyNames': {'type': 'string', 'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'.+': {'$ref': '#/definitions/file-directive-for-dependencies'}}}, 'readme': {'type': 'object', 'anyOf': [{'$ref': '#/definitions/file-directive'}, {'type': 'object', 'properties': {'content-type': {'type': 'string'}, 'file': {'$ref': '#/definitions/file-directive/properties/file'}}, 'additionalProperties': False}], 'required': ['file']}}}}, 'definitions': {'package-name': {'$id': '#/definitions/package-name', 'title': 'Valid package name', 'description': 'Valid package name (importable or :pep:`561`).', 'type': 'string', 'anyOf': [{'type': 'string', 'format': 'python-module-name-relaxed'}, {'type': 'string', 'format': 'pep561-stub-name'}]}, 'ext-module': {'$id': '#/definitions/ext-module', 'title': 'Extension module', 'description': 'Parameters to construct a :class:`setuptools.Extension` object', 'type': 'object', 'required': ['name', 'sources'], 'additionalProperties': False, 'properties': {'name': {'type': 'string', 'format': 'python-module-name-relaxed'}, 'sources': {'type': 'array', 'items': {'type': 'string'}}, 'include-dirs': {'type': 'array', 'items': {'type': 'string'}}, 'define-macros': {'type': 'array', 'items': {'type': 'array', 'items': [{'description': 'macro name', 'type': 'string'}, {'description': 'macro value', 'oneOf': [{'type': 'string'}, {'type': 'null'}]}], 'additionalItems': False}}, 'undef-macros': {'type': 'array', 'items': {'type': 'string'}}, 'library-dirs': {'type': 'array', 'items': {'type': 'string'}}, 'libraries': {'type': 'array', 'items': {'type': 'string'}}, 'runtime-library-dirs': {'type': 'array', 'items': {'type': 'string'}}, 'extra-objects': {'type': 'array', 'items': {'type': 'string'}}, 'extra-compile-args': {'type': 'array', 'items': {'type': 'string'}}, 'extra-link-args': {'type': 'array', 'items': {'type': 'string'}}, 'export-symbols': {'type': 'array', 'items': {'type': 'string'}}, 'swig-opts': {'type': 'array', 'items': {'type': 'string'}}, 'depends': {'type': 'array', 'items': {'type': 'string'}}, 'language': {'type': 'string'}, 'optional': {'type': 'boolean'}, 'py-limited-api': {'type': 'boolean'}}}, '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']}, 'file-directive-for-dependencies': {'title': "'file:' directive for dependencies", 'allOf': [{'$$description': ['**BETA**: subset of the ``requirements.txt`` format', 'without ``pip`` flags and options', '(one :pep:`508`-compliant string per line,', 'lines that are blank or start with ``#`` are excluded).', 'See `dynamic metadata', '`_.']}, {'$ref': '#/definitions/file-directive'}]}, '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', 'format': 'python-qualified-identifier'}}, '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/pyproject-toml/', '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://peps.python.org/pep-0621/#authors-maintainers', 'type': 'object', 'additionalProperties': False, '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/pyproject-toml/', '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': [{'type': 'string', 'description': 'An SPDX license identifier', 'format': 'SPDX'}, {'type': 'object', 'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'type': 'object', 'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'license-files': {'description': 'Paths or globs to paths of license files', 'type': 'array', 'items': {'type': 'string'}}, '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', 'license-files', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'allOf': [{'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']}}, {'if': {'required': ['license-files']}, 'then': {'properties': {'license': {'type': 'string'}}}}], 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://peps.python.org/pep-0621/#authors-maintainers', 'type': 'object', 'additionalProperties': False, '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://setuptools.pypa.io/en/latest/deprecated/distutils/configfile.html', 'title': '``tool.distutils`` table', '$$description': ['**EXPERIMENTAL** (NOT OFFICIALLY SUPPORTED): Use ``tool.distutils``', 'subtables to configure arguments for ``distutils`` commands.', 'Originally, ``distutils`` allowed developers to configure arguments for', '``setup.py`` commands via `distutils configuration files', '`_.', 'See also `the old Python docs _`.'], '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/userguide/pyproject_config.html', 'title': '``tool.setuptools`` table', '$$description': ['``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 some of the `Keywords', '`_', 'used by the ``setup.py`` file, and can be set via the ``tool.setuptools`` table.', 'It considers only ``setuptools`` `parameters', '`_', 'that are not covered by :pep:`621`; and intentionally excludes ``dependency_links``', 'and ``setup_requires`` (incompatible with modern workflows/standards).'], '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.', '**OBSOLETE**: only relevant for ``pkg_resources``, ``easy_install`` and', '``setup.py install`` in the context of ``eggs`` (**DEPRECATED**).'], 'type': 'boolean'}, 'script-files': {'$$description': ['Legacy way of defining scripts (entry-points are preferred).', 'Equivalent to the ``script`` keyword in ``setup.py``', '(it was renamed to avoid confusion with entry-point based ``project.scripts``', 'defined in :pep:`621`).', '**DISCOURAGED**: generic script wrappers are tricky and may not work properly.', 'Whenever possible, please use ``project.scripts`` instead.'], '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.', '**OBSOLETE**: only relevant for ``pkg_resources``, ``easy_install`` and', '``setup.py install`` in the context of ``eggs`` (**DEPRECATED**).'], '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': {'$ref': '#/definitions/package-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': {'anyOf': [{'const': ''}, {'$ref': '#/definitions/package-name'}]}, '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': {'anyOf': [{'type': 'string', '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': {'anyOf': [{'type': 'string', 'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name-relaxed'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html', 'description': '**DEPRECATED**: use implicit namespaces instead (:pep:`420`).'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name-relaxed'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'ext-modules': {'description': 'Extension modules to be compiled by setuptools', 'type': 'array', 'items': {'$ref': '#/definitions/ext-module'}}, 'data-files': {'$$description': ['``dict``-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', '**DISCOURAGED**: please notice this might not work as expected with wheels.', 'Whenever possible, consider using data files inside the package directories', '(or create a new namespace package that only contains data files).', '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.', '(likely to become standard with :pep:`639`).', "By 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`.', 'Also ensure to set ``project.dynamic`` accordingly.'], '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'}, 'dependencies': {'$ref': '#/definitions/file-directive-for-dependencies'}, 'optional-dependencies': {'type': 'object', 'propertyNames': {'type': 'string', 'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'.+': {'$ref': '#/definitions/file-directive-for-dependencies'}}}, 'readme': {'type': 'object', 'anyOf': [{'$ref': '#/definitions/file-directive'}, {'type': 'object', 'properties': {'content-type': {'type': 'string'}, 'file': {'$ref': '#/definitions/file-directive/properties/file'}}, 'additionalProperties': False}], 'required': ['file']}}}}, 'definitions': {'package-name': {'$id': '#/definitions/package-name', 'title': 'Valid package name', 'description': 'Valid package name (importable or :pep:`561`).', 'type': 'string', 'anyOf': [{'type': 'string', 'format': 'python-module-name-relaxed'}, {'type': 'string', 'format': 'pep561-stub-name'}]}, 'ext-module': {'$id': '#/definitions/ext-module', 'title': 'Extension module', 'description': 'Parameters to construct a :class:`setuptools.Extension` object', 'type': 'object', 'required': ['name', 'sources'], 'additionalProperties': False, 'properties': {'name': {'type': 'string', 'format': 'python-module-name-relaxed'}, 'sources': {'type': 'array', 'items': {'type': 'string'}}, 'include-dirs': {'type': 'array', 'items': {'type': 'string'}}, 'define-macros': {'type': 'array', 'items': {'type': 'array', 'items': [{'description': 'macro name', 'type': 'string'}, {'description': 'macro value', 'oneOf': [{'type': 'string'}, {'type': 'null'}]}], 'additionalItems': False}}, 'undef-macros': {'type': 'array', 'items': {'type': 'string'}}, 'library-dirs': {'type': 'array', 'items': {'type': 'string'}}, 'libraries': {'type': 'array', 'items': {'type': 'string'}}, 'runtime-library-dirs': {'type': 'array', 'items': {'type': 'string'}}, 'extra-objects': {'type': 'array', 'items': {'type': 'string'}}, 'extra-compile-args': {'type': 'array', 'items': {'type': 'string'}}, 'extra-link-args': {'type': 'array', 'items': {'type': 'string'}}, 'export-symbols': {'type': 'array', 'items': {'type': 'string'}}, 'swig-opts': {'type': 'array', 'items': {'type': 'string'}}, 'depends': {'type': 'array', 'items': {'type': 'string'}}, 'language': {'type': 'string'}, 'optional': {'type': 'boolean'}, 'py-limited-api': {'type': 'boolean'}}}, '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']}, 'file-directive-for-dependencies': {'title': "'file:' directive for dependencies", 'allOf': [{'$$description': ['**BETA**: subset of the ``requirements.txt`` format', 'without ``pip`` flags and options', '(one :pep:`508`-compliant string per line,', 'lines that are blank or start with ``#`` are excluded).', 'See `dynamic metadata', '`_.']}, {'$ref': '#/definitions/file-directive'}]}, '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', 'format': 'python-qualified-identifier'}}, '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']}}}}}}}}}, 'dependency-groups': {'type': 'object', 'description': 'Dependency groups following PEP 735', 'additionalProperties': False, 'patternProperties': {'^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])$': {'type': 'array', 'items': {'oneOf': [{'type': 'string', 'description': 'Python package specifiers following PEP 508', 'format': 'pep508'}, {'type': 'object', 'additionalProperties': False, 'properties': {'include-group': {'description': 'Another dependency group to include in this one', 'type': 'string', 'pattern': '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])$'}}}]}}}}}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema#', '$id': 'https://packaging.python.org/en/latest/specifications/pyproject-toml/', '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': [{'type': 'string', 'description': 'An SPDX license identifier', 'format': 'SPDX'}, {'type': 'object', 'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'type': 'object', 'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'license-files': {'description': 'Paths or globs to paths of license files', 'type': 'array', 'items': {'type': 'string'}}, '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', 'license-files', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'allOf': [{'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']}}, {'if': {'required': ['license-files']}, 'then': {'properties': {'license': {'type': 'string'}}}}], 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://peps.python.org/pep-0621/#authors-maintainers', 'type': 'object', 'additionalProperties': False, '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,8 +99,59 @@ def validate_https___packaging_python_org_en_latest_specifications_declaring_bui data__tool_keys.remove("setuptools") data__tool__setuptools = data__tool["setuptools"] validate_https___setuptools_pypa_io_en_latest_userguide_pyproject_config_html(data__tool__setuptools, custom_formats, (name_prefix or "data") + ".tool.setuptools") + if "dependency-groups" in data_keys: + data_keys.remove("dependency-groups") + data__dependencygroups = data["dependency-groups"] + if not isinstance(data__dependencygroups, (dict)): + raise JsonSchemaValueException("" + (name_prefix or "data") + ".dependency-groups must be object", value=data__dependencygroups, name="" + (name_prefix or "data") + ".dependency-groups", definition={'type': 'object', 'description': 'Dependency groups following PEP 735', 'additionalProperties': False, 'patternProperties': {'^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])$': {'type': 'array', 'items': {'oneOf': [{'type': 'string', 'description': 'Python package specifiers following PEP 508', 'format': 'pep508'}, {'type': 'object', 'additionalProperties': False, 'properties': {'include-group': {'description': 'Another dependency group to include in this one', 'type': 'string', 'pattern': '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])$'}}}]}}}}, rule='type') + data__dependencygroups_is_dict = isinstance(data__dependencygroups, dict) + if data__dependencygroups_is_dict: + data__dependencygroups_keys = set(data__dependencygroups.keys()) + for data__dependencygroups_key, data__dependencygroups_val in data__dependencygroups.items(): + if REGEX_PATTERNS['^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])$'].search(data__dependencygroups_key): + if data__dependencygroups_key in data__dependencygroups_keys: + data__dependencygroups_keys.remove(data__dependencygroups_key) + if not isinstance(data__dependencygroups_val, (list, tuple)): + raise JsonSchemaValueException("" + (name_prefix or "data") + ".dependency-groups.{data__dependencygroups_key}".format(**locals()) + " must be array", value=data__dependencygroups_val, name="" + (name_prefix or "data") + ".dependency-groups.{data__dependencygroups_key}".format(**locals()) + "", definition={'type': 'array', 'items': {'oneOf': [{'type': 'string', 'description': 'Python package specifiers following PEP 508', 'format': 'pep508'}, {'type': 'object', 'additionalProperties': False, 'properties': {'include-group': {'description': 'Another dependency group to include in this one', 'type': 'string', 'pattern': '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])$'}}}]}}, rule='type') + data__dependencygroups_val_is_list = isinstance(data__dependencygroups_val, (list, tuple)) + if data__dependencygroups_val_is_list: + data__dependencygroups_val_len = len(data__dependencygroups_val) + for data__dependencygroups_val_x, data__dependencygroups_val_item in enumerate(data__dependencygroups_val): + data__dependencygroups_val_item_one_of_count1 = 0 + if data__dependencygroups_val_item_one_of_count1 < 2: + try: + if not isinstance(data__dependencygroups_val_item, (str)): + raise JsonSchemaValueException("" + (name_prefix or "data") + ".dependency-groups.{data__dependencygroups_key}[{data__dependencygroups_val_x}]".format(**locals()) + " must be string", value=data__dependencygroups_val_item, name="" + (name_prefix or "data") + ".dependency-groups.{data__dependencygroups_key}[{data__dependencygroups_val_x}]".format(**locals()) + "", definition={'type': 'string', 'description': 'Python package specifiers following PEP 508', 'format': 'pep508'}, rule='type') + if isinstance(data__dependencygroups_val_item, str): + if not custom_formats["pep508"](data__dependencygroups_val_item): + raise JsonSchemaValueException("" + (name_prefix or "data") + ".dependency-groups.{data__dependencygroups_key}[{data__dependencygroups_val_x}]".format(**locals()) + " must be pep508", value=data__dependencygroups_val_item, name="" + (name_prefix or "data") + ".dependency-groups.{data__dependencygroups_key}[{data__dependencygroups_val_x}]".format(**locals()) + "", definition={'type': 'string', 'description': 'Python package specifiers following PEP 508', 'format': 'pep508'}, rule='format') + data__dependencygroups_val_item_one_of_count1 += 1 + except JsonSchemaValueException: pass + if data__dependencygroups_val_item_one_of_count1 < 2: + try: + if not isinstance(data__dependencygroups_val_item, (dict)): + raise JsonSchemaValueException("" + (name_prefix or "data") + ".dependency-groups.{data__dependencygroups_key}[{data__dependencygroups_val_x}]".format(**locals()) + " must be object", value=data__dependencygroups_val_item, name="" + (name_prefix or "data") + ".dependency-groups.{data__dependencygroups_key}[{data__dependencygroups_val_x}]".format(**locals()) + "", definition={'type': 'object', 'additionalProperties': False, 'properties': {'include-group': {'description': 'Another dependency group to include in this one', 'type': 'string', 'pattern': '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])$'}}}, rule='type') + data__dependencygroups_val_item_is_dict = isinstance(data__dependencygroups_val_item, dict) + if data__dependencygroups_val_item_is_dict: + data__dependencygroups_val_item_keys = set(data__dependencygroups_val_item.keys()) + if "include-group" in data__dependencygroups_val_item_keys: + data__dependencygroups_val_item_keys.remove("include-group") + data__dependencygroups_val_item__includegroup = data__dependencygroups_val_item["include-group"] + if not isinstance(data__dependencygroups_val_item__includegroup, (str)): + raise JsonSchemaValueException("" + (name_prefix or "data") + ".dependency-groups.{data__dependencygroups_key}[{data__dependencygroups_val_x}].include-group".format(**locals()) + " must be string", value=data__dependencygroups_val_item__includegroup, name="" + (name_prefix or "data") + ".dependency-groups.{data__dependencygroups_key}[{data__dependencygroups_val_x}].include-group".format(**locals()) + "", definition={'description': 'Another dependency group to include in this one', 'type': 'string', 'pattern': '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])$'}, rule='type') + if isinstance(data__dependencygroups_val_item__includegroup, str): + if not REGEX_PATTERNS['^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])$'].search(data__dependencygroups_val_item__includegroup): + raise JsonSchemaValueException("" + (name_prefix or "data") + ".dependency-groups.{data__dependencygroups_key}[{data__dependencygroups_val_x}].include-group".format(**locals()) + " must match pattern ^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])$", value=data__dependencygroups_val_item__includegroup, name="" + (name_prefix or "data") + ".dependency-groups.{data__dependencygroups_key}[{data__dependencygroups_val_x}].include-group".format(**locals()) + "", definition={'description': 'Another dependency group to include in this one', 'type': 'string', 'pattern': '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])$'}, rule='pattern') + if data__dependencygroups_val_item_keys: + raise JsonSchemaValueException("" + (name_prefix or "data") + ".dependency-groups.{data__dependencygroups_key}[{data__dependencygroups_val_x}]".format(**locals()) + " must not contain "+str(data__dependencygroups_val_item_keys)+" properties", value=data__dependencygroups_val_item, name="" + (name_prefix or "data") + ".dependency-groups.{data__dependencygroups_key}[{data__dependencygroups_val_x}]".format(**locals()) + "", definition={'type': 'object', 'additionalProperties': False, 'properties': {'include-group': {'description': 'Another dependency group to include in this one', 'type': 'string', 'pattern': '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])$'}}}, rule='additionalProperties') + data__dependencygroups_val_item_one_of_count1 += 1 + except JsonSchemaValueException: pass + if data__dependencygroups_val_item_one_of_count1 != 1: + raise JsonSchemaValueException("" + (name_prefix or "data") + ".dependency-groups.{data__dependencygroups_key}[{data__dependencygroups_val_x}]".format(**locals()) + " must be valid exactly by one definition" + (" (" + str(data__dependencygroups_val_item_one_of_count1) + " matches found)"), value=data__dependencygroups_val_item, name="" + (name_prefix or "data") + ".dependency-groups.{data__dependencygroups_key}[{data__dependencygroups_val_x}]".format(**locals()) + "", definition={'oneOf': [{'type': 'string', 'description': 'Python package specifiers following PEP 508', 'format': 'pep508'}, {'type': 'object', 'additionalProperties': False, 'properties': {'include-group': {'description': 'Another dependency group to include in this one', 'type': 'string', 'pattern': '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])$'}}}]}, rule='oneOf') + if data__dependencygroups_keys: + raise JsonSchemaValueException("" + (name_prefix or "data") + ".dependency-groups must not contain "+str(data__dependencygroups_keys)+" properties", value=data__dependencygroups, name="" + (name_prefix or "data") + ".dependency-groups", definition={'type': 'object', 'description': 'Dependency groups following PEP 735', 'additionalProperties': False, 'patternProperties': {'^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])$': {'type': 'array', 'items': {'oneOf': [{'type': 'string', 'description': 'Python package specifiers following PEP 508', 'format': 'pep508'}, {'type': 'object', 'additionalProperties': False, 'properties': {'include-group': {'description': 'Another dependency group to include in this one', 'type': 'string', 'pattern': '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])$'}}}]}}}}, 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://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/pyproject-toml/', '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://peps.python.org/pep-0621/#authors-maintainers', 'type': 'object', 'additionalProperties': False, '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://setuptools.pypa.io/en/latest/deprecated/distutils/configfile.html', 'title': '``tool.distutils`` table', '$$description': ['**EXPERIMENTAL** (NOT OFFICIALLY SUPPORTED): Use ``tool.distutils``', 'subtables to configure arguments for ``distutils`` commands.', 'Originally, ``distutils`` allowed developers to configure arguments for', '``setup.py`` commands via `distutils configuration files', '`_.', 'See also `the old Python docs _`.'], '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/userguide/pyproject_config.html', 'title': '``tool.setuptools`` table', '$$description': ['``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 some of the `Keywords', '`_', 'used by the ``setup.py`` file, and can be set via the ``tool.setuptools`` table.', 'It considers only ``setuptools`` `parameters', '`_', 'that are not covered by :pep:`621`; and intentionally excludes ``dependency_links``', 'and ``setup_requires`` (incompatible with modern workflows/standards).'], '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.', '**OBSOLETE**: only relevant for ``pkg_resources``, ``easy_install`` and', '``setup.py install`` in the context of ``eggs`` (**DEPRECATED**).'], 'type': 'boolean'}, 'script-files': {'$$description': ['Legacy way of defining scripts (entry-points are preferred).', 'Equivalent to the ``script`` keyword in ``setup.py``', '(it was renamed to avoid confusion with entry-point based ``project.scripts``', 'defined in :pep:`621`).', '**DISCOURAGED**: generic script wrappers are tricky and may not work properly.', 'Whenever possible, please use ``project.scripts`` instead.'], '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.', '**OBSOLETE**: only relevant for ``pkg_resources``, ``easy_install`` and', '``setup.py install`` in the context of ``eggs`` (**DEPRECATED**).'], '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': {'$ref': '#/definitions/package-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': {'anyOf': [{'const': ''}, {'$ref': '#/definitions/package-name'}]}, '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': {'anyOf': [{'type': 'string', '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': {'anyOf': [{'type': 'string', 'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name-relaxed'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html', 'description': '**DEPRECATED**: use implicit namespaces instead (:pep:`420`).'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name-relaxed'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'ext-modules': {'description': 'Extension modules to be compiled by setuptools', 'type': 'array', 'items': {'$ref': '#/definitions/ext-module'}}, 'data-files': {'$$description': ['``dict``-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', '**DISCOURAGED**: please notice this might not work as expected with wheels.', 'Whenever possible, consider using data files inside the package directories', '(or create a new namespace package that only contains data files).', '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.', '(likely to become standard with :pep:`639`).', "By 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`.', 'Also ensure to set ``project.dynamic`` accordingly.'], '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'}, 'dependencies': {'$ref': '#/definitions/file-directive-for-dependencies'}, 'optional-dependencies': {'type': 'object', 'propertyNames': {'type': 'string', 'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'.+': {'$ref': '#/definitions/file-directive-for-dependencies'}}}, 'readme': {'type': 'object', 'anyOf': [{'$ref': '#/definitions/file-directive'}, {'type': 'object', 'properties': {'content-type': {'type': 'string'}, 'file': {'$ref': '#/definitions/file-directive/properties/file'}}, 'additionalProperties': False}], 'required': ['file']}}}}, 'definitions': {'package-name': {'$id': '#/definitions/package-name', 'title': 'Valid package name', 'description': 'Valid package name (importable or :pep:`561`).', 'type': 'string', 'anyOf': [{'type': 'string', 'format': 'python-module-name-relaxed'}, {'type': 'string', 'format': 'pep561-stub-name'}]}, 'ext-module': {'$id': '#/definitions/ext-module', 'title': 'Extension module', 'description': 'Parameters to construct a :class:`setuptools.Extension` object', 'type': 'object', 'required': ['name', 'sources'], 'additionalProperties': False, 'properties': {'name': {'type': 'string', 'format': 'python-module-name-relaxed'}, 'sources': {'type': 'array', 'items': {'type': 'string'}}, 'include-dirs': {'type': 'array', 'items': {'type': 'string'}}, 'define-macros': {'type': 'array', 'items': {'type': 'array', 'items': [{'description': 'macro name', 'type': 'string'}, {'description': 'macro value', 'oneOf': [{'type': 'string'}, {'type': 'null'}]}], 'additionalItems': False}}, 'undef-macros': {'type': 'array', 'items': {'type': 'string'}}, 'library-dirs': {'type': 'array', 'items': {'type': 'string'}}, 'libraries': {'type': 'array', 'items': {'type': 'string'}}, 'runtime-library-dirs': {'type': 'array', 'items': {'type': 'string'}}, 'extra-objects': {'type': 'array', 'items': {'type': 'string'}}, 'extra-compile-args': {'type': 'array', 'items': {'type': 'string'}}, 'extra-link-args': {'type': 'array', 'items': {'type': 'string'}}, 'export-symbols': {'type': 'array', 'items': {'type': 'string'}}, 'swig-opts': {'type': 'array', 'items': {'type': 'string'}}, 'depends': {'type': 'array', 'items': {'type': 'string'}}, 'language': {'type': 'string'}, 'optional': {'type': 'boolean'}, 'py-limited-api': {'type': 'boolean'}}}, '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']}, 'file-directive-for-dependencies': {'title': "'file:' directive for dependencies", 'allOf': [{'$$description': ['**BETA**: subset of the ``requirements.txt`` format', 'without ``pip`` flags and options', '(one :pep:`508`-compliant string per line,', 'lines that are blank or start with ``#`` are excluded).', 'See `dynamic metadata', '`_.']}, {'$ref': '#/definitions/file-directive'}]}, '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', 'format': 'python-qualified-identifier'}}, '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/pyproject-toml/', '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://peps.python.org/pep-0621/#authors-maintainers', 'type': 'object', 'additionalProperties': False, '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/pyproject-toml/', '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': [{'type': 'string', 'description': 'An SPDX license identifier', 'format': 'SPDX'}, {'type': 'object', 'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'type': 'object', 'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'license-files': {'description': 'Paths or globs to paths of license files', 'type': 'array', 'items': {'type': 'string'}}, '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', 'license-files', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'allOf': [{'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']}}, {'if': {'required': ['license-files']}, 'then': {'properties': {'license': {'type': 'string'}}}}], 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://peps.python.org/pep-0621/#authors-maintainers', 'type': 'object', 'additionalProperties': False, '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://setuptools.pypa.io/en/latest/deprecated/distutils/configfile.html', 'title': '``tool.distutils`` table', '$$description': ['**EXPERIMENTAL** (NOT OFFICIALLY SUPPORTED): Use ``tool.distutils``', 'subtables to configure arguments for ``distutils`` commands.', 'Originally, ``distutils`` allowed developers to configure arguments for', '``setup.py`` commands via `distutils configuration files', '`_.', 'See also `the old Python docs _`.'], '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/userguide/pyproject_config.html', 'title': '``tool.setuptools`` table', '$$description': ['``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 some of the `Keywords', '`_', 'used by the ``setup.py`` file, and can be set via the ``tool.setuptools`` table.', 'It considers only ``setuptools`` `parameters', '`_', 'that are not covered by :pep:`621`; and intentionally excludes ``dependency_links``', 'and ``setup_requires`` (incompatible with modern workflows/standards).'], '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.', '**OBSOLETE**: only relevant for ``pkg_resources``, ``easy_install`` and', '``setup.py install`` in the context of ``eggs`` (**DEPRECATED**).'], 'type': 'boolean'}, 'script-files': {'$$description': ['Legacy way of defining scripts (entry-points are preferred).', 'Equivalent to the ``script`` keyword in ``setup.py``', '(it was renamed to avoid confusion with entry-point based ``project.scripts``', 'defined in :pep:`621`).', '**DISCOURAGED**: generic script wrappers are tricky and may not work properly.', 'Whenever possible, please use ``project.scripts`` instead.'], '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.', '**OBSOLETE**: only relevant for ``pkg_resources``, ``easy_install`` and', '``setup.py install`` in the context of ``eggs`` (**DEPRECATED**).'], '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': {'$ref': '#/definitions/package-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': {'anyOf': [{'const': ''}, {'$ref': '#/definitions/package-name'}]}, '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': {'anyOf': [{'type': 'string', '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': {'anyOf': [{'type': 'string', 'format': 'python-module-name'}, {'const': '*'}]}, 'patternProperties': {'^.*$': {'type': 'array', 'items': {'type': 'string'}}}}, 'namespace-packages': {'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name-relaxed'}, '$comment': 'https://setuptools.pypa.io/en/latest/userguide/package_discovery.html', 'description': '**DEPRECATED**: use implicit namespaces instead (:pep:`420`).'}, 'py-modules': {'description': 'Modules that setuptools will manipulate', 'type': 'array', 'items': {'type': 'string', 'format': 'python-module-name-relaxed'}, '$comment': 'TODO: clarify the relationship with ``packages``'}, 'ext-modules': {'description': 'Extension modules to be compiled by setuptools', 'type': 'array', 'items': {'$ref': '#/definitions/ext-module'}}, 'data-files': {'$$description': ['``dict``-like structure where each key represents a directory and', 'the value is a list of glob patterns that should be installed in them.', '**DISCOURAGED**: please notice this might not work as expected with wheels.', 'Whenever possible, consider using data files inside the package directories', '(or create a new namespace package that only contains data files).', '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.', '(likely to become standard with :pep:`639`).', "By 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`.', 'Also ensure to set ``project.dynamic`` accordingly.'], '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'}, 'dependencies': {'$ref': '#/definitions/file-directive-for-dependencies'}, 'optional-dependencies': {'type': 'object', 'propertyNames': {'type': 'string', 'format': 'pep508-identifier'}, 'additionalProperties': False, 'patternProperties': {'.+': {'$ref': '#/definitions/file-directive-for-dependencies'}}}, 'readme': {'type': 'object', 'anyOf': [{'$ref': '#/definitions/file-directive'}, {'type': 'object', 'properties': {'content-type': {'type': 'string'}, 'file': {'$ref': '#/definitions/file-directive/properties/file'}}, 'additionalProperties': False}], 'required': ['file']}}}}, 'definitions': {'package-name': {'$id': '#/definitions/package-name', 'title': 'Valid package name', 'description': 'Valid package name (importable or :pep:`561`).', 'type': 'string', 'anyOf': [{'type': 'string', 'format': 'python-module-name-relaxed'}, {'type': 'string', 'format': 'pep561-stub-name'}]}, 'ext-module': {'$id': '#/definitions/ext-module', 'title': 'Extension module', 'description': 'Parameters to construct a :class:`setuptools.Extension` object', 'type': 'object', 'required': ['name', 'sources'], 'additionalProperties': False, 'properties': {'name': {'type': 'string', 'format': 'python-module-name-relaxed'}, 'sources': {'type': 'array', 'items': {'type': 'string'}}, 'include-dirs': {'type': 'array', 'items': {'type': 'string'}}, 'define-macros': {'type': 'array', 'items': {'type': 'array', 'items': [{'description': 'macro name', 'type': 'string'}, {'description': 'macro value', 'oneOf': [{'type': 'string'}, {'type': 'null'}]}], 'additionalItems': False}}, 'undef-macros': {'type': 'array', 'items': {'type': 'string'}}, 'library-dirs': {'type': 'array', 'items': {'type': 'string'}}, 'libraries': {'type': 'array', 'items': {'type': 'string'}}, 'runtime-library-dirs': {'type': 'array', 'items': {'type': 'string'}}, 'extra-objects': {'type': 'array', 'items': {'type': 'string'}}, 'extra-compile-args': {'type': 'array', 'items': {'type': 'string'}}, 'extra-link-args': {'type': 'array', 'items': {'type': 'string'}}, 'export-symbols': {'type': 'array', 'items': {'type': 'string'}}, 'swig-opts': {'type': 'array', 'items': {'type': 'string'}}, 'depends': {'type': 'array', 'items': {'type': 'string'}}, 'language': {'type': 'string'}, 'optional': {'type': 'boolean'}, 'py-limited-api': {'type': 'boolean'}}}, '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']}, 'file-directive-for-dependencies': {'title': "'file:' directive for dependencies", 'allOf': [{'$$description': ['**BETA**: subset of the ``requirements.txt`` format', 'without ``pip`` flags and options', '(one :pep:`508`-compliant string per line,', 'lines that are blank or start with ``#`` are excluded).', 'See `dynamic metadata', '`_.']}, {'$ref': '#/definitions/file-directive'}]}, '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', 'format': 'python-qualified-identifier'}}, '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']}}}}}}}}}, 'dependency-groups': {'type': 'object', 'description': 'Dependency groups following PEP 735', 'additionalProperties': False, 'patternProperties': {'^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])$': {'type': 'array', 'items': {'oneOf': [{'type': 'string', 'description': 'Python package specifiers following PEP 508', 'format': 'pep508'}, {'type': 'object', 'additionalProperties': False, 'properties': {'include-group': {'description': 'Another dependency group to include in this one', 'type': 'string', 'pattern': '^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9._-]*[a-zA-Z0-9])$'}}}]}}}}}, 'project': {'$schema': 'http://json-schema.org/draft-07/schema#', '$id': 'https://packaging.python.org/en/latest/specifications/pyproject-toml/', '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': [{'type': 'string', 'description': 'An SPDX license identifier', 'format': 'SPDX'}, {'type': 'object', 'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'type': 'object', 'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'license-files': {'description': 'Paths or globs to paths of license files', 'type': 'array', 'items': {'type': 'string'}}, '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', 'license-files', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'allOf': [{'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']}}, {'if': {'required': ['license-files']}, 'then': {'properties': {'license': {'type': 'string'}}}}], 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://peps.python.org/pep-0621/#authors-maintainers', 'type': 'object', 'additionalProperties': False, '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_userguide_pyproject_config_html(data, custom_formats={}, name_prefix=None): @@ -177,8 +229,8 @@ def validate_https___setuptools_pypa_io_en_latest_userguide_pyproject_config_htm 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: + data__packages_one_of_count2 = 0 + if data__packages_one_of_count2 < 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': {'$id': '#/definitions/package-name', 'title': 'Valid package name', 'description': 'Valid package name (importable or :pep:`561`).', 'type': 'string', 'anyOf': [{'type': 'string', 'format': 'python-module-name-relaxed'}, {'type': 'string', 'format': 'pep561-stub-name'}]}}, rule='type') @@ -187,15 +239,15 @@ def validate_https___setuptools_pypa_io_en_latest_userguide_pyproject_config_htm data__packages_len = len(data__packages) for data__packages_x, data__packages_item in enumerate(data__packages): validate_https___setuptools_pypa_io_en_latest_userguide_pyproject_config_html__definitions_package_name(data__packages_item, custom_formats, (name_prefix or "data") + ".packages[{data__packages_x}]".format(**locals())) - data__packages_one_of_count1 += 1 + data__packages_one_of_count2 += 1 except JsonSchemaValueException: pass - if data__packages_one_of_count1 < 2: + if data__packages_one_of_count2 < 2: try: validate_https___setuptools_pypa_io_en_latest_userguide_pyproject_config_html__definitions_find_directive(data__packages, custom_formats, (name_prefix or "data") + ".packages") - data__packages_one_of_count1 += 1 + data__packages_one_of_count2 += 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': {'$id': '#/definitions/package-name', 'title': 'Valid package name', 'description': 'Valid package name (importable or :pep:`561`).', 'type': 'string', 'anyOf': [{'type': 'string', 'format': 'python-module-name-relaxed'}, {'type': 'string', 'format': 'pep561-stub-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 data__packages_one_of_count2 != 1: + raise JsonSchemaValueException("" + (name_prefix or "data") + ".packages must be valid exactly by one definition" + (" (" + str(data__packages_one_of_count2) + " 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': {'$id': '#/definitions/package-name', 'title': 'Valid package name', 'description': 'Valid package name (importable or :pep:`561`).', 'type': 'string', 'anyOf': [{'type': 'string', 'format': 'python-module-name-relaxed'}, {'type': 'string', 'format': 'pep561-stub-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"] @@ -217,19 +269,19 @@ def validate_https___setuptools_pypa_io_en_latest_userguide_pyproject_config_htm data__packagedir_property_names = True for data__packagedir_key in data__packagedir: try: - data__packagedir_key_any_of_count2 = 0 - if not data__packagedir_key_any_of_count2: + data__packagedir_key_any_of_count3 = 0 + if not data__packagedir_key_any_of_count3: 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_any_of_count2 += 1 + data__packagedir_key_any_of_count3 += 1 except JsonSchemaValueException: pass - if not data__packagedir_key_any_of_count2: + if not data__packagedir_key_any_of_count3: try: validate_https___setuptools_pypa_io_en_latest_userguide_pyproject_config_html__definitions_package_name(data__packagedir_key, custom_formats, (name_prefix or "data") + ".package-dir") - data__packagedir_key_any_of_count2 += 1 + data__packagedir_key_any_of_count3 += 1 except JsonSchemaValueException: pass - if not data__packagedir_key_any_of_count2: + if not data__packagedir_key_any_of_count3: raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-dir cannot be validated by any definition", value=data__packagedir_key, name="" + (name_prefix or "data") + ".package-dir", definition={'anyOf': [{'const': ''}, {'$id': '#/definitions/package-name', 'title': 'Valid package name', 'description': 'Valid package name (importable or :pep:`561`).', 'type': 'string', 'anyOf': [{'type': 'string', 'format': 'python-module-name-relaxed'}, {'type': 'string', 'format': 'pep561-stub-name'}]}]}, rule='anyOf') except JsonSchemaValueException: data__packagedir_property_names = False @@ -262,23 +314,23 @@ def validate_https___setuptools_pypa_io_en_latest_userguide_pyproject_config_htm data__packagedata_property_names = True for data__packagedata_key in data__packagedata: try: - data__packagedata_key_any_of_count3 = 0 - if not data__packagedata_key_any_of_count3: + data__packagedata_key_any_of_count4 = 0 + if not data__packagedata_key_any_of_count4: try: if not isinstance(data__packagedata_key, (str)): raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-data must be string", value=data__packagedata_key, name="" + (name_prefix or "data") + ".package-data", definition={'type': 'string', 'format': 'python-module-name'}, rule='type') 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={'type': 'string', 'format': 'python-module-name'}, rule='format') - data__packagedata_key_any_of_count3 += 1 + data__packagedata_key_any_of_count4 += 1 except JsonSchemaValueException: pass - if not data__packagedata_key_any_of_count3: + if not data__packagedata_key_any_of_count4: 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_any_of_count3 += 1 + data__packagedata_key_any_of_count4 += 1 except JsonSchemaValueException: pass - if not data__packagedata_key_any_of_count3: + if not data__packagedata_key_any_of_count4: raise JsonSchemaValueException("" + (name_prefix or "data") + ".package-data cannot be validated by any definition", value=data__packagedata_key, name="" + (name_prefix or "data") + ".package-data", definition={'anyOf': [{'type': 'string', 'format': 'python-module-name'}, {'const': '*'}]}, rule='anyOf') except JsonSchemaValueException: data__packagedata_property_names = False @@ -316,23 +368,23 @@ def validate_https___setuptools_pypa_io_en_latest_userguide_pyproject_config_htm data__excludepackagedata_property_names = True for data__excludepackagedata_key in data__excludepackagedata: try: - data__excludepackagedata_key_any_of_count4 = 0 - if not data__excludepackagedata_key_any_of_count4: + data__excludepackagedata_key_any_of_count5 = 0 + if not data__excludepackagedata_key_any_of_count5: try: if not isinstance(data__excludepackagedata_key, (str)): raise JsonSchemaValueException("" + (name_prefix or "data") + ".exclude-package-data must be string", value=data__excludepackagedata_key, name="" + (name_prefix or "data") + ".exclude-package-data", definition={'type': 'string', 'format': 'python-module-name'}, rule='type') 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={'type': 'string', 'format': 'python-module-name'}, rule='format') - data__excludepackagedata_key_any_of_count4 += 1 + data__excludepackagedata_key_any_of_count5 += 1 except JsonSchemaValueException: pass - if not data__excludepackagedata_key_any_of_count4: + if not data__excludepackagedata_key_any_of_count5: 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_any_of_count4 += 1 + data__excludepackagedata_key_any_of_count5 += 1 except JsonSchemaValueException: pass - if not data__excludepackagedata_key_any_of_count4: + if not data__excludepackagedata_key_any_of_count5: raise JsonSchemaValueException("" + (name_prefix or "data") + ".exclude-package-data cannot be validated by any definition", value=data__excludepackagedata_key, name="" + (name_prefix or "data") + ".exclude-package-data", definition={'anyOf': [{'type': 'string', 'format': 'python-module-name'}, {'const': '*'}]}, rule='anyOf') except JsonSchemaValueException: data__excludepackagedata_property_names = False @@ -435,19 +487,19 @@ def validate_https___setuptools_pypa_io_en_latest_userguide_pyproject_config_htm 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: + data__dynamic__version_one_of_count6 = 0 + if data__dynamic__version_one_of_count6 < 2: try: validate_https___setuptools_pypa_io_en_latest_userguide_pyproject_config_html__definitions_attr_directive(data__dynamic__version, custom_formats, (name_prefix or "data") + ".dynamic.version") - data__dynamic__version_one_of_count5 += 1 + data__dynamic__version_one_of_count6 += 1 except JsonSchemaValueException: pass - if data__dynamic__version_one_of_count5 < 2: + if data__dynamic__version_one_of_count6 < 2: try: validate_https___setuptools_pypa_io_en_latest_userguide_pyproject_config_html__definitions_file_directive(data__dynamic__version, custom_formats, (name_prefix or "data") + ".dynamic.version") - data__dynamic__version_one_of_count5 += 1 + data__dynamic__version_one_of_count6 += 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`.', 'Also ensure to set ``project.dynamic`` accordingly.'], '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', 'format': 'python-qualified-identifier'}}, '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 data__dynamic__version_one_of_count6 != 1: + raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic.version must be valid exactly by one definition" + (" (" + str(data__dynamic__version_one_of_count6) + " 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`.', 'Also ensure to set ``project.dynamic`` accordingly.'], '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', 'format': 'python-qualified-identifier'}}, '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"] @@ -498,13 +550,13 @@ def validate_https___setuptools_pypa_io_en_latest_userguide_pyproject_config_htm data__dynamic__readme = data__dynamic["readme"] if not isinstance(data__dynamic__readme, (dict)): raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic.readme must be object", value=data__dynamic__readme, name="" + (name_prefix or "data") + ".dynamic.readme", definition={'type': 'object', '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']}, {'type': 'object', 'properties': {'content-type': {'type': 'string'}, 'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'additionalProperties': False}], 'required': ['file']}, rule='type') - data__dynamic__readme_any_of_count6 = 0 - if not data__dynamic__readme_any_of_count6: + data__dynamic__readme_any_of_count7 = 0 + if not data__dynamic__readme_any_of_count7: try: validate_https___setuptools_pypa_io_en_latest_userguide_pyproject_config_html__definitions_file_directive(data__dynamic__readme, custom_formats, (name_prefix or "data") + ".dynamic.readme") - data__dynamic__readme_any_of_count6 += 1 + data__dynamic__readme_any_of_count7 += 1 except JsonSchemaValueException: pass - if not data__dynamic__readme_any_of_count6: + if not data__dynamic__readme_any_of_count7: try: if not isinstance(data__dynamic__readme, (dict)): raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic.readme must be object", value=data__dynamic__readme, name="" + (name_prefix or "data") + ".dynamic.readme", definition={'type': 'object', 'properties': {'content-type': {'type': 'string'}, 'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'additionalProperties': False}, rule='type') @@ -522,9 +574,9 @@ def validate_https___setuptools_pypa_io_en_latest_userguide_pyproject_config_htm validate_https___setuptools_pypa_io_en_latest_userguide_pyproject_config_html__definitions_file_directive_properties_file(data__dynamic__readme__file, custom_formats, (name_prefix or "data") + ".dynamic.readme.file") if data__dynamic__readme_keys: raise JsonSchemaValueException("" + (name_prefix or "data") + ".dynamic.readme must not contain "+str(data__dynamic__readme_keys)+" properties", value=data__dynamic__readme, name="" + (name_prefix or "data") + ".dynamic.readme", definition={'type': 'object', 'properties': {'content-type': {'type': 'string'}, 'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'additionalProperties': False}, rule='additionalProperties') - data__dynamic__readme_any_of_count6 += 1 + data__dynamic__readme_any_of_count7 += 1 except JsonSchemaValueException: pass - if not data__dynamic__readme_any_of_count6: + if not data__dynamic__readme_any_of_count7: 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={'type': 'object', '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']}, {'type': 'object', 'properties': {'content-type': {'type': 'string'}, 'file': {'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}}, 'additionalProperties': False}], 'required': ['file']}, rule='anyOf') data__dynamic__readme_is_dict = isinstance(data__dynamic__readme, dict) if data__dynamic__readme_is_dict: @@ -538,14 +590,14 @@ def validate_https___setuptools_pypa_io_en_latest_userguide_pyproject_config_htm return data def validate_https___setuptools_pypa_io_en_latest_userguide_pyproject_config_html__definitions_file_directive_properties_file(data, custom_formats={}, name_prefix=None): - data_one_of_count7 = 0 - if data_one_of_count7 < 2: + data_one_of_count8 = 0 + if data_one_of_count8 < 2: try: if not isinstance(data, (str)): raise JsonSchemaValueException("" + (name_prefix or "data") + " must be string", value=data, name="" + (name_prefix or "data") + "", definition={'type': 'string'}, rule='type') - data_one_of_count7 += 1 + data_one_of_count8 += 1 except JsonSchemaValueException: pass - if data_one_of_count7 < 2: + if data_one_of_count8 < 2: try: if not isinstance(data, (list, tuple)): raise JsonSchemaValueException("" + (name_prefix or "data") + " must be array", value=data, name="" + (name_prefix or "data") + "", definition={'type': 'array', 'items': {'type': 'string'}}, rule='type') @@ -555,10 +607,10 @@ def validate_https___setuptools_pypa_io_en_latest_userguide_pyproject_config_htm for data_x, data_item in enumerate(data): if not isinstance(data_item, (str)): raise JsonSchemaValueException("" + (name_prefix or "data") + "[{data_x}]".format(**locals()) + " must be string", value=data_item, name="" + (name_prefix or "data") + "[{data_x}]".format(**locals()) + "", definition={'type': 'string'}, rule='type') - data_one_of_count7 += 1 + data_one_of_count8 += 1 except JsonSchemaValueException: pass - if data_one_of_count7 != 1: - raise JsonSchemaValueException("" + (name_prefix or "data") + " must be valid exactly by one definition" + (" (" + str(data_one_of_count7) + " matches found)"), value=data, name="" + (name_prefix or "data") + "", definition={'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}, rule='oneOf') + if data_one_of_count8 != 1: + raise JsonSchemaValueException("" + (name_prefix or "data") + " must be valid exactly by one definition" + (" (" + str(data_one_of_count8) + " matches found)"), value=data, name="" + (name_prefix or "data") + "", definition={'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}, rule='oneOf') return data def validate_https___setuptools_pypa_io_en_latest_userguide_pyproject_config_html__definitions_file_directive_for_dependencies(data, custom_formats={}, name_prefix=None): @@ -577,14 +629,14 @@ def validate_https___setuptools_pypa_io_en_latest_userguide_pyproject_config_htm if "file" in data_keys: data_keys.remove("file") data__file = data["file"] - data__file_one_of_count8 = 0 - if data__file_one_of_count8 < 2: + data__file_one_of_count9 = 0 + if data__file_one_of_count9 < 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_count8 += 1 + data__file_one_of_count9 += 1 except JsonSchemaValueException: pass - if data__file_one_of_count8 < 2: + if data__file_one_of_count9 < 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') @@ -594,10 +646,10 @@ def validate_https___setuptools_pypa_io_en_latest_userguide_pyproject_config_htm 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_count8 += 1 + data__file_one_of_count9 += 1 except JsonSchemaValueException: pass - if data__file_one_of_count8 != 1: - raise JsonSchemaValueException("" + (name_prefix or "data") + ".file must be valid exactly by one definition" + (" (" + str(data__file_one_of_count8) + " matches found)"), value=data__file, name="" + (name_prefix or "data") + ".file", definition={'oneOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}, rule='oneOf') + if data__file_one_of_count9 != 1: + raise JsonSchemaValueException("" + (name_prefix or "data") + ".file must be valid exactly by one definition" + (" (" + str(data__file_one_of_count9) + " 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 @@ -682,21 +734,21 @@ def validate_https___setuptools_pypa_io_en_latest_userguide_pyproject_config_htm raise JsonSchemaValueException("" + (name_prefix or "data") + ".define-macros[{data__definemacros_x}][0]".format(**locals()) + " must be string", value=data__definemacros_item__0, name="" + (name_prefix or "data") + ".define-macros[{data__definemacros_x}][0]".format(**locals()) + "", definition={'description': 'macro name', 'type': 'string'}, rule='type') if data__definemacros_item_len > 1: data__definemacros_item__1 = data__definemacros_item[1] - data__definemacros_item__1_one_of_count9 = 0 - if data__definemacros_item__1_one_of_count9 < 2: + data__definemacros_item__1_one_of_count10 = 0 + if data__definemacros_item__1_one_of_count10 < 2: try: if not isinstance(data__definemacros_item__1, (str)): raise JsonSchemaValueException("" + (name_prefix or "data") + ".define-macros[{data__definemacros_x}][1]".format(**locals()) + " must be string", value=data__definemacros_item__1, name="" + (name_prefix or "data") + ".define-macros[{data__definemacros_x}][1]".format(**locals()) + "", definition={'type': 'string'}, rule='type') - data__definemacros_item__1_one_of_count9 += 1 + data__definemacros_item__1_one_of_count10 += 1 except JsonSchemaValueException: pass - if data__definemacros_item__1_one_of_count9 < 2: + if data__definemacros_item__1_one_of_count10 < 2: try: if not isinstance(data__definemacros_item__1, (NoneType)): raise JsonSchemaValueException("" + (name_prefix or "data") + ".define-macros[{data__definemacros_x}][1]".format(**locals()) + " must be null", value=data__definemacros_item__1, name="" + (name_prefix or "data") + ".define-macros[{data__definemacros_x}][1]".format(**locals()) + "", definition={'type': 'null'}, rule='type') - data__definemacros_item__1_one_of_count9 += 1 + data__definemacros_item__1_one_of_count10 += 1 except JsonSchemaValueException: pass - if data__definemacros_item__1_one_of_count9 != 1: - raise JsonSchemaValueException("" + (name_prefix or "data") + ".define-macros[{data__definemacros_x}][1]".format(**locals()) + " must be valid exactly by one definition" + (" (" + str(data__definemacros_item__1_one_of_count9) + " matches found)"), value=data__definemacros_item__1, name="" + (name_prefix or "data") + ".define-macros[{data__definemacros_x}][1]".format(**locals()) + "", definition={'description': 'macro value', 'oneOf': [{'type': 'string'}, {'type': 'null'}]}, rule='oneOf') + if data__definemacros_item__1_one_of_count10 != 1: + raise JsonSchemaValueException("" + (name_prefix or "data") + ".define-macros[{data__definemacros_x}][1]".format(**locals()) + " must be valid exactly by one definition" + (" (" + str(data__definemacros_item__1_one_of_count10) + " matches found)"), value=data__definemacros_item__1, name="" + (name_prefix or "data") + ".define-macros[{data__definemacros_x}][1]".format(**locals()) + "", definition={'description': 'macro value', 'oneOf': [{'type': 'string'}, {'type': 'null'}]}, rule='oneOf') if data__definemacros_item_len > 2: raise JsonSchemaValueException("" + (name_prefix or "data") + ".define-macros[{data__definemacros_x}]".format(**locals()) + " must contain only specified items", value=data__definemacros_item, name="" + (name_prefix or "data") + ".define-macros[{data__definemacros_x}]".format(**locals()) + "", definition={'type': 'array', 'items': [{'description': 'macro name', 'type': 'string'}, {'description': 'macro value', 'oneOf': [{'type': 'string'}, {'type': 'null'}]}], 'additionalItems': False}, rule='items') if "undef-macros" in data_keys: @@ -889,26 +941,26 @@ def validate_https___setuptools_pypa_io_en_latest_userguide_pyproject_config_htm def validate_https___setuptools_pypa_io_en_latest_userguide_pyproject_config_html__definitions_package_name(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/package-name', 'title': 'Valid package name', 'description': 'Valid package name (importable or :pep:`561`).', 'type': 'string', 'anyOf': [{'type': 'string', 'format': 'python-module-name-relaxed'}, {'type': 'string', 'format': 'pep561-stub-name'}]}, rule='type') - data_any_of_count10 = 0 - if not data_any_of_count10: + data_any_of_count11 = 0 + if not data_any_of_count11: try: if not isinstance(data, (str)): raise JsonSchemaValueException("" + (name_prefix or "data") + " must be string", value=data, name="" + (name_prefix or "data") + "", definition={'type': 'string', 'format': 'python-module-name-relaxed'}, rule='type') if isinstance(data, str): if not custom_formats["python-module-name-relaxed"](data): raise JsonSchemaValueException("" + (name_prefix or "data") + " must be python-module-name-relaxed", value=data, name="" + (name_prefix or "data") + "", definition={'type': 'string', 'format': 'python-module-name-relaxed'}, rule='format') - data_any_of_count10 += 1 + data_any_of_count11 += 1 except JsonSchemaValueException: pass - if not data_any_of_count10: + if not data_any_of_count11: try: if not isinstance(data, (str)): raise JsonSchemaValueException("" + (name_prefix or "data") + " must be string", value=data, name="" + (name_prefix or "data") + "", definition={'type': 'string', 'format': 'pep561-stub-name'}, rule='type') if isinstance(data, str): if not custom_formats["pep561-stub-name"](data): raise JsonSchemaValueException("" + (name_prefix or "data") + " must be pep561-stub-name", value=data, name="" + (name_prefix or "data") + "", definition={'type': 'string', 'format': 'pep561-stub-name'}, rule='format') - data_any_of_count10 += 1 + data_any_of_count11 += 1 except JsonSchemaValueException: pass - if not data_any_of_count10: + if not data_any_of_count11: raise JsonSchemaValueException("" + (name_prefix or "data") + " cannot be validated by any definition", value=data, name="" + (name_prefix or "data") + "", definition={'$id': '#/definitions/package-name', 'title': 'Valid package name', 'description': 'Valid package name (importable or :pep:`561`).', 'type': 'string', 'anyOf': [{'type': 'string', 'format': 'python-module-name-relaxed'}, {'type': 'string', 'format': 'pep561-stub-name'}]}, rule='anyOf') return data @@ -933,12 +985,63 @@ def validate_https___setuptools_pypa_io_en_latest_deprecated_distutils_configfil def validate_https___packaging_python_org_en_latest_specifications_pyproject_toml(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/pyproject-toml/', '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://peps.python.org/pep-0621/#authors-maintainers', 'type': 'object', 'additionalProperties': False, '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://peps.python.org/pep-0621/#authors-maintainers', 'type': 'object', 'additionalProperties': False, '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://peps.python.org/pep-0621/#authors-maintainers', 'type': 'object', 'additionalProperties': False, '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/pyproject-toml/', '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': [{'type': 'string', 'description': 'An SPDX license identifier', 'format': 'SPDX'}, {'type': 'object', 'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'type': 'object', 'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'license-files': {'description': 'Paths or globs to paths of license files', 'type': 'array', 'items': {'type': 'string'}}, 'authors': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://peps.python.org/pep-0621/#authors-maintainers', 'type': 'object', 'additionalProperties': False, '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://peps.python.org/pep-0621/#authors-maintainers', 'type': 'object', 'additionalProperties': False, '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', 'license-files', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'allOf': [{'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']}}, {'if': {'required': ['license-files']}, 'then': {'properties': {'license': {'type': 'string'}}}}], 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://peps.python.org/pep-0621/#authors-maintainers', 'type': 'object', 'additionalProperties': False, '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') + try: + try: + data_is_dict = isinstance(data, dict) + if data_is_dict: + data__missing_keys = set(['dynamic']) - data.keys() + if data__missing_keys: + raise JsonSchemaValueException("" + (name_prefix or "data") + " must contain " + (str(sorted(data__missing_keys)) + " 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__missing_keys = set(['version']) - data.keys() + if data__missing_keys: + raise JsonSchemaValueException("" + (name_prefix or "data") + " must contain " + (str(sorted(data__missing_keys)) + " properties"), value=data, name="" + (name_prefix or "data") + "", definition={'required': ['version'], '$$description': ['version should be statically defined in the ``version`` field']}, rule='required') + try: + data_is_dict = isinstance(data, dict) + if data_is_dict: + data__missing_keys = set(['license-files']) - data.keys() + if data__missing_keys: + raise JsonSchemaValueException("" + (name_prefix or "data") + " must contain " + (str(sorted(data__missing_keys)) + " properties"), value=data, name="" + (name_prefix or "data") + "", definition={'required': ['license-files']}, rule='required') + except JsonSchemaValueException: + pass + else: + data_is_dict = isinstance(data, dict) + if data_is_dict: + data_keys = set(data.keys()) + if "license" in data_keys: + data_keys.remove("license") + data__license = data["license"] + if not isinstance(data__license, (str)): + raise JsonSchemaValueException("" + (name_prefix or "data") + ".license must be string", value=data__license, name="" + (name_prefix or "data") + ".license", definition={'type': 'string'}, rule='type') data_is_dict = isinstance(data, dict) if data_is_dict: data__missing_keys = set(['name']) - data.keys() if data__missing_keys: - raise JsonSchemaValueException("" + (name_prefix or "data") + " must contain " + (str(sorted(data__missing_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/pyproject-toml/', '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://peps.python.org/pep-0621/#authors-maintainers', 'type': 'object', 'additionalProperties': False, '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://peps.python.org/pep-0621/#authors-maintainers', 'type': 'object', 'additionalProperties': False, '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://peps.python.org/pep-0621/#authors-maintainers', 'type': 'object', 'additionalProperties': False, '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 " + (str(sorted(data__missing_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/pyproject-toml/', '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': [{'type': 'string', 'description': 'An SPDX license identifier', 'format': 'SPDX'}, {'type': 'object', 'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'type': 'object', 'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'license-files': {'description': 'Paths or globs to paths of license files', 'type': 'array', 'items': {'type': 'string'}}, 'authors': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://peps.python.org/pep-0621/#authors-maintainers', 'type': 'object', 'additionalProperties': False, '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://peps.python.org/pep-0621/#authors-maintainers', 'type': 'object', 'additionalProperties': False, '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', 'license-files', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'allOf': [{'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']}}, {'if': {'required': ['license-files']}, 'then': {'properties': {'license': {'type': 'string'}}}}], 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://peps.python.org/pep-0621/#authors-maintainers', 'type': 'object', 'additionalProperties': False, '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") @@ -964,19 +1067,19 @@ def validate_https___packaging_python_org_en_latest_specifications_pyproject_tom if "readme" in data_keys: data_keys.remove("readme") data__readme = data["readme"] - data__readme_one_of_count11 = 0 - if data__readme_one_of_count11 < 2: + data__readme_one_of_count12 = 0 + if data__readme_one_of_count12 < 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_count11 += 1 + data__readme_one_of_count12 += 1 except JsonSchemaValueException: pass - if data__readme_one_of_count11 < 2: + if data__readme_one_of_count12 < 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_count12 = 0 - if not data__readme_any_of_count12: + data__readme_any_of_count13 = 0 + if not data__readme_any_of_count13: try: data__readme_is_dict = isinstance(data__readme, dict) if data__readme_is_dict: @@ -989,9 +1092,9 @@ def validate_https___packaging_python_org_en_latest_specifications_pyproject_tom 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_count12 += 1 + data__readme_any_of_count13 += 1 except JsonSchemaValueException: pass - if not data__readme_any_of_count12: + if not data__readme_any_of_count13: try: data__readme_is_dict = isinstance(data__readme, dict) if data__readme_is_dict: @@ -1004,9 +1107,9 @@ def validate_https___packaging_python_org_en_latest_specifications_pyproject_tom 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_count12 += 1 + data__readme_any_of_count13 += 1 except JsonSchemaValueException: pass - if not data__readme_any_of_count12: + if not data__readme_any_of_count13: 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: @@ -1019,10 +1122,10 @@ def validate_https___packaging_python_org_en_latest_specifications_pyproject_tom 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_count11 += 1 + data__readme_one_of_count12 += 1 except JsonSchemaValueException: pass - if data__readme_one_of_count11 != 1: - raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme must be valid exactly by one definition" + (" (" + str(data__readme_one_of_count11) + " 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 data__readme_one_of_count12 != 1: + raise JsonSchemaValueException("" + (name_prefix or "data") + ".readme must be valid exactly by one definition" + (" (" + str(data__readme_one_of_count12) + " 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"] @@ -1034,39 +1137,63 @@ def validate_https___packaging_python_org_en_latest_specifications_pyproject_tom if "license" in data_keys: data_keys.remove("license") data__license = data["license"] - data__license_one_of_count13 = 0 - if data__license_one_of_count13 < 2: + data__license_one_of_count14 = 0 + if data__license_one_of_count14 < 2: + try: + if not isinstance(data__license, (str)): + raise JsonSchemaValueException("" + (name_prefix or "data") + ".license must be string", value=data__license, name="" + (name_prefix or "data") + ".license", definition={'type': 'string', 'description': 'An SPDX license identifier', 'format': 'SPDX'}, rule='type') + if isinstance(data__license, str): + if not custom_formats["SPDX"](data__license): + raise JsonSchemaValueException("" + (name_prefix or "data") + ".license must be SPDX", value=data__license, name="" + (name_prefix or "data") + ".license", definition={'type': 'string', 'description': 'An SPDX license identifier', 'format': 'SPDX'}, rule='format') + data__license_one_of_count14 += 1 + except JsonSchemaValueException: pass + if data__license_one_of_count14 < 2: try: + if not isinstance(data__license, (dict)): + raise JsonSchemaValueException("" + (name_prefix or "data") + ".license must be object", value=data__license, name="" + (name_prefix or "data") + ".license", definition={'type': 'object', 'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, rule='type') data__license_is_dict = isinstance(data__license, dict) if data__license_is_dict: data__license__missing_keys = set(['file']) - data__license.keys() if data__license__missing_keys: - raise JsonSchemaValueException("" + (name_prefix or "data") + ".license must contain " + (str(sorted(data__license__missing_keys)) + " 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') + raise JsonSchemaValueException("" + (name_prefix or "data") + ".license must contain " + (str(sorted(data__license__missing_keys)) + " properties"), value=data__license, name="" + (name_prefix or "data") + ".license", definition={'type': 'object', '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_count13 += 1 + data__license_one_of_count14 += 1 except JsonSchemaValueException: pass - if data__license_one_of_count13 < 2: + if data__license_one_of_count14 < 2: try: + if not isinstance(data__license, (dict)): + raise JsonSchemaValueException("" + (name_prefix or "data") + ".license must be object", value=data__license, name="" + (name_prefix or "data") + ".license", definition={'type': 'object', '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='type') data__license_is_dict = isinstance(data__license, dict) if data__license_is_dict: data__license__missing_keys = set(['text']) - data__license.keys() if data__license__missing_keys: - raise JsonSchemaValueException("" + (name_prefix or "data") + ".license must contain " + (str(sorted(data__license__missing_keys)) + " 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') + raise JsonSchemaValueException("" + (name_prefix or "data") + ".license must contain " + (str(sorted(data__license__missing_keys)) + " properties"), value=data__license, name="" + (name_prefix or "data") + ".license", definition={'type': 'object', '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_count13 += 1 + data__license_one_of_count14 += 1 except JsonSchemaValueException: pass - if data__license_one_of_count13 != 1: - raise JsonSchemaValueException("" + (name_prefix or "data") + ".license must be valid exactly by one definition" + (" (" + str(data__license_one_of_count13) + " 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 data__license_one_of_count14 != 1: + raise JsonSchemaValueException("" + (name_prefix or "data") + ".license must be valid exactly by one definition" + (" (" + str(data__license_one_of_count14) + " matches found)"), value=data__license, name="" + (name_prefix or "data") + ".license", definition={'description': '`Project license `_.', 'oneOf': [{'type': 'string', 'description': 'An SPDX license identifier', 'format': 'SPDX'}, {'type': 'object', 'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'type': 'object', '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 "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={'description': 'Paths or globs to paths of license files', 'type': 'array', 'items': {'type': 'string'}}, 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') if "authors" in data_keys: data_keys.remove("authors") data__authors = data["authors"] @@ -1211,49 +1338,15 @@ def validate_https___packaging_python_org_en_latest_specifications_pyproject_tom 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') + 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', 'license-files', '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__dynamic_item not in ['version', 'description', 'readme', 'requires-python', 'license', 'license-files', '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', 'license-files', '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', 'license-files', '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/pyproject-toml/', '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://peps.python.org/pep-0621/#authors-maintainers', 'type': 'object', 'additionalProperties': False, '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://peps.python.org/pep-0621/#authors-maintainers', 'type': 'object', 'additionalProperties': False, '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://peps.python.org/pep-0621/#authors-maintainers', 'type': 'object', 'additionalProperties': False, '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__missing_keys = set(['dynamic']) - data.keys() - if data__missing_keys: - raise JsonSchemaValueException("" + (name_prefix or "data") + " must contain " + (str(sorted(data__missing_keys)) + " 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__missing_keys = set(['version']) - data.keys() - if data__missing_keys: - raise JsonSchemaValueException("" + (name_prefix or "data") + " must contain " + (str(sorted(data__missing_keys)) + " properties"), value=data, name="" + (name_prefix or "data") + "", definition={'required': ['version'], '$$description': ['version should be statically defined in the ``version`` field']}, rule='required') + 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/pyproject-toml/', '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': [{'type': 'string', 'description': 'An SPDX license identifier', 'format': 'SPDX'}, {'type': 'object', 'properties': {'file': {'type': 'string', '$$description': ['Relative path to the file (UTF-8) which contains the license for the', 'project.']}}, 'required': ['file']}, {'type': 'object', 'properties': {'text': {'type': 'string', '$$description': ['The license of the project whose meaning is that of the', '`License field from the core metadata', '`_.']}}, 'required': ['text']}]}, 'license-files': {'description': 'Paths or globs to paths of license files', 'type': 'array', 'items': {'type': 'string'}}, 'authors': {'type': 'array', 'items': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://peps.python.org/pep-0621/#authors-maintainers', 'type': 'object', 'additionalProperties': False, '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://peps.python.org/pep-0621/#authors-maintainers', 'type': 'object', 'additionalProperties': False, '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', 'license-files', 'authors', 'maintainers', 'keywords', 'classifiers', 'urls', 'scripts', 'gui-scripts', 'entry-points', 'dependencies', 'optional-dependencies']}}}, 'required': ['name'], 'additionalProperties': False, 'allOf': [{'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']}}, {'if': {'required': ['license-files']}, 'then': {'properties': {'license': {'type': 'string'}}}}], 'definitions': {'author': {'$id': '#/definitions/author', 'title': 'Author or Maintainer', '$comment': 'https://peps.python.org/pep-0621/#authors-maintainers', 'type': 'object', 'additionalProperties': False, '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___packaging_python_org_en_latest_specifications_pyproject_toml___definitions_dependency(data, custom_formats={}, name_prefix=None): diff --git a/setuptools/config/_validate_pyproject/formats.py b/setuptools/config/_validate_pyproject/formats.py index 153b1f0b27..1cf4a465ef 100644 --- a/setuptools/config/_validate_pyproject/formats.py +++ b/setuptools/config/_validate_pyproject/formats.py @@ -164,12 +164,15 @@ class _TroveClassifier: """ downloaded: typing.Union[None, "Literal[False]", typing.Set[str]] + """ + None => not cached yet + False => unavailable + set => cached values + """ def __init__(self) -> None: self.downloaded = 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) -> None: @@ -351,7 +354,7 @@ def python_entrypoint_reference(value: str) -> bool: obj = rest module_parts = module.split(".") - identifiers = _chain(module_parts, obj.split(".")) if rest else module_parts + identifiers = _chain(module_parts, obj.split(".")) if rest else iter(module_parts) return all(python_identifier(i.strip()) for i in identifiers) @@ -373,3 +376,27 @@ def uint(value: builtins.int) -> bool: def int(value: builtins.int) -> bool: r"""Signed 64-bit integer (:math:`-2^{63} \leq x < 2^{63}`)""" return -(2**63) <= value < 2**63 + + +try: + from packaging import licenses as _licenses + + def SPDX(value: str) -> bool: + """See :ref:`PyPA's License-Expression specification + ` (added in :pep:`639`). + """ + try: + _licenses.canonicalize_license_expression(value) + return True + except _licenses.InvalidLicenseExpression: + return False + +except ImportError: # pragma: no cover + _logger.warning( + "Could not find an up-to-date installation of `packaging`. " + "License expressions might not be validated. " + "To enforce validation, please install `packaging>=24.2`." + ) + + def SPDX(value: str) -> bool: + return True diff --git a/tox.ini b/tox.ini index 12e156a3fa..942e2b9835 100644 --- a/tox.ini +++ b/tox.ini @@ -83,7 +83,7 @@ commands = [testenv:generate-validation-code] skip_install = True deps = - validate-pyproject[all]==0.19 + validate-pyproject[all]==0.23 commands = python -m tools.generate_validation_code From fe63c2fd7c4dd91954cac0f59338d5e37816a98b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 16 Feb 2025 20:54:15 +0100 Subject: [PATCH 03/36] Adjust test example --- setuptools/tests/test_bdist_wheel.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/setuptools/tests/test_bdist_wheel.py b/setuptools/tests/test_bdist_wheel.py index 0f2e6ce136..0cc1ddfd99 100644 --- a/setuptools/tests/test_bdist_wheel.py +++ b/setuptools/tests/test_bdist_wheel.py @@ -59,7 +59,7 @@ EXAMPLES = { "dummy-dist": { "setup.py": SETUPPY_EXAMPLE, - "licenses": {"DUMMYFILE": ""}, + "licenses_dir": {"DUMMYFILE": ""}, **dict.fromkeys(DEFAULT_LICENSE_FILES | OTHER_IGNORED_FILES, ""), }, "simple-dist": { @@ -324,26 +324,26 @@ def test_licenses_default(dummy_dist, monkeypatch, tmp_path): def test_licenses_deprecated(dummy_dist, monkeypatch, tmp_path): dummy_dist.joinpath("setup.cfg").write_text( - "[metadata]\nlicense_file=licenses/DUMMYFILE", encoding="utf-8" + "[metadata]\nlicense_file=licenses_dir/DUMMYFILE", encoding="utf-8" ) monkeypatch.chdir(dummy_dist) bdist_wheel_cmd(bdist_dir=str(tmp_path)).run() with ZipFile("dist/dummy_dist-1.0-py3-none-any.whl") as wf: - license_files = {"dummy_dist-1.0.dist-info/licenses/licenses/DUMMYFILE"} + license_files = {"dummy_dist-1.0.dist-info/licenses/licenses_dir/DUMMYFILE"} assert set(wf.namelist()) == DEFAULT_FILES | license_files @pytest.mark.parametrize( ("config_file", "config"), [ - ("setup.cfg", "[metadata]\nlicense_files=licenses/*\n LICENSE"), - ("setup.cfg", "[metadata]\nlicense_files=licenses/*, LICENSE"), + ("setup.cfg", "[metadata]\nlicense_files=licenses_dir/*\n LICENSE"), + ("setup.cfg", "[metadata]\nlicense_files=licenses_dir/*, LICENSE"), ( "setup.py", SETUPPY_EXAMPLE.replace( - ")", " license_files=['licenses/DUMMYFILE', 'LICENSE'])" + ")", " license_files=['licenses_dir/DUMMYFILE', 'LICENSE'])" ), ), ], @@ -355,11 +355,11 @@ def test_licenses_override(dummy_dist, monkeypatch, tmp_path, config_file, confi with ZipFile("dist/dummy_dist-1.0-py3-none-any.whl") as wf: license_files = { "dummy_dist-1.0.dist-info/licenses/" + fname - for fname in {"licenses/DUMMYFILE", "LICENSE"} + for fname in {"licenses_dir/DUMMYFILE", "LICENSE"} } assert set(wf.namelist()) == DEFAULT_FILES | license_files metadata = wf.read("dummy_dist-1.0.dist-info/METADATA").decode("utf8") - assert "License-File: licenses/DUMMYFILE" in metadata + assert "License-File: licenses_dir/DUMMYFILE" in metadata assert "License-File: LICENSE" in metadata From 3e9b9c7fbe4b753bbc80ee8be01c9a8a9233970d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 17 Feb 2025 13:02:59 +0100 Subject: [PATCH 04/36] Use os.sep for replace --- setuptools/dist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index 962da7c34b..c6a3468123 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -419,7 +419,7 @@ def _finalize_license_files(self) -> None: self.metadata.license_files = list( map( - lambda path: path.replace("\\", "/"), + lambda path: path.replace(os.sep, "/"), unique_everseen(self._expand_patterns(patterns)), ) ) From 285d681810d38c5745283e1c5385b6eb0345dece Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 28 Oct 2024 06:01:58 +0100 Subject: [PATCH 05/36] Add initial support for License-Expression (PEP 639) --- docs/userguide/pyproject_config.rst | 2 +- newsfragments/4706.feature.rst | 1 + pyproject.toml | 2 +- setuptools/_core_metadata.py | 10 ++- setuptools/config/_apply_pyprojecttoml.py | 17 ++--- setuptools/dist.py | 1 + .../tests/config/test_apply_pyprojecttoml.py | 63 +++++++++++++++++++ 7 files changed, 84 insertions(+), 12 deletions(-) create mode 100644 newsfragments/4706.feature.rst diff --git a/docs/userguide/pyproject_config.rst b/docs/userguide/pyproject_config.rst index e988fec7ac..efc68603a9 100644 --- a/docs/userguide/pyproject_config.rst +++ b/docs/userguide/pyproject_config.rst @@ -49,7 +49,7 @@ The ``project`` table contains metadata fields as described by the readme = "README.rst" requires-python = ">=3.8" keywords = ["one", "two"] - license = {text = "BSD-3-Clause"} + license = "BSD-3-Clause" classifiers = [ "Framework :: Django", "Programming Language :: Python :: 3", diff --git a/newsfragments/4706.feature.rst b/newsfragments/4706.feature.rst new file mode 100644 index 0000000000..38b09276c0 --- /dev/null +++ b/newsfragments/4706.feature.rst @@ -0,0 +1 @@ +Added initial support for ``License-Expression`` (`PEP 639 `_). -- by :user:`cdce8p` diff --git a/pyproject.toml b/pyproject.toml index a9febdbe8c..bd22d79294 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,10 +11,10 @@ authors = [ ] description = "Easily download, build, install, upgrade, and uninstall Python packages" readme = "README.rst" +license = "MIT" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Topic :: Software Development :: Libraries :: Python Modules", diff --git a/setuptools/_core_metadata.py b/setuptools/_core_metadata.py index 850cc409f7..a407f6915b 100644 --- a/setuptools/_core_metadata.py +++ b/setuptools/_core_metadata.py @@ -88,6 +88,7 @@ def read_pkg_file(self, file): 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') + self.license_expression = _read_field_unescaped_from_msg(msg, 'license_expression') self.long_description = _read_field_unescaped_from_msg(msg, 'description') if self.long_description is None and self.metadata_version >= Version('2.1'): @@ -175,9 +176,12 @@ def write_field(key, value): if attr_val is not None: write_field(field, attr_val) - license = self.get_license() - if license: - write_field('License', rfc822_escape(license)) + if self.license_expression: + write_field('License-Expression', rfc822_escape(self.license_expression)) + else: + license = self.get_license() + if license: + write_field('License', rfc822_escape(license)) for label, url in self.project_urls.items(): write_field('Project-URL', f'{label}, {url}') diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py index 331596bdd7..6664c6158a 100644 --- a/setuptools/config/_apply_pyprojecttoml.py +++ b/setuptools/config/_apply_pyprojecttoml.py @@ -181,16 +181,19 @@ def _long_description( dist._referenced_files.add(file) -def _license(dist: Distribution, val: dict, root_dir: StrPath | None): +def _license(dist: Distribution, val: str | dict, root_dir: StrPath | None): from setuptools.config import expand - if "file" in val: - # XXX: Is it completely safe to assume static? - value = expand.read_files([val["file"]], root_dir) - _set_config(dist, "license", _static.Str(value)) - dist._referenced_files.add(val["file"]) + if isinstance(val, str): + _set_config(dist, "license_expression", _static.Str(val)) else: - _set_config(dist, "license", _static.Str(val["text"])) + if "file" in val: + # XXX: Is it completely safe to assume static? + value = expand.read_files([val["file"]], root_dir) + _set_config(dist, "license", _static.Str(value)) + dist._referenced_files.add(val["file"]) + else: + _set_config(dist, "license", _static.Str(val["text"])) def _people(dist: Distribution, val: list[dict], _root_dir: StrPath | None, kind: str): diff --git a/setuptools/dist.py b/setuptools/dist.py index c6a3468123..55175f5e57 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -288,6 +288,7 @@ class Distribution(_Distribution): 'long_description_content_type': lambda: None, 'project_urls': dict, 'provides_extras': dict, # behaves like an ordered set + 'license_expression': lambda: None, 'license_file': lambda: None, 'license_files': lambda: None, 'install_requires': list, diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index 20146b4a89..a528a33f0d 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -156,6 +156,28 @@ def main_gui(): pass def main_tomatoes(): pass """ +PEP639_LICENSE_TEXT = """\ +[project] +name = "spam" +version = "2020.0.0" +authors = [ + {email = "hi@pradyunsg.me"}, + {name = "Tzu-Ping Chung"} +] +license = {text = "MIT"} +""" + +PEP639_LICENSE_EXPRESSION = """\ +[project] +name = "spam" +version = "2020.0.0" +authors = [ + {email = "hi@pradyunsg.me"}, + {name = "Tzu-Ping Chung"} +] +license = "MIT" +""" + def _pep621_example_project( tmp_path, @@ -251,6 +273,47 @@ def test_utf8_maintainer_in_metadata( # issue-3663 assert f"Maintainer-email: {expected_maintainers_meta_value}" in content +@pytest.mark.parametrize( + ('pyproject_text', 'license', 'license_expression', 'content_str'), + ( + pytest.param( + PEP639_LICENSE_TEXT, + 'MIT', + None, + 'License: MIT', + id='license-text', + ), + pytest.param( + PEP639_LICENSE_EXPRESSION, + None, + 'MIT', + 'License-Expression: MIT', + id='license-expression', + ), + ), +) +def test_license_in_metadata( + license, + license_expression, + content_str, + pyproject_text, + tmp_path, +): + pyproject = _pep621_example_project( + tmp_path, + "README", + pyproject_text=pyproject_text, + ) + dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + assert dist.metadata.license == license + assert dist.metadata.license_expression == license_expression + pkg_file = tmp_path / "PKG-FILE" + with open(pkg_file, "w", encoding="utf-8") as fh: + dist.metadata.write_pkg_file(fh) + content = pkg_file.read_text(encoding="utf-8") + assert content_str in content + + class TestLicenseFiles: # TODO: After PEP 639 is accepted, we have to move the license-files # to the `project` table instead of `tool.setuptools` From 420766cdd02008b80d019368c37880b8565b5a7c Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 25 Nov 2024 01:05:56 +0100 Subject: [PATCH 06/36] Additional test case --- .../tests/config/test_apply_pyprojecttoml.py | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index a528a33f0d..03f950ecd1 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -315,9 +315,6 @@ def test_license_in_metadata( class TestLicenseFiles: - # TODO: After PEP 639 is accepted, we have to move the license-files - # to the `project` table instead of `tool.setuptools` - def base_pyproject(self, tmp_path, additional_text): pyproject = _pep621_example_project(tmp_path, "README") text = pyproject.read_text(encoding="utf-8") @@ -330,6 +327,24 @@ def base_pyproject(self, tmp_path, additional_text): pyproject.write_text(text, encoding="utf-8") return pyproject + def base_pyproject_license_pep639(self, 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 'license-files' not in text + assert "[tool.setuptools]" not in text + + text = re.sub( + r"(license = {file = \"LICENSE.txt\"})\n", + ("license = \"licenseref-Proprietary\"\nlicense-files = [\"_FILE*\"]\n"), + text, + count=1, + ) + pyproject.write_text(text, encoding="utf-8") + return pyproject + def test_both_license_and_license_files_defined(self, tmp_path): setuptools_config = '[tool.setuptools]\nlicense-files = ["_FILE*"]' pyproject = self.base_pyproject(tmp_path, setuptools_config) @@ -346,6 +361,18 @@ def test_both_license_and_license_files_defined(self, tmp_path): assert set(dist.metadata.license_files) == {"_FILE.rst", "_FILE.txt"} assert dist.metadata.license == "LicenseRef-Proprietary\n" + def test_both_license_and_license_files_defined_pep639(self, tmp_path): + # Set license and license-files + pyproject = self.base_pyproject_license_pep639(tmp_path) + + (tmp_path / "_FILE.txt").touch() + (tmp_path / "_FILE.rst").touch() + + dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + assert set(dist.metadata.license_files) == {"_FILE.rst", "_FILE.txt"} + assert dist.metadata.license is None + assert dist.metadata.license_expression == "LicenseRef-Proprietary" + def test_default_patterns(self, tmp_path): setuptools_config = '[tool.setuptools]\nzip-safe = false' # ^ used just to trigger section validation From 346bf17e0cc8fc6e8b0ea3e6dafa3af91009da6d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 25 Nov 2024 01:05:13 +0100 Subject: [PATCH 07/36] Normalize license expression --- setuptools/config/_apply_pyprojecttoml.py | 1 + setuptools/dist.py | 20 ++++++++++++++++ .../tests/config/test_apply_pyprojecttoml.py | 23 +++++++++++++++---- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py index 6664c6158a..6bb2bea514 100644 --- a/setuptools/config/_apply_pyprojecttoml.py +++ b/setuptools/config/_apply_pyprojecttoml.py @@ -58,6 +58,7 @@ def apply(dist: Distribution, config: dict, filename: StrPath) -> Distribution: os.chdir(root_dir) try: dist._finalize_requires() + dist._finalize_license_expression() dist._finalize_license_files() finally: os.chdir(current_directory) diff --git a/setuptools/dist.py b/setuptools/dist.py index 55175f5e57..7a13ea6a04 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -12,6 +12,7 @@ from typing import TYPE_CHECKING, Any, Union from more_itertools import partition, unique_everseen +from packaging.licenses import canonicalize_license_expression from packaging.markers import InvalidMarker, Marker from packaging.specifiers import InvalidSpecifier, SpecifierSet from packaging.version import Version @@ -27,6 +28,7 @@ from ._reqs import _StrOrIter from .config import pyprojecttoml, setupcfg from .discovery import ConfigDiscovery +from .errors import InvalidConfigError from .monkey import get_unpatched from .warnings import InformationOnly, SetuptoolsDeprecationWarning @@ -403,6 +405,23 @@ def _normalize_requires(self): (k, list(map(str, _reqs.parse(v or [])))) for k, v in extras_require.items() ) + def _finalize_license_expression(self) -> None: + """Normalize license and license_expression.""" + license_expr = self.metadata.license_expression + if license_expr: + normalized = canonicalize_license_expression(license_expr) + if license_expr != normalized: + InformationOnly.emit(f"Normalizing '{license_expr}' to '{normalized}'") + self.metadata.license_expression = normalized + + for cl in self.metadata.get_classifiers(): + if not cl.startswith("License :: "): + continue + raise InvalidConfigError( + "License classifier are deprecated in favor of the license expression. " + f"Remove the '{cl}' classifier." + ) + def _finalize_license_files(self) -> None: """Compute names of all license files which should be included.""" license_files: list[str] | None = self.metadata.license_files @@ -656,6 +675,7 @@ def parse_config_files( pyprojecttoml.apply_configuration(self, filename, ignore_option_errors) self._finalize_requires() + self._finalize_license_expression() self._finalize_license_files() def fetch_build_eggs( diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index 03f950ecd1..91883b4618 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -23,7 +23,7 @@ from setuptools.config import expand, pyprojecttoml, setupcfg from setuptools.config._apply_pyprojecttoml import _MissingDynamic, _some_attrgetter from setuptools.dist import Distribution -from setuptools.errors import RemovedConfigError +from setuptools.errors import InvalidConfigError, RemovedConfigError from .downloads import retrieve_file, urls_from_file @@ -175,7 +175,10 @@ def main_tomatoes(): pass {email = "hi@pradyunsg.me"}, {name = "Tzu-Ping Chung"} ] -license = "MIT" +license = "mit or apache-2.0" # should be normalized in metadata +classifiers = [ + "Development Status :: 5 - Production/Stable", +] """ @@ -286,8 +289,8 @@ def test_utf8_maintainer_in_metadata( # issue-3663 pytest.param( PEP639_LICENSE_EXPRESSION, None, - 'MIT', - 'License-Expression: MIT', + 'MIT OR Apache-2.0', + 'License-Expression: MIT OR Apache-2.0', id='license-expression', ), ), @@ -314,6 +317,18 @@ def test_license_in_metadata( assert content_str in content +def test_license_expression_with_bad_classifier(tmp_path): + text = PEP639_LICENSE_EXPRESSION.rsplit("\n", 2)[0] + pyproject = _pep621_example_project( + tmp_path, + "README", + f"{text}\n \"License :: OSI Approved :: MIT License\"\n]", + ) + msg = "License classifier are deprecated.*'License :: OSI Approved :: MIT License'" + with pytest.raises(InvalidConfigError, match=msg): + pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + + class TestLicenseFiles: def base_pyproject(self, tmp_path, additional_text): pyproject = _pep621_example_project(tmp_path, "README") From 31d83409f26018b79ee5459445ce7e9b87752e97 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 25 Nov 2024 01:07:59 +0100 Subject: [PATCH 08/36] Remove License-Expression field --- newsfragments/4706.feature.rst | 2 +- setuptools/_core_metadata.py | 9 +++------ setuptools/tests/config/test_apply_pyprojecttoml.py | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/newsfragments/4706.feature.rst b/newsfragments/4706.feature.rst index 38b09276c0..1d34f5f476 100644 --- a/newsfragments/4706.feature.rst +++ b/newsfragments/4706.feature.rst @@ -1 +1 @@ -Added initial support for ``License-Expression`` (`PEP 639 `_). -- by :user:`cdce8p` +Added initial support for license expression (`PEP 639 `_). -- by :user:`cdce8p` diff --git a/setuptools/_core_metadata.py b/setuptools/_core_metadata.py index a407f6915b..25937f913c 100644 --- a/setuptools/_core_metadata.py +++ b/setuptools/_core_metadata.py @@ -176,12 +176,9 @@ def write_field(key, value): if attr_val is not None: write_field(field, attr_val) - if self.license_expression: - write_field('License-Expression', rfc822_escape(self.license_expression)) - else: - license = self.get_license() - if license: - write_field('License', rfc822_escape(license)) + license = self.license_expression or self.get_license() + if license: + write_field('License', rfc822_escape(license)) for label, url in self.project_urls.items(): write_field('Project-URL', f'{label}, {url}') diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index 91883b4618..c9ab1f5718 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -290,7 +290,7 @@ def test_utf8_maintainer_in_metadata( # issue-3663 PEP639_LICENSE_EXPRESSION, None, 'MIT OR Apache-2.0', - 'License-Expression: MIT OR Apache-2.0', + 'License: MIT OR Apache-2.0', # TODO Metadata version '2.4' id='license-expression', ), ), From 3744994a6b53aeaa8e0c7ed92df6bf86d01df8a7 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 16 Feb 2025 21:48:04 +0100 Subject: [PATCH 09/36] Review --- pyproject.toml | 2 +- setuptools/_core_metadata.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bd22d79294..a9febdbe8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,10 +11,10 @@ authors = [ ] description = "Easily download, build, install, upgrade, and uninstall Python packages" readme = "README.rst" -license = "MIT" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Topic :: Software Development :: Libraries :: Python Modules", diff --git a/setuptools/_core_metadata.py b/setuptools/_core_metadata.py index 25937f913c..5342186c0e 100644 --- a/setuptools/_core_metadata.py +++ b/setuptools/_core_metadata.py @@ -88,7 +88,7 @@ def read_pkg_file(self, file): 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') - self.license_expression = _read_field_unescaped_from_msg(msg, 'license_expression') + self.license_expression = _read_field_unescaped_from_msg(msg, 'license-expression') self.long_description = _read_field_unescaped_from_msg(msg, 'description') if self.long_description is None and self.metadata_version >= Version('2.1'): From 0d8f1f2d12470efe3830e05f70996eb177287e26 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 17 Feb 2025 14:09:35 +0100 Subject: [PATCH 10/36] Replace error with warning and remove license classifier --- setuptools/dist.py | 15 +++++++++++---- .../tests/config/test_apply_pyprojecttoml.py | 18 +++++++++++++++--- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index 7a13ea6a04..3cbe0fdc11 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -28,7 +28,6 @@ from ._reqs import _StrOrIter from .config import pyprojecttoml, setupcfg from .discovery import ConfigDiscovery -from .errors import InvalidConfigError from .monkey import get_unpatched from .warnings import InformationOnly, SetuptoolsDeprecationWarning @@ -414,13 +413,21 @@ def _finalize_license_expression(self) -> None: InformationOnly.emit(f"Normalizing '{license_expr}' to '{normalized}'") self.metadata.license_expression = normalized + classifiers = [] + license_classifiers_found = False for cl in self.metadata.get_classifiers(): if not cl.startswith("License :: "): + classifiers.append(cl) continue - raise InvalidConfigError( - "License classifier are deprecated in favor of the license expression. " - f"Remove the '{cl}' classifier." + license_classifiers_found = True + SetuptoolsDeprecationWarning.emit( + "License classifier are deprecated in favor of the license expression.", + f"Please remove the '{cl}' classifier.", + see_url="https://peps.python.org/pep-0639/", + due_date=(2027, 2, 17), # Introduced 2025-02-17 ) + if license_classifiers_found: + self.metadata.set_classifiers(classifiers) def _finalize_license_files(self) -> None: """Compute names of all license files which should be included.""" diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index c9ab1f5718..089b6ae30e 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -9,6 +9,7 @@ import io import re import tarfile +import warnings from inspect import cleandoc from pathlib import Path from unittest.mock import Mock @@ -23,7 +24,8 @@ from setuptools.config import expand, pyprojecttoml, setupcfg from setuptools.config._apply_pyprojecttoml import _MissingDynamic, _some_attrgetter from setuptools.dist import Distribution -from setuptools.errors import InvalidConfigError, RemovedConfigError +from setuptools.errors import RemovedConfigError +from setuptools.warnings import SetuptoolsDeprecationWarning from .downloads import retrieve_file, urls_from_file @@ -178,6 +180,7 @@ def main_tomatoes(): pass license = "mit or apache-2.0" # should be normalized in metadata classifiers = [ "Development Status :: 5 - Production/Stable", + "Programming Language :: Python", ] """ @@ -324,10 +327,19 @@ def test_license_expression_with_bad_classifier(tmp_path): "README", f"{text}\n \"License :: OSI Approved :: MIT License\"\n]", ) - msg = "License classifier are deprecated.*'License :: OSI Approved :: MIT License'" - with pytest.raises(InvalidConfigError, match=msg): + msg = "License classifier are deprecated(?:.|\n)*'License :: OSI Approved :: MIT License'" + with pytest.raises(SetuptoolsDeprecationWarning, match=msg): pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", SetuptoolsDeprecationWarning) + dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + # Check 'License :: OSI Approved :: MIT License' is removed + assert dist.metadata.get_classifiers() == [ + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python", + ] + class TestLicenseFiles: def base_pyproject(self, tmp_path, additional_text): From 28baa9b6d0ca7f5f316724881bebc9fe156fd1fc Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 17 Feb 2025 16:42:34 +0100 Subject: [PATCH 11/36] Revert removing the license classifier --- setuptools/dist.py | 6 ------ setuptools/tests/config/test_apply_pyprojecttoml.py | 3 ++- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index 3cbe0fdc11..27e8095709 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -413,21 +413,15 @@ def _finalize_license_expression(self) -> None: InformationOnly.emit(f"Normalizing '{license_expr}' to '{normalized}'") self.metadata.license_expression = normalized - classifiers = [] - license_classifiers_found = False for cl in self.metadata.get_classifiers(): if not cl.startswith("License :: "): - classifiers.append(cl) continue - license_classifiers_found = True SetuptoolsDeprecationWarning.emit( "License classifier are deprecated in favor of the license expression.", f"Please remove the '{cl}' classifier.", see_url="https://peps.python.org/pep-0639/", due_date=(2027, 2, 17), # Introduced 2025-02-17 ) - if license_classifiers_found: - self.metadata.set_classifiers(classifiers) def _finalize_license_files(self) -> None: """Compute names of all license files which should be included.""" diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index 089b6ae30e..468a1ba01d 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -334,10 +334,11 @@ def test_license_expression_with_bad_classifier(tmp_path): with warnings.catch_warnings(): warnings.simplefilter("ignore", SetuptoolsDeprecationWarning) dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) - # Check 'License :: OSI Approved :: MIT License' is removed + # Check license classifier is still included assert dist.metadata.get_classifiers() == [ "Development Status :: 5 - Production/Stable", "Programming Language :: Python", + "License :: OSI Approved :: MIT License", ] From 9bf4bb9a2822a1c1a488935613b5adb349a1c593 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 17 Feb 2025 14:35:19 +0100 Subject: [PATCH 12/36] Bump core metadata version to 2.4 --- newsfragments/4830.feature.rst | 1 + setuptools/_core_metadata.py | 7 ++++--- .../tests/config/test_apply_pyprojecttoml.py | 15 +++++++++++++-- setuptools/tests/test_egg_info.py | 8 ++++---- 4 files changed, 22 insertions(+), 9 deletions(-) create mode 100644 newsfragments/4830.feature.rst diff --git a/newsfragments/4830.feature.rst b/newsfragments/4830.feature.rst new file mode 100644 index 0000000000..f21d17515a --- /dev/null +++ b/newsfragments/4830.feature.rst @@ -0,0 +1 @@ +Bump core metadata version to ``2.4``. -- by :user:`cdce8p` diff --git a/setuptools/_core_metadata.py b/setuptools/_core_metadata.py index 5342186c0e..60b47b375e 100644 --- a/setuptools/_core_metadata.py +++ b/setuptools/_core_metadata.py @@ -28,7 +28,7 @@ def get_metadata_version(self): mv = getattr(self, 'metadata_version', None) if mv is None: - mv = Version('2.2') + mv = Version('2.4') self.metadata_version = mv return mv @@ -176,8 +176,9 @@ def write_field(key, value): if attr_val is not None: write_field(field, attr_val) - license = self.license_expression or self.get_license() - if license: + if license_expression := self.license_expression: + write_field('License-Expression', license_expression) + elif license := self.get_license(): write_field('License', rfc822_escape(license)) for label, url in self.project_urls.items(): diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index 468a1ba01d..c437988702 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -280,20 +280,28 @@ def test_utf8_maintainer_in_metadata( # issue-3663 @pytest.mark.parametrize( - ('pyproject_text', 'license', 'license_expression', 'content_str'), + ( + 'pyproject_text', + 'license', + 'license_expression', + 'content_str', + 'not_content_str', + ), ( pytest.param( PEP639_LICENSE_TEXT, 'MIT', None, 'License: MIT', + 'License-Expression: ', id='license-text', ), pytest.param( PEP639_LICENSE_EXPRESSION, None, 'MIT OR Apache-2.0', - 'License: MIT OR Apache-2.0', # TODO Metadata version '2.4' + 'License-Expression: MIT OR Apache-2.0', + 'License: ', id='license-expression', ), ), @@ -302,6 +310,7 @@ def test_license_in_metadata( license, license_expression, content_str, + not_content_str, pyproject_text, tmp_path, ): @@ -317,7 +326,9 @@ def test_license_in_metadata( with open(pkg_file, "w", encoding="utf-8") as fh: dist.metadata.write_pkg_file(fh) content = pkg_file.read_text(encoding="utf-8") + assert "Metadata-Version: 2.4" in content assert content_str in content + assert not_content_str not in content def test_license_expression_with_bad_classifier(tmp_path): diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index 9756d7c519..528e2c13d8 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -522,7 +522,7 @@ def test_provides_extra(self, tmpdir_cwd, env): with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: pkg_info_lines = fp.read().split('\n') assert 'Provides-Extra: foobar' in pkg_info_lines - assert 'Metadata-Version: 2.2' in pkg_info_lines + assert 'Metadata-Version: 2.4' in pkg_info_lines def test_doesnt_provides_extra(self, tmpdir_cwd, env): self._setup_script_with_requires( @@ -1089,7 +1089,7 @@ def test_metadata_version(self, tmpdir_cwd, env): with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: pkg_info_lines = fp.read().split('\n') # Update metadata version if changed - assert self._extract_mv_version(pkg_info_lines) == (2, 2) + assert self._extract_mv_version(pkg_info_lines) == (2, 4) def test_long_description_content_type(self, tmpdir_cwd, env): # Test that specifying a `long_description_content_type` keyword arg to @@ -1116,7 +1116,7 @@ def test_long_description_content_type(self, tmpdir_cwd, env): pkg_info_lines = fp.read().split('\n') expected_line = 'Description-Content-Type: text/markdown' assert expected_line in pkg_info_lines - assert 'Metadata-Version: 2.2' in pkg_info_lines + assert 'Metadata-Version: 2.4' in pkg_info_lines def test_long_description(self, tmpdir_cwd, env): # Test that specifying `long_description` and `long_description_content_type` @@ -1135,7 +1135,7 @@ def test_long_description(self, tmpdir_cwd, env): egg_info_dir = os.path.join('.', 'foo.egg-info') with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: pkg_info_lines = fp.read().split('\n') - assert 'Metadata-Version: 2.2' in pkg_info_lines + assert 'Metadata-Version: 2.4' in pkg_info_lines assert '' == pkg_info_lines[-1] # last line should be empty long_desc_lines = pkg_info_lines[pkg_info_lines.index('') :] assert 'This is a long description' in long_desc_lines From 6410380863bc2a643c984c1ef282cf3d9086bd41 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 17 Feb 2025 16:28:24 +0000 Subject: [PATCH 13/36] Prevent deprecated license classifiers from being written to core metadata --- setuptools/dist.py | 25 +++++++++++-------- .../tests/config/test_apply_pyprojecttoml.py | 8 +++--- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index 27e8095709..fcedf679e7 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -406,22 +406,27 @@ def _normalize_requires(self): def _finalize_license_expression(self) -> None: """Normalize license and license_expression.""" + classifiers = self.metadata.get_classifiers() + license_classifiers = {cl for cl in classifiers if cl.startswith("License :: ")} + + if license_classifiers: + SetuptoolsDeprecationWarning.emit( + "License classifier are deprecated in favor of the license expression.", + "Please remove the classifiers:\n\n" + "\n".join(license_classifiers), + see_url="https://peps.python.org/pep-0639/", + due_date=(2027, 2, 17), # Introduced 2025-02-17 + ) + license_expr = self.metadata.license_expression if license_expr: normalized = canonicalize_license_expression(license_expr) if license_expr != normalized: InformationOnly.emit(f"Normalizing '{license_expr}' to '{normalized}'") self.metadata.license_expression = normalized - - for cl in self.metadata.get_classifiers(): - if not cl.startswith("License :: "): - continue - SetuptoolsDeprecationWarning.emit( - "License classifier are deprecated in favor of the license expression.", - f"Please remove the '{cl}' classifier.", - see_url="https://peps.python.org/pep-0639/", - due_date=(2027, 2, 17), # Introduced 2025-02-17 - ) + if license_classifiers: + # Filter classifiers but preserve "static-ness" of metadata + filtered = [cl for cl in classifiers if cl not in license_classifiers] + self.metadata.set_classifiers(classifiers.__class__(filtered)) def _finalize_license_files(self) -> None: """Compute names of all license files which should be included.""" diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index 468a1ba01d..eb1ca671fd 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -327,18 +327,18 @@ def test_license_expression_with_bad_classifier(tmp_path): "README", f"{text}\n \"License :: OSI Approved :: MIT License\"\n]", ) - msg = "License classifier are deprecated(?:.|\n)*'License :: OSI Approved :: MIT License'" - with pytest.raises(SetuptoolsDeprecationWarning, match=msg): + msg = "License classifier are deprecated" + with pytest.raises(SetuptoolsDeprecationWarning, match=msg) as exc: pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + assert "License :: OSI Approved :: MIT License" in str(exc.value) with warnings.catch_warnings(): warnings.simplefilter("ignore", SetuptoolsDeprecationWarning) dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) - # Check license classifier is still included + # Check 'License :: OSI Approved :: MIT License' is removed assert dist.metadata.get_classifiers() == [ "Development Status :: 5 - Production/Stable", "Programming Language :: Python", - "License :: OSI Approved :: MIT License", ] From 778e679b4d95e9bed8c41052883052e39e44e881 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 17 Feb 2025 16:36:39 +0000 Subject: [PATCH 14/36] Improve message in warning --- setuptools/dist.py | 2 +- setuptools/tests/config/test_apply_pyprojecttoml.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index fcedf679e7..dc301a6369 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -411,7 +411,7 @@ def _finalize_license_expression(self) -> None: if license_classifiers: SetuptoolsDeprecationWarning.emit( - "License classifier are deprecated in favor of the license expression.", + "License classifiers are deprecated in favor of the license expression.", "Please remove the classifiers:\n\n" + "\n".join(license_classifiers), see_url="https://peps.python.org/pep-0639/", due_date=(2027, 2, 17), # Introduced 2025-02-17 diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index eb1ca671fd..d858236278 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -327,7 +327,7 @@ def test_license_expression_with_bad_classifier(tmp_path): "README", f"{text}\n \"License :: OSI Approved :: MIT License\"\n]", ) - msg = "License classifier are deprecated" + msg = "License classifiers are deprecated" with pytest.raises(SetuptoolsDeprecationWarning, match=msg) as exc: pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) assert "License :: OSI Approved :: MIT License" in str(exc.value) From ee51110a3781937c09cb03c6af373f0245c3bbe1 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 17 Feb 2025 16:41:31 +0000 Subject: [PATCH 15/36] Use a more explicit method for preserving static-ness of classifiers --- setuptools/dist.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index dc301a6369..cc627f8583 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -425,8 +425,9 @@ def _finalize_license_expression(self) -> None: self.metadata.license_expression = normalized if license_classifiers: # Filter classifiers but preserve "static-ness" of metadata - filtered = [cl for cl in classifiers if cl not in license_classifiers] - self.metadata.set_classifiers(classifiers.__class__(filtered)) + list_ = _static.List if _static.is_static(classifiers) else list + filtered = (cl for cl in classifiers if cl not in license_classifiers) + self.metadata.set_classifiers(list_(filtered)) def _finalize_license_files(self) -> None: """Compute names of all license files which should be included.""" From 9bdad9f8fa39705abc2664a6cd090250c1772006 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 17 Feb 2025 17:00:44 +0000 Subject: [PATCH 16/36] Add news fragment --- newsfragments/4833.feature.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 newsfragments/4833.feature.rst diff --git a/newsfragments/4833.feature.rst b/newsfragments/4833.feature.rst new file mode 100644 index 0000000000..6a61c5ca05 --- /dev/null +++ b/newsfragments/4833.feature.rst @@ -0,0 +1,3 @@ +Added deprecation warning for license classifiers, +according to `PEP 639 +`_. From ea4095d0d2311fb4266b5ae9aa00f3e5be08c9b5 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 17 Feb 2025 18:04:14 +0000 Subject: [PATCH 17/36] Keep warning about license classifiers but raise an error if license expression is used --- setuptools/dist.py | 30 +++++++++++-------- .../tests/config/test_apply_pyprojecttoml.py | 29 +++++++++++------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index cc627f8583..9d7118a5d2 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -28,6 +28,7 @@ from ._reqs import _StrOrIter from .config import pyprojecttoml, setupcfg from .discovery import ConfigDiscovery +from .errors import InvalidConfigError from .monkey import get_unpatched from .warnings import InformationOnly, SetuptoolsDeprecationWarning @@ -407,15 +408,7 @@ def _normalize_requires(self): def _finalize_license_expression(self) -> None: """Normalize license and license_expression.""" classifiers = self.metadata.get_classifiers() - license_classifiers = {cl for cl in classifiers if cl.startswith("License :: ")} - - if license_classifiers: - SetuptoolsDeprecationWarning.emit( - "License classifiers are deprecated in favor of the license expression.", - "Please remove the classifiers:\n\n" + "\n".join(license_classifiers), - see_url="https://peps.python.org/pep-0639/", - due_date=(2027, 2, 17), # Introduced 2025-02-17 - ) + license_classifiers = [cl for cl in classifiers if cl.startswith("License :: ")] license_expr = self.metadata.license_expression if license_expr: @@ -424,10 +417,21 @@ def _finalize_license_expression(self) -> None: InformationOnly.emit(f"Normalizing '{license_expr}' to '{normalized}'") self.metadata.license_expression = normalized if license_classifiers: - # Filter classifiers but preserve "static-ness" of metadata - list_ = _static.List if _static.is_static(classifiers) else list - filtered = (cl for cl in classifiers if cl not in license_classifiers) - self.metadata.set_classifiers(list_(filtered)) + raise InvalidConfigError( + "License classifiers have been superseded by license expressions " + "(see https://peps.python.org/pep-0639/). Please remove:\n\n" + + "\n".join(license_classifiers), + ) + elif license_classifiers: + SetuptoolsDeprecationWarning.emit( + "License classifiers are deprecated.", + "Please consider removing the following classifiers in favor of a " + "SPDX license expression:\n\n" + "\n".join(license_classifiers), + see_url="https://peps.python.org/pep-0639/", + # Warning introduced on 2025-02-17 + # TODO: Should we add a due date? It may affect old/unmaintained + # packages in the ecosystem and cause problems... + ) def _finalize_license_files(self) -> None: """Compute names of all license files which should be included.""" diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index d858236278..62baf7f827 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -9,7 +9,6 @@ import io import re import tarfile -import warnings from inspect import cleandoc from pathlib import Path from unittest.mock import Mock @@ -24,7 +23,7 @@ from setuptools.config import expand, pyprojecttoml, setupcfg from setuptools.config._apply_pyprojecttoml import _MissingDynamic, _some_attrgetter from setuptools.dist import Distribution -from setuptools.errors import RemovedConfigError +from setuptools.errors import InvalidConfigError, RemovedConfigError from setuptools.warnings import SetuptoolsDeprecationWarning from .downloads import retrieve_file, urls_from_file @@ -320,25 +319,35 @@ def test_license_in_metadata( assert content_str in content -def test_license_expression_with_bad_classifier(tmp_path): +def test_license_classifier_with_license_expression(tmp_path): text = PEP639_LICENSE_EXPRESSION.rsplit("\n", 2)[0] pyproject = _pep621_example_project( tmp_path, "README", f"{text}\n \"License :: OSI Approved :: MIT License\"\n]", ) - msg = "License classifiers are deprecated" - with pytest.raises(SetuptoolsDeprecationWarning, match=msg) as exc: + msg = "License classifiers have been superseded by license expressions" + with pytest.raises(InvalidConfigError, match=msg) as exc: pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) assert "License :: OSI Approved :: MIT License" in str(exc.value) - with warnings.catch_warnings(): - warnings.simplefilter("ignore", SetuptoolsDeprecationWarning) + +def test_license_classifier_without_license_expression(tmp_path): + text = """\ + [project] + name = "spam" + version = "2020.0.0" + license = {text = "mit or apache-2.0"} + classifiers = ["License :: OSI Approved :: MIT License"] + """ + pyproject = _pep621_example_project(tmp_path, "README", text) + + msg = "License classifiers are deprecated(?:.|\n)*MIT License" + with pytest.warns(SetuptoolsDeprecationWarning, match=msg): dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) - # Check 'License :: OSI Approved :: MIT License' is removed + # Check license classifier is still included assert dist.metadata.get_classifiers() == [ - "Development Status :: 5 - Production/Stable", - "Programming Language :: Python", + "License :: OSI Approved :: MIT License" ] From 3af67b8183f808e91bab8648132b4d6a91704e3e Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 17 Feb 2025 18:06:20 +0000 Subject: [PATCH 18/36] Update newsfragment --- newsfragments/4833.feature.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/newsfragments/4833.feature.rst b/newsfragments/4833.feature.rst index 6a61c5ca05..d8801becf7 100644 --- a/newsfragments/4833.feature.rst +++ b/newsfragments/4833.feature.rst @@ -1,3 +1,2 @@ -Added deprecation warning for license classifiers, -according to `PEP 639 -`_. +Added exception (or warning) when deprecated license classifiers are used, +according to `PEP 639 `_. From 3b71b5f9f4a277f8ffd95f60aee1fc10f7e0e011 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 17 Feb 2025 18:18:29 +0000 Subject: [PATCH 19/36] Use a better docs URL for warning --- setuptools/dist.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index 9d7118a5d2..44e600df5c 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -423,11 +423,12 @@ def _finalize_license_expression(self) -> None: + "\n".join(license_classifiers), ) elif license_classifiers: + pypa_guides = "guides/licensing-examples-and-user-scenarios/" SetuptoolsDeprecationWarning.emit( "License classifiers are deprecated.", "Please consider removing the following classifiers in favor of a " "SPDX license expression:\n\n" + "\n".join(license_classifiers), - see_url="https://peps.python.org/pep-0639/", + see_url=f"https://packaging.python.org/en/latest/{pypa_guides}", # Warning introduced on 2025-02-17 # TODO: Should we add a due date? It may affect old/unmaintained # packages in the ecosystem and cause problems... From 0587478f1e7fdac262ee5d034bbdb292c5e8ae90 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 17 Feb 2025 18:27:00 +0000 Subject: [PATCH 20/36] Ensure _apply_pyproject sets field on dist.metadata object not on dist --- setuptools/config/_apply_pyprojecttoml.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py index 6bb2bea514..bcb3c03894 100644 --- a/setuptools/config/_apply_pyprojecttoml.py +++ b/setuptools/config/_apply_pyprojecttoml.py @@ -423,6 +423,7 @@ def _acessor(obj): "provides_extras", "license_file", "license_files", + "license_expression", } _PREPROCESS = { From 29302de33aa57e4df01edcafcd331362e09ca8cd Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 18 Feb 2025 10:31:37 +0000 Subject: [PATCH 21/36] Update URL for warning --- setuptools/dist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index 44e600df5c..d202dbf504 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -423,7 +423,7 @@ def _finalize_license_expression(self) -> None: + "\n".join(license_classifiers), ) elif license_classifiers: - pypa_guides = "guides/licensing-examples-and-user-scenarios/" + pypa_guides = "guides/writing-pyproject-toml/#license" SetuptoolsDeprecationWarning.emit( "License classifiers are deprecated.", "Please consider removing the following classifiers in favor of a " From a20512e7f513b9a0471845777170560b64cedd39 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 18 Feb 2025 10:43:30 +0000 Subject: [PATCH 22/36] Fix bypassed assertion in tests --- setuptools/tests/config/test_apply_pyprojecttoml.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index 62baf7f827..5b7ca0f40d 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -329,7 +329,8 @@ def test_license_classifier_with_license_expression(tmp_path): msg = "License classifiers have been superseded by license expressions" with pytest.raises(InvalidConfigError, match=msg) as exc: pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) - assert "License :: OSI Approved :: MIT License" in str(exc.value) + + assert "License :: OSI Approved :: MIT License" in str(exc.value) def test_license_classifier_without_license_expression(tmp_path): @@ -345,10 +346,9 @@ def test_license_classifier_without_license_expression(tmp_path): msg = "License classifiers are deprecated(?:.|\n)*MIT License" with pytest.warns(SetuptoolsDeprecationWarning, match=msg): dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) - # Check license classifier is still included - assert dist.metadata.get_classifiers() == [ - "License :: OSI Approved :: MIT License" - ] + + # Check license classifier is still included + assert dist.metadata.get_classifiers() == ["License :: OSI Approved :: MIT License"] class TestLicenseFiles: From ab277d305add64e07a62ecf16d2f555ef2145c39 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 18 Feb 2025 12:41:16 +0100 Subject: [PATCH 23/36] Deprecate tools.setuptools.license-files --- docs/userguide/miscellaneous.rst | 2 +- docs/userguide/pyproject_config.rst | 2 +- newsfragments/4837.feature.rst | 3 +++ setuptools/config/_apply_pyprojecttoml.py | 21 +++++++++++++++++-- .../tests/config/test_apply_pyprojecttoml.py | 19 ++++++++++++++--- setuptools/tests/test_build_meta.py | 10 +++++++-- 6 files changed, 48 insertions(+), 9 deletions(-) create mode 100644 newsfragments/4837.feature.rst diff --git a/docs/userguide/miscellaneous.rst b/docs/userguide/miscellaneous.rst index 7354bd3a86..1b493fba3b 100644 --- a/docs/userguide/miscellaneous.rst +++ b/docs/userguide/miscellaneous.rst @@ -24,7 +24,7 @@ files are included in a source distribution by default: in ``pyproject.toml`` and/or equivalent in ``setup.cfg``/``setup.py``; note that if you don't explicitly set this parameter, ``setuptools`` will include any files that match the following glob patterns: - ``LICENSE*``, ``LICENCE*``, ``COPYING*``, ``NOTICE*``, ``AUTHORS**``; + ``LICEN[CS]E*``, ``COPYING*``, ``NOTICE*``, ``AUTHORS**``; - ``pyproject.toml``; - ``setup.cfg``; - ``setup.py``; diff --git a/docs/userguide/pyproject_config.rst b/docs/userguide/pyproject_config.rst index efc68603a9..4730bdddbe 100644 --- a/docs/userguide/pyproject_config.rst +++ b/docs/userguide/pyproject_config.rst @@ -99,7 +99,7 @@ Key Value Type (TOML) Notes See :doc:`/userguide/datafiles`. ``exclude-package-data`` table/inline-table Empty by default. See :doc:`/userguide/datafiles`. ------------------------- --------------------------- ------------------------- -``license-files`` array of glob patterns **Provisional** - likely to change with :pep:`639` +``license-files`` array of glob patterns **Deprecated** - use ``project.license-files`` instead. See :doc:`PyPUG:guides/writing-pyproject-toml/#license-files` (by default: ``['LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*']``) ``data-files`` table/inline-table **Discouraged** - check :doc:`/userguide/datafiles`. Whenever possible, consider using data files inside the package directories. diff --git a/newsfragments/4837.feature.rst b/newsfragments/4837.feature.rst new file mode 100644 index 0000000000..4ad97b9513 --- /dev/null +++ b/newsfragments/4837.feature.rst @@ -0,0 +1,3 @@ +Deprecated ``tools.setuptools.license-files`` in favor of ``project.license-files`` +and added exception if ``project.license-files`` and ``tools.setuptools.license-files`` +are used together. -- by :user:`cdce8p` diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py index bcb3c03894..ccd8f7215d 100644 --- a/setuptools/config/_apply_pyprojecttoml.py +++ b/setuptools/config/_apply_pyprojecttoml.py @@ -22,9 +22,9 @@ from .. import _static from .._path import StrPath -from ..errors import RemovedConfigError +from ..errors import InvalidConfigError, RemovedConfigError from ..extension import Extension -from ..warnings import SetuptoolsWarning +from ..warnings import SetuptoolsDeprecationWarning, SetuptoolsWarning if TYPE_CHECKING: from typing_extensions import TypeAlias @@ -100,6 +100,23 @@ def _apply_tool_table(dist: Distribution, config: dict, filename: StrPath): """ raise RemovedConfigError("\n".join([cleandoc(msg), suggestion])) + if norm_key == "license_files": + if dist.metadata.license_files: + raise InvalidConfigError( + "'project.license-files' is defined already. " + "Remove 'tool.setuptools.license-files'." + ) + + pypa_guides = "guides/writing-pyproject-toml/#license-files" + SetuptoolsDeprecationWarning.emit( + "'tool.setuptools.license-files' is deprecated in favor of " + "'project.license-files'", + see_url=f"https://packaging.python.org/en/latest/{pypa_guides}", + ) + # Warning introduced on 2025-02-18 + # TODO: Should we add a due date? It may affect old/unmaintained + # packages in the ecosystem and cause problems... + norm_key = TOOL_TABLE_RENAMES.get(norm_key, norm_key) corresp = TOOL_TABLE_CORRESPONDENCE.get(norm_key, norm_key) if callable(corresp): diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index c437988702..29f6ffaf34 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -24,7 +24,7 @@ from setuptools.config import expand, pyprojecttoml, setupcfg from setuptools.config._apply_pyprojecttoml import _MissingDynamic, _some_attrgetter from setuptools.dist import Distribution -from setuptools.errors import RemovedConfigError +from setuptools.errors import InvalidConfigError, RemovedConfigError from setuptools.warnings import SetuptoolsDeprecationWarning from .downloads import retrieve_file, urls_from_file @@ -366,7 +366,7 @@ def base_pyproject(self, tmp_path, additional_text): pyproject.write_text(text, encoding="utf-8") return pyproject - def base_pyproject_license_pep639(self, tmp_path): + def base_pyproject_license_pep639(self, tmp_path, additional_text=""): pyproject = _pep621_example_project(tmp_path, "README") text = pyproject.read_text(encoding="utf-8") @@ -381,6 +381,8 @@ def base_pyproject_license_pep639(self, tmp_path): text, count=1, ) + if additional_text: + text = f"{text}\n{additional_text}\n" pyproject.write_text(text, encoding="utf-8") return pyproject @@ -396,7 +398,9 @@ def test_both_license_and_license_files_defined(self, tmp_path): license = tmp_path / "LICENSE.txt" license.write_text("LicenseRef-Proprietary\n", encoding="utf-8") - dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + msg = "'tool.setuptools.license-files' is deprecated in favor of 'project.license-files'" + with pytest.warns(SetuptoolsDeprecationWarning, match=msg): + 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" @@ -412,6 +416,15 @@ def test_both_license_and_license_files_defined_pep639(self, tmp_path): assert dist.metadata.license is None assert dist.metadata.license_expression == "LicenseRef-Proprietary" + def test_license_files_defined_twice(self, tmp_path): + # Set project.license-files and tools.setuptools.license-files + setuptools_config = '[tool.setuptools]\nlicense-files = ["_FILE*"]' + pyproject = self.base_pyproject_license_pep639(tmp_path, setuptools_config) + + msg = "'project.license-files' is defined already. Remove 'tool.setuptools.license-files'" + with pytest.raises(InvalidConfigError, match=msg): + pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + def test_default_patterns(self, tmp_path): setuptools_config = '[tool.setuptools]\nzip-safe = false' # ^ used just to trigger section validation diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index b26fd2f5b0..8598578475 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -6,6 +6,7 @@ import signal import sys import tarfile +import warnings from concurrent import futures from pathlib import Path from typing import Any, Callable @@ -15,6 +16,8 @@ from jaraco import path from packaging.requirements import Requirement +from setuptools.warnings import SetuptoolsDeprecationWarning + from .textwrap import DALS SETUP_SCRIPT_STUB = "__import__('setuptools').setup()" @@ -384,8 +387,11 @@ def test_build_with_pyproject_config(self, tmpdir, 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 warnings.catch_warnings(): + msg = "'tool.setuptools.license-files' is deprecated in favor of 'project.license-files'" + warnings.filterwarnings("ignore", msg, SetuptoolsDeprecationWarning) + 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()) From b44b4f19eda2a083d3b3bcd94939bb3b217431d0 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 18 Feb 2025 13:28:11 +0100 Subject: [PATCH 24/36] Suggestions --- setuptools/config/_apply_pyprojecttoml.py | 32 +++++++++++------------ 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py index ccd8f7215d..9c60196e54 100644 --- a/setuptools/config/_apply_pyprojecttoml.py +++ b/setuptools/config/_apply_pyprojecttoml.py @@ -89,6 +89,21 @@ def _apply_tool_table(dist: Distribution, config: dict, filename: StrPath): if not tool_table: return # short-circuit + if "license-files" in tool_table: + if dist.metadata.license_files: + raise InvalidConfigError( + "'project.license-files' is defined already. " + "Remove 'tool.setuptools.license-files'." + ) + + pypa_guides = "guides/writing-pyproject-toml/#license-files" + SetuptoolsDeprecationWarning.emit( + "'tool.setuptools.license-files' is deprecated in favor of " + "'project.license-files'", + see_url=f"https://packaging.python.org/en/latest/{pypa_guides}", + due_date=(2026, 2, 18), # Warning introduced on 2025-02-18 + ) + for field, value in tool_table.items(): norm_key = json_compatible_key(field) @@ -100,23 +115,6 @@ def _apply_tool_table(dist: Distribution, config: dict, filename: StrPath): """ raise RemovedConfigError("\n".join([cleandoc(msg), suggestion])) - if norm_key == "license_files": - if dist.metadata.license_files: - raise InvalidConfigError( - "'project.license-files' is defined already. " - "Remove 'tool.setuptools.license-files'." - ) - - pypa_guides = "guides/writing-pyproject-toml/#license-files" - SetuptoolsDeprecationWarning.emit( - "'tool.setuptools.license-files' is deprecated in favor of " - "'project.license-files'", - see_url=f"https://packaging.python.org/en/latest/{pypa_guides}", - ) - # Warning introduced on 2025-02-18 - # TODO: Should we add a due date? It may affect old/unmaintained - # packages in the ecosystem and cause problems... - norm_key = TOOL_TABLE_RENAMES.get(norm_key, norm_key) corresp = TOOL_TABLE_CORRESPONDENCE.get(norm_key, norm_key) if callable(corresp): From 7e50cb1a0812aac9d65c5cc0a0d212805c93aa7b Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 18 Feb 2025 13:20:49 +0000 Subject: [PATCH 25/36] Attempt to fix sphinx warnings --- newsfragments/4706.feature.rst | 2 +- newsfragments/4728.feature.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/newsfragments/4706.feature.rst b/newsfragments/4706.feature.rst index 1d34f5f476..1c06206078 100644 --- a/newsfragments/4706.feature.rst +++ b/newsfragments/4706.feature.rst @@ -1 +1 @@ -Added initial support for license expression (`PEP 639 `_). -- by :user:`cdce8p` +Added initial support for license expression (:pep:`639#add-license-expression-field`). -- by :user:`cdce8p` diff --git a/newsfragments/4728.feature.rst b/newsfragments/4728.feature.rst index 61906656c0..5eb4fa6a40 100644 --- a/newsfragments/4728.feature.rst +++ b/newsfragments/4728.feature.rst @@ -1 +1 @@ -Store ``License-File``s in ``.dist-info/licenses`` subfolder and added support for recursive globs for ``license_files`` (`PEP 639 `_). -- by :user:`cdce8p` +Store ``License-File``s in ``.dist-info/licenses`` subfolder and added support for recursive globs for ``license_files`` (:pep:`639#add-license-expression-field`). -- by :user:`cdce8p` From 7f442369f457889bd7028de56f3a6549d1a180a1 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 18 Feb 2025 13:32:10 +0000 Subject: [PATCH 26/36] Attempt to improve display of pep links --- newsfragments/4706.feature.rst | 2 +- newsfragments/4728.feature.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/newsfragments/4706.feature.rst b/newsfragments/4706.feature.rst index 1c06206078..be8aea6456 100644 --- a/newsfragments/4706.feature.rst +++ b/newsfragments/4706.feature.rst @@ -1 +1 @@ -Added initial support for license expression (:pep:`639#add-license-expression-field`). -- by :user:`cdce8p` +Added initial support for license expression (:pep:`PEP 639 <639#add-license-expression-field>`). -- by :user:`cdce8p` diff --git a/newsfragments/4728.feature.rst b/newsfragments/4728.feature.rst index 5eb4fa6a40..ea19b31a36 100644 --- a/newsfragments/4728.feature.rst +++ b/newsfragments/4728.feature.rst @@ -1 +1 @@ -Store ``License-File``s in ``.dist-info/licenses`` subfolder and added support for recursive globs for ``license_files`` (:pep:`639#add-license-expression-field`). -- by :user:`cdce8p` +Store ``License-File``s in ``.dist-info/licenses`` subfolder and added support for recursive globs for ``license_files`` (:pep:`PEP 639 <639#add-license-expression-field>`). -- by :user:`cdce8p` From d6abdced01b80617c89a5878775439821264b28e Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Tue, 18 Feb 2025 15:00:30 +0100 Subject: [PATCH 27/36] Fix reference --- docs/userguide/pyproject_config.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/userguide/pyproject_config.rst b/docs/userguide/pyproject_config.rst index 4730bdddbe..3bf99bed1a 100644 --- a/docs/userguide/pyproject_config.rst +++ b/docs/userguide/pyproject_config.rst @@ -99,7 +99,7 @@ Key Value Type (TOML) Notes See :doc:`/userguide/datafiles`. ``exclude-package-data`` table/inline-table Empty by default. See :doc:`/userguide/datafiles`. ------------------------- --------------------------- ------------------------- -``license-files`` array of glob patterns **Deprecated** - use ``project.license-files`` instead. See :doc:`PyPUG:guides/writing-pyproject-toml/#license-files` +``license-files`` array of glob patterns **Deprecated** - use ``project.license-files`` instead. See :doc:`PyPUG:guides/writing-pyproject-toml` (by default: ``['LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*']``) ``data-files`` table/inline-table **Discouraged** - check :doc:`/userguide/datafiles`. Whenever possible, consider using data files inside the package directories. From 62bd9444e652407961b283d8212f9d9d21077a82 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 18 Feb 2025 18:17:10 +0000 Subject: [PATCH 28/36] Add deprecation warning for project.license as a table in pyproject.toml --- setuptools/config/_apply_pyprojecttoml.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py index 9c60196e54..f23d0d2de3 100644 --- a/setuptools/config/_apply_pyprojecttoml.py +++ b/setuptools/config/_apply_pyprojecttoml.py @@ -203,6 +203,14 @@ def _license(dist: Distribution, val: str | dict, root_dir: StrPath | None): if isinstance(val, str): _set_config(dist, "license_expression", _static.Str(val)) else: + pypa_guides = "guides/writing-pyproject-toml/#license" + SetuptoolsDeprecationWarning.emit( + "`project.license` as a TOML table is deprecated", + "Please use a simple string containing a SPDX expression for " + "`project.license`. You can also use `project.license-files`.", + see_url=f"https://packaging.python.org/en/latest/{pypa_guides}", + due_date=(2026, 2, 18), # Introduced on 2025-02-18 + ) if "file" in val: # XXX: Is it completely safe to assume static? value = expand.read_files([val["file"]], root_dir) From e58f51414ce074628014c435eaa1f71df4dfa191 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 18 Feb 2025 18:18:24 +0000 Subject: [PATCH 29/36] Adequate tests to warning --- .../tests/config/test_apply_pyprojecttoml.py | 86 +++++++++++++------ setuptools/tests/test_build_meta.py | 8 +- setuptools/tests/test_sdist.py | 14 ++- 3 files changed, 77 insertions(+), 31 deletions(-) diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index c489c99bd6..848f44745f 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -93,7 +93,7 @@ def test_apply_pyproject_equivalent_to_setupcfg(url, monkeypatch, tmp_path): description = "Lovely Spam! Wonderful Spam!" readme = "README.rst" requires-python = ">=3.8" -license = {file = "LICENSE.txt"} +license-files = ["LICENSE.txt"] # Updated to be PEP 639 compliant keywords = ["egg", "bacon", "sausage", "tomatoes", "Lobster Thermidor"] authors = [ {email = "hi@pradyunsg.me"}, @@ -206,7 +206,6 @@ def test_pep621_example(tmp_path): """Make sure the example in PEP 621 works""" pyproject = _pep621_example_project(tmp_path) dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) - assert dist.metadata.license == "--- LICENSE stub ---" assert set(dist.metadata.license_files) == {"LICENSE.txt"} @@ -294,6 +293,11 @@ def test_utf8_maintainer_in_metadata( # issue-3663 'License: MIT', 'License-Expression: ', id='license-text', + marks=[ + pytest.mark.filterwarnings( + "ignore:.project.license. as a TOML table is deprecated", + ) + ], ), pytest.param( PEP639_LICENSE_EXPRESSION, @@ -354,8 +358,12 @@ def test_license_classifier_without_license_expression(tmp_path): """ pyproject = _pep621_example_project(tmp_path, "README", text) - msg = "License classifiers are deprecated(?:.|\n)*MIT License" - with pytest.warns(SetuptoolsDeprecationWarning, match=msg): + msg1 = "License classifiers are deprecated(?:.|\n)*MIT License" + msg2 = ".project.license. as a TOML table is deprecated" + with ( + pytest.warns(SetuptoolsDeprecationWarning, match=msg1), + pytest.warns(SetuptoolsDeprecationWarning, match=msg2), + ): dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) # Check license classifier is still included @@ -363,38 +371,38 @@ def test_license_classifier_without_license_expression(tmp_path): class TestLicenseFiles: - def base_pyproject(self, tmp_path, additional_text): - 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 = f"{text}\n{additional_text}\n" - pyproject.write_text(text, encoding="utf-8") - return pyproject - - def base_pyproject_license_pep639(self, tmp_path, additional_text=""): - pyproject = _pep621_example_project(tmp_path, "README") - text = pyproject.read_text(encoding="utf-8") + def base_pyproject( + self, + tmp_path, + additional_text="", + license_toml='license = {file = "LICENSE.txt"}\n', + ): + text = PEP639_LICENSE_EXPRESSION # Sanity-check - assert 'license = {file = "LICENSE.txt"}' in text + assert 'license = "mit or apache-2.0"' in text assert 'license-files' not in text assert "[tool.setuptools]" not in text text = re.sub( - r"(license = {file = \"LICENSE.txt\"})\n", - ("license = \"licenseref-Proprietary\"\nlicense-files = [\"_FILE*\"]\n"), + r"(license = .*)\n", + license_toml, text, count=1, ) - if additional_text: - text = f"{text}\n{additional_text}\n" - pyproject.write_text(text, encoding="utf-8") + assert license_toml in text # sanity check + text = f"{text}\n{additional_text}\n" + pyproject = _pep621_example_project(tmp_path, "README", pyproject_text=text) return pyproject + def base_pyproject_license_pep639(self, tmp_path, additional_text=""): + return self.base_pyproject( + tmp_path, + additional_text=additional_text, + license_toml='license = "licenseref-Proprietary"' + '\nlicense-files = ["_FILE*"]\n', + ) + def test_both_license_and_license_files_defined(self, tmp_path): setuptools_config = '[tool.setuptools]\nlicense-files = ["_FILE*"]' pyproject = self.base_pyproject(tmp_path, setuptools_config) @@ -407,8 +415,12 @@ def test_both_license_and_license_files_defined(self, tmp_path): license = tmp_path / "LICENSE.txt" license.write_text("LicenseRef-Proprietary\n", encoding="utf-8") - msg = "'tool.setuptools.license-files' is deprecated in favor of 'project.license-files'" - with pytest.warns(SetuptoolsDeprecationWarning, match=msg): + msg1 = "'tool.setuptools.license-files' is deprecated in favor of 'project.license-files'" + msg2 = ".project.license. as a TOML table is deprecated" + with ( + pytest.warns(SetuptoolsDeprecationWarning, match=msg1), + pytest.warns(SetuptoolsDeprecationWarning, match=msg2), + ): 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" @@ -437,7 +449,7 @@ def test_license_files_defined_twice(self, tmp_path): def test_default_patterns(self, tmp_path): setuptools_config = '[tool.setuptools]\nzip-safe = false' # ^ used just to trigger section validation - pyproject = self.base_pyproject(tmp_path, setuptools_config) + pyproject = self.base_pyproject(tmp_path, setuptools_config, license_toml="") license_files = "LICENCE-a.html COPYING-abc.txt AUTHORS-xyz NOTICE,def".split() @@ -445,9 +457,27 @@ def test_default_patterns(self, tmp_path): (tmp_path / fname).write_text(f"{fname}\n", encoding="utf-8") dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + assert (tmp_path / "LICENSE.txt").exists() # from base example assert set(dist.metadata.license_files) == {*license_files, "LICENSE.txt"} + def test_deprecated_file_expands_to_text(self, tmp_path): + """Make sure the old example with ``license = {text = ...}`` works""" + + assert 'license-files = ["LICENSE.txt"]' in PEP621_EXAMPLE # sanity check + text = PEP621_EXAMPLE.replace( + 'license-files = ["LICENSE.txt"]', + 'license = {file = "LICENSE.txt"}', + ) + pyproject = _pep621_example_project(tmp_path, pyproject_text=text) + + msg = ".project.license. as a TOML table is deprecated" + with pytest.warns(SetuptoolsDeprecationWarning, match=msg): + dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject) + + assert dist.metadata.license == "--- LICENSE stub ---" + assert set(dist.metadata.license_files) == {"LICENSE.txt"} # auto-filled + class TestPyModules: # https://github.com/pypa/setuptools/issues/4316 diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index 8598578475..624bba862e 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -387,9 +387,13 @@ def test_build_with_pyproject_config(self, tmpdir, setup_script): build_backend = self.get_build_backend() with tmpdir.as_cwd(): path.build(files) + msgs = [ + "'tool.setuptools.license-files' is deprecated in favor of 'project.license-files'", + "`project.license` as a TOML table is deprecated", + ] with warnings.catch_warnings(): - msg = "'tool.setuptools.license-files' is deprecated in favor of 'project.license-files'" - warnings.filterwarnings("ignore", msg, SetuptoolsDeprecationWarning) + for msg in msgs: + warnings.filterwarnings("ignore", msg, SetuptoolsDeprecationWarning) sdist_path = build_backend.build_sdist("temp") wheel_file = build_backend.build_wheel("temp") diff --git a/setuptools/tests/test_sdist.py b/setuptools/tests/test_sdist.py index 3ee0511b1c..19d8ddf6da 100644 --- a/setuptools/tests/test_sdist.py +++ b/setuptools/tests/test_sdist.py @@ -708,12 +708,21 @@ def test_sdist_with_latin1_encoded_filename(self): [project] name = "testing" readme = "USAGE.rst" - license = {file = "DOWHATYOUWANT"} + license-files = ["DOWHATYOUWANT"] dynamic = ["version"] [tool.setuptools.dynamic] version = {file = ["src/VERSION.txt"]} """, "pyproject.toml - directive with str instead of list": """ + [project] + name = "testing" + readme = "USAGE.rst" + license-files = ["DOWHATYOUWANT"] + dynamic = ["version"] + [tool.setuptools.dynamic] + version = {file = "src/VERSION.txt"} + """, + "pyproject.toml - deprecated license table with file entry": """ [project] name = "testing" readme = "USAGE.rst" @@ -725,6 +734,9 @@ def test_sdist_with_latin1_encoded_filename(self): } @pytest.mark.parametrize("config", _EXAMPLE_DIRECTIVES.keys()) + @pytest.mark.filterwarnings( + "ignore:.project.license. as a TOML table is deprecated" + ) def test_add_files_referenced_by_config_directives(self, source_dir, config): config_file, _, _ = config.partition(" - ") config_text = self._EXAMPLE_DIRECTIVES[config] From 68397dc45fe2daa80921fcf58a34f9b96c953984 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Tue, 18 Feb 2025 18:34:06 +0000 Subject: [PATCH 30/36] Add news fragment --- newsfragments/4840.feature.rst | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 newsfragments/4840.feature.rst diff --git a/newsfragments/4840.feature.rst b/newsfragments/4840.feature.rst new file mode 100644 index 0000000000..a033fd2afb --- /dev/null +++ b/newsfragments/4840.feature.rst @@ -0,0 +1,5 @@ +Deprecated ``project.license`` as a TOML table in +``pyproject.toml``. Users are expected to move towards using +``project.license-files`` and/or SPDX expressions (as strings) in +``pyproject.license``. +See :pep:`PEP 639 <639#deprecate-license-key-table-subkeys>`. From e0a6de58a053a73d6800463487bd12a26e2af963 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 19 Feb 2025 10:30:03 +0000 Subject: [PATCH 31/36] Ensure _finalize_license_expression preserve "static-ness" --- setuptools/dist.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/setuptools/dist.py b/setuptools/dist.py index d202dbf504..15b1967be3 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -406,13 +406,24 @@ def _normalize_requires(self): ) def _finalize_license_expression(self) -> None: - """Normalize license and license_expression.""" + """ + Normalize license and license_expression. + >>> dist = Distribution({"license_expression": _static.Str("mit aNd gpl-3.0-OR-later")}) + >>> _static.is_static(dist.metadata.license_expression) + True + >>> dist._finalize_license_expression() + >>> _static.is_static(dist.metadata.license_expression) # preserve static-ness" + True + >>> print(dist.metadata.license_expression) + MIT AND GPL-3.0-or-later + """ classifiers = self.metadata.get_classifiers() license_classifiers = [cl for cl in classifiers if cl.startswith("License :: ")] license_expr = self.metadata.license_expression if license_expr: - normalized = canonicalize_license_expression(license_expr) + str_ = _static.Str if _static.is_static(license_expr) else str + normalized = str_(canonicalize_license_expression(license_expr)) if license_expr != normalized: InformationOnly.emit(f"Normalizing '{license_expr}' to '{normalized}'") self.metadata.license_expression = normalized From 9ba666dbfc9fed66e846af66a2d052efaae79613 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 19 Feb 2025 10:33:40 +0000 Subject: [PATCH 32/36] Ensure PEP 639 implementation plays nicely with PEP 643 --- setuptools/_core_metadata.py | 7 +++- setuptools/config/_apply_pyprojecttoml.py | 3 +- setuptools/tests/test_core_metadata.py | 42 +++++++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/setuptools/_core_metadata.py b/setuptools/_core_metadata.py index 60b47b375e..975e9ceaa1 100644 --- a/setuptools/_core_metadata.py +++ b/setuptools/_core_metadata.py @@ -304,7 +304,12 @@ def _distribution_fullname(name: str, version: str) -> str: "home-page": "url", "keywords": "keywords", "license": "license", - # "license-file": "license_files", # XXX: does PEP 639 exempt Dynamic ?? + # XXX: License-File is complicated because the user gives globs that are expanded + # during the build. Without special handling it is likely always + # marked as Dynamic, which is an acceptable outcome according to: + # https://github.com/pypa/setuptools/issues/4629#issuecomment-2331233677 + "license-file": "license_files", + "license-expression": "license_expression", # PEP 639 "maintainer": "maintainer", "maintainer-email": "maintainer_email", "obsoletes": "obsoletes", diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py index f23d0d2de3..12c8b23108 100644 --- a/setuptools/config/_apply_pyprojecttoml.py +++ b/setuptools/config/_apply_pyprojecttoml.py @@ -459,7 +459,8 @@ def _acessor(obj): "description": _attrgetter("metadata.description"), "readme": _attrgetter("metadata.long_description"), "requires-python": _some_attrgetter("python_requires", "metadata.python_requires"), - "license": _attrgetter("metadata.license"), + "license": _some_attrgetter("metadata.license_expression", "metadata.license"), + # XXX: Should we wait until someone requires `license_files`? "authors": _some_attrgetter("metadata.author", "metadata.author_email"), "maintainers": _some_attrgetter("metadata.maintainer", "metadata.maintainer_email"), "keywords": _attrgetter("metadata.keywords"), diff --git a/setuptools/tests/test_core_metadata.py b/setuptools/tests/test_core_metadata.py index b1edb79b40..548cb869f7 100644 --- a/setuptools/tests/test_core_metadata.py +++ b/setuptools/tests/test_core_metadata.py @@ -12,6 +12,7 @@ from pathlib import Path from unittest.mock import Mock +import jaraco.path import pytest from packaging.metadata import Metadata from packaging.requirements import Requirement @@ -442,6 +443,7 @@ class TestPEP643: readme = {text = "Long\\ndescription", content-type = "text/plain"} keywords = ["one", "two"] dependencies = ["requests"] + license = "AGPL-3.0-or-later" [tool.setuptools] provides = ["abcd"] obsoletes = ["abcd"] @@ -490,6 +492,46 @@ def test_modified_fields_marked_as_dynamic(self, file, fields, tmpdir_cwd): metadata = _get_metadata(dist) assert set(metadata.get_all("Dynamic")) == set(fields) + @pytest.mark.parametrize( + "extra_toml", + [ + "# Let setuptools autofill license-files", + "license-files = ['LICENSE*', 'AUTHORS*', 'NOTICE']", + ], + ) + def test_license_files_dynamic(self, extra_toml, tmpdir_cwd): + # For simplicity (and for the time being) setuptools is not making + # any special handling to guarantee `License-File` is considered static. + # Instead we rely in the fact that, although suboptimal, it is OK to have + # it as dynamics, as per: + # https://github.com/pypa/setuptools/issues/4629#issuecomment-2331233677 + files = { + "pyproject.toml": self.STATIC_CONFIG["pyproject.toml"].replace( + 'license = "AGPL-3.0-or-later"', + f"dynamic = ['license']\n{extra_toml}", + ), + "LICENSE.md": "--- mock license ---", + "NOTICE": "--- mock notice ---", + "AUTHORS.txt": "--- me ---", + } + # Sanity checks: + assert extra_toml in files["pyproject.toml"] + assert 'license = "AGPL-3.0-or-later"' not in extra_toml + + jaraco.path.build(files) + dist = _makedist(license_expression="AGPL-3.0-or-later") + metadata = _get_metadata(dist) + assert set(metadata.get_all("Dynamic")) == { + 'license-file', + 'license-expression', + } + assert metadata.get("License-Expression") == "AGPL-3.0-or-later" + assert set(metadata.get_all("License-File")) == { + "NOTICE", + "AUTHORS.txt", + "LICENSE.md", + } + def _makedist(**attrs): dist = Distribution(attrs) From 17dc3dfe4c24c2cf022c3d45e241a691045fee3a Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 19 Feb 2025 13:18:56 +0000 Subject: [PATCH 33/36] Improve license/license_expression relationship with 'dynamic' in pyproject.toml --- setuptools/config/_apply_pyprojecttoml.py | 6 +++++- setuptools/tests/config/test_apply_pyprojecttoml.py | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py index 12c8b23108..7f4c3d8b7e 100644 --- a/setuptools/config/_apply_pyprojecttoml.py +++ b/setuptools/config/_apply_pyprojecttoml.py @@ -201,6 +201,9 @@ def _license(dist: Distribution, val: str | dict, root_dir: StrPath | None): from setuptools.config import expand if isinstance(val, str): + if getattr(dist.metadata, "license", None): + SetuptoolsWarning.emit("`license` overwritten by `pyproject.toml`") + dist.metadata.license = None _set_config(dist, "license_expression", _static.Str(val)) else: pypa_guides = "guides/writing-pyproject-toml/#license" @@ -476,8 +479,9 @@ def _acessor(obj): _RESET_PREVIOUSLY_DEFINED: dict = { # Fix improper setting: given in `setup.py`, but not listed in `dynamic` + # Use "immutable" data structures to avoid in-place modification # dict: pyproject name => value to which reset - "license": _static.EMPTY_DICT, + "license": "", "authors": _static.EMPTY_LIST, "maintainers": _static.EMPTY_LIST, "keywords": _static.EMPTY_LIST, diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index 848f44745f..bf8775ee36 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -554,6 +554,11 @@ def pyproject(self, tmp_path, dynamic, extra_content=""): @pytest.mark.parametrize( ("attr", "field", "value"), [ + ("license_expression", "license", "MIT"), + pytest.param( + *("license", "license", "Not SPDX"), + marks=[pytest.mark.filterwarnings("ignore:.*license. overwritten")], + ), ("classifiers", "classifiers", ["Private :: Classifier"]), ("entry_points", "scripts", {"console_scripts": ["foobar=foobar:main"]}), ("entry_points", "gui-scripts", {"gui_scripts": ["bazquux=bazquux:main"]}), @@ -579,6 +584,7 @@ def test_not_listed_in_dynamic(self, tmp_path, attr, field, value): @pytest.mark.parametrize( ("attr", "field", "value"), [ + ("license_expression", "license", "MIT"), ("install_requires", "dependencies", []), ("extras_require", "optional-dependencies", {}), ("install_requires", "dependencies", ["six"]), From 01396dbd1f79f5d41fa015783163b8a2cfe44d07 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 19 Feb 2025 13:41:32 +0000 Subject: [PATCH 34/36] Add comments and test about dynamic x license_files --- setuptools/config/_apply_pyprojecttoml.py | 4 +++- .../tests/config/test_apply_pyprojecttoml.py | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py index 7f4c3d8b7e..06c9e6413f 100644 --- a/setuptools/config/_apply_pyprojecttoml.py +++ b/setuptools/config/_apply_pyprojecttoml.py @@ -479,9 +479,11 @@ def _acessor(obj): _RESET_PREVIOUSLY_DEFINED: dict = { # Fix improper setting: given in `setup.py`, but not listed in `dynamic` - # Use "immutable" data structures to avoid in-place modification + # Use "immutable" data structures to avoid in-place modification. # dict: pyproject name => value to which reset "license": "", + # XXX: `license-file` is currently not considered in the context of `dynamic`. + # See TestPresetField.test_license_files_exempt_from_dynamic "authors": _static.EMPTY_LIST, "maintainers": _static.EMPTY_LIST, "keywords": _static.EMPTY_LIST, diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index bf8775ee36..bcea9a8847 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -598,6 +598,26 @@ def test_listed_in_dynamic(self, tmp_path, attr, field, value): dist_value = _some_attrgetter(f"metadata.{attr}", attr)(dist) assert dist_value == value + def test_license_files_exempt_from_dynamic(self, monkeypatch, tmp_path): + """ + license-file is currently not considered in the context of dynamic. + As per 2025-02-19, https://packaging.python.org/en/latest/specifications/pyproject-toml/#license-files + allows setuptools to fill-in `license-files` the way it sees fit: + + > If the license-files key is not defined, tools can decide how to handle license files. + > For example they can choose not to include any files or use their own + > logic to discover the appropriate files in the distribution. + + Using license_files from setup.py to fill-in the value is in accordance + with this rule. + """ + monkeypatch.chdir(tmp_path) + pyproject = self.pyproject(tmp_path, []) + dist = makedist(tmp_path, license_files=["LIC*"]) + (tmp_path / "LIC1").write_text("42", encoding="utf-8") + dist = pyprojecttoml.apply_configuration(dist, pyproject) + assert dist.metadata.license_files == ["LIC1"] + def test_warning_overwritten_dependencies(self, tmp_path): src = "[project]\nname='pkg'\nversion='0.1'\ndependencies=['click']\n" pyproject = tmp_path / "pyproject.toml" From 282177c5df9c236a9824b3e2fb4561c66a979973 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 19 Feb 2025 14:00:10 +0000 Subject: [PATCH 35/36] Apply suggestions from code review --- setuptools/config/_apply_pyprojecttoml.py | 3 ++- setuptools/dist.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py index 06c9e6413f..ffa3fc3c49 100644 --- a/setuptools/config/_apply_pyprojecttoml.py +++ b/setuptools/config/_apply_pyprojecttoml.py @@ -463,7 +463,8 @@ def _acessor(obj): "readme": _attrgetter("metadata.long_description"), "requires-python": _some_attrgetter("python_requires", "metadata.python_requires"), "license": _some_attrgetter("metadata.license_expression", "metadata.license"), - # XXX: Should we wait until someone requires `license_files`? + # XXX: `license-file` is currently not considered in the context of `dynamic`. + # See TestPresetField.test_license_files_exempt_from_dynamic "authors": _some_attrgetter("metadata.author", "metadata.author_email"), "maintainers": _some_attrgetter("metadata.maintainer", "metadata.maintainer_email"), "keywords": _attrgetter("metadata.keywords"), diff --git a/setuptools/dist.py b/setuptools/dist.py index 15b1967be3..133948eb08 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -412,7 +412,7 @@ def _finalize_license_expression(self) -> None: >>> _static.is_static(dist.metadata.license_expression) True >>> dist._finalize_license_expression() - >>> _static.is_static(dist.metadata.license_expression) # preserve static-ness" + >>> _static.is_static(dist.metadata.license_expression) # preserve "static-ness" True >>> print(dist.metadata.license_expression) MIT AND GPL-3.0-or-later From 0bb59c250dbf20b49b20dd3fd9badcfaa0da6aaf Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 20 Feb 2025 11:58:13 +0100 Subject: [PATCH 36/36] Update link in userguide --- docs/userguide/pyproject_config.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/userguide/pyproject_config.rst b/docs/userguide/pyproject_config.rst index 3bf99bed1a..e4cee52aa3 100644 --- a/docs/userguide/pyproject_config.rst +++ b/docs/userguide/pyproject_config.rst @@ -99,7 +99,8 @@ Key Value Type (TOML) Notes See :doc:`/userguide/datafiles`. ``exclude-package-data`` table/inline-table Empty by default. See :doc:`/userguide/datafiles`. ------------------------- --------------------------- ------------------------- -``license-files`` array of glob patterns **Deprecated** - use ``project.license-files`` instead. See :doc:`PyPUG:guides/writing-pyproject-toml` +``license-files`` array of glob patterns **Deprecated** - use ``project.license-files`` instead. See + :external+PyPUG:ref:`Writing your pyproject.toml ` (by default: ``['LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*']``) ``data-files`` table/inline-table **Discouraged** - check :doc:`/userguide/datafiles`. Whenever possible, consider using data files inside the package directories.