adjust PluginInfo to use PluginType enum (#85277)

* normalization fixups

Co-authored-by: Matt Clay <matt@mystile.com>
pull/83775/merge
Matt Davis 6 months ago committed by GitHub
parent 9f0a8075e3
commit 43c0132caa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -21,6 +21,11 @@ def plugin_info(value: _messages.PluginInfo) -> dict[str, str]:
return dataclasses.asdict(value) return dataclasses.asdict(value)
def plugin_type(value: _messages.PluginType) -> str:
"""Render PluginType as a string."""
return value.value
def error_summary(value: _messages.ErrorSummary) -> str: def error_summary(value: _messages.ErrorSummary) -> str:
"""Render ErrorSummary as a formatted traceback for backward-compatibility with pre-2.19 TaskResult.exception.""" """Render ErrorSummary as a formatted traceback for backward-compatibility with pre-2.19 TaskResult.exception."""
if _traceback._is_traceback_enabled(_traceback.TracebackEvent.ERROR): if _traceback._is_traceback_enabled(_traceback.TracebackEvent.ERROR):
@ -56,6 +61,7 @@ def encrypted_string(value: EncryptedString) -> str | VaultExceptionMarker:
_type_transform_mapping: dict[type, t.Callable[[t.Any], t.Any]] = { _type_transform_mapping: dict[type, t.Callable[[t.Any], t.Any]] = {
_captured.CapturedErrorSummary: error_summary, _captured.CapturedErrorSummary: error_summary,
_messages.PluginInfo: plugin_info, _messages.PluginInfo: plugin_info,
_messages.PluginType: plugin_type,
_messages.ErrorSummary: error_summary, _messages.ErrorSummary: error_summary,
_messages.WarningSummary: warning_summary, _messages.WarningSummary: warning_summary,
_messages.DeprecationSummary: deprecation_summary, _messages.DeprecationSummary: deprecation_summary,

@ -5,6 +5,7 @@ import collections.abc as c
import copy import copy
import dataclasses import dataclasses
import datetime import datetime
import enum
import inspect import inspect
import sys import sys
@ -216,7 +217,7 @@ class AnsibleTagHelper:
return value return value
class AnsibleSerializable(metaclass=abc.ABCMeta): class AnsibleSerializable:
__slots__ = _NO_INSTANCE_STORAGE __slots__ = _NO_INSTANCE_STORAGE
_known_type_map: t.ClassVar[t.Dict[str, t.Type['AnsibleSerializable']]] = {} _known_type_map: t.ClassVar[t.Dict[str, t.Type['AnsibleSerializable']]] = {}
@ -274,6 +275,27 @@ class AnsibleSerializable(metaclass=abc.ABCMeta):
return f'{name}({arg_string})' return f'{name}({arg_string})'
class AnsibleSerializableEnum(AnsibleSerializable, enum.Enum):
"""Base class for serializable enumerations."""
def _as_dict(self) -> t.Dict[str, t.Any]:
return dict(value=self.value)
@classmethod
def _from_dict(cls, d: t.Dict[str, t.Any]) -> t.Self:
return cls(d['value'].lower())
def __str__(self) -> str:
return self.value
def __repr__(self) -> str:
return f'<{self.__class__.__name__}.{self.name}>'
@staticmethod
def _generate_next_value_(name, start, count, last_values):
return name.lower()
class AnsibleSerializableWrapper(AnsibleSerializable, t.Generic[_T], metaclass=abc.ABCMeta): class AnsibleSerializableWrapper(AnsibleSerializable, t.Generic[_T], metaclass=abc.ABCMeta):
__slots__ = ('_value',) __slots__ = ('_value',)

@ -5,7 +5,7 @@ import pathlib
import sys import sys
import typing as t import typing as t
from ansible.module_utils._internal import _stack, _messages, _validation from ansible.module_utils._internal import _stack, _messages, _validation, _plugin_info
def deprecator_from_collection_name(collection_name: str | None) -> _messages.PluginInfo | None: def deprecator_from_collection_name(collection_name: str | None) -> _messages.PluginInfo | None:
@ -19,7 +19,7 @@ def deprecator_from_collection_name(collection_name: str | None) -> _messages.Pl
return _messages.PluginInfo( return _messages.PluginInfo(
resolved_name=collection_name, resolved_name=collection_name,
type=_COLLECTION_ONLY_TYPE, type=None,
) )
@ -54,7 +54,7 @@ def _path_as_core_plugininfo(path: str) -> _messages.PluginInfo | None:
if match := re.match(r'plugins/(?P<plugin_type>\w+)/(?P<plugin_name>\w+)', relpath): if match := re.match(r'plugins/(?P<plugin_type>\w+)/(?P<plugin_name>\w+)', relpath):
plugin_name = match.group("plugin_name") plugin_name = match.group("plugin_name")
plugin_type = match.group("plugin_type") plugin_type = _plugin_info.normalize_plugin_type(match.group("plugin_type"))
if plugin_type not in _DEPRECATOR_PLUGIN_TYPES: if plugin_type not in _DEPRECATOR_PLUGIN_TYPES:
# The plugin type isn't a known deprecator type, so we have to assume the caller is intermediate code. # The plugin type isn't a known deprecator type, so we have to assume the caller is intermediate code.
@ -65,13 +65,13 @@ def _path_as_core_plugininfo(path: str) -> _messages.PluginInfo | None:
elif match := re.match(r'modules/(?P<module_name>\w+)', relpath): elif match := re.match(r'modules/(?P<module_name>\w+)', relpath):
# AnsiballZ Python package for core modules # AnsiballZ Python package for core modules
plugin_name = match.group("module_name") plugin_name = match.group("module_name")
plugin_type = "module" plugin_type = _messages.PluginType.MODULE
elif match := re.match(r'legacy/(?P<module_name>\w+)', relpath): elif match := re.match(r'legacy/(?P<module_name>\w+)', relpath):
# AnsiballZ Python package for non-core library/role modules # AnsiballZ Python package for non-core library/role modules
namespace = 'ansible.legacy' namespace = 'ansible.legacy'
plugin_name = match.group("module_name") plugin_name = match.group("module_name")
plugin_type = "module" plugin_type = _messages.PluginType.MODULE
else: else:
return ANSIBLE_CORE_DEPRECATOR # non-plugin core path, safe to use ansible-core for the same reason as the non-deprecator plugin type case above return ANSIBLE_CORE_DEPRECATOR # non-plugin core path, safe to use ansible-core for the same reason as the non-deprecator plugin type case above
@ -85,7 +85,7 @@ def _path_as_collection_plugininfo(path: str) -> _messages.PluginInfo | None:
if not (match := re.search(r'/ansible_collections/(?P<ns>\w+)/(?P<coll>\w+)/plugins/(?P<plugin_type>\w+)/(?P<plugin_name>\w+)', path)): if not (match := re.search(r'/ansible_collections/(?P<ns>\w+)/(?P<coll>\w+)/plugins/(?P<plugin_type>\w+)/(?P<plugin_name>\w+)', path)):
return None return None
plugin_type = match.group('plugin_type') plugin_type = _plugin_info.normalize_plugin_type(match.group('plugin_type'))
if plugin_type in _AMBIGUOUS_DEPRECATOR_PLUGIN_TYPES: if plugin_type in _AMBIGUOUS_DEPRECATOR_PLUGIN_TYPES:
# We're able to detect the namespace, collection and plugin type -- but we have no way to identify the plugin name currently. # We're able to detect the namespace, collection and plugin type -- but we have no way to identify the plugin name currently.
@ -93,9 +93,6 @@ def _path_as_collection_plugininfo(path: str) -> _messages.PluginInfo | None:
# In the future we could improve the detection and/or make it easier for a caller to identify the plugin name. # In the future we could improve the detection and/or make it easier for a caller to identify the plugin name.
return deprecator_from_collection_name('.'.join((match.group('ns'), match.group('coll')))) return deprecator_from_collection_name('.'.join((match.group('ns'), match.group('coll'))))
if plugin_type == 'modules':
plugin_type = 'module'
if plugin_type not in _DEPRECATOR_PLUGIN_TYPES: if plugin_type not in _DEPRECATOR_PLUGIN_TYPES:
# The plugin type isn't a known deprecator type, so we have to assume the caller is intermediate code. # The plugin type isn't a known deprecator type, so we have to assume the caller is intermediate code.
# We have no way of knowing if the intermediate code is deprecating its own feature, or acting on behalf of another plugin. # We have no way of knowing if the intermediate code is deprecating its own feature, or acting on behalf of another plugin.
@ -107,46 +104,43 @@ def _path_as_collection_plugininfo(path: str) -> _messages.PluginInfo | None:
return _messages.PluginInfo(resolved_name=name, type=plugin_type) return _messages.PluginInfo(resolved_name=name, type=plugin_type)
_COLLECTION_ONLY_TYPE: t.Final = 'collection'
"""Ersatz placeholder plugin type for use by a `PluginInfo` instance that references only a collection."""
_ANSIBLE_MODULE_BASE_PATH: t.Final = pathlib.Path(sys.modules['ansible'].__file__).parent _ANSIBLE_MODULE_BASE_PATH: t.Final = pathlib.Path(sys.modules['ansible'].__file__).parent
"""Runtime-detected base path of the `ansible` Python package to distinguish between Ansible-owned and external code.""" """Runtime-detected base path of the `ansible` Python package to distinguish between Ansible-owned and external code."""
ANSIBLE_CORE_DEPRECATOR: t.Final = deprecator_from_collection_name('ansible.builtin') ANSIBLE_CORE_DEPRECATOR: t.Final = deprecator_from_collection_name('ansible.builtin')
"""Singleton `PluginInfo` instance for ansible-core callers where the plugin can/should not be identified in messages.""" """Singleton `PluginInfo` instance for ansible-core callers where the plugin can/should not be identified in messages."""
INDETERMINATE_DEPRECATOR: t.Final = _messages.PluginInfo(resolved_name='indeterminate', type='indeterminate') INDETERMINATE_DEPRECATOR: t.Final = _messages.PluginInfo(resolved_name=None, type=None)
"""Singleton `PluginInfo` instance for indeterminate deprecator.""" """Singleton `PluginInfo` instance for indeterminate deprecator."""
_DEPRECATOR_PLUGIN_TYPES: t.Final = frozenset( _DEPRECATOR_PLUGIN_TYPES: t.Final = frozenset(
{ {
'action', _messages.PluginType.ACTION,
'become', _messages.PluginType.BECOME,
'cache', _messages.PluginType.CACHE,
'callback', _messages.PluginType.CALLBACK,
'cliconf', _messages.PluginType.CLICONF,
'connection', _messages.PluginType.CONNECTION,
# doc_fragments - no code execution # DOC_FRAGMENTS - no code execution
# filter - basename inadequate to identify plugin # FILTER - basename inadequate to identify plugin
'httpapi', _messages.PluginType.HTTPAPI,
'inventory', _messages.PluginType.INVENTORY,
'lookup', _messages.PluginType.LOOKUP,
'module', # only for collections _messages.PluginType.MODULE, # only for collections
'netconf', _messages.PluginType.NETCONF,
'shell', _messages.PluginType.SHELL,
'strategy', _messages.PluginType.STRATEGY,
'terminal', _messages.PluginType.TERMINAL,
# test - basename inadequate to identify plugin # TEST - basename inadequate to identify plugin
'vars', _messages.PluginType.VARS,
} }
) )
"""Plugin types which are valid for identifying a deprecator for deprecation purposes.""" """Plugin types which are valid for identifying a deprecator for deprecation purposes."""
_AMBIGUOUS_DEPRECATOR_PLUGIN_TYPES: t.Final = frozenset( _AMBIGUOUS_DEPRECATOR_PLUGIN_TYPES: t.Final = frozenset(
{ {
'filter', _messages.PluginType.FILTER,
'test', _messages.PluginType.TEST,
} }
) )
"""Plugin types for which basename cannot be used to identify the plugin name.""" """Plugin types for which basename cannot be used to identify the plugin name."""

@ -87,6 +87,7 @@ For controller-to-module, type behavior is profile dependent.
_common_module_response_types: frozenset[type[AnsibleSerializable]] = frozenset( _common_module_response_types: frozenset[type[AnsibleSerializable]] = frozenset(
{ {
_messages.PluginInfo, _messages.PluginInfo,
_messages.PluginType,
_messages.Event, _messages.Event,
_messages.EventChain, _messages.EventChain,
_messages.ErrorSummary, _messages.ErrorSummary,

@ -8,6 +8,7 @@ A future release will remove the provisional status.
from __future__ import annotations as _annotations from __future__ import annotations as _annotations
import dataclasses as _dataclasses import dataclasses as _dataclasses
import enum as _enum
import sys as _sys import sys as _sys
import typing as _t import typing as _t
@ -21,14 +22,37 @@ else:
_dataclass_kwargs = dict(frozen=True) _dataclass_kwargs = dict(frozen=True)
class PluginType(_datatag.AnsibleSerializableEnum):
"""Enum of Ansible plugin types."""
ACTION = _enum.auto()
BECOME = _enum.auto()
CACHE = _enum.auto()
CALLBACK = _enum.auto()
CLICONF = _enum.auto()
CONNECTION = _enum.auto()
DOC_FRAGMENTS = _enum.auto()
FILTER = _enum.auto()
HTTPAPI = _enum.auto()
INVENTORY = _enum.auto()
LOOKUP = _enum.auto()
MODULE = _enum.auto()
NETCONF = _enum.auto()
SHELL = _enum.auto()
STRATEGY = _enum.auto()
TERMINAL = _enum.auto()
TEST = _enum.auto()
VARS = _enum.auto()
@_dataclasses.dataclass(**_dataclass_kwargs) @_dataclasses.dataclass(**_dataclass_kwargs)
class PluginInfo(_datatag.AnsibleSerializableDataclass): class PluginInfo(_datatag.AnsibleSerializableDataclass):
"""Information about a loaded plugin.""" """Information about a loaded plugin."""
resolved_name: str resolved_name: _t.Optional[str]
"""The resolved canonical plugin name; always fully-qualified for collection plugins.""" """The resolved canonical plugin name; always fully-qualified for collection plugins."""
type: str type: _t.Optional[PluginType]
"""The plugin type.""" """The plugin type."""

@ -21,5 +21,18 @@ def get_plugin_info(value: HasPluginInfo) -> _messages.PluginInfo:
"""Utility method that returns a `PluginInfo` from an object implementing the `HasPluginInfo` protocol.""" """Utility method that returns a `PluginInfo` from an object implementing the `HasPluginInfo` protocol."""
return _messages.PluginInfo( return _messages.PluginInfo(
resolved_name=value.ansible_name, resolved_name=value.ansible_name,
type=value.plugin_type, type=normalize_plugin_type(value.plugin_type),
) )
def normalize_plugin_type(value: str) -> _messages.PluginType | None:
"""Normalize value and return it as a PluginType, or None if the value does match any known plugin type."""
value = value.lower()
if value == 'modules':
value = 'module'
try:
return _messages.PluginType(value)
except ValueError:
return None

@ -603,20 +603,18 @@ class Display(metaclass=Singleton):
else: else:
removal_fragment = 'This feature will be removed' removal_fragment = 'This feature will be removed'
if not deprecator or deprecator.type == _deprecator.INDETERMINATE_DEPRECATOR.type: if not deprecator or not deprecator.type:
collection = None # indeterminate has no resolved_name or type
plugin_fragment = '' # collections have a resolved_name but no type
elif deprecator.type == _deprecator._COLLECTION_ONLY_TYPE: collection = deprecator.resolved_name if deprecator else None
collection = deprecator.resolved_name
plugin_fragment = '' plugin_fragment = ''
else: else:
parts = deprecator.resolved_name.split('.') parts = deprecator.resolved_name.split('.')
plugin_name = parts[-1] plugin_name = parts[-1]
# DTFIX1: normalize 'modules' -> 'module' before storing it so we can eliminate the normalization here plugin_type_name = str(deprecator.type) if deprecator.type is _messages.PluginType.MODULE else f'{deprecator.type} plugin'
plugin_type = "module" if deprecator.type in ("module", "modules") else f'{deprecator.type} plugin'
collection = '.'.join(parts[:2]) if len(parts) > 2 else None collection = '.'.join(parts[:2]) if len(parts) > 2 else None
plugin_fragment = f'{plugin_type} {plugin_name!r}' plugin_fragment = f'{plugin_type_name} {plugin_name!r}'
if collection and plugin_fragment: if collection and plugin_fragment:
plugin_fragment += ' in' plugin_fragment += ' in'

@ -7,6 +7,6 @@ from ansible.plugins.lookup import LookupBase
class LookupModule(LookupBase): class LookupModule(LookupBase):
def run(self, terms, variables=None, **kwargs): def run(self, terms, variables=None, **kwargs):
return [_messages.PluginInfo( return [_messages.PluginInfo(
resolved_name='resolved_name', resolved_name='ns.col.module',
type='type', type=_messages.PluginType.MODULE,
)] )]

@ -43,8 +43,8 @@
vars: vars:
some_var: Hello some_var: Hello
expected_plugin_info: expected_plugin_info:
resolved_name: resolved_name resolved_name: ns.col.module
type: type type: module
- name: test the python_literal_eval filter - name: test the python_literal_eval filter
assert: assert:

@ -48,6 +48,7 @@ MODULE_UTILS_BASIC_FILES = frozenset(('ansible/__init__.py',
'ansible/module_utils/_internal/_patches/_socket_patch.py', 'ansible/module_utils/_internal/_patches/_socket_patch.py',
'ansible/module_utils/_internal/_patches/_sys_intern_patch.py', 'ansible/module_utils/_internal/_patches/_sys_intern_patch.py',
'ansible/module_utils/_internal/_patches/__init__.py', 'ansible/module_utils/_internal/_patches/__init__.py',
'ansible/module_utils/_internal/_plugin_info.py',
'ansible/module_utils/_internal/_stack.py', 'ansible/module_utils/_internal/_stack.py',
'ansible/module_utils/_internal/_text_utils.py', 'ansible/module_utils/_internal/_text_utils.py',
'ansible/module_utils/common/collections.py', 'ansible/module_utils/common/collections.py',

@ -33,26 +33,25 @@ def do_stuff():
return super().exec_module(module) return super().exec_module(module)
@pytest.mark.parametrize("python_fq_name,expected_resolved_name,expected_plugin_type", ( @pytest.mark.parametrize("python_fq_name,expected_plugin_info", (
# legacy module callers # legacy module callers
('ansible.legacy.blah', 'ansible.legacy.blah', 'module'), ('ansible.legacy.blah', _messages.PluginInfo(resolved_name='ansible.legacy.blah', type=_messages.PluginType.MODULE)),
# core callers # core callers
('ansible.modules.ping', 'ansible.builtin.ping', 'module'), ('ansible.modules.ping', _messages.PluginInfo(resolved_name='ansible.builtin.ping', type=_messages.PluginType.MODULE)),
('ansible.plugins.filters.core', _deprecator.ANSIBLE_CORE_DEPRECATOR.resolved_name, _deprecator.ANSIBLE_CORE_DEPRECATOR.type), ('ansible.plugins.filter.core', _deprecator.ANSIBLE_CORE_DEPRECATOR),
('ansible.plugins.tests.core', _deprecator.ANSIBLE_CORE_DEPRECATOR.resolved_name, _deprecator.ANSIBLE_CORE_DEPRECATOR.type), ('ansible.plugins.test.core', _deprecator.ANSIBLE_CORE_DEPRECATOR),
('ansible.nonplugin_something', _deprecator.ANSIBLE_CORE_DEPRECATOR.resolved_name, _deprecator.ANSIBLE_CORE_DEPRECATOR.type), ('ansible.nonplugin_something', _deprecator.ANSIBLE_CORE_DEPRECATOR),
# collections plugin callers # collections plugin callers
('ansible_collections.foo.bar.plugins.modules.module_thing', 'foo.bar.module_thing', 'module'), ('ansible_collections.foo.bar.plugins.modules.module_thing', _messages.PluginInfo(resolved_name='foo.bar.module_thing', type=_messages.PluginType.MODULE)),
('ansible_collections.foo.bar.plugins.filter.somefilter', 'foo.bar', _deprecator._COLLECTION_ONLY_TYPE), ('ansible_collections.foo.bar.plugins.filter.somefilter', _messages.PluginInfo(resolved_name='foo.bar', type=None)),
('ansible_collections.foo.bar.plugins.test.sometest', 'foo.bar', _deprecator._COLLECTION_ONLY_TYPE), ('ansible_collections.foo.bar.plugins.test.sometest', _messages.PluginInfo(resolved_name='foo.bar', type=None)),
# indeterminate callers (e.g. collection module_utils- must specify since they might be calling on behalf of another # indeterminate callers (e.g. collection module_utils- must specify since they might be calling on behalf of another
('ansible_collections.foo.bar.plugins.module_utils.something', ('ansible_collections.foo.bar.plugins.module_utils.something', _deprecator.INDETERMINATE_DEPRECATOR),
_deprecator.INDETERMINATE_DEPRECATOR.resolved_name, _deprecator.INDETERMINATE_DEPRECATOR.type),
# other callers # other callers
('something.else', None, None), ('something.else', None),
('ansible_collections.foo.bar.nonplugin_something', None, None), ('ansible_collections.foo.bar.nonplugin_something', None),
)) ))
def test_get_caller_plugin_info(python_fq_name: str, expected_resolved_name: str, expected_plugin_type: str): def test_get_caller_plugin_info(python_fq_name: str, expected_plugin_info: _messages.PluginInfo):
"""Validates the expected `PluginInfo` values received from various types of core/non-core/collection callers.""" """Validates the expected `PluginInfo` values received from various types of core/non-core/collection callers."""
# invoke a standalone fake loader that generates a Python module with the specified FQ python name (converted to a corresponding __file__ entry) that # invoke a standalone fake loader that generates a Python module with the specified FQ python name (converted to a corresponding __file__ entry) that
# pretends as if it called `get_caller_plugin_info()` and returns its result # pretends as if it called `get_caller_plugin_info()` and returns its result
@ -64,10 +63,4 @@ def test_get_caller_plugin_info(python_fq_name: str, expected_resolved_name: str
pi: _messages.PluginInfo = mod.do_stuff() pi: _messages.PluginInfo = mod.do_stuff()
if not expected_resolved_name and not expected_plugin_type: assert pi == expected_plugin_info
assert pi is None
return
assert pi is not None
assert pi.resolved_name == expected_resolved_name
assert pi.type == expected_plugin_type

@ -5,6 +5,7 @@ import collections.abc as c
import copy import copy
import dataclasses import dataclasses
import datetime import datetime
import enum
import inspect import inspect
import json import json
@ -26,6 +27,7 @@ from ansible.module_utils._internal import _messages
from ansible.module_utils._internal._datatag import ( from ansible.module_utils._internal._datatag import (
AnsibleSerializable, AnsibleSerializable,
AnsibleSerializableEnum,
AnsibleSingletonTagBase, AnsibleSingletonTagBase,
AnsibleTaggedObject, AnsibleTaggedObject,
NotTaggableError, NotTaggableError,
@ -83,7 +85,8 @@ message_instances = [
_messages.ErrorSummary(event=_messages.Event(msg="bla", formatted_traceback="tb")), _messages.ErrorSummary(event=_messages.Event(msg="bla", formatted_traceback="tb")),
_messages.WarningSummary(event=_messages.Event(msg="bla", formatted_source_context="sc", formatted_traceback="tb")), _messages.WarningSummary(event=_messages.Event(msg="bla", formatted_source_context="sc", formatted_traceback="tb")),
_messages.DeprecationSummary(event=_messages.Event(msg="bla", formatted_source_context="sc", formatted_traceback="tb"), version="1.2.3"), _messages.DeprecationSummary(event=_messages.Event(msg="bla", formatted_source_context="sc", formatted_traceback="tb"), version="1.2.3"),
_messages.PluginInfo(resolved_name='a.b.c', type='module'), _messages.PluginInfo(resolved_name='a.b.c', type=_messages.PluginType.MODULE),
_messages.PluginType.MODULE,
] ]
@ -97,7 +100,7 @@ def assert_round_trip(original_value, round_tripped_value, via_copy=False):
return return
# singleton values should rehydrate as the shared singleton instance, all others should be a new instance # singleton values should rehydrate as the shared singleton instance, all others should be a new instance
if isinstance(original_value, AnsibleSingletonTagBase): if isinstance(original_value, (AnsibleSingletonTagBase, enum.Enum)):
assert original_value is round_tripped_value assert original_value is round_tripped_value
else: else:
assert original_value is not round_tripped_value assert original_value is not round_tripped_value
@ -483,6 +486,8 @@ class TestDatatagTarget(AutoParamSupport):
excluded_type_names = { excluded_type_names = {
AnsibleTaggedObject.__name__, # base class, cannot be abstract AnsibleTaggedObject.__name__, # base class, cannot be abstract
AnsibleSerializableDataclass.__name__, # base class, cannot be abstract AnsibleSerializableDataclass.__name__, # base class, cannot be abstract
AnsibleSerializable.__name__, # base class, cannot be abstract
AnsibleSerializableEnum.__name__, # base class, cannot be abstract
# these types are all controller-only, so it's easier to have static type names instead of importing them # these types are all controller-only, so it's easier to have static type names instead of importing them
'JinjaConstTemplate', # serialization not required 'JinjaConstTemplate', # serialization not required
'_EncryptedSource', # serialization not required '_EncryptedSource', # serialization not required
@ -643,7 +648,7 @@ class TestDatatagTarget(AutoParamSupport):
"""Assert that __slots__ are properly defined on the given serializable type.""" """Assert that __slots__ are properly defined on the given serializable type."""
if value in (AnsibleSerializable, AnsibleTaggedObject): if value in (AnsibleSerializable, AnsibleTaggedObject):
expect_slots = True # non-dataclass base types have no attributes, but still use slots expect_slots = True # non-dataclass base types have no attributes, but still use slots
elif issubclass(value, (int, bytes, tuple)): elif issubclass(value, (int, bytes, tuple, enum.Enum)):
# non-empty slots are not supported by these variable-length data types # non-empty slots are not supported by these variable-length data types
# see: https://docs.python.org/3/reference/datamodel.html # see: https://docs.python.org/3/reference/datamodel.html
expect_slots = False expect_slots = False

@ -837,6 +837,7 @@ class TestVaultLib(unittest.TestCase):
def test_verify_encrypted_string_methods(): def test_verify_encrypted_string_methods():
"""Verify all relevant methods on `str` are implemented on `EncryptedString`.""" """Verify all relevant methods on `str` are implemented on `EncryptedString`."""
str_methods_to_not_implement = { str_methods_to_not_implement = {
'__class__',
'__dir__', '__dir__',
'__getattribute__', # generated by Python, identical to the one on `str` in Python 3.13+, but different on earlier versions '__getattribute__', # generated by Python, identical to the one on `str` in Python 3.13+, but different on earlier versions
'__getnewargs__', '__getnewargs__',

@ -182,14 +182,14 @@ Origin: /some/path
A_DATE = datetime.date(2025, 1, 1) A_DATE = datetime.date(2025, 1, 1)
CORE = deprecator_from_collection_name('ansible.builtin') CORE = deprecator_from_collection_name('ansible.builtin')
CORE_MODULE = _messages.PluginInfo(resolved_name='ansible.builtin.ping', type='module') CORE_MODULE = _messages.PluginInfo(resolved_name='ansible.builtin.ping', type=_messages.PluginType.MODULE)
CORE_PLUGIN = _messages.PluginInfo(resolved_name='ansible.builtin.debug', type='action') CORE_PLUGIN = _messages.PluginInfo(resolved_name='ansible.builtin.debug', type=_messages.PluginType.ACTION)
COLL = deprecator_from_collection_name('ns.col') COLL = deprecator_from_collection_name('ns.col')
COLL_MODULE = _messages.PluginInfo(resolved_name='ns.col.ping', type='module') COLL_MODULE = _messages.PluginInfo(resolved_name='ns.col.ping', type=_messages.PluginType.MODULE)
COLL_PLUGIN = _messages.PluginInfo(resolved_name='ns.col.debug', type='action') COLL_PLUGIN = _messages.PluginInfo(resolved_name='ns.col.debug', type=_messages.PluginType.ACTION)
INDETERMINATE = _deprecator.INDETERMINATE_DEPRECATOR INDETERMINATE = _deprecator.INDETERMINATE_DEPRECATOR
LEGACY_MODULE = _messages.PluginInfo(resolved_name='ping', type='module') LEGACY_MODULE = _messages.PluginInfo(resolved_name='ping', type=_messages.PluginType.MODULE)
LEGACY_PLUGIN = _messages.PluginInfo(resolved_name='debug', type='action') LEGACY_PLUGIN = _messages.PluginInfo(resolved_name='debug', type=_messages.PluginType.ACTION)
@pytest.mark.parametrize('kwargs, expected', ( @pytest.mark.parametrize('kwargs, expected', (

Loading…
Cancel
Save