diff --git a/backend/src/hatchling/metadata/core.py b/backend/src/hatchling/metadata/core.py index e4b47a8d7..5e65bb956 100644 --- a/backend/src/hatchling/metadata/core.py +++ b/backend/src/hatchling/metadata/core.py @@ -977,8 +977,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: @@ -994,7 +992,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_NO_VERIFY_TROVE_CLASSIFIERS") + 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): @@ -1002,15 +1009,19 @@ 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*)') + sort_key = lambda value: [(a, int(b) if b else None) for a, b in split_re.findall(value)] + sorted_classifiers = sorted(classifiers, key=sort_key) 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 76fd6601f..17de13766 100644 --- a/tests/backend/metadata/test_core.py +++ b/tests/backend/metadata/test_core.py @@ -950,12 +950,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_NO_VERIFY_TROVE_CLASSIFIERS", 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_NO_VERIFY_TROVE_CLASSIFIERS", "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',