From 5a50cc1e7ac1d78c8af7c1a610c51ac5067ac19d Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Sun, 26 Apr 2026 14:44:44 +0200 Subject: [PATCH] Add better argspec typing. (#1011) --- .pylintrc | 4 +- plugins/module_utils/_argspec.py | 148 ++++++++++++++---- .../module_backends/certificate_acme.py | 2 +- .../module_backends/certificate_ownca.py | 2 +- .../module_backends/certificate_selfsigned.py | 2 +- 5 files changed, 119 insertions(+), 39 deletions(-) diff --git a/.pylintrc b/.pylintrc index 3e4bc42f..79d45e50 100644 --- a/.pylintrc +++ b/.pylintrc @@ -118,7 +118,7 @@ good-names=i, good-names-rgxs= # Include a hint for the correct naming format with invalid-name. -include-naming-hint=no +include-naming-hint=yes # Naming style matching correct inline iteration names. inlinevar-naming-style=any @@ -157,7 +157,7 @@ property-classes=abc.abstractproperty # Regular expression matching correct type alias names. If left empty, type # alias names will be checked with the set naming style. -#typealias-rgx= +typealias-rgx = ^_{0,2}(?!T[A-Z]|Type)[A-Z]+[a-z0-9]+(?:[A-Z][a-z0-9]+)*T?$ # Regular expression matching correct type variable names. If left empty, type # variable names will be checked with the set naming style. diff --git a/plugins/module_utils/_argspec.py b/plugins/module_utils/_argspec.py index ff059622..db64c1e2 100644 --- a/plugins/module_utils/_argspec.py +++ b/plugins/module_utils/_argspec.py @@ -11,38 +11,124 @@ import typing as t from ansible.module_utils.basic import AnsibleModule -_T = t.TypeVar("_T") +if t.TYPE_CHECKING: + import datetime # pragma: no cover + from collections.abc import Callable, Mapping, Sequence # pragma: no cover + _T = t.TypeVar("_T") # pragma: no cover -def _ensure_list(value: list[_T] | tuple[_T] | None) -> list[_T]: - if value is None: - return [] - return list(value) + ArgSpecType = t.Literal[ # pragma: no cover + "bits", + "bool", + "bytes", + "dict", + "float", + "int", + "json", + "jsonarg", + "list", + "path", + "raw", + "sid", + "str", + ] + MutuallyExclusiveT = t.Union[ # pragma: no cover # noqa: UP007 + Sequence[str], Sequence[Sequence[str]] + ] + MutuallyExclusiveMutT = list[Sequence[str]] # pragma: no cover + RequiredTogetherT = Sequence[Sequence[str]] # pragma: no cover + RequiredTogetherMutT = list[Sequence[str]] # pragma: no cover + RequiredOneOfT = Sequence[Sequence[str]] # pragma: no cover + RequiredOneOfMutT = list[Sequence[str]] # pragma: no cover + RequiredIfT = Sequence[ # pragma: no cover + t.Union[ # noqa: UP007 + list[object], + tuple[str, object, Sequence[str]], + tuple[str, object, Sequence[str], bool], + ] + ] + RequiredIfMutT = list[ # pragma: no cover + t.Union[ # noqa: UP007 + list[object], + tuple[str, object, Sequence[str]], + tuple[str, object, Sequence[str], bool], + ] + ] + RequiredByT = Mapping[str, Sequence[str]] # pragma: no cover + RequiredByMutT = dict[str, Sequence[str]] # pragma: no cover + + class DeprecatedAlias(t.TypedDict): # pragma: no cover + name: str + date: t.NotRequired[datetime.date | str] + version: t.NotRequired[str] + collection_name: str + + class OneArgumentSpecT(t.TypedDict): # pragma: no cover + type: t.NotRequired[ArgSpecType | Callable[[object], object]] + elements: t.NotRequired[ArgSpecType] + default: t.NotRequired[object] + # For fallback elements, the first element of the sequence has to be a callable, the others sequences or dicts. + # Unfortunately there is no way to specify this in a generic way... + fallback: t.NotRequired[ + Sequence[ + Callable[[object], object] | Sequence[object] | Mapping[str, object] + ] + ] + choices: t.NotRequired[Sequence[object]] + context: t.NotRequired[Mapping[object, object]] + required: t.NotRequired[bool] + no_log: t.NotRequired[bool] + aliases: t.NotRequired[Sequence[str]] + apply_defaults: t.NotRequired[bool] + removed_in_version: t.NotRequired[str] + removed_at_date: t.NotRequired[datetime.date | str] + removed_from_collection: t.NotRequired[str] + options: t.NotRequired[Mapping[str, OneArgumentSpecT]] # recursive! + deprecated_aliases: t.NotRequired[Sequence[DeprecatedAlias]] + + mutually_exclusive: t.NotRequired[MutuallyExclusiveT] + required_together: t.NotRequired[RequiredTogetherT] + required_one_of: t.NotRequired[RequiredOneOfT] + required_if: t.NotRequired[RequiredIfT] + required_by: t.NotRequired[RequiredByT] + + ArgumentSpecT = Mapping[str, OneArgumentSpecT] # pragma: no cover + ArgumentSpecMutT = dict[str, OneArgumentSpecT] # pragma: no cover class ArgumentSpec: def __init__( self, - argument_spec: dict[str, t.Any] | None = None, + argument_spec: ArgumentSpecT | None = None, *, - mutually_exclusive: list[list[str] | tuple[str, ...]] | None = None, - required_together: list[list[str] | tuple[str, ...]] | None = None, - required_one_of: list[list[str] | tuple[str, ...]] | None = None, - required_if: ( - list[ - tuple[str, t.Any, list[str] | tuple[str, ...]] - | tuple[str, t.Any, list[str] | tuple[str, ...], bool] - ] - | None - ) = None, - required_by: dict[str, tuple[str, ...] | list[str]] | None = None, + required_together: RequiredTogetherT | None = None, + required_if: RequiredIfT | None = None, + required_one_of: RequiredOneOfT | None = None, + mutually_exclusive: MutuallyExclusiveT | None = None, + required_by: RequiredByT | None = None, ) -> None: - self.argument_spec = argument_spec or {} - self.mutually_exclusive = _ensure_list(mutually_exclusive) - self.required_together = _ensure_list(required_together) - self.required_one_of = _ensure_list(required_one_of) - self.required_if = _ensure_list(required_if) - self.required_by = required_by or {} + self.argument_spec: ArgumentSpecMutT = {} + self.required_together: RequiredTogetherMutT = [] + self.required_if: RequiredIfMutT = [] + self.required_one_of: RequiredOneOfMutT = [] + self.mutually_exclusive: MutuallyExclusiveMutT = [] + self.required_by: RequiredByMutT = {} + if argument_spec: + self.argument_spec.update(argument_spec) + if required_together: + self.required_together.extend(required_together) + if required_if: + self.required_if.extend(required_if) + if required_one_of: + self.required_one_of.extend(required_one_of) + if mutually_exclusive: + if all(isinstance(me, str) for me in mutually_exclusive): + # mutually_exclusive is a Sequence[str] + self.mutually_exclusive.append(mutually_exclusive) # type: ignore + else: + self.mutually_exclusive.extend(mutually_exclusive) + if required_by: + self.required_by.update(required_by) def update_argspec(self, **kwargs: t.Any) -> t.Self: self.argument_spec.update(kwargs) @@ -51,17 +137,11 @@ class ArgumentSpec: def update( self, *, - mutually_exclusive: list[list[str] | tuple[str, ...]] | None = None, - required_together: list[list[str] | tuple[str, ...]] | None = None, - required_one_of: list[list[str] | tuple[str, ...]] | None = None, - required_if: ( - list[ - tuple[str, t.Any, list[str] | tuple[str, ...]] - | tuple[str, t.Any, list[str] | tuple[str, ...], bool] - ] - | None - ) = None, - required_by: dict[str, tuple[str, ...] | list[str]] | None = None, + required_together: RequiredTogetherT | None = None, + required_if: RequiredIfT | None = None, + required_one_of: RequiredOneOfT | None = None, + mutually_exclusive: MutuallyExclusiveT | None = None, + required_by: RequiredByT | None = None, ) -> t.Self: if mutually_exclusive: self.mutually_exclusive.extend(mutually_exclusive) diff --git a/plugins/module_utils/_crypto/module_backends/certificate_acme.py b/plugins/module_utils/_crypto/module_backends/certificate_acme.py index cc18cc8e..c6cf85fe 100644 --- a/plugins/module_utils/_crypto/module_backends/certificate_acme.py +++ b/plugins/module_utils/_crypto/module_backends/certificate_acme.py @@ -128,7 +128,7 @@ class AcmeCertificateProvider(CertificateProvider): def add_acme_provider_to_argument_spec(argument_spec: ArgumentSpec) -> None: - argument_spec.argument_spec["provider"]["choices"].append("acme") + argument_spec.argument_spec["provider"]["choices"].append("acme") # type: ignore argument_spec.argument_spec.update( { "acme_accountkey_path": {"type": "path"}, diff --git a/plugins/module_utils/_crypto/module_backends/certificate_ownca.py b/plugins/module_utils/_crypto/module_backends/certificate_ownca.py index 044446d1..6389ff0a 100644 --- a/plugins/module_utils/_crypto/module_backends/certificate_ownca.py +++ b/plugins/module_utils/_crypto/module_backends/certificate_ownca.py @@ -340,7 +340,7 @@ class OwnCACertificateProvider(CertificateProvider): def add_ownca_provider_to_argument_spec(argument_spec: ArgumentSpec) -> None: - argument_spec.argument_spec["provider"]["choices"].append("ownca") + argument_spec.argument_spec["provider"]["choices"].append("ownca") # type: ignore argument_spec.argument_spec.update( { "ownca_path": {"type": "path"}, diff --git a/plugins/module_utils/_crypto/module_backends/certificate_selfsigned.py b/plugins/module_utils/_crypto/module_backends/certificate_selfsigned.py index ce26630a..1eded312 100644 --- a/plugins/module_utils/_crypto/module_backends/certificate_selfsigned.py +++ b/plugins/module_utils/_crypto/module_backends/certificate_selfsigned.py @@ -243,7 +243,7 @@ class SelfSignedCertificateProvider(CertificateProvider): def add_selfsigned_provider_to_argument_spec(argument_spec: ArgumentSpec) -> None: - argument_spec.argument_spec["provider"]["choices"].append("selfsigned") + argument_spec.argument_spec["provider"]["choices"].append("selfsigned") # type: ignore argument_spec.argument_spec.update( { "selfsigned_version": {