From fadd8d12c97db94f8ae476d12a18dabf65c0db71 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Fri, 20 Sep 2024 21:23:24 +0200 Subject: [PATCH] Allow information on removed collections. --- changelogs/fragments/173-schema-removal.yml | 2 + src/antsibull_core/collection_meta.py | 75 ++++++++++++++++++- src/antsibull_core/schemas/collection_meta.py | 29 ++++++- tests/functional/test_collection_meta.py | 35 +++++++++ 4 files changed, 137 insertions(+), 4 deletions(-) create mode 100644 changelogs/fragments/173-schema-removal.yml diff --git a/changelogs/fragments/173-schema-removal.yml b/changelogs/fragments/173-schema-removal.yml new file mode 100644 index 0000000..e24eb43 --- /dev/null +++ b/changelogs/fragments/173-schema-removal.yml @@ -0,0 +1,2 @@ +minor_changes: + - "Allow information on removed collections in collection metadata schema (https://github.com/ansible-community/antsibull-core/pull/173)." diff --git a/src/antsibull_core/collection_meta.py b/src/antsibull_core/collection_meta.py index c04a5d6..5f6e0de 100644 --- a/src/antsibull_core/collection_meta.py +++ b/src/antsibull_core/collection_meta.py @@ -22,6 +22,8 @@ CollectionMetadata, CollectionsMetadata, RemovalInformation, + RemovedCollectionMetadata, + RemovedRemovalInformation, ) if t.TYPE_CHECKING: @@ -42,6 +44,12 @@ def __init__(self, *, all_collections: list[str], major_release: int): self.all_collections = all_collections self.major_release = major_release + def _validate_removal_base( + self, collection: str, removal: RemovalInformation, prefix: str + ) -> None: + if removal.reason == "renamed" and removal.new_name == collection: + self.errors.append(f"{prefix} new_name: Must not be the collection's name") + def _validate_removal( self, collection: str, removal: RemovalInformation, prefix: str ) -> None: @@ -63,8 +71,7 @@ def _validate_removal( f" must not be larger than the current major version {self.major_release}" ) - if removal.reason == "renamed" and removal.new_name == collection: - self.errors.append(f"{prefix} new_name: Must not be the collection's name") + self._validate_removal_base(collection, removal, prefix) def _validate_collection( self, collection: str, meta: CollectionMetadata, prefix: str @@ -75,7 +82,42 @@ def _validate_collection( if meta.removal: self._validate_removal(collection, meta.removal, f"{prefix} removal ->") - def validate(self, data: CollectionsMetadata) -> None: + def _validate_removal_for_removed( + self, collection: str, removal: RemovedRemovalInformation, prefix: str + ) -> None: + if removal.major_version != self.major_release: + self.errors.append( + f"{prefix} major_version: Removal major version {removal.major_version} must" + f" be current major version {self.major_release}" + ) + + if ( + removal.announce_version is not None + and removal.announce_version.major >= self.major_release + ): + self.errors.append( + f"{prefix} announce_version: Major version of {removal.announce_version}" + f" must be less than the current major version {self.major_release}" + ) + + self._validate_removal_base(collection, removal, prefix) + + def _validate_removed_collection( + self, collection: str, meta: RemovedCollectionMetadata, prefix: str + ) -> None: + if meta.repository is None: + self.errors.append(f"{prefix} repository: Required field not provided") + + self._validate_removal_for_removed( + collection, meta.removal, f"{prefix} removal ->" + ) + if meta.removed_version.major != self.major_release: + self.errors.append( + f"{prefix} removed_version: Major version of {meta.removed_version}" + f" must be the current major version {self.major_release}" + ) + + def _validate_collections(self, data: CollectionsMetadata) -> None: # Check order sorted_list = sorted(data.collections) raw_list = list(data.collections) @@ -105,6 +147,33 @@ def validate(self, data: CollectionsMetadata) -> None: for collection in sorted(remaining_collections): self.errors.append(f"collections: No metadata present for {collection}") + def _validate_removed_collections(self, data: CollectionsMetadata) -> None: + # Check order + sorted_list = sorted(data.removed_collections) + raw_list = list(data.removed_collections) + if raw_list != sorted_list: + for raw_entry, sorted_entry in zip(raw_list, sorted_list): + if raw_entry != sorted_entry: + self.errors.append( + "The removed collection list must be sorted; " + f"{sorted_entry!r} must come before {raw_entry}" + ) + break + + # Validate removed collection data + for collection, removed_meta in data.removed_collections.items(): + if collection in self.all_collections: + self.errors.append( + f"removed_collections -> {collection}: Collection in ansible.in" + ) + self._validate_removed_collection( + collection, removed_meta, f"removed_collections -> {collection} ->" + ) + + def validate(self, data: CollectionsMetadata) -> None: + self._validate_collections(data) + self._validate_removed_collections(data) + def lint_collection_meta( *, collection_meta_path: StrPath, major_release: int, all_collections: list[str] diff --git a/src/antsibull_core/schemas/collection_meta.py b/src/antsibull_core/schemas/collection_meta.py index f88aec8..bb1aad3 100644 --- a/src/antsibull_core/schemas/collection_meta.py +++ b/src/antsibull_core/schemas/collection_meta.py @@ -112,7 +112,15 @@ def _check_reason_is_not_renamed(self) -> Self: return self -class CollectionMetadata(p.BaseModel): +class RemovedRemovalInformation(RemovalInformation): + """ + Stores metadata on when and why a collection was removed. + """ + + major_version: int + + +class BaseCollectionMetadata(p.BaseModel): """ Stores metadata about one collection. """ @@ -124,15 +132,34 @@ class CollectionMetadata(p.BaseModel): repository: t.Optional[str] = None tag_version_regex: t.Optional[str] = None maintainers: list[str] = [] + + +class CollectionMetadata(BaseCollectionMetadata): + """ + Stores metadata about one collection. + """ + removal: t.Optional[RemovalInformation] = None +class RemovedCollectionMetadata(BaseCollectionMetadata): + """ + Stores metadata about a removed collection. + """ + + model_config = p.ConfigDict(arbitrary_types_allowed=True) + + removal: RemovedRemovalInformation + removed_version: PydanticPypiVersion + + class CollectionsMetadata(p.BaseModel): """ Stores metadata about a set of collections. """ collections: dict[str, CollectionMetadata] + removed_collections: dict[str, RemovedCollectionMetadata] = {} @staticmethod def load_from(deps_dir: StrPath | None) -> CollectionsMetadata: diff --git a/tests/functional/test_collection_meta.py b/tests/functional/test_collection_meta.py index faf4d69..d69f78a 100644 --- a/tests/functional/test_collection_meta.py +++ b/tests/functional/test_collection_meta.py @@ -137,6 +137,22 @@ reason_text: The collection wasn't cow friendly, so the Steering Committee decided to kick it out. discussion: https://forum.ansible.com/... announce_version: 9.3.0 +removed_collections: + bad.baz2: + repository: https://github.com/ansible-collections/collection_template + removed_version: 10.2.1 + removal: + major_version: 9 + reason: renamed + new_name: bad.bar2 + announce_version: 9.3.0 + redirect_replacement_major_version: 7 + bad.baz1: + removed_version: 9.1.0 + removal: + major_version: 8 + reason: deprecated + announce_version: 7.1.0 """, [ "foo.bar", @@ -153,6 +169,7 @@ ], [ "The collection list must be sorted; 'baz.bam' must come before foo.bar", + "The removed collection list must be sorted; 'bad.baz1' must come before bad.baz2", "collections -> bad.bar1 -> removal -> announce_version: Major version of 10.1.0 must not be larger than the current major version 9", "collections -> bad.bar1 -> removal -> major_version: Removal major version 7 must be larger than current major version 9", "collections -> bad.bar1: Collection not in ansible.in", @@ -162,6 +179,10 @@ "collections -> baz.bam: Collection not in ansible.in", "collections -> foo.bar -> repository: Required field not provided", "collections: No metadata present for not.there", + "removed_collections -> bad.baz1 -> removal -> major_version: Removal major version 8 must be current major version 9", + "removed_collections -> bad.baz1 -> repository: Required field not provided", + "removed_collections -> bad.baz2 -> removal -> announce_version: Major version of 9.3.0 must be less than the current major version 9", + "removed_collections -> bad.baz2 -> removed_version: Major version of 10.2.1 must be the current major version 9", ], ), ( @@ -271,6 +292,18 @@ removal: major_version: 11 reason: foo +removed_collections: + bad.foo1: + repository: https://github.com/ansible-collections/collection_template + removed_version: 1.0.0 + removal: + major_version: TBD + reason: deprecated + bad.foo2: + repository: https://github.com/ansible-collections/collection_template + removal: + major_version: 10 + reason: deprecated extra_stuff: baz """, [], @@ -292,6 +325,8 @@ "collections -> bad.foo8 -> removal: Value error, new_name must not be provided if reason is not 'renamed'", "collections -> bad.foo9 -> removal: Value error, redirect_replacement_major_version must not be provided if reason is not 'renamed'", "extra_stuff: Extra inputs are not permitted", + "removed_collections -> bad.foo1 -> removal -> major_version: Input should be a valid integer, unable to parse string as an integer", + "removed_collections -> bad.foo2 -> removed_version: Field required", ], ), ]