From c70a1781705eb86b4ee9e4b3c1fb834ab93b44eb Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Mon, 14 Oct 2024 00:21:36 -0400 Subject: [PATCH 01/13] Improve distribution variant config docs --- docs/plugins/environment/virtual.md | 2 +- docs/tutorials/python/manage.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plugins/environment/virtual.md b/docs/plugins/environment/virtual.md index a20ebd6bc..0e36fcb49 100644 --- a/docs/plugins/environment/virtual.md +++ b/docs/plugins/environment/virtual.md @@ -72,7 +72,7 @@ The following options are recognized for internal Python resolution. The source of distributions is the [python-build-standalone](https://github.com/indygreg/python-build-standalone) project. -Some distributions have [variants](https://gregoryszorc.com/docs/python-build-standalone/main/running.html) that may be configured. Options may be combined. +Some distributions have [variants](https://gregoryszorc.com/docs/python-build-standalone/main/running.html) that may be configured with environment variables. Options may be combined. | Option | Platforms | Allowed values | | --- | --- | --- | diff --git a/docs/tutorials/python/manage.md b/docs/tutorials/python/manage.md index 43ca6c650..1ba0f5b6f 100644 --- a/docs/tutorials/python/manage.md +++ b/docs/tutorials/python/manage.md @@ -126,7 +126,7 @@ When there are no updates available for a distribution, a warning will be displa ``` $ hatch python update 3.12 -The latest version is already installed: 3.12.3 +The latest version is already installed: 3.12.7 ``` ## Removal From 9650a1dd3b606cf4777f42bd95874c2a9be0d6a2 Mon Sep 17 00:00:00 2001 From: Ben Mares Date: Mon, 21 Oct 2024 01:46:49 +0100 Subject: [PATCH 02/13] Fix failing test due to unsorted iterdir (#1761) --- tests/cli/env/test_create.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/cli/env/test_create.py b/tests/cli/env/test_create.py index 7106f4479..f5de70c02 100644 --- a/tests/cli/env/test_create.py +++ b/tests/cli/env/test_create.py @@ -1560,13 +1560,11 @@ def update(self, metadata): storage_path = storage_dirs[0] assert len(storage_path.name) == 8 - env_dirs = list(storage_path.iterdir()) - assert len(env_dirs) == 2 + env_dirs = sorted(storage_path.iterdir()) + assert [d.name for d in env_dirs] == ['hatch-build', 'test'] env_path = env_dirs[1] - assert env_path.name == 'test' - with UVVirtualEnv(env_path, platform): output = platform.run_command([uv_on_path, 'pip', 'freeze'], check=True, capture_output=True).stdout.decode( 'utf-8' From 750b4984ef13b205e7d0624baab2ad589e9d32bd Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Fri, 8 Nov 2024 00:39:08 +0100 Subject: [PATCH 03/13] Upgrade Ruff to 0.6.8 (#1588) Co-authored-by: Ofek Lev --- backend/src/hatchling/metadata/spec.py | 2 +- docs/.hooks/render_ruff_defaults.py | 1 + ruff_defaults.toml | 12 +- src/hatch/cli/fmt/core.py | 131 +++++++++++++--------- src/hatch/env/internal/static_analysis.py | 2 +- 5 files changed, 85 insertions(+), 63 deletions(-) diff --git a/backend/src/hatchling/metadata/spec.py b/backend/src/hatchling/metadata/spec.py index 16cb6d1c5..a83ea3db1 100644 --- a/backend/src/hatchling/metadata/spec.py +++ b/backend/src/hatchling/metadata/spec.py @@ -174,7 +174,7 @@ def project_metadata_from_core_metadata(core_metadata: str) -> dict[str, Any]: left, _, right = marker if left.value == 'extra': extra = right.value - del markers[i] + del markers[i] # noqa: B909 # If there was only one marker then there will be an unnecessary # trailing semicolon in the string representation if not markers: diff --git a/docs/.hooks/render_ruff_defaults.py b/docs/.hooks/render_ruff_defaults.py index e6a73c373..8f4d0fdaf 100644 --- a/docs/.hooks/render_ruff_defaults.py +++ b/docs/.hooks/render_ruff_defaults.py @@ -270,6 +270,7 @@ def run(self, lines): # noqa: PLR6301 'PLR0915', 'PLR0916', 'PLR0917', + 'PLR1701', 'PLR1702', 'PLR1706', 'PT004', diff --git a/ruff_defaults.toml b/ruff_defaults.toml index 698e8e5a0..9acc585a3 100644 --- a/ruff_defaults.toml +++ b/ruff_defaults.toml @@ -10,13 +10,12 @@ select = [ "A002", "A003", "ARG001", + "ARG001", "ARG002", "ARG003", "ARG004", "ARG005", "ASYNC100", - "ASYNC101", - "ASYNC102", "B002", "B003", "B004", @@ -130,7 +129,6 @@ select = [ "E742", "E743", "E902", - "E999", "EM101", "EM102", "EM103", @@ -323,7 +321,6 @@ select = [ "PLR0203", "PLR0206", "PLR0402", - "PLR1701", "PLR1704", "PLR1711", "PLR1714", @@ -466,7 +463,6 @@ select = [ "RUF022", "RUF023", "RUF024", - "RUF025", "RUF026", "RUF027", "RUF028", @@ -590,11 +586,6 @@ select = [ "TID251", "TID252", "TID253", - "TRIO100", - "TRIO105", - "TRIO109", - "TRIO110", - "TRIO115", "TRY002", "TRY003", "TRY004", @@ -628,7 +619,6 @@ select = [ "UP024", "UP025", "UP026", - "UP027", "UP028", "UP029", "UP030", diff --git a/src/hatch/cli/fmt/core.py b/src/hatch/cli/fmt/core.py index 8b079d89a..de454a342 100644 --- a/src/hatch/cli/fmt/core.py +++ b/src/hatch/cli/fmt/core.py @@ -167,8 +167,16 @@ def get_config(self, section: str) -> dict[str, Any]: 'ARG004', 'ARG005', 'ASYNC100', - 'ASYNC101', - 'ASYNC102', + 'ASYNC105', + 'ASYNC109', + 'ASYNC110', + 'ASYNC115', + 'ASYNC210', + 'ASYNC220', + 'ASYNC221', + 'ASYNC222', + 'ASYNC230', + 'ASYNC251', 'B002', 'B003', 'B004', @@ -307,6 +315,17 @@ def get_config(self, section: str) -> dict[str, Any]: 'FBT001', 'FBT002', 'FLY002', + 'FURB105', + 'FURB129', + 'FURB136', + 'FURB161', + 'FURB163', + 'FURB167', + 'FURB168', + 'FURB169', + 'FURB177', + 'FURB181', + 'FURB187', 'G001', 'G002', 'G003', @@ -349,6 +368,7 @@ def get_config(self, section: str) -> dict[str, Any]: 'PERF102', 'PERF401', 'PERF402', + 'PERF403', 'PGH005', 'PIE790', 'PIE794', @@ -364,18 +384,28 @@ def get_config(self, section: str) -> dict[str, Any]: 'PLC0205', 'PLC0208', 'PLC0414', + 'PLC2401', + 'PLC2403', 'PLC3002', 'PLE0100', 'PLE0101', + 'PLE0115', 'PLE0116', 'PLE0117', 'PLE0118', 'PLE0237', 'PLE0241', 'PLE0302', + 'PLE0303', + 'PLE0305', 'PLE0307', + 'PLE0308', + 'PLE0309', 'PLE0604', 'PLE0605', + 'PLE0643', + 'PLE0704', + 'PLE1132', 'PLE1142', 'PLE1205', 'PLE1206', @@ -383,6 +413,8 @@ def get_config(self, section: str) -> dict[str, Any]: 'PLE1307', 'PLE1310', 'PLE1507', + 'PLE1519', + 'PLE1520', 'PLE1700', 'PLE2502', 'PLE2510', @@ -394,23 +426,34 @@ def get_config(self, section: str) -> dict[str, Any]: 'PLR0133', 'PLR0206', 'PLR0402', - 'PLR1701', + 'PLR1704', 'PLR1711', 'PLR1714', 'PLR1722', + 'PLR1730', + 'PLR1736', 'PLR2004', + 'PLR2044', 'PLR5501', 'PLW0120', 'PLW0127', + 'PLW0128', 'PLW0129', 'PLW0131', + 'PLW0133', + 'PLW0211', + 'PLW0245', 'PLW0406', 'PLW0602', 'PLW0603', + 'PLW0604', + 'PLW0642', 'PLW0711', + 'PLW1501', 'PLW1508', 'PLW1509', 'PLW1510', + 'PLW2101', 'PLW2901', 'PLW3301', 'PT001', @@ -485,7 +528,9 @@ def get_config(self, section: str) -> dict[str, Any]: 'PYI054', 'PYI055', 'PYI056', + 'PYI057', 'PYI058', + 'PYI062', 'RET503', 'RET504', 'RET505', @@ -510,7 +555,10 @@ def get_config(self, section: str) -> dict[str, Any]: 'RUF018', 'RUF019', 'RUF020', + 'RUF024', + 'RUF026', 'RUF100', + 'RUF101', 'S101', 'S102', 'S103', @@ -563,6 +611,7 @@ def get_config(self, section: str) -> dict[str, Any]: 'S607', 'S608', 'S609', + 'S610', 'S611', 'S612', 'S701', @@ -615,11 +664,6 @@ def get_config(self, section: str) -> dict[str, Any]: 'TID251', 'TID252', 'TID253', - 'TRIO100', - 'TRIO105', - 'TRIO109', - 'TRIO110', - 'TRIO115', 'TRY002', 'TRY003', 'TRY004', @@ -685,7 +729,20 @@ def get_config(self, section: str) -> dict[str, Any]: 'YTT303', ) PREVIEW_RULES: tuple[str, ...] = ( + 'A004', + 'A005', + 'A006', + 'ASYNC116', + 'B039', + 'B901', 'B909', + 'C420', + 'DOC201', + 'DOC202', + 'DOC402', + 'DOC403', + 'DOC501', + 'DOC502', 'E112', 'E113', 'E115', @@ -693,6 +750,7 @@ def get_config(self, section: str) -> dict[str, Any]: 'E201', 'E202', 'E203', + 'E204', 'E211', 'E221', 'E222', @@ -717,88 +775,61 @@ def get_config(self, section: str) -> dict[str, Any]: 'E274', 'E275', 'E502', - 'FURB105', + 'FAST001', + 'FAST002', + 'FAST003', 'FURB110', 'FURB113', 'FURB116', 'FURB118', - 'FURB129', 'FURB131', 'FURB132', - 'FURB136', 'FURB142', 'FURB145', 'FURB148', 'FURB152', + 'FURB154', 'FURB157', - 'FURB161', - 'FURB163', 'FURB164', 'FURB166', - 'FURB167', - 'FURB168', - 'FURB169', 'FURB171', - 'FURB177', 'FURB180', - 'FURB181', - 'FURB187', + 'FURB188', 'FURB192', - 'PERF403', + 'PLC0206', 'PLC0415', 'PLC1901', - 'PLC2401', - 'PLC2403', 'PLC2701', 'PLC2801', - 'PLE0115', - 'PLE0303', 'PLE0304', - 'PLE0305', - 'PLE0308', - 'PLE0309', - 'PLE0643', - 'PLE0704', - 'PLE1132', 'PLE1141', - 'PLE1519', - 'PLE1520', 'PLE4703', 'PLR0202', 'PLR0203', - 'PLR1704', - 'PLR1730', 'PLR1733', - 'PLR1736', - 'PLR2044', 'PLR6104', 'PLR6201', 'PLR6301', 'PLW0108', - 'PLW0128', - 'PLW0133', 'PLW0177', - 'PLW0211', - 'PLW0245', - 'PLW0604', - 'PLW0642', - 'PLW1501', 'PLW1514', 'PLW1641', - 'PLW2101', 'PLW3201', 'PYI059', - 'PYI062', + 'PYI063', + 'PYI064', + 'PYI066', 'RUF021', 'RUF022', 'RUF023', - 'RUF024', - 'RUF025', - 'RUF026', 'RUF027', 'RUF028', 'RUF029', - 'RUF101', + 'RUF030', + 'RUF031', + 'RUF032', + 'RUF033', + 'RUF034', 'S401', 'S402', 'S403', @@ -811,8 +842,8 @@ def get_config(self, section: str) -> dict[str, Any]: 'S412', 'S413', 'S415', - 'S610', 'UP042', + 'UP043', 'W391', ) PER_FILE_IGNORED_RULES: dict[str, list[str]] = { diff --git a/src/hatch/env/internal/static_analysis.py b/src/hatch/env/internal/static_analysis.py index c54895a8f..c4bd01a42 100644 --- a/src/hatch/env/internal/static_analysis.py +++ b/src/hatch/env/internal/static_analysis.py @@ -17,4 +17,4 @@ def get_default_config() -> dict[str, Any]: } -RUFF_DEFAULT_VERSION: str = '0.4.5' +RUFF_DEFAULT_VERSION: str = '0.6.8' From a2bcbcbadb5152d78bfb471a77b33f5c3aa0aefc Mon Sep 17 00:00:00 2001 From: Leah Wasser Date: Thu, 7 Nov 2024 16:41:51 -0700 Subject: [PATCH 04/13] feat(docs): contributing file update & envt fix (#1779) --- docs/community/contributing.md | 2 +- hatch.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/community/contributing.md b/docs/community/contributing.md index 88ad71b5d..40dfa2813 100644 --- a/docs/community/contributing.md +++ b/docs/community/contributing.md @@ -65,4 +65,4 @@ Build and validate the documentation website: ```bash hatch run docs:build-check -``` +``` \ No newline at end of file diff --git a/hatch.toml b/hatch.toml index 51b950c92..695204d3c 100644 --- a/hatch.toml +++ b/hatch.toml @@ -65,6 +65,7 @@ dependencies = [ # Validation # https://github.com/linkchecker/linkchecker/pull/669#issuecomment-1267236287 "linkchecker @ git+https://github.com/linkchecker/linkchecker.git@d9265bb71c2054bf57b8c5734a4825d62505c779", + "griffe<1.0", ] pre-install-commands = [ "python scripts/install_mkdocs_material_insiders.py", From 3d312c788951a6a7c5760b155a647800c5f56c4d Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Sat, 9 Nov 2024 11:17:23 -0500 Subject: [PATCH 05/13] Fix CI (#1789) --- .github/workflows/build-distributions.yml | 2 +- .github/workflows/build-hatch.yml | 8 ++++---- .github/workflows/build-hatchling.yml | 2 +- .github/workflows/cli.yml | 2 +- .github/workflows/docs-dev.yml | 2 +- .github/workflows/docs-release.yml | 2 +- .github/workflows/test.yml | 4 ++-- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build-distributions.yml b/.github/workflows/build-distributions.yml index aac9c4d8e..542031b4e 100644 --- a/.github/workflows/build-distributions.yml +++ b/.github/workflows/build-distributions.yml @@ -32,7 +32,7 @@ jobs: python-version: ${{ env.DIST_PYTHON_VERSION }} - name: Install UV - run: curl -LsSf https://github.com/astral-sh/uv/releases/latest/download/uv-installer.sh | sh + uses: astral-sh/setup-uv@v3 - name: Install Hatch if: inputs.version diff --git a/.github/workflows/build-hatch.yml b/.github/workflows/build-hatch.yml index 7645d0bec..d37a8ea43 100644 --- a/.github/workflows/build-hatch.yml +++ b/.github/workflows/build-hatch.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ env.PYTHON_VERSION }} - name: Install UV - run: curl -LsSf https://github.com/astral-sh/uv/releases/latest/download/uv-installer.sh | sh + uses: astral-sh/setup-uv@v3 - name: Install tools run: uv pip install --system build hatch @@ -158,7 +158,7 @@ jobs: python-version: ${{ env.PYTHON_VERSION }} - name: Install UV - run: curl -LsSf https://github.com/astral-sh/uv/releases/latest/download/uv-installer.sh | sh + uses: astral-sh/setup-uv@v3 - name: Install Hatch run: |- @@ -284,7 +284,7 @@ jobs: python-version: ${{ env.PYTHON_VERSION }} - name: Install UV - run: curl -LsSf https://github.com/astral-sh/uv/releases/latest/download/uv-installer.sh | sh + uses: astral-sh/setup-uv@v3 - name: Install PyOxidizer ${{ env.PYOXIDIZER_VERSION }} run: uv pip install --system pyoxidizer==${{ env.PYOXIDIZER_VERSION }} @@ -370,7 +370,7 @@ jobs: python-version: ${{ env.PYTHON_VERSION }} - name: Install UV - run: curl -LsSf https://github.com/astral-sh/uv/releases/latest/download/uv-installer.sh | sh + uses: astral-sh/setup-uv@v3 - name: Install PyOxidizer ${{ env.PYOXIDIZER_VERSION }} run: uv pip install --system pyoxidizer==${{ env.PYOXIDIZER_VERSION }} diff --git a/.github/workflows/build-hatchling.yml b/.github/workflows/build-hatchling.yml index 19b4c5bb1..803d13ec9 100644 --- a/.github/workflows/build-hatchling.yml +++ b/.github/workflows/build-hatchling.yml @@ -22,7 +22,7 @@ jobs: python-version: ${{ env.PYTHON_VERSION }} - name: Install UV - run: curl -LsSf https://github.com/astral-sh/uv/releases/latest/download/uv-installer.sh | sh + uses: astral-sh/setup-uv@v3 - name: Install build dependencies run: uv pip install --system --upgrade build diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 9859eac61..e384a9de9 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -30,7 +30,7 @@ jobs: python-version: ${{ env.STABLE_PYTHON_VERSION }} - name: Install UV - run: curl -LsSf https://github.com/astral-sh/uv/releases/latest/download/uv-installer.sh | sh + uses: astral-sh/setup-uv@v3 - name: Install hyperfine uses: taiki-e/install-action@v2 diff --git a/.github/workflows/docs-dev.yml b/.github/workflows/docs-dev.yml index 4def6ab65..f8a3c72a0 100644 --- a/.github/workflows/docs-dev.yml +++ b/.github/workflows/docs-dev.yml @@ -33,7 +33,7 @@ jobs: run: python scripts/validate_history.py - name: Install UV - run: curl -LsSf https://github.com/astral-sh/uv/releases/latest/download/uv-installer.sh | sh + uses: astral-sh/setup-uv@v3 - name: Install ourself run: | diff --git a/.github/workflows/docs-release.yml b/.github/workflows/docs-release.yml index 594858936..54e6e59cd 100644 --- a/.github/workflows/docs-release.yml +++ b/.github/workflows/docs-release.yml @@ -31,7 +31,7 @@ jobs: run: python scripts/validate_history.py - name: Install UV - run: curl -LsSf https://github.com/astral-sh/uv/releases/latest/download/uv-installer.sh | sh + uses: astral-sh/setup-uv@v3 - name: Install ourself run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1e3052598..0d4b62408 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,8 +34,8 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Install UV - run: curl -LsSf https://github.com/astral-sh/uv/releases/latest/download/uv-installer.sh | sh + - name: Install uv + uses: astral-sh/setup-uv@v3 - name: Install ourself run: | From f8a2eaa2e0ce80a931837539d8f565ceeab75961 Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Sat, 9 Nov 2024 11:35:16 -0500 Subject: [PATCH 06/13] Bump `packaging` to 24.2 (#1788) --- backend/pyproject.toml | 2 +- backend/scripts/update_licenses.py | 71 -- backend/src/hatchling/licenses/__init__.py | 0 backend/src/hatchling/licenses/parse.py | 93 --- backend/src/hatchling/licenses/supported.py | 713 -------------------- backend/src/hatchling/metadata/core.py | 4 +- docs/history/hatch.md | 1 + docs/history/hatchling.md | 4 + hatch.toml | 4 - pyproject.toml | 2 +- src/hatch/template/default.py | 2 +- tests/backend/licenses/__init__.py | 0 tests/backend/licenses/test_parse.py | 56 -- tests/backend/licenses/test_supported.py | 31 - tests/backend/metadata/test_core.py | 2 +- 15 files changed, 11 insertions(+), 974 deletions(-) delete mode 100644 backend/scripts/update_licenses.py delete mode 100644 backend/src/hatchling/licenses/__init__.py delete mode 100644 backend/src/hatchling/licenses/parse.py delete mode 100644 backend/src/hatchling/licenses/supported.py delete mode 100644 tests/backend/licenses/__init__.py delete mode 100644 tests/backend/licenses/test_parse.py delete mode 100644 tests/backend/licenses/test_supported.py diff --git a/backend/pyproject.toml b/backend/pyproject.toml index d90a6a262..722b57317 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -35,7 +35,7 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", ] dependencies = [ - "packaging>=23.2", + "packaging>=24.2", "pathspec>=0.10.1", "pluggy>=1.0.0", "tomli>=1.2.2; python_version < '3.11'", diff --git a/backend/scripts/update_licenses.py b/backend/scripts/update_licenses.py deleted file mode 100644 index a7820b43f..000000000 --- a/backend/scripts/update_licenses.py +++ /dev/null @@ -1,71 +0,0 @@ -import json -import pathlib -import time -from contextlib import closing -from io import StringIO - -import httpx - -LATEST_API = 'https://api.github.com/repos/spdx/license-list-data/releases/latest' -LICENSES_URL = 'https://raw.githubusercontent.com/spdx/license-list-data/v{}/json/licenses.json' -EXCEPTIONS_URL = 'https://raw.githubusercontent.com/spdx/license-list-data/v{}/json/exceptions.json' - - -def download_data(url): - for _ in range(600): - try: - response = httpx.get(url) - response.raise_for_status() - except Exception: # noqa: BLE001 - time.sleep(1) - continue - else: - return json.loads(response.content.decode('utf-8')) - - message = 'Download failed' - raise ConnectionError(message) - - -def main(): - latest_version = download_data(LATEST_API)['tag_name'][1:] - - licenses = {} - for license_data in download_data(LICENSES_URL.format(latest_version))['licenses']: - license_id = license_data['licenseId'] - deprecated = license_data['isDeprecatedLicenseId'] - licenses[license_id.lower()] = {'id': license_id, 'deprecated': deprecated} - - exceptions = {} - for exception_data in download_data(EXCEPTIONS_URL.format(latest_version))['exceptions']: - exception_id = exception_data['licenseExceptionId'] - deprecated = exception_data['isDeprecatedLicenseId'] - exceptions[exception_id.lower()] = {'id': exception_id, 'deprecated': deprecated} - - project_root = pathlib.Path(__file__).resolve().parent.parent - data_file = project_root / 'src' / 'hatchling' / 'licenses' / 'supported.py' - - with closing(StringIO()) as file_contents: - file_contents.write( - f"""\ -from __future__ import annotations - -VERSION = {latest_version!r}\n\nLICENSES: dict[str, dict[str, str | bool]] = {{ -""" - ) - - for normalized_name, data in sorted(licenses.items()): - file_contents.write(f' {normalized_name!r}: {data!r},\n') - - file_contents.write('}\n\nEXCEPTIONS: dict[str, dict[str, str | bool]] = {\n') - - for normalized_name, data in sorted(exceptions.items()): - file_contents.write(f' {normalized_name!r}: {data!r},\n') - - file_contents.write('}\n') - - with data_file.open('w', encoding='utf-8') as f: - f.write(file_contents.getvalue()) - - -if __name__ == '__main__': - main() diff --git a/backend/src/hatchling/licenses/__init__.py b/backend/src/hatchling/licenses/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/backend/src/hatchling/licenses/parse.py b/backend/src/hatchling/licenses/parse.py deleted file mode 100644 index 8597655c6..000000000 --- a/backend/src/hatchling/licenses/parse.py +++ /dev/null @@ -1,93 +0,0 @@ -from __future__ import annotations - -from typing import cast - -from hatchling.licenses.supported import EXCEPTIONS, LICENSES - - -def get_valid_licenses() -> dict[str, dict[str, str | bool]]: - valid_licenses = LICENSES.copy() - - # https://peps.python.org/pep-0639/#should-custom-license-identifiers-be-allowed - public_license = 'LicenseRef-Public-Domain' - valid_licenses[public_license.lower()] = {'id': public_license, 'deprecated': False} - - proprietary_license = 'LicenseRef-Proprietary' - valid_licenses[proprietary_license.lower()] = {'id': proprietary_license, 'deprecated': False} - - return valid_licenses - - -def normalize_license_expression(raw_license_expression: str) -> str: - if not raw_license_expression: - return raw_license_expression - - valid_licenses = get_valid_licenses() - - # First normalize to lower case so we can look up licenses/exceptions - # and so boolean operators are Python-compatible - license_expression = raw_license_expression.lower() - - # Then pad parentheses so tokenization can be achieved by merely splitting on white space - license_expression = license_expression.replace('(', ' ( ').replace(')', ' ) ') - - # Now we begin parsing - tokens = license_expression.split() - - # Rather than implementing boolean logic we create an expression that Python can parse. - # Everything that is not involved with the grammar itself is treated as `False` and the - # expression should evaluate as such. - python_tokens = [] - for token in tokens: - if token not in {'or', 'and', 'with', '(', ')'}: - python_tokens.append('False') - elif token == 'with': # noqa: S105 - python_tokens.append('or') - elif token == '(' and python_tokens and python_tokens[-1] not in {'or', 'and'}: # noqa: S105 - message = f'invalid license expression: {raw_license_expression}' - raise ValueError(message) - else: - python_tokens.append(token) - - python_expression = ' '.join(python_tokens) - try: - result = eval(python_expression) # noqa: S307 - except Exception: # noqa: BLE001 - result = True - - if result is not False: - message = f'invalid license expression: {raw_license_expression}' - raise ValueError(message) from None - - # Take a final pass to check for unknown licenses/exceptions - normalized_tokens = [] - for token in tokens: - if token in {'or', 'and', 'with', '(', ')'}: - normalized_tokens.append(token.upper()) - continue - - if normalized_tokens and normalized_tokens[-1] == 'WITH': - if token not in EXCEPTIONS: - message = f'unknown license exception: {token}' - raise ValueError(message) - - normalized_tokens.append(cast(str, EXCEPTIONS[token]['id'])) - else: - if token.endswith('+'): - final_token = token[:-1] - suffix = '+' - else: - final_token = token - suffix = '' - - if final_token not in valid_licenses: - message = f'unknown license: {final_token}' - raise ValueError(message) - - normalized_tokens.append(cast(str, valid_licenses[final_token]['id']) + suffix) - - # Construct the normalized expression - normalized_expression = ' '.join(normalized_tokens) - - # Fix internal padding for parentheses - return normalized_expression.replace('( ', '(').replace(' )', ')') diff --git a/backend/src/hatchling/licenses/supported.py b/backend/src/hatchling/licenses/supported.py deleted file mode 100644 index 43de8636c..000000000 --- a/backend/src/hatchling/licenses/supported.py +++ /dev/null @@ -1,713 +0,0 @@ -from __future__ import annotations - -VERSION = '3.23' - -LICENSES: dict[str, dict[str, str | bool]] = { - '0bsd': {'id': '0BSD', 'deprecated': False}, - 'aal': {'id': 'AAL', 'deprecated': False}, - 'abstyles': {'id': 'Abstyles', 'deprecated': False}, - 'adacore-doc': {'id': 'AdaCore-doc', 'deprecated': False}, - 'adobe-2006': {'id': 'Adobe-2006', 'deprecated': False}, - 'adobe-display-postscript': {'id': 'Adobe-Display-PostScript', 'deprecated': False}, - 'adobe-glyph': {'id': 'Adobe-Glyph', 'deprecated': False}, - 'adobe-utopia': {'id': 'Adobe-Utopia', 'deprecated': False}, - 'adsl': {'id': 'ADSL', 'deprecated': False}, - 'afl-1.1': {'id': 'AFL-1.1', 'deprecated': False}, - 'afl-1.2': {'id': 'AFL-1.2', 'deprecated': False}, - 'afl-2.0': {'id': 'AFL-2.0', 'deprecated': False}, - 'afl-2.1': {'id': 'AFL-2.1', 'deprecated': False}, - 'afl-3.0': {'id': 'AFL-3.0', 'deprecated': False}, - 'afmparse': {'id': 'Afmparse', 'deprecated': False}, - 'agpl-1.0': {'id': 'AGPL-1.0', 'deprecated': True}, - 'agpl-1.0-only': {'id': 'AGPL-1.0-only', 'deprecated': False}, - 'agpl-1.0-or-later': {'id': 'AGPL-1.0-or-later', 'deprecated': False}, - 'agpl-3.0': {'id': 'AGPL-3.0', 'deprecated': True}, - 'agpl-3.0-only': {'id': 'AGPL-3.0-only', 'deprecated': False}, - 'agpl-3.0-or-later': {'id': 'AGPL-3.0-or-later', 'deprecated': False}, - 'aladdin': {'id': 'Aladdin', 'deprecated': False}, - 'amdplpa': {'id': 'AMDPLPA', 'deprecated': False}, - 'aml': {'id': 'AML', 'deprecated': False}, - 'aml-glslang': {'id': 'AML-glslang', 'deprecated': False}, - 'ampas': {'id': 'AMPAS', 'deprecated': False}, - 'antlr-pd': {'id': 'ANTLR-PD', 'deprecated': False}, - 'antlr-pd-fallback': {'id': 'ANTLR-PD-fallback', 'deprecated': False}, - 'apache-1.0': {'id': 'Apache-1.0', 'deprecated': False}, - 'apache-1.1': {'id': 'Apache-1.1', 'deprecated': False}, - 'apache-2.0': {'id': 'Apache-2.0', 'deprecated': False}, - 'apafml': {'id': 'APAFML', 'deprecated': False}, - 'apl-1.0': {'id': 'APL-1.0', 'deprecated': False}, - 'app-s2p': {'id': 'App-s2p', 'deprecated': False}, - 'apsl-1.0': {'id': 'APSL-1.0', 'deprecated': False}, - 'apsl-1.1': {'id': 'APSL-1.1', 'deprecated': False}, - 'apsl-1.2': {'id': 'APSL-1.2', 'deprecated': False}, - 'apsl-2.0': {'id': 'APSL-2.0', 'deprecated': False}, - 'arphic-1999': {'id': 'Arphic-1999', 'deprecated': False}, - 'artistic-1.0': {'id': 'Artistic-1.0', 'deprecated': False}, - 'artistic-1.0-cl8': {'id': 'Artistic-1.0-cl8', 'deprecated': False}, - 'artistic-1.0-perl': {'id': 'Artistic-1.0-Perl', 'deprecated': False}, - 'artistic-2.0': {'id': 'Artistic-2.0', 'deprecated': False}, - 'aswf-digital-assets-1.0': {'id': 'ASWF-Digital-Assets-1.0', 'deprecated': False}, - 'aswf-digital-assets-1.1': {'id': 'ASWF-Digital-Assets-1.1', 'deprecated': False}, - 'baekmuk': {'id': 'Baekmuk', 'deprecated': False}, - 'bahyph': {'id': 'Bahyph', 'deprecated': False}, - 'barr': {'id': 'Barr', 'deprecated': False}, - 'bcrypt-solar-designer': {'id': 'bcrypt-Solar-Designer', 'deprecated': False}, - 'beerware': {'id': 'Beerware', 'deprecated': False}, - 'bitstream-charter': {'id': 'Bitstream-Charter', 'deprecated': False}, - 'bitstream-vera': {'id': 'Bitstream-Vera', 'deprecated': False}, - 'bittorrent-1.0': {'id': 'BitTorrent-1.0', 'deprecated': False}, - 'bittorrent-1.1': {'id': 'BitTorrent-1.1', 'deprecated': False}, - 'blessing': {'id': 'blessing', 'deprecated': False}, - 'blueoak-1.0.0': {'id': 'BlueOak-1.0.0', 'deprecated': False}, - 'boehm-gc': {'id': 'Boehm-GC', 'deprecated': False}, - 'borceux': {'id': 'Borceux', 'deprecated': False}, - 'brian-gladman-2-clause': {'id': 'Brian-Gladman-2-Clause', 'deprecated': False}, - 'brian-gladman-3-clause': {'id': 'Brian-Gladman-3-Clause', 'deprecated': False}, - 'bsd-1-clause': {'id': 'BSD-1-Clause', 'deprecated': False}, - 'bsd-2-clause': {'id': 'BSD-2-Clause', 'deprecated': False}, - 'bsd-2-clause-darwin': {'id': 'BSD-2-Clause-Darwin', 'deprecated': False}, - 'bsd-2-clause-freebsd': {'id': 'BSD-2-Clause-FreeBSD', 'deprecated': True}, - 'bsd-2-clause-netbsd': {'id': 'BSD-2-Clause-NetBSD', 'deprecated': True}, - 'bsd-2-clause-patent': {'id': 'BSD-2-Clause-Patent', 'deprecated': False}, - 'bsd-2-clause-views': {'id': 'BSD-2-Clause-Views', 'deprecated': False}, - 'bsd-3-clause': {'id': 'BSD-3-Clause', 'deprecated': False}, - 'bsd-3-clause-acpica': {'id': 'BSD-3-Clause-acpica', 'deprecated': False}, - 'bsd-3-clause-attribution': {'id': 'BSD-3-Clause-Attribution', 'deprecated': False}, - 'bsd-3-clause-clear': {'id': 'BSD-3-Clause-Clear', 'deprecated': False}, - 'bsd-3-clause-flex': {'id': 'BSD-3-Clause-flex', 'deprecated': False}, - 'bsd-3-clause-hp': {'id': 'BSD-3-Clause-HP', 'deprecated': False}, - 'bsd-3-clause-lbnl': {'id': 'BSD-3-Clause-LBNL', 'deprecated': False}, - 'bsd-3-clause-modification': {'id': 'BSD-3-Clause-Modification', 'deprecated': False}, - 'bsd-3-clause-no-military-license': {'id': 'BSD-3-Clause-No-Military-License', 'deprecated': False}, - 'bsd-3-clause-no-nuclear-license': {'id': 'BSD-3-Clause-No-Nuclear-License', 'deprecated': False}, - 'bsd-3-clause-no-nuclear-license-2014': {'id': 'BSD-3-Clause-No-Nuclear-License-2014', 'deprecated': False}, - 'bsd-3-clause-no-nuclear-warranty': {'id': 'BSD-3-Clause-No-Nuclear-Warranty', 'deprecated': False}, - 'bsd-3-clause-open-mpi': {'id': 'BSD-3-Clause-Open-MPI', 'deprecated': False}, - 'bsd-3-clause-sun': {'id': 'BSD-3-Clause-Sun', 'deprecated': False}, - 'bsd-4-clause': {'id': 'BSD-4-Clause', 'deprecated': False}, - 'bsd-4-clause-shortened': {'id': 'BSD-4-Clause-Shortened', 'deprecated': False}, - 'bsd-4-clause-uc': {'id': 'BSD-4-Clause-UC', 'deprecated': False}, - 'bsd-4.3reno': {'id': 'BSD-4.3RENO', 'deprecated': False}, - 'bsd-4.3tahoe': {'id': 'BSD-4.3TAHOE', 'deprecated': False}, - 'bsd-advertising-acknowledgement': {'id': 'BSD-Advertising-Acknowledgement', 'deprecated': False}, - 'bsd-attribution-hpnd-disclaimer': {'id': 'BSD-Attribution-HPND-disclaimer', 'deprecated': False}, - 'bsd-inferno-nettverk': {'id': 'BSD-Inferno-Nettverk', 'deprecated': False}, - 'bsd-protection': {'id': 'BSD-Protection', 'deprecated': False}, - 'bsd-source-beginning-file': {'id': 'BSD-Source-beginning-file', 'deprecated': False}, - 'bsd-source-code': {'id': 'BSD-Source-Code', 'deprecated': False}, - 'bsd-systemics': {'id': 'BSD-Systemics', 'deprecated': False}, - 'bsd-systemics-w3works': {'id': 'BSD-Systemics-W3Works', 'deprecated': False}, - 'bsl-1.0': {'id': 'BSL-1.0', 'deprecated': False}, - 'busl-1.1': {'id': 'BUSL-1.1', 'deprecated': False}, - 'bzip2-1.0.5': {'id': 'bzip2-1.0.5', 'deprecated': True}, - 'bzip2-1.0.6': {'id': 'bzip2-1.0.6', 'deprecated': False}, - 'c-uda-1.0': {'id': 'C-UDA-1.0', 'deprecated': False}, - 'cal-1.0': {'id': 'CAL-1.0', 'deprecated': False}, - 'cal-1.0-combined-work-exception': {'id': 'CAL-1.0-Combined-Work-Exception', 'deprecated': False}, - 'caldera': {'id': 'Caldera', 'deprecated': False}, - 'caldera-no-preamble': {'id': 'Caldera-no-preamble', 'deprecated': False}, - 'catosl-1.1': {'id': 'CATOSL-1.1', 'deprecated': False}, - 'cc-by-1.0': {'id': 'CC-BY-1.0', 'deprecated': False}, - 'cc-by-2.0': {'id': 'CC-BY-2.0', 'deprecated': False}, - 'cc-by-2.5': {'id': 'CC-BY-2.5', 'deprecated': False}, - 'cc-by-2.5-au': {'id': 'CC-BY-2.5-AU', 'deprecated': False}, - 'cc-by-3.0': {'id': 'CC-BY-3.0', 'deprecated': False}, - 'cc-by-3.0-at': {'id': 'CC-BY-3.0-AT', 'deprecated': False}, - 'cc-by-3.0-au': {'id': 'CC-BY-3.0-AU', 'deprecated': False}, - 'cc-by-3.0-de': {'id': 'CC-BY-3.0-DE', 'deprecated': False}, - 'cc-by-3.0-igo': {'id': 'CC-BY-3.0-IGO', 'deprecated': False}, - 'cc-by-3.0-nl': {'id': 'CC-BY-3.0-NL', 'deprecated': False}, - 'cc-by-3.0-us': {'id': 'CC-BY-3.0-US', 'deprecated': False}, - 'cc-by-4.0': {'id': 'CC-BY-4.0', 'deprecated': False}, - 'cc-by-nc-1.0': {'id': 'CC-BY-NC-1.0', 'deprecated': False}, - 'cc-by-nc-2.0': {'id': 'CC-BY-NC-2.0', 'deprecated': False}, - 'cc-by-nc-2.5': {'id': 'CC-BY-NC-2.5', 'deprecated': False}, - 'cc-by-nc-3.0': {'id': 'CC-BY-NC-3.0', 'deprecated': False}, - 'cc-by-nc-3.0-de': {'id': 'CC-BY-NC-3.0-DE', 'deprecated': False}, - 'cc-by-nc-4.0': {'id': 'CC-BY-NC-4.0', 'deprecated': False}, - 'cc-by-nc-nd-1.0': {'id': 'CC-BY-NC-ND-1.0', 'deprecated': False}, - 'cc-by-nc-nd-2.0': {'id': 'CC-BY-NC-ND-2.0', 'deprecated': False}, - 'cc-by-nc-nd-2.5': {'id': 'CC-BY-NC-ND-2.5', 'deprecated': False}, - 'cc-by-nc-nd-3.0': {'id': 'CC-BY-NC-ND-3.0', 'deprecated': False}, - 'cc-by-nc-nd-3.0-de': {'id': 'CC-BY-NC-ND-3.0-DE', 'deprecated': False}, - 'cc-by-nc-nd-3.0-igo': {'id': 'CC-BY-NC-ND-3.0-IGO', 'deprecated': False}, - 'cc-by-nc-nd-4.0': {'id': 'CC-BY-NC-ND-4.0', 'deprecated': False}, - 'cc-by-nc-sa-1.0': {'id': 'CC-BY-NC-SA-1.0', 'deprecated': False}, - 'cc-by-nc-sa-2.0': {'id': 'CC-BY-NC-SA-2.0', 'deprecated': False}, - 'cc-by-nc-sa-2.0-de': {'id': 'CC-BY-NC-SA-2.0-DE', 'deprecated': False}, - 'cc-by-nc-sa-2.0-fr': {'id': 'CC-BY-NC-SA-2.0-FR', 'deprecated': False}, - 'cc-by-nc-sa-2.0-uk': {'id': 'CC-BY-NC-SA-2.0-UK', 'deprecated': False}, - 'cc-by-nc-sa-2.5': {'id': 'CC-BY-NC-SA-2.5', 'deprecated': False}, - 'cc-by-nc-sa-3.0': {'id': 'CC-BY-NC-SA-3.0', 'deprecated': False}, - 'cc-by-nc-sa-3.0-de': {'id': 'CC-BY-NC-SA-3.0-DE', 'deprecated': False}, - 'cc-by-nc-sa-3.0-igo': {'id': 'CC-BY-NC-SA-3.0-IGO', 'deprecated': False}, - 'cc-by-nc-sa-4.0': {'id': 'CC-BY-NC-SA-4.0', 'deprecated': False}, - 'cc-by-nd-1.0': {'id': 'CC-BY-ND-1.0', 'deprecated': False}, - 'cc-by-nd-2.0': {'id': 'CC-BY-ND-2.0', 'deprecated': False}, - 'cc-by-nd-2.5': {'id': 'CC-BY-ND-2.5', 'deprecated': False}, - 'cc-by-nd-3.0': {'id': 'CC-BY-ND-3.0', 'deprecated': False}, - 'cc-by-nd-3.0-de': {'id': 'CC-BY-ND-3.0-DE', 'deprecated': False}, - 'cc-by-nd-4.0': {'id': 'CC-BY-ND-4.0', 'deprecated': False}, - 'cc-by-sa-1.0': {'id': 'CC-BY-SA-1.0', 'deprecated': False}, - 'cc-by-sa-2.0': {'id': 'CC-BY-SA-2.0', 'deprecated': False}, - 'cc-by-sa-2.0-uk': {'id': 'CC-BY-SA-2.0-UK', 'deprecated': False}, - 'cc-by-sa-2.1-jp': {'id': 'CC-BY-SA-2.1-JP', 'deprecated': False}, - 'cc-by-sa-2.5': {'id': 'CC-BY-SA-2.5', 'deprecated': False}, - 'cc-by-sa-3.0': {'id': 'CC-BY-SA-3.0', 'deprecated': False}, - 'cc-by-sa-3.0-at': {'id': 'CC-BY-SA-3.0-AT', 'deprecated': False}, - 'cc-by-sa-3.0-de': {'id': 'CC-BY-SA-3.0-DE', 'deprecated': False}, - 'cc-by-sa-3.0-igo': {'id': 'CC-BY-SA-3.0-IGO', 'deprecated': False}, - 'cc-by-sa-4.0': {'id': 'CC-BY-SA-4.0', 'deprecated': False}, - 'cc-pddc': {'id': 'CC-PDDC', 'deprecated': False}, - 'cc0-1.0': {'id': 'CC0-1.0', 'deprecated': False}, - 'cddl-1.0': {'id': 'CDDL-1.0', 'deprecated': False}, - 'cddl-1.1': {'id': 'CDDL-1.1', 'deprecated': False}, - 'cdl-1.0': {'id': 'CDL-1.0', 'deprecated': False}, - 'cdla-permissive-1.0': {'id': 'CDLA-Permissive-1.0', 'deprecated': False}, - 'cdla-permissive-2.0': {'id': 'CDLA-Permissive-2.0', 'deprecated': False}, - 'cdla-sharing-1.0': {'id': 'CDLA-Sharing-1.0', 'deprecated': False}, - 'cecill-1.0': {'id': 'CECILL-1.0', 'deprecated': False}, - 'cecill-1.1': {'id': 'CECILL-1.1', 'deprecated': False}, - 'cecill-2.0': {'id': 'CECILL-2.0', 'deprecated': False}, - 'cecill-2.1': {'id': 'CECILL-2.1', 'deprecated': False}, - 'cecill-b': {'id': 'CECILL-B', 'deprecated': False}, - 'cecill-c': {'id': 'CECILL-C', 'deprecated': False}, - 'cern-ohl-1.1': {'id': 'CERN-OHL-1.1', 'deprecated': False}, - 'cern-ohl-1.2': {'id': 'CERN-OHL-1.2', 'deprecated': False}, - 'cern-ohl-p-2.0': {'id': 'CERN-OHL-P-2.0', 'deprecated': False}, - 'cern-ohl-s-2.0': {'id': 'CERN-OHL-S-2.0', 'deprecated': False}, - 'cern-ohl-w-2.0': {'id': 'CERN-OHL-W-2.0', 'deprecated': False}, - 'cfitsio': {'id': 'CFITSIO', 'deprecated': False}, - 'check-cvs': {'id': 'check-cvs', 'deprecated': False}, - 'checkmk': {'id': 'checkmk', 'deprecated': False}, - 'clartistic': {'id': 'ClArtistic', 'deprecated': False}, - 'clips': {'id': 'Clips', 'deprecated': False}, - 'cmu-mach': {'id': 'CMU-Mach', 'deprecated': False}, - 'cmu-mach-nodoc': {'id': 'CMU-Mach-nodoc', 'deprecated': False}, - 'cnri-jython': {'id': 'CNRI-Jython', 'deprecated': False}, - 'cnri-python': {'id': 'CNRI-Python', 'deprecated': False}, - 'cnri-python-gpl-compatible': {'id': 'CNRI-Python-GPL-Compatible', 'deprecated': False}, - 'coil-1.0': {'id': 'COIL-1.0', 'deprecated': False}, - 'community-spec-1.0': {'id': 'Community-Spec-1.0', 'deprecated': False}, - 'condor-1.1': {'id': 'Condor-1.1', 'deprecated': False}, - 'copyleft-next-0.3.0': {'id': 'copyleft-next-0.3.0', 'deprecated': False}, - 'copyleft-next-0.3.1': {'id': 'copyleft-next-0.3.1', 'deprecated': False}, - 'cornell-lossless-jpeg': {'id': 'Cornell-Lossless-JPEG', 'deprecated': False}, - 'cpal-1.0': {'id': 'CPAL-1.0', 'deprecated': False}, - 'cpl-1.0': {'id': 'CPL-1.0', 'deprecated': False}, - 'cpol-1.02': {'id': 'CPOL-1.02', 'deprecated': False}, - 'cronyx': {'id': 'Cronyx', 'deprecated': False}, - 'crossword': {'id': 'Crossword', 'deprecated': False}, - 'crystalstacker': {'id': 'CrystalStacker', 'deprecated': False}, - 'cua-opl-1.0': {'id': 'CUA-OPL-1.0', 'deprecated': False}, - 'cube': {'id': 'Cube', 'deprecated': False}, - 'curl': {'id': 'curl', 'deprecated': False}, - 'd-fsl-1.0': {'id': 'D-FSL-1.0', 'deprecated': False}, - 'dec-3-clause': {'id': 'DEC-3-Clause', 'deprecated': False}, - 'diffmark': {'id': 'diffmark', 'deprecated': False}, - 'dl-de-by-2.0': {'id': 'DL-DE-BY-2.0', 'deprecated': False}, - 'dl-de-zero-2.0': {'id': 'DL-DE-ZERO-2.0', 'deprecated': False}, - 'doc': {'id': 'DOC', 'deprecated': False}, - 'dotseqn': {'id': 'Dotseqn', 'deprecated': False}, - 'drl-1.0': {'id': 'DRL-1.0', 'deprecated': False}, - 'drl-1.1': {'id': 'DRL-1.1', 'deprecated': False}, - 'dsdp': {'id': 'DSDP', 'deprecated': False}, - 'dtoa': {'id': 'dtoa', 'deprecated': False}, - 'dvipdfm': {'id': 'dvipdfm', 'deprecated': False}, - 'ecl-1.0': {'id': 'ECL-1.0', 'deprecated': False}, - 'ecl-2.0': {'id': 'ECL-2.0', 'deprecated': False}, - 'ecos-2.0': {'id': 'eCos-2.0', 'deprecated': True}, - 'efl-1.0': {'id': 'EFL-1.0', 'deprecated': False}, - 'efl-2.0': {'id': 'EFL-2.0', 'deprecated': False}, - 'egenix': {'id': 'eGenix', 'deprecated': False}, - 'elastic-2.0': {'id': 'Elastic-2.0', 'deprecated': False}, - 'entessa': {'id': 'Entessa', 'deprecated': False}, - 'epics': {'id': 'EPICS', 'deprecated': False}, - 'epl-1.0': {'id': 'EPL-1.0', 'deprecated': False}, - 'epl-2.0': {'id': 'EPL-2.0', 'deprecated': False}, - 'erlpl-1.1': {'id': 'ErlPL-1.1', 'deprecated': False}, - 'etalab-2.0': {'id': 'etalab-2.0', 'deprecated': False}, - 'eudatagrid': {'id': 'EUDatagrid', 'deprecated': False}, - 'eupl-1.0': {'id': 'EUPL-1.0', 'deprecated': False}, - 'eupl-1.1': {'id': 'EUPL-1.1', 'deprecated': False}, - 'eupl-1.2': {'id': 'EUPL-1.2', 'deprecated': False}, - 'eurosym': {'id': 'Eurosym', 'deprecated': False}, - 'fair': {'id': 'Fair', 'deprecated': False}, - 'fbm': {'id': 'FBM', 'deprecated': False}, - 'fdk-aac': {'id': 'FDK-AAC', 'deprecated': False}, - 'ferguson-twofish': {'id': 'Ferguson-Twofish', 'deprecated': False}, - 'frameworx-1.0': {'id': 'Frameworx-1.0', 'deprecated': False}, - 'freebsd-doc': {'id': 'FreeBSD-DOC', 'deprecated': False}, - 'freeimage': {'id': 'FreeImage', 'deprecated': False}, - 'fsfap': {'id': 'FSFAP', 'deprecated': False}, - 'fsfap-no-warranty-disclaimer': {'id': 'FSFAP-no-warranty-disclaimer', 'deprecated': False}, - 'fsful': {'id': 'FSFUL', 'deprecated': False}, - 'fsfullr': {'id': 'FSFULLR', 'deprecated': False}, - 'fsfullrwd': {'id': 'FSFULLRWD', 'deprecated': False}, - 'ftl': {'id': 'FTL', 'deprecated': False}, - 'furuseth': {'id': 'Furuseth', 'deprecated': False}, - 'fwlw': {'id': 'fwlw', 'deprecated': False}, - 'gcr-docs': {'id': 'GCR-docs', 'deprecated': False}, - 'gd': {'id': 'GD', 'deprecated': False}, - 'gfdl-1.1': {'id': 'GFDL-1.1', 'deprecated': True}, - 'gfdl-1.1-invariants-only': {'id': 'GFDL-1.1-invariants-only', 'deprecated': False}, - 'gfdl-1.1-invariants-or-later': {'id': 'GFDL-1.1-invariants-or-later', 'deprecated': False}, - 'gfdl-1.1-no-invariants-only': {'id': 'GFDL-1.1-no-invariants-only', 'deprecated': False}, - 'gfdl-1.1-no-invariants-or-later': {'id': 'GFDL-1.1-no-invariants-or-later', 'deprecated': False}, - 'gfdl-1.1-only': {'id': 'GFDL-1.1-only', 'deprecated': False}, - 'gfdl-1.1-or-later': {'id': 'GFDL-1.1-or-later', 'deprecated': False}, - 'gfdl-1.2': {'id': 'GFDL-1.2', 'deprecated': True}, - 'gfdl-1.2-invariants-only': {'id': 'GFDL-1.2-invariants-only', 'deprecated': False}, - 'gfdl-1.2-invariants-or-later': {'id': 'GFDL-1.2-invariants-or-later', 'deprecated': False}, - 'gfdl-1.2-no-invariants-only': {'id': 'GFDL-1.2-no-invariants-only', 'deprecated': False}, - 'gfdl-1.2-no-invariants-or-later': {'id': 'GFDL-1.2-no-invariants-or-later', 'deprecated': False}, - 'gfdl-1.2-only': {'id': 'GFDL-1.2-only', 'deprecated': False}, - 'gfdl-1.2-or-later': {'id': 'GFDL-1.2-or-later', 'deprecated': False}, - 'gfdl-1.3': {'id': 'GFDL-1.3', 'deprecated': True}, - 'gfdl-1.3-invariants-only': {'id': 'GFDL-1.3-invariants-only', 'deprecated': False}, - 'gfdl-1.3-invariants-or-later': {'id': 'GFDL-1.3-invariants-or-later', 'deprecated': False}, - 'gfdl-1.3-no-invariants-only': {'id': 'GFDL-1.3-no-invariants-only', 'deprecated': False}, - 'gfdl-1.3-no-invariants-or-later': {'id': 'GFDL-1.3-no-invariants-or-later', 'deprecated': False}, - 'gfdl-1.3-only': {'id': 'GFDL-1.3-only', 'deprecated': False}, - 'gfdl-1.3-or-later': {'id': 'GFDL-1.3-or-later', 'deprecated': False}, - 'giftware': {'id': 'Giftware', 'deprecated': False}, - 'gl2ps': {'id': 'GL2PS', 'deprecated': False}, - 'glide': {'id': 'Glide', 'deprecated': False}, - 'glulxe': {'id': 'Glulxe', 'deprecated': False}, - 'glwtpl': {'id': 'GLWTPL', 'deprecated': False}, - 'gnuplot': {'id': 'gnuplot', 'deprecated': False}, - 'gpl-1.0': {'id': 'GPL-1.0', 'deprecated': True}, - 'gpl-1.0+': {'id': 'GPL-1.0+', 'deprecated': True}, - 'gpl-1.0-only': {'id': 'GPL-1.0-only', 'deprecated': False}, - 'gpl-1.0-or-later': {'id': 'GPL-1.0-or-later', 'deprecated': False}, - 'gpl-2.0': {'id': 'GPL-2.0', 'deprecated': True}, - 'gpl-2.0+': {'id': 'GPL-2.0+', 'deprecated': True}, - 'gpl-2.0-only': {'id': 'GPL-2.0-only', 'deprecated': False}, - 'gpl-2.0-or-later': {'id': 'GPL-2.0-or-later', 'deprecated': False}, - 'gpl-2.0-with-autoconf-exception': {'id': 'GPL-2.0-with-autoconf-exception', 'deprecated': True}, - 'gpl-2.0-with-bison-exception': {'id': 'GPL-2.0-with-bison-exception', 'deprecated': True}, - 'gpl-2.0-with-classpath-exception': {'id': 'GPL-2.0-with-classpath-exception', 'deprecated': True}, - 'gpl-2.0-with-font-exception': {'id': 'GPL-2.0-with-font-exception', 'deprecated': True}, - 'gpl-2.0-with-gcc-exception': {'id': 'GPL-2.0-with-GCC-exception', 'deprecated': True}, - 'gpl-3.0': {'id': 'GPL-3.0', 'deprecated': True}, - 'gpl-3.0+': {'id': 'GPL-3.0+', 'deprecated': True}, - 'gpl-3.0-only': {'id': 'GPL-3.0-only', 'deprecated': False}, - 'gpl-3.0-or-later': {'id': 'GPL-3.0-or-later', 'deprecated': False}, - 'gpl-3.0-with-autoconf-exception': {'id': 'GPL-3.0-with-autoconf-exception', 'deprecated': True}, - 'gpl-3.0-with-gcc-exception': {'id': 'GPL-3.0-with-GCC-exception', 'deprecated': True}, - 'graphics-gems': {'id': 'Graphics-Gems', 'deprecated': False}, - 'gsoap-1.3b': {'id': 'gSOAP-1.3b', 'deprecated': False}, - 'gtkbook': {'id': 'gtkbook', 'deprecated': False}, - 'haskellreport': {'id': 'HaskellReport', 'deprecated': False}, - 'hdparm': {'id': 'hdparm', 'deprecated': False}, - 'hippocratic-2.1': {'id': 'Hippocratic-2.1', 'deprecated': False}, - 'hp-1986': {'id': 'HP-1986', 'deprecated': False}, - 'hp-1989': {'id': 'HP-1989', 'deprecated': False}, - 'hpnd': {'id': 'HPND', 'deprecated': False}, - 'hpnd-dec': {'id': 'HPND-DEC', 'deprecated': False}, - 'hpnd-doc': {'id': 'HPND-doc', 'deprecated': False}, - 'hpnd-doc-sell': {'id': 'HPND-doc-sell', 'deprecated': False}, - 'hpnd-export-us': {'id': 'HPND-export-US', 'deprecated': False}, - 'hpnd-export-us-modify': {'id': 'HPND-export-US-modify', 'deprecated': False}, - 'hpnd-fenneberg-livingston': {'id': 'HPND-Fenneberg-Livingston', 'deprecated': False}, - 'hpnd-inria-imag': {'id': 'HPND-INRIA-IMAG', 'deprecated': False}, - 'hpnd-kevlin-henney': {'id': 'HPND-Kevlin-Henney', 'deprecated': False}, - 'hpnd-markus-kuhn': {'id': 'HPND-Markus-Kuhn', 'deprecated': False}, - 'hpnd-mit-disclaimer': {'id': 'HPND-MIT-disclaimer', 'deprecated': False}, - 'hpnd-pbmplus': {'id': 'HPND-Pbmplus', 'deprecated': False}, - 'hpnd-sell-mit-disclaimer-xserver': {'id': 'HPND-sell-MIT-disclaimer-xserver', 'deprecated': False}, - 'hpnd-sell-regexpr': {'id': 'HPND-sell-regexpr', 'deprecated': False}, - 'hpnd-sell-variant': {'id': 'HPND-sell-variant', 'deprecated': False}, - 'hpnd-sell-variant-mit-disclaimer': {'id': 'HPND-sell-variant-MIT-disclaimer', 'deprecated': False}, - 'hpnd-uc': {'id': 'HPND-UC', 'deprecated': False}, - 'htmltidy': {'id': 'HTMLTIDY', 'deprecated': False}, - 'ibm-pibs': {'id': 'IBM-pibs', 'deprecated': False}, - 'icu': {'id': 'ICU', 'deprecated': False}, - 'iec-code-components-eula': {'id': 'IEC-Code-Components-EULA', 'deprecated': False}, - 'ijg': {'id': 'IJG', 'deprecated': False}, - 'ijg-short': {'id': 'IJG-short', 'deprecated': False}, - 'imagemagick': {'id': 'ImageMagick', 'deprecated': False}, - 'imatix': {'id': 'iMatix', 'deprecated': False}, - 'imlib2': {'id': 'Imlib2', 'deprecated': False}, - 'info-zip': {'id': 'Info-ZIP', 'deprecated': False}, - 'inner-net-2.0': {'id': 'Inner-Net-2.0', 'deprecated': False}, - 'intel': {'id': 'Intel', 'deprecated': False}, - 'intel-acpi': {'id': 'Intel-ACPI', 'deprecated': False}, - 'interbase-1.0': {'id': 'Interbase-1.0', 'deprecated': False}, - 'ipa': {'id': 'IPA', 'deprecated': False}, - 'ipl-1.0': {'id': 'IPL-1.0', 'deprecated': False}, - 'isc': {'id': 'ISC', 'deprecated': False}, - 'isc-veillard': {'id': 'ISC-Veillard', 'deprecated': False}, - 'jam': {'id': 'Jam', 'deprecated': False}, - 'jasper-2.0': {'id': 'JasPer-2.0', 'deprecated': False}, - 'jpl-image': {'id': 'JPL-image', 'deprecated': False}, - 'jpnic': {'id': 'JPNIC', 'deprecated': False}, - 'json': {'id': 'JSON', 'deprecated': False}, - 'kastrup': {'id': 'Kastrup', 'deprecated': False}, - 'kazlib': {'id': 'Kazlib', 'deprecated': False}, - 'knuth-ctan': {'id': 'Knuth-CTAN', 'deprecated': False}, - 'lal-1.2': {'id': 'LAL-1.2', 'deprecated': False}, - 'lal-1.3': {'id': 'LAL-1.3', 'deprecated': False}, - 'latex2e': {'id': 'Latex2e', 'deprecated': False}, - 'latex2e-translated-notice': {'id': 'Latex2e-translated-notice', 'deprecated': False}, - 'leptonica': {'id': 'Leptonica', 'deprecated': False}, - 'lgpl-2.0': {'id': 'LGPL-2.0', 'deprecated': True}, - 'lgpl-2.0+': {'id': 'LGPL-2.0+', 'deprecated': True}, - 'lgpl-2.0-only': {'id': 'LGPL-2.0-only', 'deprecated': False}, - 'lgpl-2.0-or-later': {'id': 'LGPL-2.0-or-later', 'deprecated': False}, - 'lgpl-2.1': {'id': 'LGPL-2.1', 'deprecated': True}, - 'lgpl-2.1+': {'id': 'LGPL-2.1+', 'deprecated': True}, - 'lgpl-2.1-only': {'id': 'LGPL-2.1-only', 'deprecated': False}, - 'lgpl-2.1-or-later': {'id': 'LGPL-2.1-or-later', 'deprecated': False}, - 'lgpl-3.0': {'id': 'LGPL-3.0', 'deprecated': True}, - 'lgpl-3.0+': {'id': 'LGPL-3.0+', 'deprecated': True}, - 'lgpl-3.0-only': {'id': 'LGPL-3.0-only', 'deprecated': False}, - 'lgpl-3.0-or-later': {'id': 'LGPL-3.0-or-later', 'deprecated': False}, - 'lgpllr': {'id': 'LGPLLR', 'deprecated': False}, - 'libpng': {'id': 'Libpng', 'deprecated': False}, - 'libpng-2.0': {'id': 'libpng-2.0', 'deprecated': False}, - 'libselinux-1.0': {'id': 'libselinux-1.0', 'deprecated': False}, - 'libtiff': {'id': 'libtiff', 'deprecated': False}, - 'libutil-david-nugent': {'id': 'libutil-David-Nugent', 'deprecated': False}, - 'liliq-p-1.1': {'id': 'LiLiQ-P-1.1', 'deprecated': False}, - 'liliq-r-1.1': {'id': 'LiLiQ-R-1.1', 'deprecated': False}, - 'liliq-rplus-1.1': {'id': 'LiLiQ-Rplus-1.1', 'deprecated': False}, - 'linux-man-pages-1-para': {'id': 'Linux-man-pages-1-para', 'deprecated': False}, - 'linux-man-pages-copyleft': {'id': 'Linux-man-pages-copyleft', 'deprecated': False}, - 'linux-man-pages-copyleft-2-para': {'id': 'Linux-man-pages-copyleft-2-para', 'deprecated': False}, - 'linux-man-pages-copyleft-var': {'id': 'Linux-man-pages-copyleft-var', 'deprecated': False}, - 'linux-openib': {'id': 'Linux-OpenIB', 'deprecated': False}, - 'loop': {'id': 'LOOP', 'deprecated': False}, - 'lpd-document': {'id': 'LPD-document', 'deprecated': False}, - 'lpl-1.0': {'id': 'LPL-1.0', 'deprecated': False}, - 'lpl-1.02': {'id': 'LPL-1.02', 'deprecated': False}, - 'lppl-1.0': {'id': 'LPPL-1.0', 'deprecated': False}, - 'lppl-1.1': {'id': 'LPPL-1.1', 'deprecated': False}, - 'lppl-1.2': {'id': 'LPPL-1.2', 'deprecated': False}, - 'lppl-1.3a': {'id': 'LPPL-1.3a', 'deprecated': False}, - 'lppl-1.3c': {'id': 'LPPL-1.3c', 'deprecated': False}, - 'lsof': {'id': 'lsof', 'deprecated': False}, - 'lucida-bitmap-fonts': {'id': 'Lucida-Bitmap-Fonts', 'deprecated': False}, - 'lzma-sdk-9.11-to-9.20': {'id': 'LZMA-SDK-9.11-to-9.20', 'deprecated': False}, - 'lzma-sdk-9.22': {'id': 'LZMA-SDK-9.22', 'deprecated': False}, - 'mackerras-3-clause': {'id': 'Mackerras-3-Clause', 'deprecated': False}, - 'mackerras-3-clause-acknowledgment': {'id': 'Mackerras-3-Clause-acknowledgment', 'deprecated': False}, - 'magaz': {'id': 'magaz', 'deprecated': False}, - 'mailprio': {'id': 'mailprio', 'deprecated': False}, - 'makeindex': {'id': 'MakeIndex', 'deprecated': False}, - 'martin-birgmeier': {'id': 'Martin-Birgmeier', 'deprecated': False}, - 'mcphee-slideshow': {'id': 'McPhee-slideshow', 'deprecated': False}, - 'metamail': {'id': 'metamail', 'deprecated': False}, - 'minpack': {'id': 'Minpack', 'deprecated': False}, - 'miros': {'id': 'MirOS', 'deprecated': False}, - 'mit': {'id': 'MIT', 'deprecated': False}, - 'mit-0': {'id': 'MIT-0', 'deprecated': False}, - 'mit-advertising': {'id': 'MIT-advertising', 'deprecated': False}, - 'mit-cmu': {'id': 'MIT-CMU', 'deprecated': False}, - 'mit-enna': {'id': 'MIT-enna', 'deprecated': False}, - 'mit-feh': {'id': 'MIT-feh', 'deprecated': False}, - 'mit-festival': {'id': 'MIT-Festival', 'deprecated': False}, - 'mit-modern-variant': {'id': 'MIT-Modern-Variant', 'deprecated': False}, - 'mit-open-group': {'id': 'MIT-open-group', 'deprecated': False}, - 'mit-testregex': {'id': 'MIT-testregex', 'deprecated': False}, - 'mit-wu': {'id': 'MIT-Wu', 'deprecated': False}, - 'mitnfa': {'id': 'MITNFA', 'deprecated': False}, - 'mmixware': {'id': 'MMIXware', 'deprecated': False}, - 'motosoto': {'id': 'Motosoto', 'deprecated': False}, - 'mpeg-ssg': {'id': 'MPEG-SSG', 'deprecated': False}, - 'mpi-permissive': {'id': 'mpi-permissive', 'deprecated': False}, - 'mpich2': {'id': 'mpich2', 'deprecated': False}, - 'mpl-1.0': {'id': 'MPL-1.0', 'deprecated': False}, - 'mpl-1.1': {'id': 'MPL-1.1', 'deprecated': False}, - 'mpl-2.0': {'id': 'MPL-2.0', 'deprecated': False}, - 'mpl-2.0-no-copyleft-exception': {'id': 'MPL-2.0-no-copyleft-exception', 'deprecated': False}, - 'mplus': {'id': 'mplus', 'deprecated': False}, - 'ms-lpl': {'id': 'MS-LPL', 'deprecated': False}, - 'ms-pl': {'id': 'MS-PL', 'deprecated': False}, - 'ms-rl': {'id': 'MS-RL', 'deprecated': False}, - 'mtll': {'id': 'MTLL', 'deprecated': False}, - 'mulanpsl-1.0': {'id': 'MulanPSL-1.0', 'deprecated': False}, - 'mulanpsl-2.0': {'id': 'MulanPSL-2.0', 'deprecated': False}, - 'multics': {'id': 'Multics', 'deprecated': False}, - 'mup': {'id': 'Mup', 'deprecated': False}, - 'naist-2003': {'id': 'NAIST-2003', 'deprecated': False}, - 'nasa-1.3': {'id': 'NASA-1.3', 'deprecated': False}, - 'naumen': {'id': 'Naumen', 'deprecated': False}, - 'nbpl-1.0': {'id': 'NBPL-1.0', 'deprecated': False}, - 'ncgl-uk-2.0': {'id': 'NCGL-UK-2.0', 'deprecated': False}, - 'ncsa': {'id': 'NCSA', 'deprecated': False}, - 'net-snmp': {'id': 'Net-SNMP', 'deprecated': False}, - 'netcdf': {'id': 'NetCDF', 'deprecated': False}, - 'newsletr': {'id': 'Newsletr', 'deprecated': False}, - 'ngpl': {'id': 'NGPL', 'deprecated': False}, - 'nicta-1.0': {'id': 'NICTA-1.0', 'deprecated': False}, - 'nist-pd': {'id': 'NIST-PD', 'deprecated': False}, - 'nist-pd-fallback': {'id': 'NIST-PD-fallback', 'deprecated': False}, - 'nist-software': {'id': 'NIST-Software', 'deprecated': False}, - 'nlod-1.0': {'id': 'NLOD-1.0', 'deprecated': False}, - 'nlod-2.0': {'id': 'NLOD-2.0', 'deprecated': False}, - 'nlpl': {'id': 'NLPL', 'deprecated': False}, - 'nokia': {'id': 'Nokia', 'deprecated': False}, - 'nosl': {'id': 'NOSL', 'deprecated': False}, - 'noweb': {'id': 'Noweb', 'deprecated': False}, - 'npl-1.0': {'id': 'NPL-1.0', 'deprecated': False}, - 'npl-1.1': {'id': 'NPL-1.1', 'deprecated': False}, - 'nposl-3.0': {'id': 'NPOSL-3.0', 'deprecated': False}, - 'nrl': {'id': 'NRL', 'deprecated': False}, - 'ntp': {'id': 'NTP', 'deprecated': False}, - 'ntp-0': {'id': 'NTP-0', 'deprecated': False}, - 'nunit': {'id': 'Nunit', 'deprecated': True}, - 'o-uda-1.0': {'id': 'O-UDA-1.0', 'deprecated': False}, - 'occt-pl': {'id': 'OCCT-PL', 'deprecated': False}, - 'oclc-2.0': {'id': 'OCLC-2.0', 'deprecated': False}, - 'odbl-1.0': {'id': 'ODbL-1.0', 'deprecated': False}, - 'odc-by-1.0': {'id': 'ODC-By-1.0', 'deprecated': False}, - 'offis': {'id': 'OFFIS', 'deprecated': False}, - 'ofl-1.0': {'id': 'OFL-1.0', 'deprecated': False}, - 'ofl-1.0-no-rfn': {'id': 'OFL-1.0-no-RFN', 'deprecated': False}, - 'ofl-1.0-rfn': {'id': 'OFL-1.0-RFN', 'deprecated': False}, - 'ofl-1.1': {'id': 'OFL-1.1', 'deprecated': False}, - 'ofl-1.1-no-rfn': {'id': 'OFL-1.1-no-RFN', 'deprecated': False}, - 'ofl-1.1-rfn': {'id': 'OFL-1.1-RFN', 'deprecated': False}, - 'ogc-1.0': {'id': 'OGC-1.0', 'deprecated': False}, - 'ogdl-taiwan-1.0': {'id': 'OGDL-Taiwan-1.0', 'deprecated': False}, - 'ogl-canada-2.0': {'id': 'OGL-Canada-2.0', 'deprecated': False}, - 'ogl-uk-1.0': {'id': 'OGL-UK-1.0', 'deprecated': False}, - 'ogl-uk-2.0': {'id': 'OGL-UK-2.0', 'deprecated': False}, - 'ogl-uk-3.0': {'id': 'OGL-UK-3.0', 'deprecated': False}, - 'ogtsl': {'id': 'OGTSL', 'deprecated': False}, - 'oldap-1.1': {'id': 'OLDAP-1.1', 'deprecated': False}, - 'oldap-1.2': {'id': 'OLDAP-1.2', 'deprecated': False}, - 'oldap-1.3': {'id': 'OLDAP-1.3', 'deprecated': False}, - 'oldap-1.4': {'id': 'OLDAP-1.4', 'deprecated': False}, - 'oldap-2.0': {'id': 'OLDAP-2.0', 'deprecated': False}, - 'oldap-2.0.1': {'id': 'OLDAP-2.0.1', 'deprecated': False}, - 'oldap-2.1': {'id': 'OLDAP-2.1', 'deprecated': False}, - 'oldap-2.2': {'id': 'OLDAP-2.2', 'deprecated': False}, - 'oldap-2.2.1': {'id': 'OLDAP-2.2.1', 'deprecated': False}, - 'oldap-2.2.2': {'id': 'OLDAP-2.2.2', 'deprecated': False}, - 'oldap-2.3': {'id': 'OLDAP-2.3', 'deprecated': False}, - 'oldap-2.4': {'id': 'OLDAP-2.4', 'deprecated': False}, - 'oldap-2.5': {'id': 'OLDAP-2.5', 'deprecated': False}, - 'oldap-2.6': {'id': 'OLDAP-2.6', 'deprecated': False}, - 'oldap-2.7': {'id': 'OLDAP-2.7', 'deprecated': False}, - 'oldap-2.8': {'id': 'OLDAP-2.8', 'deprecated': False}, - 'olfl-1.3': {'id': 'OLFL-1.3', 'deprecated': False}, - 'oml': {'id': 'OML', 'deprecated': False}, - 'openpbs-2.3': {'id': 'OpenPBS-2.3', 'deprecated': False}, - 'openssl': {'id': 'OpenSSL', 'deprecated': False}, - 'openssl-standalone': {'id': 'OpenSSL-standalone', 'deprecated': False}, - 'openvision': {'id': 'OpenVision', 'deprecated': False}, - 'opl-1.0': {'id': 'OPL-1.0', 'deprecated': False}, - 'opl-uk-3.0': {'id': 'OPL-UK-3.0', 'deprecated': False}, - 'opubl-1.0': {'id': 'OPUBL-1.0', 'deprecated': False}, - 'oset-pl-2.1': {'id': 'OSET-PL-2.1', 'deprecated': False}, - 'osl-1.0': {'id': 'OSL-1.0', 'deprecated': False}, - 'osl-1.1': {'id': 'OSL-1.1', 'deprecated': False}, - 'osl-2.0': {'id': 'OSL-2.0', 'deprecated': False}, - 'osl-2.1': {'id': 'OSL-2.1', 'deprecated': False}, - 'osl-3.0': {'id': 'OSL-3.0', 'deprecated': False}, - 'padl': {'id': 'PADL', 'deprecated': False}, - 'parity-6.0.0': {'id': 'Parity-6.0.0', 'deprecated': False}, - 'parity-7.0.0': {'id': 'Parity-7.0.0', 'deprecated': False}, - 'pddl-1.0': {'id': 'PDDL-1.0', 'deprecated': False}, - 'php-3.0': {'id': 'PHP-3.0', 'deprecated': False}, - 'php-3.01': {'id': 'PHP-3.01', 'deprecated': False}, - 'pixar': {'id': 'Pixar', 'deprecated': False}, - 'plexus': {'id': 'Plexus', 'deprecated': False}, - 'pnmstitch': {'id': 'pnmstitch', 'deprecated': False}, - 'polyform-noncommercial-1.0.0': {'id': 'PolyForm-Noncommercial-1.0.0', 'deprecated': False}, - 'polyform-small-business-1.0.0': {'id': 'PolyForm-Small-Business-1.0.0', 'deprecated': False}, - 'postgresql': {'id': 'PostgreSQL', 'deprecated': False}, - 'psf-2.0': {'id': 'PSF-2.0', 'deprecated': False}, - 'psfrag': {'id': 'psfrag', 'deprecated': False}, - 'psutils': {'id': 'psutils', 'deprecated': False}, - 'python-2.0': {'id': 'Python-2.0', 'deprecated': False}, - 'python-2.0.1': {'id': 'Python-2.0.1', 'deprecated': False}, - 'python-ldap': {'id': 'python-ldap', 'deprecated': False}, - 'qhull': {'id': 'Qhull', 'deprecated': False}, - 'qpl-1.0': {'id': 'QPL-1.0', 'deprecated': False}, - 'qpl-1.0-inria-2004': {'id': 'QPL-1.0-INRIA-2004', 'deprecated': False}, - 'radvd': {'id': 'radvd', 'deprecated': False}, - 'rdisc': {'id': 'Rdisc', 'deprecated': False}, - 'rhecos-1.1': {'id': 'RHeCos-1.1', 'deprecated': False}, - 'rpl-1.1': {'id': 'RPL-1.1', 'deprecated': False}, - 'rpl-1.5': {'id': 'RPL-1.5', 'deprecated': False}, - 'rpsl-1.0': {'id': 'RPSL-1.0', 'deprecated': False}, - 'rsa-md': {'id': 'RSA-MD', 'deprecated': False}, - 'rscpl': {'id': 'RSCPL', 'deprecated': False}, - 'ruby': {'id': 'Ruby', 'deprecated': False}, - 'sax-pd': {'id': 'SAX-PD', 'deprecated': False}, - 'sax-pd-2.0': {'id': 'SAX-PD-2.0', 'deprecated': False}, - 'saxpath': {'id': 'Saxpath', 'deprecated': False}, - 'scea': {'id': 'SCEA', 'deprecated': False}, - 'schemereport': {'id': 'SchemeReport', 'deprecated': False}, - 'sendmail': {'id': 'Sendmail', 'deprecated': False}, - 'sendmail-8.23': {'id': 'Sendmail-8.23', 'deprecated': False}, - 'sgi-b-1.0': {'id': 'SGI-B-1.0', 'deprecated': False}, - 'sgi-b-1.1': {'id': 'SGI-B-1.1', 'deprecated': False}, - 'sgi-b-2.0': {'id': 'SGI-B-2.0', 'deprecated': False}, - 'sgi-opengl': {'id': 'SGI-OpenGL', 'deprecated': False}, - 'sgp4': {'id': 'SGP4', 'deprecated': False}, - 'shl-0.5': {'id': 'SHL-0.5', 'deprecated': False}, - 'shl-0.51': {'id': 'SHL-0.51', 'deprecated': False}, - 'simpl-2.0': {'id': 'SimPL-2.0', 'deprecated': False}, - 'sissl': {'id': 'SISSL', 'deprecated': False}, - 'sissl-1.2': {'id': 'SISSL-1.2', 'deprecated': False}, - 'sl': {'id': 'SL', 'deprecated': False}, - 'sleepycat': {'id': 'Sleepycat', 'deprecated': False}, - 'smlnj': {'id': 'SMLNJ', 'deprecated': False}, - 'smppl': {'id': 'SMPPL', 'deprecated': False}, - 'snia': {'id': 'SNIA', 'deprecated': False}, - 'snprintf': {'id': 'snprintf', 'deprecated': False}, - 'softsurfer': {'id': 'softSurfer', 'deprecated': False}, - 'soundex': {'id': 'Soundex', 'deprecated': False}, - 'spencer-86': {'id': 'Spencer-86', 'deprecated': False}, - 'spencer-94': {'id': 'Spencer-94', 'deprecated': False}, - 'spencer-99': {'id': 'Spencer-99', 'deprecated': False}, - 'spl-1.0': {'id': 'SPL-1.0', 'deprecated': False}, - 'ssh-keyscan': {'id': 'ssh-keyscan', 'deprecated': False}, - 'ssh-openssh': {'id': 'SSH-OpenSSH', 'deprecated': False}, - 'ssh-short': {'id': 'SSH-short', 'deprecated': False}, - 'ssleay-standalone': {'id': 'SSLeay-standalone', 'deprecated': False}, - 'sspl-1.0': {'id': 'SSPL-1.0', 'deprecated': False}, - 'standardml-nj': {'id': 'StandardML-NJ', 'deprecated': True}, - 'sugarcrm-1.1.3': {'id': 'SugarCRM-1.1.3', 'deprecated': False}, - 'sun-ppp': {'id': 'Sun-PPP', 'deprecated': False}, - 'sunpro': {'id': 'SunPro', 'deprecated': False}, - 'swl': {'id': 'SWL', 'deprecated': False}, - 'swrule': {'id': 'swrule', 'deprecated': False}, - 'symlinks': {'id': 'Symlinks', 'deprecated': False}, - 'tapr-ohl-1.0': {'id': 'TAPR-OHL-1.0', 'deprecated': False}, - 'tcl': {'id': 'TCL', 'deprecated': False}, - 'tcp-wrappers': {'id': 'TCP-wrappers', 'deprecated': False}, - 'termreadkey': {'id': 'TermReadKey', 'deprecated': False}, - 'tgppl-1.0': {'id': 'TGPPL-1.0', 'deprecated': False}, - 'tmate': {'id': 'TMate', 'deprecated': False}, - 'torque-1.1': {'id': 'TORQUE-1.1', 'deprecated': False}, - 'tosl': {'id': 'TOSL', 'deprecated': False}, - 'tpdl': {'id': 'TPDL', 'deprecated': False}, - 'tpl-1.0': {'id': 'TPL-1.0', 'deprecated': False}, - 'ttwl': {'id': 'TTWL', 'deprecated': False}, - 'ttyp0': {'id': 'TTYP0', 'deprecated': False}, - 'tu-berlin-1.0': {'id': 'TU-Berlin-1.0', 'deprecated': False}, - 'tu-berlin-2.0': {'id': 'TU-Berlin-2.0', 'deprecated': False}, - 'ucar': {'id': 'UCAR', 'deprecated': False}, - 'ucl-1.0': {'id': 'UCL-1.0', 'deprecated': False}, - 'ulem': {'id': 'ulem', 'deprecated': False}, - 'umich-merit': {'id': 'UMich-Merit', 'deprecated': False}, - 'unicode-3.0': {'id': 'Unicode-3.0', 'deprecated': False}, - 'unicode-dfs-2015': {'id': 'Unicode-DFS-2015', 'deprecated': False}, - 'unicode-dfs-2016': {'id': 'Unicode-DFS-2016', 'deprecated': False}, - 'unicode-tou': {'id': 'Unicode-TOU', 'deprecated': False}, - 'unixcrypt': {'id': 'UnixCrypt', 'deprecated': False}, - 'unlicense': {'id': 'Unlicense', 'deprecated': False}, - 'upl-1.0': {'id': 'UPL-1.0', 'deprecated': False}, - 'urt-rle': {'id': 'URT-RLE', 'deprecated': False}, - 'vim': {'id': 'Vim', 'deprecated': False}, - 'vostrom': {'id': 'VOSTROM', 'deprecated': False}, - 'vsl-1.0': {'id': 'VSL-1.0', 'deprecated': False}, - 'w3c': {'id': 'W3C', 'deprecated': False}, - 'w3c-19980720': {'id': 'W3C-19980720', 'deprecated': False}, - 'w3c-20150513': {'id': 'W3C-20150513', 'deprecated': False}, - 'w3m': {'id': 'w3m', 'deprecated': False}, - 'watcom-1.0': {'id': 'Watcom-1.0', 'deprecated': False}, - 'widget-workshop': {'id': 'Widget-Workshop', 'deprecated': False}, - 'wsuipa': {'id': 'Wsuipa', 'deprecated': False}, - 'wtfpl': {'id': 'WTFPL', 'deprecated': False}, - 'wxwindows': {'id': 'wxWindows', 'deprecated': True}, - 'x11': {'id': 'X11', 'deprecated': False}, - 'x11-distribute-modifications-variant': {'id': 'X11-distribute-modifications-variant', 'deprecated': False}, - 'xdebug-1.03': {'id': 'Xdebug-1.03', 'deprecated': False}, - 'xerox': {'id': 'Xerox', 'deprecated': False}, - 'xfig': {'id': 'Xfig', 'deprecated': False}, - 'xfree86-1.1': {'id': 'XFree86-1.1', 'deprecated': False}, - 'xinetd': {'id': 'xinetd', 'deprecated': False}, - 'xkeyboard-config-zinoviev': {'id': 'xkeyboard-config-Zinoviev', 'deprecated': False}, - 'xlock': {'id': 'xlock', 'deprecated': False}, - 'xnet': {'id': 'Xnet', 'deprecated': False}, - 'xpp': {'id': 'xpp', 'deprecated': False}, - 'xskat': {'id': 'XSkat', 'deprecated': False}, - 'ypl-1.0': {'id': 'YPL-1.0', 'deprecated': False}, - 'ypl-1.1': {'id': 'YPL-1.1', 'deprecated': False}, - 'zed': {'id': 'Zed', 'deprecated': False}, - 'zeeff': {'id': 'Zeeff', 'deprecated': False}, - 'zend-2.0': {'id': 'Zend-2.0', 'deprecated': False}, - 'zimbra-1.3': {'id': 'Zimbra-1.3', 'deprecated': False}, - 'zimbra-1.4': {'id': 'Zimbra-1.4', 'deprecated': False}, - 'zlib': {'id': 'Zlib', 'deprecated': False}, - 'zlib-acknowledgement': {'id': 'zlib-acknowledgement', 'deprecated': False}, - 'zpl-1.1': {'id': 'ZPL-1.1', 'deprecated': False}, - 'zpl-2.0': {'id': 'ZPL-2.0', 'deprecated': False}, - 'zpl-2.1': {'id': 'ZPL-2.1', 'deprecated': False}, -} - -EXCEPTIONS: dict[str, dict[str, str | bool]] = { - '389-exception': {'id': '389-exception', 'deprecated': False}, - 'asterisk-exception': {'id': 'Asterisk-exception', 'deprecated': False}, - 'autoconf-exception-2.0': {'id': 'Autoconf-exception-2.0', 'deprecated': False}, - 'autoconf-exception-3.0': {'id': 'Autoconf-exception-3.0', 'deprecated': False}, - 'autoconf-exception-generic': {'id': 'Autoconf-exception-generic', 'deprecated': False}, - 'autoconf-exception-generic-3.0': {'id': 'Autoconf-exception-generic-3.0', 'deprecated': False}, - 'autoconf-exception-macro': {'id': 'Autoconf-exception-macro', 'deprecated': False}, - 'bison-exception-1.24': {'id': 'Bison-exception-1.24', 'deprecated': False}, - 'bison-exception-2.2': {'id': 'Bison-exception-2.2', 'deprecated': False}, - 'bootloader-exception': {'id': 'Bootloader-exception', 'deprecated': False}, - 'classpath-exception-2.0': {'id': 'Classpath-exception-2.0', 'deprecated': False}, - 'clisp-exception-2.0': {'id': 'CLISP-exception-2.0', 'deprecated': False}, - 'cryptsetup-openssl-exception': {'id': 'cryptsetup-OpenSSL-exception', 'deprecated': False}, - 'digirule-foss-exception': {'id': 'DigiRule-FOSS-exception', 'deprecated': False}, - 'ecos-exception-2.0': {'id': 'eCos-exception-2.0', 'deprecated': False}, - 'fawkes-runtime-exception': {'id': 'Fawkes-Runtime-exception', 'deprecated': False}, - 'fltk-exception': {'id': 'FLTK-exception', 'deprecated': False}, - 'fmt-exception': {'id': 'fmt-exception', 'deprecated': False}, - 'font-exception-2.0': {'id': 'Font-exception-2.0', 'deprecated': False}, - 'freertos-exception-2.0': {'id': 'freertos-exception-2.0', 'deprecated': False}, - 'gcc-exception-2.0': {'id': 'GCC-exception-2.0', 'deprecated': False}, - 'gcc-exception-2.0-note': {'id': 'GCC-exception-2.0-note', 'deprecated': False}, - 'gcc-exception-3.1': {'id': 'GCC-exception-3.1', 'deprecated': False}, - 'gmsh-exception': {'id': 'Gmsh-exception', 'deprecated': False}, - 'gnat-exception': {'id': 'GNAT-exception', 'deprecated': False}, - 'gnome-examples-exception': {'id': 'GNOME-examples-exception', 'deprecated': False}, - 'gnu-compiler-exception': {'id': 'GNU-compiler-exception', 'deprecated': False}, - 'gnu-javamail-exception': {'id': 'gnu-javamail-exception', 'deprecated': False}, - 'gpl-3.0-interface-exception': {'id': 'GPL-3.0-interface-exception', 'deprecated': False}, - 'gpl-3.0-linking-exception': {'id': 'GPL-3.0-linking-exception', 'deprecated': False}, - 'gpl-3.0-linking-source-exception': {'id': 'GPL-3.0-linking-source-exception', 'deprecated': False}, - 'gpl-cc-1.0': {'id': 'GPL-CC-1.0', 'deprecated': False}, - 'gstreamer-exception-2005': {'id': 'GStreamer-exception-2005', 'deprecated': False}, - 'gstreamer-exception-2008': {'id': 'GStreamer-exception-2008', 'deprecated': False}, - 'i2p-gpl-java-exception': {'id': 'i2p-gpl-java-exception', 'deprecated': False}, - 'kicad-libraries-exception': {'id': 'KiCad-libraries-exception', 'deprecated': False}, - 'lgpl-3.0-linking-exception': {'id': 'LGPL-3.0-linking-exception', 'deprecated': False}, - 'libpri-openh323-exception': {'id': 'libpri-OpenH323-exception', 'deprecated': False}, - 'libtool-exception': {'id': 'Libtool-exception', 'deprecated': False}, - 'linux-syscall-note': {'id': 'Linux-syscall-note', 'deprecated': False}, - 'llgpl': {'id': 'LLGPL', 'deprecated': False}, - 'llvm-exception': {'id': 'LLVM-exception', 'deprecated': False}, - 'lzma-exception': {'id': 'LZMA-exception', 'deprecated': False}, - 'mif-exception': {'id': 'mif-exception', 'deprecated': False}, - 'nokia-qt-exception-1.1': {'id': 'Nokia-Qt-exception-1.1', 'deprecated': True}, - 'ocaml-lgpl-linking-exception': {'id': 'OCaml-LGPL-linking-exception', 'deprecated': False}, - 'occt-exception-1.0': {'id': 'OCCT-exception-1.0', 'deprecated': False}, - 'openjdk-assembly-exception-1.0': {'id': 'OpenJDK-assembly-exception-1.0', 'deprecated': False}, - 'openvpn-openssl-exception': {'id': 'openvpn-openssl-exception', 'deprecated': False}, - 'ps-or-pdf-font-exception-20170817': {'id': 'PS-or-PDF-font-exception-20170817', 'deprecated': False}, - 'qpl-1.0-inria-2004-exception': {'id': 'QPL-1.0-INRIA-2004-exception', 'deprecated': False}, - 'qt-gpl-exception-1.0': {'id': 'Qt-GPL-exception-1.0', 'deprecated': False}, - 'qt-lgpl-exception-1.1': {'id': 'Qt-LGPL-exception-1.1', 'deprecated': False}, - 'qwt-exception-1.0': {'id': 'Qwt-exception-1.0', 'deprecated': False}, - 'sane-exception': {'id': 'SANE-exception', 'deprecated': False}, - 'shl-2.0': {'id': 'SHL-2.0', 'deprecated': False}, - 'shl-2.1': {'id': 'SHL-2.1', 'deprecated': False}, - 'stunnel-exception': {'id': 'stunnel-exception', 'deprecated': False}, - 'swi-exception': {'id': 'SWI-exception', 'deprecated': False}, - 'swift-exception': {'id': 'Swift-exception', 'deprecated': False}, - 'texinfo-exception': {'id': 'Texinfo-exception', 'deprecated': False}, - 'u-boot-exception-2.0': {'id': 'u-boot-exception-2.0', 'deprecated': False}, - 'ubdl-exception': {'id': 'UBDL-exception', 'deprecated': False}, - 'universal-foss-exception-1.0': {'id': 'Universal-FOSS-exception-1.0', 'deprecated': False}, - 'vsftpd-openssl-exception': {'id': 'vsftpd-openssl-exception', 'deprecated': False}, - 'wxwindows-exception-3.1': {'id': 'WxWindows-exception-3.1', 'deprecated': False}, - 'x11vnc-openssl-exception': {'id': 'x11vnc-openssl-exception', 'deprecated': False}, -} diff --git a/backend/src/hatchling/metadata/core.py b/backend/src/hatchling/metadata/core.py index e4b47a8d7..97359325d 100644 --- a/backend/src/hatchling/metadata/core.py +++ b/backend/src/hatchling/metadata/core.py @@ -674,10 +674,10 @@ def license(self) -> str: self._license = '' self._license_expression = '' elif isinstance(data, str): - from hatchling.licenses.parse import normalize_license_expression + from packaging.licenses import canonicalize_license_expression try: - self._license_expression = normalize_license_expression(data) + self._license_expression = str(canonicalize_license_expression(data)) except ValueError as e: message = f'Error parsing field `project.license` - {e}' raise ValueError(message) from None diff --git a/docs/history/hatch.md b/docs/history/hatch.md index 486d1fb61..e8988d7c5 100644 --- a/docs/history/hatch.md +++ b/docs/history/hatch.md @@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - The `version` command accepts a `--force` option, allowing for downgrades when an explicit version number is given. - Build environments can now be configured, the default build environment is `hatch-build` - The environment interface now has the following methods and properties in order to better support builds on remote machines: `project_root`, `sep`, `pathsep`, `fs_context` +- Bump the minimum supported version of `packaging` to 24.2 ## [1.13.0](https://github.com/pypa/hatch/releases/tag/hatch-v1.13.0) - 2024-10-13 ## {: #hatch-v1.13.0 } diff --git a/docs/history/hatchling.md b/docs/history/hatchling.md index 363e7d787..eff824151 100644 --- a/docs/history/hatchling.md +++ b/docs/history/hatchling.md @@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## Unreleased +***Added:*** + +- Bump the minimum supported version of `packaging` to 24.2 + ## [1.25.0](https://github.com/pypa/hatch/releases/tag/hatchling-v1.25.0) - 2024-06-22 ## {: #hatchling-v1.25.0 } ***Changed:*** diff --git a/hatch.toml b/hatch.toml index 695204d3c..766ee1ffb 100644 --- a/hatch.toml +++ b/hatch.toml @@ -117,11 +117,7 @@ update-hatch = [ "update-distributions", "update-ruff", ] -update-hatchling = [ - "update-licenses", -] update-distributions = "python scripts/update_distributions.py" -update-licenses = "python backend/scripts/update_licenses.py" update-ruff = [ "{env:HATCH_UV} pip install --upgrade ruff", "python scripts/update_ruff.py", diff --git a/pyproject.toml b/pyproject.toml index 4fd7e3da8..66b1c6306 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ dependencies = [ "httpx>=0.22.0", "hyperlink>=21.0.0", "keyring>=23.5.0", - "packaging>=23.2", + "packaging>=24.2", "pexpect~=4.8", "platformdirs>=2.5.0", "pyproject-hooks", diff --git a/src/hatch/template/default.py b/src/hatch/template/default.py index 71acb3e97..450784352 100644 --- a/src/hatch/template/default.py +++ b/src/hatch/template/default.py @@ -38,7 +38,7 @@ def initialize_config(self, config): license_file_name = f'{license_id}.txt' cached_license_path = cached_licenses_dir / license_file_name if not cached_license_path.is_file(): - from hatchling.licenses.supported import VERSION + from packaging.licenses._spdx import VERSION # noqa: PLC2701 url = f'https://raw.githubusercontent.com/spdx/license-list-data/v{VERSION}/text/{license_file_name}' for _ in range(5): diff --git a/tests/backend/licenses/__init__.py b/tests/backend/licenses/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/backend/licenses/test_parse.py b/tests/backend/licenses/test_parse.py deleted file mode 100644 index 9480a7c37..000000000 --- a/tests/backend/licenses/test_parse.py +++ /dev/null @@ -1,56 +0,0 @@ -import re - -import pytest - -from hatchling.licenses.parse import normalize_license_expression - - -@pytest.mark.parametrize( - 'expression', - [ - 'or', - 'and', - 'with', - 'mit or', - 'mit and', - 'mit with', - 'or mit', - 'and mit', - 'with mit', - '(mit', - 'mit)', - 'mit or or apache-2.0', - 'mit or apache-2.0 (bsd-3-clause and MPL-2.0)', - ], -) -def test_syntax_errors(expression): - with pytest.raises(ValueError, match=re.escape(f'invalid license expression: {expression}')): - normalize_license_expression(expression) - - -def test_unknown_license(): - with pytest.raises(ValueError, match='unknown license: foo'): - normalize_license_expression('mit or foo') - - -def test_unknown_license_exception(): - with pytest.raises(ValueError, match='unknown license exception: foo'): - normalize_license_expression('mit with foo') - - -@pytest.mark.parametrize( - ('raw', 'normalized'), - [ - ('mIt', 'MIT'), - ('mit or apache-2.0', 'MIT OR Apache-2.0'), - ('mit and apache-2.0', 'MIT AND Apache-2.0'), - ('gpl-2.0-or-later with bison-exception-2.2', 'GPL-2.0-or-later WITH Bison-exception-2.2'), - ('mit or apache-2.0 and (bsd-3-clause or mpl-2.0)', 'MIT OR Apache-2.0 AND (BSD-3-Clause OR MPL-2.0)'), - ('mit and (apache-2.0+ or mpl-2.0+)', 'MIT AND (Apache-2.0+ OR MPL-2.0+)'), - # Valid non-SPDX values - ('licenseref-public-domain', 'LicenseRef-Public-Domain'), - ('licenseref-proprietary', 'LicenseRef-Proprietary'), - ], -) -def test_normalization(raw, normalized): - assert normalize_license_expression(raw) == normalized diff --git a/tests/backend/licenses/test_supported.py b/tests/backend/licenses/test_supported.py deleted file mode 100644 index fbbb7adfe..000000000 --- a/tests/backend/licenses/test_supported.py +++ /dev/null @@ -1,31 +0,0 @@ -from hatchling.licenses.supported import EXCEPTIONS, LICENSES - - -def test_licenses(): - assert isinstance(LICENSES, dict) - assert list(LICENSES) == sorted(LICENSES) - - for name, data in LICENSES.items(): - assert isinstance(data, dict) - - assert 'id' in data - assert isinstance(data['id'], str) - assert data['id'].lower() == name - - assert 'deprecated' in data - assert isinstance(data['deprecated'], bool) - - -def test_exceptions(): - assert isinstance(EXCEPTIONS, dict) - assert list(EXCEPTIONS) == sorted(EXCEPTIONS) - - for name, data in EXCEPTIONS.items(): - assert isinstance(data, dict) - - assert 'id' in data - assert isinstance(data['id'], str) - assert data['id'].lower() == name - - assert 'deprecated' in data - assert isinstance(data['deprecated'], bool) diff --git a/tests/backend/metadata/test_core.py b/tests/backend/metadata/test_core.py index 76fd6601f..c401b8f3c 100644 --- a/tests/backend/metadata/test_core.py +++ b/tests/backend/metadata/test_core.py @@ -558,7 +558,7 @@ def test_normalization(self, isolation): def test_invalid_expression(self, isolation): metadata = ProjectMetadata(str(isolation), None, {'project': {'license': 'mit or foo'}}) - with pytest.raises(ValueError, match='Error parsing field `project.license` - unknown license: foo'): + with pytest.raises(ValueError, match="Error parsing field `project.license` - Unknown license: 'foo'"): _ = metadata.core.license_expression def test_multiple_options(self, isolation): From 28f233c535508247ffa9a40dab82c1d1cb2b700b Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Sat, 9 Nov 2024 16:22:35 -0500 Subject: [PATCH 07/13] Update the default version of core metadata to 2.4 (#1790) --- backend/src/hatchling/metadata/core.py | 66 +--- backend/src/hatchling/metadata/spec.py | 93 ++++- docs/history/hatchling.md | 5 + tests/backend/builders/test_wheel.py | 2 +- tests/backend/metadata/test_core.py | 68 +--- tests/backend/metadata/test_spec.py | 451 ++++++++++++++++++++++++- 6 files changed, 558 insertions(+), 127 deletions(-) diff --git a/backend/src/hatchling/metadata/core.py b/backend/src/hatchling/metadata/core.py index 97359325d..32e6a2f70 100644 --- a/backend/src/hatchling/metadata/core.py +++ b/backend/src/hatchling/metadata/core.py @@ -734,65 +734,35 @@ def license_files(self) -> list[str]: https://peps.python.org/pep-0639/ """ if self._license_files is None: - if 'license-files' not in self.config: - data = {'globs': ['LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*']} - else: + if 'license-files' in self.config: + globs = self.config['license-files'] if 'license-files' in self.dynamic: message = ( 'Metadata field `license-files` cannot be both statically defined and ' 'listed in field `project.dynamic`' ) raise ValueError(message) + else: + globs = ['LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*'] - data = self.config['license-files'] - if not isinstance(data, dict): - message = 'Field `project.license-files` must be a table' - raise TypeError(message) - - if 'paths' in data and 'globs' in data: - message = 'Cannot specify both `paths` and `globs` in the `project.license-files` table' - raise ValueError(message) - - license_files = [] - if 'paths' in data: - paths = data['paths'] - if not isinstance(paths, list): - message = 'Field `paths` in the `project.license-files` table must be an array' - raise TypeError(message) - - for i, relative_path in enumerate(paths, 1): - if not isinstance(relative_path, str): - message = f'Entry #{i} in field `paths` in the `project.license-files` table must be a string' - raise TypeError(message) + from glob import glob - path = os.path.normpath(os.path.join(self.root, relative_path)) - if not os.path.isfile(path): - message = f'License file does not exist: {relative_path}' - raise OSError(message) - - license_files.append(os.path.relpath(path, self.root).replace('\\', '/')) - elif 'globs' in data: - from glob import glob + license_files: list[str] = [] + if not isinstance(globs, list): + message = 'Field `project.license-files` must be an array' + raise TypeError(message) - globs = data['globs'] - if not isinstance(globs, list): - message = 'Field `globs` in the `project.license-files` table must be an array' + for i, pattern in enumerate(globs, 1): + if not isinstance(pattern, str): + message = f'Entry #{i} of field `project.license-files` must be a string' raise TypeError(message) - for i, pattern in enumerate(globs, 1): - if not isinstance(pattern, str): - message = f'Entry #{i} in field `globs` in the `project.license-files` table must be a string' - raise TypeError(message) - - full_pattern = os.path.normpath(os.path.join(self.root, pattern)) - license_files.extend( - os.path.relpath(path, self.root).replace('\\', '/') - for path in glob(full_pattern) - if os.path.isfile(path) - ) - else: - message = 'Must specify either `paths` or `globs` in the `project.license-files` table if defined' - raise ValueError(message) + full_pattern = os.path.normpath(os.path.join(self.root, pattern)) + license_files.extend( + os.path.relpath(path, self.root).replace('\\', '/') + for path in glob(full_pattern) + if os.path.isfile(path) + ) self._license_files = sorted(license_files) diff --git a/backend/src/hatchling/metadata/spec.py b/backend/src/hatchling/metadata/spec.py index a83ea3db1..3f9154101 100644 --- a/backend/src/hatchling/metadata/spec.py +++ b/backend/src/hatchling/metadata/spec.py @@ -5,8 +5,8 @@ if TYPE_CHECKING: from hatchling.metadata.core import ProjectMetadata -DEFAULT_METADATA_VERSION = '2.3' -LATEST_METADATA_VERSION = '2.3' +DEFAULT_METADATA_VERSION = '2.4' +LATEST_METADATA_VERSION = '2.4' CORE_METADATA_PROJECT_FIELDS = { 'Author': ('authors',), 'Author-email': ('authors',), @@ -56,6 +56,7 @@ def get_core_metadata_constructors() -> dict[str, Callable]: '2.1': construct_metadata_file_2_1, '2.2': construct_metadata_file_2_2, '2.3': construct_metadata_file_2_3, + '2.4': construct_metadata_file_2_4, } @@ -102,7 +103,7 @@ def project_metadata_from_core_metadata(core_metadata: str) -> dict[str, Any]: metadata['license'] = {'text': license_text} if (license_files := message.get_all('License-File')) is not None: - metadata['license-files'] = {'paths': license_files} + metadata['license-files'] = license_files if (summary := message.get('Summary')) is not None: metadata['description'] = summary @@ -430,12 +431,96 @@ def construct_metadata_file_2_2(metadata: ProjectMetadata, extra_dependencies: t def construct_metadata_file_2_3(metadata: ProjectMetadata, extra_dependencies: tuple[str] | None = None) -> str: """ - https://peps.python.org/pep-0639/ + https://peps.python.org/pep-0685/ """ metadata_file = 'Metadata-Version: 2.3\n' metadata_file += f'Name: {metadata.core.raw_name}\n' metadata_file += f'Version: {metadata.version}\n' + if metadata.core.dynamic: + # Ordered set + for field in { + core_metadata_field: None + for project_field in metadata.core.dynamic + for core_metadata_field in PROJECT_CORE_METADATA_FIELDS.get(project_field, ()) + }: + metadata_file += f'Dynamic: {field}\n' + + if metadata.core.description: + metadata_file += f'Summary: {metadata.core.description}\n' + + if metadata.core.urls: + for label, url in metadata.core.urls.items(): + metadata_file += f'Project-URL: {label}, {url}\n' + + authors_data = metadata.core.authors_data + if authors_data['name']: + metadata_file += f"Author: {', '.join(authors_data['name'])}\n" + if authors_data['email']: + metadata_file += f"Author-email: {', '.join(authors_data['email'])}\n" + + maintainers_data = metadata.core.maintainers_data + if maintainers_data['name']: + metadata_file += f"Maintainer: {', '.join(maintainers_data['name'])}\n" + if maintainers_data['email']: + metadata_file += f"Maintainer-email: {', '.join(maintainers_data['email'])}\n" + + if metadata.core.license: + license_start = 'License: ' + indent = ' ' * (len(license_start) - 1) + metadata_file += license_start + + for i, line in enumerate(metadata.core.license.splitlines()): + if i == 0: + metadata_file += f'{line}\n' + else: + metadata_file += f'{indent}{line}\n' + + if metadata.core.keywords: + metadata_file += f"Keywords: {','.join(metadata.core.keywords)}\n" + + if metadata.core.classifiers: + for classifier in metadata.core.classifiers: + metadata_file += f'Classifier: {classifier}\n' + + if metadata.core.requires_python: + metadata_file += f'Requires-Python: {metadata.core.requires_python}\n' + + if metadata.core.dependencies: + for dependency in metadata.core.dependencies: + metadata_file += f'Requires-Dist: {dependency}\n' + + if extra_dependencies: + for dependency in extra_dependencies: + metadata_file += f'Requires-Dist: {dependency}\n' + + if metadata.core.optional_dependencies: + for option, dependencies in metadata.core.optional_dependencies.items(): + metadata_file += f'Provides-Extra: {option}\n' + for dependency in dependencies: + if ';' in dependency: + dep_name, dep_env_marker = dependency.split(';', maxsplit=1) + metadata_file += f'Requires-Dist: {dep_name}; ({dep_env_marker.strip()}) and extra == {option!r}\n' + elif '@ ' in dependency: + metadata_file += f'Requires-Dist: {dependency} ; extra == {option!r}\n' + else: + metadata_file += f'Requires-Dist: {dependency}; extra == {option!r}\n' + + if metadata.core.readme: + metadata_file += f'Description-Content-Type: {metadata.core.readme_content_type}\n' + metadata_file += f'\n{metadata.core.readme}' + + return metadata_file + + +def construct_metadata_file_2_4(metadata: ProjectMetadata, extra_dependencies: tuple[str] | None = None) -> str: + """ + https://peps.python.org/pep-0639/ + """ + metadata_file = 'Metadata-Version: 2.4\n' + metadata_file += f'Name: {metadata.core.raw_name}\n' + metadata_file += f'Version: {metadata.version}\n' + if metadata.core.dynamic: # Ordered set for field in { diff --git a/docs/history/hatchling.md b/docs/history/hatchling.md index eff824151..8da9c1cea 100644 --- a/docs/history/hatchling.md +++ b/docs/history/hatchling.md @@ -10,8 +10,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ***Added:*** +- Update the default version of core metadata to 2.4 - Bump the minimum supported version of `packaging` to 24.2 +***Fixed:*** + +- No longer write package metadata for license expressions and files for versions of core metadata prior to 2.4 + ## [1.25.0](https://github.com/pypa/hatch/releases/tag/hatchling-v1.25.0) - 2024-06-22 ## {: #hatchling-v1.25.0 } ***Changed:*** diff --git a/tests/backend/builders/test_wheel.py b/tests/backend/builders/test_wheel.py index bdc4e99dd..dceb4f37c 100644 --- a/tests/backend/builders/test_wheel.py +++ b/tests/backend/builders/test_wheel.py @@ -909,7 +909,7 @@ def test_default_multiple_licenses(self, hatch, helpers, config_file, temp_dir): (project_path / 'LICENSES' / 'test').mkdir() config = { - 'project': {'name': project_name, 'dynamic': ['version'], 'license-files': {'globs': ['LICENSES/*']}}, + 'project': {'name': project_name, 'dynamic': ['version'], 'license-files': ['LICENSES/*']}, 'tool': { 'hatch': { 'version': {'path': 'my_app/__about__.py'}, diff --git a/tests/backend/metadata/test_core.py b/tests/backend/metadata/test_core.py index c401b8f3c..c1c19588d 100644 --- a/tests/backend/metadata/test_core.py +++ b/tests/backend/metadata/test_core.py @@ -621,54 +621,16 @@ def test_dynamic(self, isolation): ): _ = metadata.core.license_files - def test_not_table(self, isolation): + def test_not_array(self, isolation): metadata = ProjectMetadata(str(isolation), None, {'project': {'license-files': 9000}}) - with pytest.raises(TypeError, match='Field `project.license-files` must be a table'): - _ = metadata.core.license_files - - def test_multiple_options(self, isolation): - metadata = ProjectMetadata(str(isolation), None, {'project': {'license-files': {'paths': [], 'globs': []}}}) - - with pytest.raises( - ValueError, match='Cannot specify both `paths` and `globs` in the `project.license-files` table' - ): - _ = metadata.core.license_files - - def test_no_option(self, isolation): - metadata = ProjectMetadata(str(isolation), None, {'project': {'license-files': {}}}) - - with pytest.raises( - ValueError, match='Must specify either `paths` or `globs` in the `project.license-files` table if defined' - ): - _ = metadata.core.license_files - - def test_paths_not_array(self, isolation): - metadata = ProjectMetadata(str(isolation), None, {'project': {'license-files': {'paths': 9000}}}) - - with pytest.raises(TypeError, match='Field `paths` in the `project.license-files` table must be an array'): + with pytest.raises(TypeError, match='Field `project.license-files` must be an array'): _ = metadata.core.license_files - def test_paths_entry_not_string(self, isolation): - metadata = ProjectMetadata(str(isolation), None, {'project': {'license-files': {'paths': [9000]}}}) - - with pytest.raises( - TypeError, match='Entry #1 in field `paths` in the `project.license-files` table must be a string' - ): - _ = metadata.core.license_files - - def test_globs_not_array(self, isolation): - metadata = ProjectMetadata(str(isolation), None, {'project': {'license-files': {'globs': 9000}}}) - - with pytest.raises(TypeError, match='Field `globs` in the `project.license-files` table must be an array'): - _ = metadata.core.license_files - - def test_globs_entry_not_string(self, isolation): - metadata = ProjectMetadata(str(isolation), None, {'project': {'license-files': {'globs': [9000]}}}) + def test_entry_not_string(self, isolation): + metadata = ProjectMetadata(str(isolation), None, {'project': {'license-files': [9000]}}) - with pytest.raises( - TypeError, match='Entry #1 in field `globs` in the `project.license-files` table must be a string' - ): + with pytest.raises(TypeError, match='Entry #1 of field `project.license-files` must be a string'): _ = metadata.core.license_files def test_default_globs_no_licenses(self, isolation): @@ -693,7 +655,7 @@ def test_default_globs_with_licenses(self, temp_dir): assert metadata.core.license_files == sorted(expected) def test_globs_with_licenses(self, temp_dir): - metadata = ProjectMetadata(str(temp_dir), None, {'project': {'license-files': {'globs': ['LICENSES/*']}}}) + metadata = ProjectMetadata(str(temp_dir), None, {'project': {'license-files': ['LICENSES/*']}}) licenses_dir = temp_dir / 'LICENSES' licenses_dir.mkdir() @@ -709,7 +671,7 @@ def test_paths_with_licenses(self, temp_dir): metadata = ProjectMetadata( str(temp_dir), None, - {'project': {'license-files': {'paths': ['LICENSES/Apache-2.0.txt', 'LICENSES/MIT.txt', 'COPYING']}}}, + {'project': {'license-files': ['LICENSES/Apache-2.0.txt', 'LICENSES/MIT.txt', 'COPYING']}}, ) licenses_dir = temp_dir / 'LICENSES' @@ -722,20 +684,6 @@ def test_paths_with_licenses(self, temp_dir): assert metadata.core.license_files == ['COPYING', 'LICENSES/Apache-2.0.txt', 'LICENSES/MIT.txt'] - def test_paths_missing_license(self, temp_dir): - metadata = ProjectMetadata( - str(temp_dir), - None, - {'project': {'license-files': {'paths': ['LICENSES/MIT.txt']}}}, - ) - - licenses_dir = temp_dir / 'LICENSES' - licenses_dir.mkdir() - (licenses_dir / 'Apache-2.0.txt').touch() - - with pytest.raises(OSError, match='License file does not exist: LICENSES/MIT.txt'): - _ = metadata.core.license_files - class TestAuthors: def test_dynamic(self, isolation): @@ -1661,7 +1609,7 @@ def test_license_files(self, temp_dir, latest_spec): raw_metadata = { 'name': 'My.App', 'version': '0.0.1', - 'license-files': {'paths': ['LICENSES/Apache-2.0.txt', 'LICENSES/MIT.txt']}, + 'license-files': ['LICENSES/Apache-2.0.txt', 'LICENSES/MIT.txt'], } metadata = ProjectMetadata(str(temp_dir), None, {'project': raw_metadata}) diff --git a/tests/backend/metadata/test_spec.py b/tests/backend/metadata/test_spec.py index 50a9b6073..569b710de 100644 --- a/tests/backend/metadata/test_spec.py +++ b/tests/backend/metadata/test_spec.py @@ -131,7 +131,7 @@ def test_license_files(self): assert project_metadata_from_core_metadata(core_metadata) == { 'name': 'My.App', 'version': '0.1.0', - 'license-files': {'paths': ['LICENSES/Apache-2.0.txt', 'LICENSES/MIT.txt']}, + 'license-files': ['LICENSES/Apache-2.0.txt', 'LICENSES/MIT.txt'], } def test_license_expression(self): @@ -1660,6 +1660,429 @@ def test_license(self, constructor, isolation, helpers): """ ) + def test_keywords_single(self, constructor, isolation, helpers): + metadata = ProjectMetadata( + str(isolation), None, {'project': {'name': 'My.App', 'version': '0.1.0', 'keywords': ['foo']}} + ) + + assert constructor(metadata) == helpers.dedent( + """ + Metadata-Version: 2.3 + Name: My.App + Version: 0.1.0 + Keywords: foo + """ + ) + + def test_keywords_multiple(self, constructor, isolation, helpers): + metadata = ProjectMetadata( + str(isolation), None, {'project': {'name': 'My.App', 'version': '0.1.0', 'keywords': ['foo', 'bar']}} + ) + + assert constructor(metadata) == helpers.dedent( + """ + Metadata-Version: 2.3 + Name: My.App + Version: 0.1.0 + Keywords: bar,foo + """ + ) + + def test_classifiers(self, constructor, isolation, helpers): + classifiers = [ + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.9', + ] + metadata = ProjectMetadata( + str(isolation), None, {'project': {'name': 'My.App', 'version': '0.1.0', 'classifiers': classifiers}} + ) + + assert constructor(metadata) == helpers.dedent( + """ + Metadata-Version: 2.3 + Name: My.App + Version: 0.1.0 + Classifier: Programming Language :: Python :: 3.9 + Classifier: Programming Language :: Python :: 3.11 + """ + ) + + def test_requires_python(self, constructor, isolation, helpers): + metadata = ProjectMetadata( + str(isolation), None, {'project': {'name': 'My.App', 'version': '0.1.0', 'requires-python': '>=1,<2'}} + ) + + assert constructor(metadata) == helpers.dedent( + """ + Metadata-Version: 2.3 + Name: My.App + Version: 0.1.0 + Requires-Python: <2,>=1 + """ + ) + + def test_dependencies(self, constructor, isolation, helpers): + metadata = ProjectMetadata( + str(isolation), + None, + {'project': {'name': 'My.App', 'version': '0.1.0', 'dependencies': ['foo==1', 'bar==5']}}, + ) + + assert constructor(metadata) == helpers.dedent( + """ + Metadata-Version: 2.3 + Name: My.App + Version: 0.1.0 + Requires-Dist: bar==5 + Requires-Dist: foo==1 + """ + ) + + def test_optional_dependencies(self, constructor, isolation, helpers): + metadata = ProjectMetadata( + str(isolation), + None, + { + 'project': { + 'name': 'My.App', + 'version': '0.1.0', + 'optional-dependencies': { + 'feature2': ['foo==1; python_version < "3"', 'bar==5'], + 'feature1': ['foo==1', 'bar==5; python_version < "3"'], + }, + } + }, + ) + + assert constructor(metadata) == helpers.dedent( + """ + Metadata-Version: 2.3 + Name: My.App + Version: 0.1.0 + Provides-Extra: feature1 + Requires-Dist: bar==5; (python_version < '3') and extra == 'feature1' + Requires-Dist: foo==1; extra == 'feature1' + Provides-Extra: feature2 + Requires-Dist: bar==5; extra == 'feature2' + Requires-Dist: foo==1; (python_version < '3') and extra == 'feature2' + """ + ) + + def test_extra_runtime_dependencies(self, constructor, isolation, helpers): + metadata = ProjectMetadata( + str(isolation), + None, + {'project': {'name': 'My.App', 'version': '0.1.0', 'dependencies': ['foo==1', 'bar==5']}}, + ) + + assert constructor(metadata, extra_dependencies=['baz==9']) == helpers.dedent( + """ + Metadata-Version: 2.3 + Name: My.App + Version: 0.1.0 + Requires-Dist: bar==5 + Requires-Dist: foo==1 + Requires-Dist: baz==9 + """ + ) + + def test_readme(self, constructor, isolation, helpers): + metadata = ProjectMetadata( + str(isolation), + None, + { + 'project': { + 'name': 'My.App', + 'version': '0.1.0', + 'readme': {'content-type': 'text/markdown', 'text': 'test content\n'}, + } + }, + ) + + assert constructor(metadata) == helpers.dedent( + """ + Metadata-Version: 2.3 + Name: My.App + Version: 0.1.0 + Description-Content-Type: text/markdown + + test content + """ + ) + + def test_all(self, constructor, temp_dir, helpers): + metadata = ProjectMetadata( + str(temp_dir), + None, + { + 'project': { + 'name': 'My.App', + 'version': '0.1.0', + 'description': 'foo', + 'urls': {'foo': 'bar', 'bar': 'baz'}, + 'authors': [{'email': 'bar@domain', 'name': 'foo'}], + 'maintainers': [{'email': 'bar@domain', 'name': 'foo'}], + 'keywords': ['foo', 'bar'], + 'classifiers': [ + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.9', + ], + 'requires-python': '>=1,<2', + 'dependencies': ['foo==1', 'bar==5'], + 'optional-dependencies': { + 'feature2': ['foo==1; python_version < "3"', 'bar==5'], + 'feature1': ['foo==1', 'bar==5; python_version < "3"'], + 'feature3': ['baz @ file:///path/to/project'], + }, + 'readme': {'content-type': 'text/markdown', 'text': 'test content\n'}, + }, + 'tool': {'hatch': {'metadata': {'allow-direct-references': True}}}, + }, + ) + + licenses_dir = temp_dir / 'LICENSES' + licenses_dir.mkdir() + (licenses_dir / 'MIT.txt').touch() + (licenses_dir / 'Apache-2.0.txt').touch() + + assert constructor(metadata) == helpers.dedent( + """ + Metadata-Version: 2.3 + Name: My.App + Version: 0.1.0 + Summary: foo + Project-URL: foo, bar + Project-URL: bar, baz + Author-email: foo + Maintainer-email: foo + Keywords: bar,foo + Classifier: Programming Language :: Python :: 3.9 + Classifier: Programming Language :: Python :: 3.11 + Requires-Python: <2,>=1 + Requires-Dist: bar==5 + Requires-Dist: foo==1 + Provides-Extra: feature1 + Requires-Dist: bar==5; (python_version < '3') and extra == 'feature1' + Requires-Dist: foo==1; extra == 'feature1' + Provides-Extra: feature2 + Requires-Dist: bar==5; extra == 'feature2' + Requires-Dist: foo==1; (python_version < '3') and extra == 'feature2' + Provides-Extra: feature3 + Requires-Dist: baz@ file:///path/to/project ; extra == 'feature3' + Description-Content-Type: text/markdown + + test content + """ + ) + + +@pytest.mark.parametrize('constructor', [get_core_metadata_constructors()['2.4']]) +class TestCoreMetadataV24: + def test_default(self, constructor, isolation, helpers): + metadata = ProjectMetadata(str(isolation), None, {'project': {'name': 'My.App', 'version': '0.1.0'}}) + + assert constructor(metadata) == helpers.dedent( + """ + Metadata-Version: 2.4 + Name: My.App + Version: 0.1.0 + """ + ) + + def test_description(self, constructor, isolation, helpers): + metadata = ProjectMetadata( + str(isolation), None, {'project': {'name': 'My.App', 'version': '0.1.0', 'description': 'foo'}} + ) + + assert constructor(metadata) == helpers.dedent( + """ + Metadata-Version: 2.4 + Name: My.App + Version: 0.1.0 + Summary: foo + """ + ) + + def test_dynamic(self, constructor, isolation, helpers): + metadata = ProjectMetadata( + str(isolation), + None, + {'project': {'name': 'My.App', 'version': '0.1.0', 'dynamic': ['authors', 'classifiers']}}, + ) + + assert constructor(metadata) == helpers.dedent( + """ + Metadata-Version: 2.4 + Name: My.App + Version: 0.1.0 + Dynamic: Author + Dynamic: Author-email + Dynamic: Classifier + """ + ) + + def test_urls(self, constructor, isolation, helpers): + metadata = ProjectMetadata( + str(isolation), + None, + {'project': {'name': 'My.App', 'version': '0.1.0', 'urls': {'foo': 'bar', 'bar': 'baz'}}}, + ) + + assert constructor(metadata) == helpers.dedent( + """ + Metadata-Version: 2.4 + Name: My.App + Version: 0.1.0 + Project-URL: foo, bar + Project-URL: bar, baz + """ + ) + + def test_authors_name(self, constructor, isolation, helpers): + metadata = ProjectMetadata( + str(isolation), None, {'project': {'name': 'My.App', 'version': '0.1.0', 'authors': [{'name': 'foo'}]}} + ) + + assert constructor(metadata) == helpers.dedent( + """ + Metadata-Version: 2.4 + Name: My.App + Version: 0.1.0 + Author: foo + """ + ) + + def test_authors_email(self, constructor, isolation, helpers): + metadata = ProjectMetadata( + str(isolation), + None, + {'project': {'name': 'My.App', 'version': '0.1.0', 'authors': [{'email': 'foo@domain'}]}}, + ) + + assert constructor(metadata) == helpers.dedent( + """ + Metadata-Version: 2.4 + Name: My.App + Version: 0.1.0 + Author-email: foo@domain + """ + ) + + def test_authors_name_and_email(self, constructor, isolation, helpers): + metadata = ProjectMetadata( + str(isolation), + None, + {'project': {'name': 'My.App', 'version': '0.1.0', 'authors': [{'email': 'bar@domain', 'name': 'foo'}]}}, + ) + + assert constructor(metadata) == helpers.dedent( + """ + Metadata-Version: 2.4 + Name: My.App + Version: 0.1.0 + Author-email: foo + """ + ) + + def test_authors_multiple(self, constructor, isolation, helpers): + metadata = ProjectMetadata( + str(isolation), + None, + {'project': {'name': 'My.App', 'version': '0.1.0', 'authors': [{'name': 'foo'}, {'name': 'bar'}]}}, + ) + + assert constructor(metadata) == helpers.dedent( + """ + Metadata-Version: 2.4 + Name: My.App + Version: 0.1.0 + Author: foo, bar + """ + ) + + def test_maintainers_name(self, constructor, isolation, helpers): + metadata = ProjectMetadata( + str(isolation), None, {'project': {'name': 'My.App', 'version': '0.1.0', 'maintainers': [{'name': 'foo'}]}} + ) + + assert constructor(metadata) == helpers.dedent( + """ + Metadata-Version: 2.4 + Name: My.App + Version: 0.1.0 + Maintainer: foo + """ + ) + + def test_maintainers_email(self, constructor, isolation, helpers): + metadata = ProjectMetadata( + str(isolation), + None, + {'project': {'name': 'My.App', 'version': '0.1.0', 'maintainers': [{'email': 'foo@domain'}]}}, + ) + + assert constructor(metadata) == helpers.dedent( + """ + Metadata-Version: 2.4 + Name: My.App + Version: 0.1.0 + Maintainer-email: foo@domain + """ + ) + + def test_maintainers_name_and_email(self, constructor, isolation, helpers): + metadata = ProjectMetadata( + str(isolation), + None, + { + 'project': { + 'name': 'My.App', + 'version': '0.1.0', + 'maintainers': [{'email': 'bar@domain', 'name': 'foo'}], + } + }, + ) + + assert constructor(metadata) == helpers.dedent( + """ + Metadata-Version: 2.4 + Name: My.App + Version: 0.1.0 + Maintainer-email: foo + """ + ) + + def test_maintainers_multiple(self, constructor, isolation, helpers): + metadata = ProjectMetadata( + str(isolation), + None, + {'project': {'name': 'My.App', 'version': '0.1.0', 'maintainers': [{'name': 'foo'}, {'name': 'bar'}]}}, + ) + + assert constructor(metadata) == helpers.dedent( + """ + Metadata-Version: 2.4 + Name: My.App + Version: 0.1.0 + Maintainer: foo, bar + """ + ) + + def test_license(self, constructor, isolation, helpers): + metadata = ProjectMetadata( + str(isolation), None, {'project': {'name': 'My.App', 'version': '0.1.0', 'license': {'text': 'foo\nbar'}}} + ) + + assert constructor(metadata) == helpers.dedent( + """ + Metadata-Version: 2.4 + Name: My.App + Version: 0.1.0 + License: foo + bar + """ + ) + def test_license_expression(self, constructor, isolation, helpers): metadata = ProjectMetadata( str(isolation), @@ -1669,7 +2092,7 @@ def test_license_expression(self, constructor, isolation, helpers): assert constructor(metadata) == helpers.dedent( """ - Metadata-Version: 2.3 + Metadata-Version: 2.4 Name: My.App Version: 0.1.0 License-Expression: MIT OR Apache-2.0 @@ -1680,7 +2103,7 @@ def test_license_files(self, constructor, temp_dir, helpers): metadata = ProjectMetadata( str(temp_dir), None, - {'project': {'name': 'My.App', 'version': '0.1.0', 'license-files': {'globs': ['LICENSES/*']}}}, + {'project': {'name': 'My.App', 'version': '0.1.0', 'license-files': ['LICENSES/*']}}, ) licenses_dir = temp_dir / 'LICENSES' @@ -1690,7 +2113,7 @@ def test_license_files(self, constructor, temp_dir, helpers): assert constructor(metadata) == helpers.dedent( """ - Metadata-Version: 2.3 + Metadata-Version: 2.4 Name: My.App Version: 0.1.0 License-File: LICENSES/Apache-2.0.txt @@ -1705,7 +2128,7 @@ def test_keywords_single(self, constructor, isolation, helpers): assert constructor(metadata) == helpers.dedent( """ - Metadata-Version: 2.3 + Metadata-Version: 2.4 Name: My.App Version: 0.1.0 Keywords: foo @@ -1719,7 +2142,7 @@ def test_keywords_multiple(self, constructor, isolation, helpers): assert constructor(metadata) == helpers.dedent( """ - Metadata-Version: 2.3 + Metadata-Version: 2.4 Name: My.App Version: 0.1.0 Keywords: bar,foo @@ -1737,7 +2160,7 @@ def test_classifiers(self, constructor, isolation, helpers): assert constructor(metadata) == helpers.dedent( """ - Metadata-Version: 2.3 + Metadata-Version: 2.4 Name: My.App Version: 0.1.0 Classifier: Programming Language :: Python :: 3.9 @@ -1752,7 +2175,7 @@ def test_requires_python(self, constructor, isolation, helpers): assert constructor(metadata) == helpers.dedent( """ - Metadata-Version: 2.3 + Metadata-Version: 2.4 Name: My.App Version: 0.1.0 Requires-Python: <2,>=1 @@ -1768,7 +2191,7 @@ def test_dependencies(self, constructor, isolation, helpers): assert constructor(metadata) == helpers.dedent( """ - Metadata-Version: 2.3 + Metadata-Version: 2.4 Name: My.App Version: 0.1.0 Requires-Dist: bar==5 @@ -1794,7 +2217,7 @@ def test_optional_dependencies(self, constructor, isolation, helpers): assert constructor(metadata) == helpers.dedent( """ - Metadata-Version: 2.3 + Metadata-Version: 2.4 Name: My.App Version: 0.1.0 Provides-Extra: feature1 @@ -1815,7 +2238,7 @@ def test_extra_runtime_dependencies(self, constructor, isolation, helpers): assert constructor(metadata, extra_dependencies=['baz==9']) == helpers.dedent( """ - Metadata-Version: 2.3 + Metadata-Version: 2.4 Name: My.App Version: 0.1.0 Requires-Dist: bar==5 @@ -1839,7 +2262,7 @@ def test_readme(self, constructor, isolation, helpers): assert constructor(metadata) == helpers.dedent( """ - Metadata-Version: 2.3 + Metadata-Version: 2.4 Name: My.App Version: 0.1.0 Description-Content-Type: text/markdown @@ -1861,7 +2284,7 @@ def test_all(self, constructor, temp_dir, helpers): 'authors': [{'email': 'bar@domain', 'name': 'foo'}], 'maintainers': [{'email': 'bar@domain', 'name': 'foo'}], 'license': 'mit or apache-2.0', - 'license-files': {'globs': ['LICENSES/*']}, + 'license-files': ['LICENSES/*'], 'keywords': ['foo', 'bar'], 'classifiers': [ 'Programming Language :: Python :: 3.11', @@ -1887,7 +2310,7 @@ def test_all(self, constructor, temp_dir, helpers): assert constructor(metadata) == helpers.dedent( """ - Metadata-Version: 2.3 + Metadata-Version: 2.4 Name: My.App Version: 0.1.0 Summary: foo From a664f3c1c56afbec581e9d44e7db1f3beaa87b0c Mon Sep 17 00:00:00 2001 From: Ben Mares Date: Sat, 9 Nov 2024 21:41:25 +0000 Subject: [PATCH 08/13] Add pixi to excluded directories (#1762) --- backend/src/hatchling/builders/constants.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/hatchling/builders/constants.py b/backend/src/hatchling/builders/constants.py index b16b199bf..ac718da73 100644 --- a/backend/src/hatchling/builders/constants.py +++ b/backend/src/hatchling/builders/constants.py @@ -21,6 +21,8 @@ '.pytest_cache', # Mypy '.mypy_cache', + # pixi + '.pixi', )) EXCLUDED_FILES = frozenset(( # https://en.wikipedia.org/wiki/.DS_Store From 58c0982d0325608e3ca8c68c392d94c98585c889 Mon Sep 17 00:00:00 2001 From: Eitan <49152796+eitanV81@users.noreply.github.com> Date: Sun, 10 Nov 2024 00:09:06 +0200 Subject: [PATCH 09/13] Enable Zip64 Support for Large Wheel Files in Reproducible Mode (#1576) Co-authored-by: Ofek Lev --- backend/src/hatchling/builders/wheel.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/hatchling/builders/wheel.py b/backend/src/hatchling/builders/wheel.py index 7ef38a1bf..e6bc55262 100644 --- a/backend/src/hatchling/builders/wheel.py +++ b/backend/src/hatchling/builders/wheel.py @@ -102,6 +102,8 @@ def add_file(self, included_file: IncludedFile) -> tuple[str, str, str]: set_zip_info_mode(zip_info, new_mode) if stat.S_ISDIR(file_stat.st_mode): # no cov zip_info.external_attr |= 0x10 + else: + zip_info.file_size = file_stat.st_size else: zip_info = zipfile.ZipInfo.from_file(included_file.path, relative_path) From 4a97018eb65e469a780ef2d2773ebbe4fe006aa9 Mon Sep 17 00:00:00 2001 From: Ben Mares Date: Sat, 9 Nov 2024 22:35:22 +0000 Subject: [PATCH 10/13] MAINT: Convert manually cached BuilderConfig properties to cached_property (#1760) Co-authored-by: Ofek Lev --- backend/src/hatchling/builders/config.py | 1064 ++++++++++------------ 1 file changed, 483 insertions(+), 581 deletions(-) diff --git a/backend/src/hatchling/builders/config.py b/backend/src/hatchling/builders/config.py index da4a9f9b9..6457df9f5 100644 --- a/backend/src/hatchling/builders/config.py +++ b/backend/src/hatchling/builders/config.py @@ -30,24 +30,6 @@ def __init__( self.__plugin_name = plugin_name self.__build_config = build_config self.__target_config = target_config - self.__hook_config: dict[str, Any] | None = None - self.__versions: list[str] | None = None - self.__dependencies: list[str] | None = None - self.__sources: dict[str, str] | None = None - self.__packages: list[str] | None = None - self.__only_include: dict[str, str] | None = None - self.__force_include: dict[str, str] | None = None - self.__vcs_exclusion_files: dict[str, list[str]] | None = None - - # Possible pathspec.GitIgnoreSpec - self.__include_spec: pathspec.GitIgnoreSpec | None = None - self.__exclude_spec: pathspec.GitIgnoreSpec | None = None - self.__artifact_spec: pathspec.GitIgnoreSpec | None = None - - # These are used to create the pathspecs and will never be `None` after the first match attempt - self.__include_patterns: list[str] | None = None - self.__exclude_patterns: list[str] | None = None - self.__artifact_patterns: list[str] | None = None # This is used when the only file selection is based on forced inclusion or build-time artifacts. This # instructs to `exclude` every encountered path without doing pattern matching that matches everything. @@ -58,17 +40,6 @@ def __init__( self.build_force_include: dict[str, str] = {} self.build_reserved_paths: set[str] = set() - # Common options - self.__directory: str | None = None - self.__skip_excluded_dirs: bool | None = None - self.__ignore_vcs: bool | None = None - self.__only_packages: bool | None = None - self.__reproducible: bool | None = None - self.__dev_mode_dirs: list[str] | None = None - self.__dev_mode_exact: bool | None = None - self.__require_runtime_dependencies: bool | None = None - self.__require_runtime_features: list[str] | None = None - @property def builder(self) -> BuilderInterface: return self.__builder @@ -141,524 +112,472 @@ def directory_is_excluded(self, name: str, relative_path: str) -> bool: or (self.skip_excluded_dirs and self.path_is_excluded(f'{relative_directory}/')) ) - @property + @cached_property def include_spec(self) -> pathspec.GitIgnoreSpec | None: - if self.__include_patterns is None: - if 'include' in self.target_config: - include_config = self.target_config - include_location = f'tool.hatch.build.targets.{self.plugin_name}.include' - else: - include_config = self.build_config - include_location = 'tool.hatch.build.include' - - all_include_patterns = [] - - include_patterns = include_config.get('include', self.default_include()) - if not isinstance(include_patterns, list): - message = f'Field `{include_location}` must be an array of strings' + if 'include' in self.target_config: + include_config = self.target_config + include_location = f'tool.hatch.build.targets.{self.plugin_name}.include' + else: + include_config = self.build_config + include_location = 'tool.hatch.build.include' + + all_include_patterns = [] + + include_patterns = include_config.get('include', self.default_include()) + if not isinstance(include_patterns, list): + message = f'Field `{include_location}` must be an array of strings' + raise TypeError(message) + + for i, include_pattern in enumerate(include_patterns, 1): + if not isinstance(include_pattern, str): + message = f'Pattern #{i} in field `{include_location}` must be a string' raise TypeError(message) - for i, include_pattern in enumerate(include_patterns, 1): - if not isinstance(include_pattern, str): - message = f'Pattern #{i} in field `{include_location}` must be a string' - raise TypeError(message) - - if not include_pattern: - message = f'Pattern #{i} in field `{include_location}` cannot be an empty string' - raise ValueError(message) - - all_include_patterns.append(include_pattern) + if not include_pattern: + message = f'Pattern #{i} in field `{include_location}` cannot be an empty string' + raise ValueError(message) - # Matching only at the root requires a forward slash, back slashes do not work. As such, - # normalize to forward slashes for consistency. - all_include_patterns.extend(f"/{relative_path.replace(os.sep, '/')}/" for relative_path in self.packages) + all_include_patterns.append(include_pattern) - if all_include_patterns: - self.__include_spec = pathspec.GitIgnoreSpec.from_lines(all_include_patterns) + # Matching only at the root requires a forward slash, back slashes do not work. As such, + # normalize to forward slashes for consistency. + all_include_patterns.extend(f"/{relative_path.replace(os.sep, '/')}/" for relative_path in self.packages) - self.__include_patterns = all_include_patterns + if all_include_patterns: + return pathspec.GitIgnoreSpec.from_lines(all_include_patterns) + return None - return self.__include_spec - - @property + @cached_property def exclude_spec(self) -> pathspec.GitIgnoreSpec | None: - if self.__exclude_patterns is None: - if 'exclude' in self.target_config: - exclude_config = self.target_config - exclude_location = f'tool.hatch.build.targets.{self.plugin_name}.exclude' - else: - exclude_config = self.build_config - exclude_location = 'tool.hatch.build.exclude' - - all_exclude_patterns = self.default_global_exclude() - - if not self.ignore_vcs: - all_exclude_patterns.extend(self.load_vcs_exclusion_patterns()) - - exclude_patterns = exclude_config.get('exclude', self.default_exclude()) - if not isinstance(exclude_patterns, list): - message = f'Field `{exclude_location}` must be an array of strings' + if 'exclude' in self.target_config: + exclude_config = self.target_config + exclude_location = f'tool.hatch.build.targets.{self.plugin_name}.exclude' + else: + exclude_config = self.build_config + exclude_location = 'tool.hatch.build.exclude' + + all_exclude_patterns = self.default_global_exclude() + + if not self.ignore_vcs: + all_exclude_patterns.extend(self.load_vcs_exclusion_patterns()) + + exclude_patterns = exclude_config.get('exclude', self.default_exclude()) + if not isinstance(exclude_patterns, list): + message = f'Field `{exclude_location}` must be an array of strings' + raise TypeError(message) + + for i, exclude_pattern in enumerate(exclude_patterns, 1): + if not isinstance(exclude_pattern, str): + message = f'Pattern #{i} in field `{exclude_location}` must be a string' raise TypeError(message) - for i, exclude_pattern in enumerate(exclude_patterns, 1): - if not isinstance(exclude_pattern, str): - message = f'Pattern #{i} in field `{exclude_location}` must be a string' - raise TypeError(message) - - if not exclude_pattern: - message = f'Pattern #{i} in field `{exclude_location}` cannot be an empty string' - raise ValueError(message) + if not exclude_pattern: + message = f'Pattern #{i} in field `{exclude_location}` cannot be an empty string' + raise ValueError(message) - all_exclude_patterns.append(exclude_pattern) + all_exclude_patterns.append(exclude_pattern) - if all_exclude_patterns: - self.__exclude_spec = pathspec.GitIgnoreSpec.from_lines(all_exclude_patterns) - - self.__exclude_patterns = all_exclude_patterns - - return self.__exclude_spec + if all_exclude_patterns: + return pathspec.GitIgnoreSpec.from_lines(all_exclude_patterns) + return None @property def artifact_spec(self) -> pathspec.GitIgnoreSpec | None: - if self.__artifact_patterns is None: - if 'artifacts' in self.target_config: - artifact_config = self.target_config - artifact_location = f'tool.hatch.build.targets.{self.plugin_name}.artifacts' - else: - artifact_config = self.build_config - artifact_location = 'tool.hatch.build.artifacts' - - all_artifact_patterns = [] - - artifact_patterns = artifact_config.get('artifacts', []) - if not isinstance(artifact_patterns, list): - message = f'Field `{artifact_location}` must be an array of strings' + if 'artifacts' in self.target_config: + artifact_config = self.target_config + artifact_location = f'tool.hatch.build.targets.{self.plugin_name}.artifacts' + else: + artifact_config = self.build_config + artifact_location = 'tool.hatch.build.artifacts' + + all_artifact_patterns = [] + + artifact_patterns = artifact_config.get('artifacts', []) + if not isinstance(artifact_patterns, list): + message = f'Field `{artifact_location}` must be an array of strings' + raise TypeError(message) + + for i, artifact_pattern in enumerate(artifact_patterns, 1): + if not isinstance(artifact_pattern, str): + message = f'Pattern #{i} in field `{artifact_location}` must be a string' raise TypeError(message) - for i, artifact_pattern in enumerate(artifact_patterns, 1): - if not isinstance(artifact_pattern, str): - message = f'Pattern #{i} in field `{artifact_location}` must be a string' - raise TypeError(message) - - if not artifact_pattern: - message = f'Pattern #{i} in field `{artifact_location}` cannot be an empty string' - raise ValueError(message) - - all_artifact_patterns.append(artifact_pattern) + if not artifact_pattern: + message = f'Pattern #{i} in field `{artifact_location}` cannot be an empty string' + raise ValueError(message) - if all_artifact_patterns: - self.__artifact_spec = pathspec.GitIgnoreSpec.from_lines(all_artifact_patterns) + all_artifact_patterns.append(artifact_pattern) - self.__artifact_patterns = all_artifact_patterns + if all_artifact_patterns: + return pathspec.GitIgnoreSpec.from_lines(all_artifact_patterns) + return None - return self.__artifact_spec - - @property + @cached_property def hook_config(self) -> dict[str, Any]: - if self.__hook_config is None: - hook_config: dict[str, dict[str, Any]] = {} + hook_config: dict[str, dict[str, Any]] = {} - global_hook_config = self.build_config.get('hooks', {}) - if not isinstance(global_hook_config, dict): - message = 'Field `tool.hatch.build.hooks` must be a table' + global_hook_config = self.build_config.get('hooks', {}) + if not isinstance(global_hook_config, dict): + message = 'Field `tool.hatch.build.hooks` must be a table' + raise TypeError(message) + + for hook_name, config in global_hook_config.items(): + if not isinstance(config, dict): + message = f'Field `tool.hatch.build.hooks.{hook_name}` must be a table' raise TypeError(message) - for hook_name, config in global_hook_config.items(): - if not isinstance(config, dict): - message = f'Field `tool.hatch.build.hooks.{hook_name}` must be a table' - raise TypeError(message) + hook_config.setdefault(hook_name, config) - hook_config.setdefault(hook_name, config) + target_hook_config = self.target_config.get('hooks', {}) + if not isinstance(target_hook_config, dict): + message = f'Field `tool.hatch.build.targets.{self.plugin_name}.hooks` must be a table' + raise TypeError(message) - target_hook_config = self.target_config.get('hooks', {}) - if not isinstance(target_hook_config, dict): - message = f'Field `tool.hatch.build.targets.{self.plugin_name}.hooks` must be a table' + for hook_name, config in target_hook_config.items(): + if not isinstance(config, dict): + message = f'Field `tool.hatch.build.targets.{self.plugin_name}.hooks.{hook_name}` must be a table' raise TypeError(message) - for hook_name, config in target_hook_config.items(): - if not isinstance(config, dict): - message = f'Field `tool.hatch.build.targets.{self.plugin_name}.hooks.{hook_name}` must be a table' - raise TypeError(message) - - hook_config[hook_name] = config - - if not env_var_enabled(BuildEnvVars.NO_HOOKS): - all_hooks_enabled = env_var_enabled(BuildEnvVars.HOOKS_ENABLE) - final_hook_config = { - hook_name: config - for hook_name, config in hook_config.items() - if ( - all_hooks_enabled - or config.get('enable-by-default', True) - or env_var_enabled(f'{BuildEnvVars.HOOK_ENABLE_PREFIX}{hook_name.upper()}') - ) - } - else: - final_hook_config = {} - - self.__hook_config = final_hook_config + hook_config[hook_name] = config + + if not env_var_enabled(BuildEnvVars.NO_HOOKS): + all_hooks_enabled = env_var_enabled(BuildEnvVars.HOOKS_ENABLE) + final_hook_config = { + hook_name: config + for hook_name, config in hook_config.items() + if ( + all_hooks_enabled + or config.get('enable-by-default', True) + or env_var_enabled(f'{BuildEnvVars.HOOK_ENABLE_PREFIX}{hook_name.upper()}') + ) + } + else: + final_hook_config = {} - return self.__hook_config + return final_hook_config - @property + @cached_property def directory(self) -> str: - if self.__directory is None: - if 'directory' in self.target_config: - directory = self.target_config['directory'] - if not isinstance(directory, str): - message = f'Field `tool.hatch.build.targets.{self.plugin_name}.directory` must be a string' - raise TypeError(message) - else: - directory = self.build_config.get('directory', DEFAULT_BUILD_DIRECTORY) - if not isinstance(directory, str): - message = 'Field `tool.hatch.build.directory` must be a string' - raise TypeError(message) - - self.__directory = self.normalize_build_directory(directory) + if 'directory' in self.target_config: + directory = self.target_config['directory'] + if not isinstance(directory, str): + message = f'Field `tool.hatch.build.targets.{self.plugin_name}.directory` must be a string' + raise TypeError(message) + else: + directory = self.build_config.get('directory', DEFAULT_BUILD_DIRECTORY) + if not isinstance(directory, str): + message = 'Field `tool.hatch.build.directory` must be a string' + raise TypeError(message) - return self.__directory + return self.normalize_build_directory(directory) - @property + @cached_property def skip_excluded_dirs(self) -> bool: - if self.__skip_excluded_dirs is None: - if 'skip-excluded-dirs' in self.target_config: - skip_excluded_dirs = self.target_config['skip-excluded-dirs'] - if not isinstance(skip_excluded_dirs, bool): - message = ( - f'Field `tool.hatch.build.targets.{self.plugin_name}.skip-excluded-dirs` must be a boolean' - ) - raise TypeError(message) - else: - skip_excluded_dirs = self.build_config.get('skip-excluded-dirs', False) - if not isinstance(skip_excluded_dirs, bool): - message = 'Field `tool.hatch.build.skip-excluded-dirs` must be a boolean' - raise TypeError(message) - - self.__skip_excluded_dirs = skip_excluded_dirs + if 'skip-excluded-dirs' in self.target_config: + skip_excluded_dirs = self.target_config['skip-excluded-dirs'] + if not isinstance(skip_excluded_dirs, bool): + message = f'Field `tool.hatch.build.targets.{self.plugin_name}.skip-excluded-dirs` must be a boolean' + raise TypeError(message) + else: + skip_excluded_dirs = self.build_config.get('skip-excluded-dirs', False) + if not isinstance(skip_excluded_dirs, bool): + message = 'Field `tool.hatch.build.skip-excluded-dirs` must be a boolean' + raise TypeError(message) - return self.__skip_excluded_dirs + return skip_excluded_dirs - @property + @cached_property def ignore_vcs(self) -> bool: - if self.__ignore_vcs is None: - if 'ignore-vcs' in self.target_config: - ignore_vcs = self.target_config['ignore-vcs'] - if not isinstance(ignore_vcs, bool): - message = f'Field `tool.hatch.build.targets.{self.plugin_name}.ignore-vcs` must be a boolean' - raise TypeError(message) - else: - ignore_vcs = self.build_config.get('ignore-vcs', False) - if not isinstance(ignore_vcs, bool): - message = 'Field `tool.hatch.build.ignore-vcs` must be a boolean' - raise TypeError(message) - - self.__ignore_vcs = ignore_vcs + if 'ignore-vcs' in self.target_config: + ignore_vcs = self.target_config['ignore-vcs'] + if not isinstance(ignore_vcs, bool): + message = f'Field `tool.hatch.build.targets.{self.plugin_name}.ignore-vcs` must be a boolean' + raise TypeError(message) + else: + ignore_vcs = self.build_config.get('ignore-vcs', False) + if not isinstance(ignore_vcs, bool): + message = 'Field `tool.hatch.build.ignore-vcs` must be a boolean' + raise TypeError(message) - return self.__ignore_vcs + return ignore_vcs - @property + @cached_property def require_runtime_dependencies(self) -> bool: - if self.__require_runtime_dependencies is None: - if 'require-runtime-dependencies' in self.target_config: - require_runtime_dependencies = self.target_config['require-runtime-dependencies'] - if not isinstance(require_runtime_dependencies, bool): - message = ( - f'Field `tool.hatch.build.targets.{self.plugin_name}.require-runtime-dependencies` ' - f'must be a boolean' - ) - raise TypeError(message) - else: - require_runtime_dependencies = self.build_config.get('require-runtime-dependencies', False) - if not isinstance(require_runtime_dependencies, bool): - message = 'Field `tool.hatch.build.require-runtime-dependencies` must be a boolean' - raise TypeError(message) - - self.__require_runtime_dependencies = require_runtime_dependencies + if 'require-runtime-dependencies' in self.target_config: + require_runtime_dependencies = self.target_config['require-runtime-dependencies'] + if not isinstance(require_runtime_dependencies, bool): + message = ( + f'Field `tool.hatch.build.targets.{self.plugin_name}.require-runtime-dependencies` ' + f'must be a boolean' + ) + raise TypeError(message) + else: + require_runtime_dependencies = self.build_config.get('require-runtime-dependencies', False) + if not isinstance(require_runtime_dependencies, bool): + message = 'Field `tool.hatch.build.require-runtime-dependencies` must be a boolean' + raise TypeError(message) - return self.__require_runtime_dependencies + return require_runtime_dependencies - @property + @cached_property def require_runtime_features(self) -> list[str]: - if self.__require_runtime_features is None: - if 'require-runtime-features' in self.target_config: - features_config = self.target_config - features_location = f'tool.hatch.build.targets.{self.plugin_name}.require-runtime-features' - else: - features_config = self.build_config - features_location = 'tool.hatch.build.require-runtime-features' - - require_runtime_features = features_config.get('require-runtime-features', []) - if not isinstance(require_runtime_features, list): - message = f'Field `{features_location}` must be an array' + if 'require-runtime-features' in self.target_config: + features_config = self.target_config + features_location = f'tool.hatch.build.targets.{self.plugin_name}.require-runtime-features' + else: + features_config = self.build_config + features_location = 'tool.hatch.build.require-runtime-features' + + require_runtime_features = features_config.get('require-runtime-features', []) + if not isinstance(require_runtime_features, list): + message = f'Field `{features_location}` must be an array' + raise TypeError(message) + + all_features: dict[str, None] = {} + for i, raw_feature in enumerate(require_runtime_features, 1): + if not isinstance(raw_feature, str): + message = f'Feature #{i} of field `{features_location}` must be a string' raise TypeError(message) - all_features: dict[str, None] = {} - for i, raw_feature in enumerate(require_runtime_features, 1): - if not isinstance(raw_feature, str): - message = f'Feature #{i} of field `{features_location}` must be a string' - raise TypeError(message) - - if not raw_feature: - message = f'Feature #{i} of field `{features_location}` cannot be an empty string' - raise ValueError(message) - - feature = normalize_project_name(raw_feature) - if feature not in self.builder.metadata.core.optional_dependencies: - message = ( - f'Feature `{feature}` of field `{features_location}` is not defined in ' - f'field `project.optional-dependencies`' - ) - raise ValueError(message) + if not raw_feature: + message = f'Feature #{i} of field `{features_location}` cannot be an empty string' + raise ValueError(message) - all_features[feature] = None + feature = normalize_project_name(raw_feature) + if feature not in self.builder.metadata.core.optional_dependencies: + message = ( + f'Feature `{feature}` of field `{features_location}` is not defined in ' + f'field `project.optional-dependencies`' + ) + raise ValueError(message) - self.__require_runtime_features = list(all_features) + all_features[feature] = None - return self.__require_runtime_features + return list(all_features) - @property + @cached_property def only_packages(self) -> bool: """ Whether or not the target should ignore non-artifact files that do not reside within a Python package. """ - if self.__only_packages is None: - if 'only-packages' in self.target_config: - only_packages = self.target_config['only-packages'] - if not isinstance(only_packages, bool): - message = f'Field `tool.hatch.build.targets.{self.plugin_name}.only-packages` must be a boolean' - raise TypeError(message) - else: - only_packages = self.build_config.get('only-packages', False) - if not isinstance(only_packages, bool): - message = 'Field `tool.hatch.build.only-packages` must be a boolean' - raise TypeError(message) - - self.__only_packages = only_packages + if 'only-packages' in self.target_config: + only_packages = self.target_config['only-packages'] + if not isinstance(only_packages, bool): + message = f'Field `tool.hatch.build.targets.{self.plugin_name}.only-packages` must be a boolean' + raise TypeError(message) + else: + only_packages = self.build_config.get('only-packages', False) + if not isinstance(only_packages, bool): + message = 'Field `tool.hatch.build.only-packages` must be a boolean' + raise TypeError(message) - return self.__only_packages + return only_packages - @property + @cached_property def reproducible(self) -> bool: """ Whether or not the target should be built in a reproducible manner, defaulting to true. """ - if self.__reproducible is None: - if 'reproducible' in self.target_config: - reproducible = self.target_config['reproducible'] - if not isinstance(reproducible, bool): - message = f'Field `tool.hatch.build.targets.{self.plugin_name}.reproducible` must be a boolean' - raise TypeError(message) - else: - reproducible = self.build_config.get('reproducible', True) - if not isinstance(reproducible, bool): - message = 'Field `tool.hatch.build.reproducible` must be a boolean' - raise TypeError(message) - - self.__reproducible = reproducible + if 'reproducible' in self.target_config: + reproducible = self.target_config['reproducible'] + if not isinstance(reproducible, bool): + message = f'Field `tool.hatch.build.targets.{self.plugin_name}.reproducible` must be a boolean' + raise TypeError(message) + else: + reproducible = self.build_config.get('reproducible', True) + if not isinstance(reproducible, bool): + message = 'Field `tool.hatch.build.reproducible` must be a boolean' + raise TypeError(message) - return self.__reproducible + return reproducible - @property + @cached_property def dev_mode_dirs(self) -> list[str]: """ Directories which must be added to Python's search path in [dev mode](../config/environment/overview.md#dev-mode). """ - if self.__dev_mode_dirs is None: - if 'dev-mode-dirs' in self.target_config: - dev_mode_dirs_config = self.target_config - dev_mode_dirs_location = f'tool.hatch.build.targets.{self.plugin_name}.dev-mode-dirs' - else: - dev_mode_dirs_config = self.build_config - dev_mode_dirs_location = 'tool.hatch.build.dev-mode-dirs' - - all_dev_mode_dirs = [] - - dev_mode_dirs = dev_mode_dirs_config.get('dev-mode-dirs', []) - if not isinstance(dev_mode_dirs, list): - message = f'Field `{dev_mode_dirs_location}` must be an array of strings' + if 'dev-mode-dirs' in self.target_config: + dev_mode_dirs_config = self.target_config + dev_mode_dirs_location = f'tool.hatch.build.targets.{self.plugin_name}.dev-mode-dirs' + else: + dev_mode_dirs_config = self.build_config + dev_mode_dirs_location = 'tool.hatch.build.dev-mode-dirs' + + all_dev_mode_dirs = [] + + dev_mode_dirs = dev_mode_dirs_config.get('dev-mode-dirs', []) + if not isinstance(dev_mode_dirs, list): + message = f'Field `{dev_mode_dirs_location}` must be an array of strings' + raise TypeError(message) + + for i, dev_mode_dir in enumerate(dev_mode_dirs, 1): + if not isinstance(dev_mode_dir, str): + message = f'Directory #{i} in field `{dev_mode_dirs_location}` must be a string' raise TypeError(message) - for i, dev_mode_dir in enumerate(dev_mode_dirs, 1): - if not isinstance(dev_mode_dir, str): - message = f'Directory #{i} in field `{dev_mode_dirs_location}` must be a string' - raise TypeError(message) + if not dev_mode_dir: + message = f'Directory #{i} in field `{dev_mode_dirs_location}` cannot be an empty string' + raise ValueError(message) - if not dev_mode_dir: - message = f'Directory #{i} in field `{dev_mode_dirs_location}` cannot be an empty string' - raise ValueError(message) + all_dev_mode_dirs.append(dev_mode_dir) - all_dev_mode_dirs.append(dev_mode_dir) + return all_dev_mode_dirs - self.__dev_mode_dirs = all_dev_mode_dirs + @cached_property + def dev_mode_exact(self) -> bool: + if 'dev-mode-exact' in self.target_config: + dev_mode_exact = self.target_config['dev-mode-exact'] + if not isinstance(dev_mode_exact, bool): + message = f'Field `tool.hatch.build.targets.{self.plugin_name}.dev-mode-exact` must be a boolean' + raise TypeError(message) + else: + dev_mode_exact = self.build_config.get('dev-mode-exact', False) + if not isinstance(dev_mode_exact, bool): + message = 'Field `tool.hatch.build.dev-mode-exact` must be a boolean' + raise TypeError(message) - return self.__dev_mode_dirs + return dev_mode_exact - @property - def dev_mode_exact(self) -> bool: - if self.__dev_mode_exact is None: - if 'dev-mode-exact' in self.target_config: - dev_mode_exact = self.target_config['dev-mode-exact'] - if not isinstance(dev_mode_exact, bool): - message = f'Field `tool.hatch.build.targets.{self.plugin_name}.dev-mode-exact` must be a boolean' - raise TypeError(message) - else: - dev_mode_exact = self.build_config.get('dev-mode-exact', False) - if not isinstance(dev_mode_exact, bool): - message = 'Field `tool.hatch.build.dev-mode-exact` must be a boolean' - raise TypeError(message) + @cached_property + def versions(self) -> list[str]: + # Used as an ordered set + all_versions: dict[str, None] = {} + + versions = self.target_config.get('versions', []) + if not isinstance(versions, list): + message = f'Field `tool.hatch.build.targets.{self.plugin_name}.versions` must be an array of strings' + raise TypeError(message) + + for i, version in enumerate(versions, 1): + if not isinstance(version, str): + message = ( + f'Version #{i} in field `tool.hatch.build.targets.{self.plugin_name}.versions` must be a string' + ) + raise TypeError(message) - self.__dev_mode_exact = dev_mode_exact + if not version: + message = ( + f'Version #{i} in field `tool.hatch.build.targets.{self.plugin_name}.versions` ' + f'cannot be an empty string' + ) + raise ValueError(message) - return self.__dev_mode_exact + all_versions[version] = None - @property - def versions(self) -> list[str]: - if self.__versions is None: - # Used as an ordered set - all_versions: dict[str, None] = {} + if not all_versions: + default_versions = self.__builder.get_default_versions() + for version in default_versions: + all_versions[version] = None + else: + unknown_versions = set(all_versions) - set(self.__builder.get_version_api()) + if unknown_versions: + message = ( + f'Unknown versions in field `tool.hatch.build.targets.{self.plugin_name}.versions`: ' + f'{", ".join(map(str, sorted(unknown_versions)))}' + ) + raise ValueError(message) + + return list(all_versions) + + @cached_property + def dependencies(self) -> list[str]: + # Used as an ordered set + dependencies: dict[str, None] = {} + + target_dependencies = self.target_config.get('dependencies', []) + if not isinstance(target_dependencies, list): + message = f'Field `tool.hatch.build.targets.{self.plugin_name}.dependencies` must be an array' + raise TypeError(message) + + for i, dependency in enumerate(target_dependencies, 1): + if not isinstance(dependency, str): + message = ( + f'Dependency #{i} of field `tool.hatch.build.targets.{self.plugin_name}.dependencies` ' + f'must be a string' + ) + raise TypeError(message) + + dependencies[dependency] = None + + global_dependencies = self.build_config.get('dependencies', []) + if not isinstance(global_dependencies, list): + message = 'Field `tool.hatch.build.dependencies` must be an array' + raise TypeError(message) + + for i, dependency in enumerate(global_dependencies, 1): + if not isinstance(dependency, str): + message = f'Dependency #{i} of field `tool.hatch.build.dependencies` must be a string' + raise TypeError(message) + + dependencies[dependency] = None + + require_runtime_dependencies = self.require_runtime_dependencies + require_runtime_features = dict.fromkeys(self.require_runtime_features) + for hook_name, config in self.hook_config.items(): + hook_require_runtime_dependencies = config.get('require-runtime-dependencies', False) + if not isinstance(hook_require_runtime_dependencies, bool): + message = f'Option `require-runtime-dependencies` of build hook `{hook_name}` must be a boolean' + raise TypeError(message) + + if hook_require_runtime_dependencies: + require_runtime_dependencies = True - versions = self.target_config.get('versions', []) - if not isinstance(versions, list): - message = f'Field `tool.hatch.build.targets.{self.plugin_name}.versions` must be an array of strings' + hook_require_runtime_features = config.get('require-runtime-features', []) + if not isinstance(hook_require_runtime_features, list): + message = f'Option `require-runtime-features` of build hook `{hook_name}` must be an array' raise TypeError(message) - for i, version in enumerate(versions, 1): - if not isinstance(version, str): + for i, raw_feature in enumerate(hook_require_runtime_features, 1): + if not isinstance(raw_feature, str): message = ( - f'Version #{i} in field `tool.hatch.build.targets.{self.plugin_name}.versions` must be a string' + f'Feature #{i} of option `require-runtime-features` of build hook `{hook_name}` ' + f'must be a string' ) raise TypeError(message) - if not version: + if not raw_feature: message = ( - f'Version #{i} in field `tool.hatch.build.targets.{self.plugin_name}.versions` ' + f'Feature #{i} of option `require-runtime-features` of build hook `{hook_name}` ' f'cannot be an empty string' ) raise ValueError(message) - all_versions[version] = None - - if not all_versions: - default_versions = self.__builder.get_default_versions() - for version in default_versions: - all_versions[version] = None - else: - unknown_versions = set(all_versions) - set(self.__builder.get_version_api()) - if unknown_versions: + feature = normalize_project_name(raw_feature) + if feature not in self.builder.metadata.core.optional_dependencies: message = ( - f'Unknown versions in field `tool.hatch.build.targets.{self.plugin_name}.versions`: ' - f'{", ".join(map(str, sorted(unknown_versions)))}' + f'Feature `{feature}` of option `require-runtime-features` of build hook `{hook_name}` ' + f'is not defined in field `project.optional-dependencies`' ) raise ValueError(message) - self.__versions = list(all_versions) - - return self.__versions - - @property - def dependencies(self) -> list[str]: - if self.__dependencies is None: - # Used as an ordered set - dependencies: dict[str, None] = {} + require_runtime_features[feature] = None - target_dependencies = self.target_config.get('dependencies', []) - if not isinstance(target_dependencies, list): - message = f'Field `tool.hatch.build.targets.{self.plugin_name}.dependencies` must be an array' + hook_dependencies = config.get('dependencies', []) + if not isinstance(hook_dependencies, list): + message = f'Option `dependencies` of build hook `{hook_name}` must be an array' raise TypeError(message) - for i, dependency in enumerate(target_dependencies, 1): + for i, dependency in enumerate(hook_dependencies, 1): if not isinstance(dependency, str): - message = ( - f'Dependency #{i} of field `tool.hatch.build.targets.{self.plugin_name}.dependencies` ' - f'must be a string' - ) + message = f'Dependency #{i} of option `dependencies` of build hook `{hook_name}` must be a string' raise TypeError(message) dependencies[dependency] = None - global_dependencies = self.build_config.get('dependencies', []) - if not isinstance(global_dependencies, list): - message = 'Field `tool.hatch.build.dependencies` must be an array' - raise TypeError(message) - - for i, dependency in enumerate(global_dependencies, 1): - if not isinstance(dependency, str): - message = f'Dependency #{i} of field `tool.hatch.build.dependencies` must be a string' - raise TypeError(message) - + if require_runtime_dependencies: + for dependency in self.builder.metadata.core.dependencies: dependencies[dependency] = None - require_runtime_dependencies = self.require_runtime_dependencies - require_runtime_features = dict.fromkeys(self.require_runtime_features) - for hook_name, config in self.hook_config.items(): - hook_require_runtime_dependencies = config.get('require-runtime-dependencies', False) - if not isinstance(hook_require_runtime_dependencies, bool): - message = f'Option `require-runtime-dependencies` of build hook `{hook_name}` must be a boolean' - raise TypeError(message) - - if hook_require_runtime_dependencies: - require_runtime_dependencies = True - - hook_require_runtime_features = config.get('require-runtime-features', []) - if not isinstance(hook_require_runtime_features, list): - message = f'Option `require-runtime-features` of build hook `{hook_name}` must be an array' - raise TypeError(message) - - for i, raw_feature in enumerate(hook_require_runtime_features, 1): - if not isinstance(raw_feature, str): - message = ( - f'Feature #{i} of option `require-runtime-features` of build hook `{hook_name}` ' - f'must be a string' - ) - raise TypeError(message) - - if not raw_feature: - message = ( - f'Feature #{i} of option `require-runtime-features` of build hook `{hook_name}` ' - f'cannot be an empty string' - ) - raise ValueError(message) - - feature = normalize_project_name(raw_feature) - if feature not in self.builder.metadata.core.optional_dependencies: - message = ( - f'Feature `{feature}` of option `require-runtime-features` of build hook `{hook_name}` ' - f'is not defined in field `project.optional-dependencies`' - ) - raise ValueError(message) - - require_runtime_features[feature] = None - - hook_dependencies = config.get('dependencies', []) - if not isinstance(hook_dependencies, list): - message = f'Option `dependencies` of build hook `{hook_name}` must be an array' - raise TypeError(message) - - for i, dependency in enumerate(hook_dependencies, 1): - if not isinstance(dependency, str): - message = ( - f'Dependency #{i} of option `dependencies` of build hook `{hook_name}` must be a string' - ) - raise TypeError(message) - + if require_runtime_features: + for feature in require_runtime_features: + for dependency in self.builder.metadata.core.optional_dependencies[feature]: dependencies[dependency] = None - if require_runtime_dependencies: - for dependency in self.builder.metadata.core.dependencies: - dependencies[dependency] = None - - if require_runtime_features: - for feature in require_runtime_features: - for dependency in self.builder.metadata.core.optional_dependencies[feature]: - dependencies[dependency] = None + for dependency in self.dynamic_dependencies: + dependencies[dependency] = None - for dependency in self.dynamic_dependencies: - dependencies[dependency] = None - - self.__dependencies = list(dependencies) - - return self.__dependencies + return list(dependencies) @cached_property def dynamic_dependencies(self) -> list[str]: @@ -681,154 +600,140 @@ def dynamic_dependencies(self) -> list[str]: return dependencies - @property + @cached_property def sources(self) -> dict[str, str]: - if self.__sources is None: - if 'sources' in self.target_config: - sources_config = self.target_config - sources_location = f'tool.hatch.build.targets.{self.plugin_name}.sources' - else: - sources_config = self.build_config - sources_location = 'tool.hatch.build.sources' - - sources = {} - - raw_sources = sources_config.get('sources', []) - if isinstance(raw_sources, list): - for i, source in enumerate(raw_sources, 1): - if not isinstance(source, str): - message = f'Source #{i} in field `{sources_location}` must be a string' - raise TypeError(message) - - if not source: - message = f'Source #{i} in field `{sources_location}` cannot be an empty string' - raise ValueError(message) - - sources[normalize_relative_directory(source)] = '' - elif isinstance(raw_sources, dict): - for source, path in raw_sources.items(): - if not isinstance(path, str): - message = f'Path for source `{source}` in field `{sources_location}` must be a string' - raise TypeError(message) - - normalized_path = normalize_relative_path(path) - if normalized_path == '.': - normalized_path = '' - else: - normalized_path += os.sep - - sources[normalize_relative_directory(source) if source else source] = normalized_path - else: - message = f'Field `{sources_location}` must be a mapping or array of strings' - raise TypeError(message) - - for relative_path in self.packages: - source, _package = os.path.split(relative_path) - if source and normalize_relative_directory(relative_path) not in sources: - sources[normalize_relative_directory(source)] = '' - - self.__sources = dict(sorted(sources.items())) - - return self.__sources - - @property - def packages(self) -> list[str]: - if self.__packages is None: - if 'packages' in self.target_config: - package_config = self.target_config - package_location = f'tool.hatch.build.targets.{self.plugin_name}.packages' - else: - package_config = self.build_config - package_location = 'tool.hatch.build.packages' - - packages = package_config.get('packages', self.default_packages()) - if not isinstance(packages, list): - message = f'Field `{package_location}` must be an array of strings' - raise TypeError(message) - - for i, package in enumerate(packages, 1): - if not isinstance(package, str): - message = f'Package #{i} in field `{package_location}` must be a string' + if 'sources' in self.target_config: + sources_config = self.target_config + sources_location = f'tool.hatch.build.targets.{self.plugin_name}.sources' + else: + sources_config = self.build_config + sources_location = 'tool.hatch.build.sources' + + sources = {} + + raw_sources = sources_config.get('sources', []) + if isinstance(raw_sources, list): + for i, source in enumerate(raw_sources, 1): + if not isinstance(source, str): + message = f'Source #{i} in field `{sources_location}` must be a string' raise TypeError(message) - if not package: - message = f'Package #{i} in field `{package_location}` cannot be an empty string' + if not source: + message = f'Source #{i} in field `{sources_location}` cannot be an empty string' raise ValueError(message) - self.__packages = sorted(normalize_relative_path(package) for package in packages) + sources[normalize_relative_directory(source)] = '' + elif isinstance(raw_sources, dict): + for source, path in raw_sources.items(): + if not isinstance(path, str): + message = f'Path for source `{source}` in field `{sources_location}` must be a string' + raise TypeError(message) - return self.__packages + normalized_path = normalize_relative_path(path) + if normalized_path == '.': + normalized_path = '' + else: + normalized_path += os.sep - @property - def force_include(self) -> dict[str, str]: - if self.__force_include is None: - if 'force-include' in self.target_config: - force_include_config = self.target_config - force_include_location = f'tool.hatch.build.targets.{self.plugin_name}.force-include' - else: - force_include_config = self.build_config - force_include_location = 'tool.hatch.build.force-include' - - force_include = force_include_config.get('force-include', {}) - if not isinstance(force_include, dict): - message = f'Field `{force_include_location}` must be a mapping' - raise TypeError(message) + sources[normalize_relative_directory(source) if source else source] = normalized_path + else: + message = f'Field `{sources_location}` must be a mapping or array of strings' + raise TypeError(message) - for i, (source, relative_path) in enumerate(force_include.items(), 1): - if not source: - message = f'Source #{i} in field `{force_include_location}` cannot be an empty string' - raise ValueError(message) + for relative_path in self.packages: + source, _package = os.path.split(relative_path) + if source and normalize_relative_directory(relative_path) not in sources: + sources[normalize_relative_directory(source)] = '' - if not isinstance(relative_path, str): - message = f'Path for source `{source}` in field `{force_include_location}` must be a string' - raise TypeError(message) + return dict(sorted(sources.items())) - if not relative_path: - message = ( - f'Path for source `{source}` in field `{force_include_location}` cannot be an empty string' - ) - raise ValueError(message) + @cached_property + def packages(self) -> list[str]: + if 'packages' in self.target_config: + package_config = self.target_config + package_location = f'tool.hatch.build.targets.{self.plugin_name}.packages' + else: + package_config = self.build_config + package_location = 'tool.hatch.build.packages' + + packages = package_config.get('packages', self.default_packages()) + if not isinstance(packages, list): + message = f'Field `{package_location}` must be an array of strings' + raise TypeError(message) + + for i, package in enumerate(packages, 1): + if not isinstance(package, str): + message = f'Package #{i} in field `{package_location}` must be a string' + raise TypeError(message) - self.__force_include = normalize_inclusion_map(force_include, self.root) + if not package: + message = f'Package #{i} in field `{package_location}` cannot be an empty string' + raise ValueError(message) - return self.__force_include + return sorted(normalize_relative_path(package) for package in packages) - @property - def only_include(self) -> dict[str, str]: - if self.__only_include is None: - if 'only-include' in self.target_config: - only_include_config = self.target_config - only_include_location = f'tool.hatch.build.targets.{self.plugin_name}.only-include' - else: - only_include_config = self.build_config - only_include_location = 'tool.hatch.build.only-include' - - only_include = only_include_config.get('only-include', self.default_only_include()) or self.packages - if not isinstance(only_include, list): - message = f'Field `{only_include_location}` must be an array' + @cached_property + def force_include(self) -> dict[str, str]: + if 'force-include' in self.target_config: + force_include_config = self.target_config + force_include_location = f'tool.hatch.build.targets.{self.plugin_name}.force-include' + else: + force_include_config = self.build_config + force_include_location = 'tool.hatch.build.force-include' + + force_include = force_include_config.get('force-include', {}) + if not isinstance(force_include, dict): + message = f'Field `{force_include_location}` must be a mapping' + raise TypeError(message) + + for i, (source, relative_path) in enumerate(force_include.items(), 1): + if not source: + message = f'Source #{i} in field `{force_include_location}` cannot be an empty string' + raise ValueError(message) + + if not isinstance(relative_path, str): + message = f'Path for source `{source}` in field `{force_include_location}` must be a string' raise TypeError(message) - inclusion_map = {} + if not relative_path: + message = f'Path for source `{source}` in field `{force_include_location}` cannot be an empty string' + raise ValueError(message) - for i, relative_path in enumerate(only_include, 1): - if not isinstance(relative_path, str): - message = f'Path #{i} in field `{only_include_location}` must be a string' - raise TypeError(message) + return normalize_inclusion_map(force_include, self.root) - normalized_path = normalize_relative_path(relative_path) - if not normalized_path or normalized_path.startswith(('~', '..')): - message = f'Path #{i} in field `{only_include_location}` must be relative: {relative_path}' - raise ValueError(message) + @cached_property + def only_include(self) -> dict[str, str]: + if 'only-include' in self.target_config: + only_include_config = self.target_config + only_include_location = f'tool.hatch.build.targets.{self.plugin_name}.only-include' + else: + only_include_config = self.build_config + only_include_location = 'tool.hatch.build.only-include' + + only_include = only_include_config.get('only-include', self.default_only_include()) or self.packages + if not isinstance(only_include, list): + message = f'Field `{only_include_location}` must be an array' + raise TypeError(message) + + inclusion_map = {} + + for i, relative_path in enumerate(only_include, 1): + if not isinstance(relative_path, str): + message = f'Path #{i} in field `{only_include_location}` must be a string' + raise TypeError(message) - if normalized_path in inclusion_map: - message = f'Duplicate path in field `{only_include_location}`: {normalized_path}' - raise ValueError(message) + normalized_path = normalize_relative_path(relative_path) + if not normalized_path or normalized_path.startswith(('~', '..')): + message = f'Path #{i} in field `{only_include_location}` must be relative: {relative_path}' + raise ValueError(message) - inclusion_map[normalized_path] = normalized_path + if normalized_path in inclusion_map: + message = f'Duplicate path in field `{only_include_location}`: {normalized_path}' + raise ValueError(message) - self.__only_include = normalize_inclusion_map(inclusion_map, self.root) + inclusion_map[normalized_path] = normalized_path - return self.__only_include + return normalize_inclusion_map(inclusion_map, self.root) def get_distribution_path(self, relative_path: str) -> str: # src/foo/bar.py -> foo/bar.py @@ -841,22 +746,19 @@ def get_distribution_path(self, relative_path: str) -> str: return relative_path - @property + @cached_property def vcs_exclusion_files(self) -> dict[str, list[str]]: - if self.__vcs_exclusion_files is None: - exclusion_files: dict[str, list[str]] = {'git': [], 'hg': []} - - local_gitignore = locate_file(self.root, '.gitignore', boundary='.git') - if local_gitignore is not None: - exclusion_files['git'].append(local_gitignore) + exclusion_files: dict[str, list[str]] = {'git': [], 'hg': []} - local_hgignore = locate_file(self.root, '.hgignore', boundary='.hg') - if local_hgignore is not None: - exclusion_files['hg'].append(local_hgignore) + local_gitignore = locate_file(self.root, '.gitignore', boundary='.git') + if local_gitignore is not None: + exclusion_files['git'].append(local_gitignore) - self.__vcs_exclusion_files = exclusion_files + local_hgignore = locate_file(self.root, '.hgignore', boundary='.hg') + if local_hgignore is not None: + exclusion_files['hg'].append(local_hgignore) - return self.__vcs_exclusion_files + return exclusion_files def load_vcs_exclusion_patterns(self) -> list[str]: patterns = [] From e1ade7505c00564c1f06fd11a51123f4711ffee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Sun, 10 Nov 2024 02:47:54 +0000 Subject: [PATCH 11/13] Support disabling trove classifier verification (#1620) Co-authored-by: Ofek Lev --- backend/src/hatchling/metadata/core.py | 31 ++++++++++++++++++++------ tests/backend/metadata/test_core.py | 27 +++++++++++++++++++++- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/backend/src/hatchling/metadata/core.py b/backend/src/hatchling/metadata/core.py index 32e6a2f70..23bba1905 100644 --- a/backend/src/hatchling/metadata/core.py +++ b/backend/src/hatchling/metadata/core.py @@ -947,8 +947,6 @@ def classifiers(self) -> list[str]: if self._classifiers is None: import bisect - import trove_classifiers - if 'classifiers' in self.config: classifiers = self.config['classifiers'] if 'classifiers' in self.dynamic: @@ -964,7 +962,16 @@ def classifiers(self) -> list[str]: message = 'Field `project.classifiers` must be an array' raise TypeError(message) - known_classifiers = trove_classifiers.classifiers | self._extra_classifiers + verify_classifiers = not os.environ.get('HATCH_METADATA_CLASSIFIERS_NO_VERIFY') + if verify_classifiers: + import trove_classifiers + + known_classifiers = trove_classifiers.classifiers | self._extra_classifiers + sorted_classifiers = list(trove_classifiers.sorted_classifiers) + + for classifier in sorted(self._extra_classifiers - trove_classifiers.classifiers): + bisect.insort(sorted_classifiers, classifier) + unique_classifiers = set() for i, classifier in enumerate(classifiers, 1): @@ -972,15 +979,25 @@ def classifiers(self) -> list[str]: message = f'Classifier #{i} of field `project.classifiers` must be a string' raise TypeError(message) - if not self.__classifier_is_private(classifier) and classifier not in known_classifiers: + if ( + not self.__classifier_is_private(classifier) + and verify_classifiers + and classifier not in known_classifiers + ): message = f'Unknown classifier in field `project.classifiers`: {classifier}' raise ValueError(message) unique_classifiers.add(classifier) - sorted_classifiers = list(trove_classifiers.sorted_classifiers) - for classifier in sorted(self._extra_classifiers - trove_classifiers.classifiers): - bisect.insort(sorted_classifiers, classifier) + if not verify_classifiers: + import re + + # combined text-numeric sort that ensures that Python versions sort correctly + split_re = re.compile(r'(\D*)(\d*)') + sorted_classifiers = sorted( + classifiers, + key=lambda value: ([(a, int(b) if b else None) for a, b in split_re.findall(value)]), + ) self._classifiers = sorted( unique_classifiers, key=lambda c: -1 if self.__classifier_is_private(c) else sorted_classifiers.index(c) diff --git a/tests/backend/metadata/test_core.py b/tests/backend/metadata/test_core.py index c1c19588d..2795c694d 100644 --- a/tests/backend/metadata/test_core.py +++ b/tests/backend/metadata/test_core.py @@ -898,12 +898,37 @@ def test_entry_not_string(self, isolation): with pytest.raises(TypeError, match='Classifier #1 of field `project.classifiers` must be a string'): _ = metadata.core.classifiers - def test_entry_unknown(self, isolation): + def test_entry_unknown(self, isolation, monkeypatch): + monkeypatch.delenv('HATCH_METADATA_CLASSIFIERS_NO_VERIFY', False) metadata = ProjectMetadata(str(isolation), None, {'project': {'classifiers': ['foo']}}) with pytest.raises(ValueError, match='Unknown classifier in field `project.classifiers`: foo'): _ = metadata.core.classifiers + def test_entry_unknown_no_verify(self, isolation, monkeypatch): + monkeypatch.setenv('HATCH_METADATA_CLASSIFIERS_NO_VERIFY', '1') + classifiers = [ + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.9', + 'Development Status :: 4 - Beta', + 'Private :: Do Not Upload', + 'Foo', + ] + metadata = ProjectMetadata(str(isolation), None, {'project': {'classifiers': classifiers}}) + + assert ( + metadata.core.classifiers + == metadata.core.classifiers + == [ + 'Private :: Do Not Upload', + 'Development Status :: 4 - Beta', + 'Foo', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.11', + ] + ) + def test_correct(self, isolation): classifiers = [ 'Programming Language :: Python :: 3.11', From 27cf52aeff88a81c82931724d025e40d642d6f1b Mon Sep 17 00:00:00 2001 From: James Hilliard Date: Sat, 9 Nov 2024 20:07:49 -0700 Subject: [PATCH 12/13] Prevent matching bogus parent gitignores (#1643) Co-authored-by: Ofek Lev --- backend/src/hatchling/builders/config.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/backend/src/hatchling/builders/config.py b/backend/src/hatchling/builders/config.py index 6457df9f5..059e67f2a 100644 --- a/backend/src/hatchling/builders/config.py +++ b/backend/src/hatchling/builders/config.py @@ -785,6 +785,11 @@ def load_vcs_exclusion_patterns(self) -> list[str]: if glob_mode: patterns.append(line) + # validate project root is not excluded by vcs + exclude_spec = pathspec.GitIgnoreSpec.from_lines(patterns) + if exclude_spec.match_file(self.root): + return patterns + return patterns def normalize_build_directory(self, build_directory: str) -> str: From 9ccc839eedb64f12fb28de841fabf8ebe269027b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 9 Nov 2024 22:24:01 -0500 Subject: [PATCH 13/13] Bump pypa/gh-action-pypi-publish from 1.9.0 to 1.11.0 (#1776) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Ofek Lev --- .github/workflows/build-hatch.yml | 2 +- .github/workflows/build-hatchling.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-hatch.yml b/.github/workflows/build-hatch.yml index d37a8ea43..ecd328094 100644 --- a/.github/workflows/build-hatch.yml +++ b/.github/workflows/build-hatch.yml @@ -89,7 +89,7 @@ jobs: path: dist - name: Push Python artifacts to PyPI - uses: pypa/gh-action-pypi-publish@v1.9.0 + uses: pypa/gh-action-pypi-publish@v1.11.0 with: skip-existing: true diff --git a/.github/workflows/build-hatchling.yml b/.github/workflows/build-hatchling.yml index 803d13ec9..af4908407 100644 --- a/.github/workflows/build-hatchling.yml +++ b/.github/workflows/build-hatchling.yml @@ -52,6 +52,6 @@ jobs: path: dist - name: Push build artifacts to PyPI - uses: pypa/gh-action-pypi-publish@v1.9.0 + uses: pypa/gh-action-pypi-publish@v1.11.0 with: skip-existing: true