ansible-core 2.19.2rc1__py3-none-any.whl → 2.20.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ansible-core might be problematic. Click here for more details.

Files changed (202) hide show
  1. ansible/_internal/__init__.py +1 -4
  2. ansible/_internal/_ansiballz/_builder.py +1 -3
  3. ansible/_internal/_collection_proxy.py +7 -9
  4. ansible/_internal/_display_utils.py +145 -0
  5. ansible/_internal/_json/__init__.py +3 -4
  6. ansible/_internal/_templating/_engine.py +1 -1
  7. ansible/_internal/_templating/_jinja_plugins.py +1 -2
  8. ansible/_internal/_wrapt.py +105 -301
  9. ansible/cli/__init__.py +11 -10
  10. ansible/cli/adhoc.py +1 -2
  11. ansible/cli/arguments/option_helpers.py +1 -1
  12. ansible/cli/config.py +5 -6
  13. ansible/cli/doc.py +67 -67
  14. ansible/cli/galaxy.py +15 -24
  15. ansible/cli/inventory.py +0 -1
  16. ansible/cli/playbook.py +0 -1
  17. ansible/cli/pull.py +0 -1
  18. ansible/cli/scripts/ansible_connection_cli_stub.py +1 -1
  19. ansible/config/base.yml +1 -25
  20. ansible/config/manager.py +0 -2
  21. ansible/executor/play_iterator.py +42 -20
  22. ansible/executor/playbook_executor.py +0 -9
  23. ansible/executor/powershell/async_watchdog.ps1 +24 -4
  24. ansible/executor/task_executor.py +32 -22
  25. ansible/executor/task_queue_manager.py +1 -3
  26. ansible/galaxy/api.py +33 -80
  27. ansible/galaxy/collection/__init__.py +4 -17
  28. ansible/galaxy/dependency_resolution/dataclasses.py +0 -10
  29. ansible/galaxy/dependency_resolution/providers.py +1 -2
  30. ansible/galaxy/role.py +1 -33
  31. ansible/inventory/manager.py +2 -3
  32. ansible/keyword_desc.yml +0 -3
  33. ansible/module_utils/_internal/_datatag/__init__.py +2 -10
  34. ansible/module_utils/_internal/_no_six.py +86 -0
  35. ansible/module_utils/_text.py +28 -8
  36. ansible/module_utils/ansible_release.py +2 -2
  37. ansible/module_utils/basic.py +27 -24
  38. ansible/module_utils/common/_collections_compat.py +11 -2
  39. ansible/module_utils/common/collections.py +8 -3
  40. ansible/module_utils/common/dict_transformations.py +1 -2
  41. ansible/module_utils/common/network.py +4 -2
  42. ansible/module_utils/common/parameters.py +32 -41
  43. ansible/module_utils/common/text/converters.py +109 -23
  44. ansible/module_utils/common/text/formatters.py +6 -2
  45. ansible/module_utils/common/validation.py +11 -9
  46. ansible/module_utils/connection.py +8 -3
  47. ansible/module_utils/facts/hardware/linux.py +23 -7
  48. ansible/module_utils/facts/hardware/netbsd.py +1 -1
  49. ansible/module_utils/facts/hardware/sunos.py +2 -1
  50. ansible/module_utils/facts/packages.py +6 -2
  51. ansible/module_utils/facts/system/distribution.py +2 -1
  52. ansible/module_utils/facts/system/env.py +6 -3
  53. ansible/module_utils/facts/system/local.py +3 -1
  54. ansible/module_utils/parsing/convert_bool.py +6 -2
  55. ansible/module_utils/service.py +2 -3
  56. ansible/module_utils/six/__init__.py +11 -6
  57. ansible/module_utils/urls.py +6 -2
  58. ansible/module_utils/yumdnf.py +0 -5
  59. ansible/modules/apt.py +18 -13
  60. ansible/modules/apt_repository.py +1 -1
  61. ansible/modules/assemble.py +5 -9
  62. ansible/modules/blockinfile.py +39 -23
  63. ansible/modules/cron.py +26 -35
  64. ansible/modules/deb822_repository.py +83 -12
  65. ansible/modules/dnf.py +3 -7
  66. ansible/modules/dnf5.py +4 -6
  67. ansible/modules/expect.py +0 -3
  68. ansible/modules/find.py +1 -2
  69. ansible/modules/get_url.py +1 -1
  70. ansible/modules/git.py +4 -5
  71. ansible/modules/include_vars.py +1 -1
  72. ansible/modules/lineinfile.py +71 -63
  73. ansible/modules/package_facts.py +1 -1
  74. ansible/modules/pip.py +8 -2
  75. ansible/modules/replace.py +6 -6
  76. ansible/modules/service.py +3 -4
  77. ansible/modules/stat.py +20 -0
  78. ansible/modules/uri.py +9 -10
  79. ansible/modules/user.py +1 -2
  80. ansible/modules/wait_for.py +2 -2
  81. ansible/modules/wait_for_connection.py +2 -1
  82. ansible/modules/yum_repository.py +1 -16
  83. ansible/parsing/dataloader.py +24 -31
  84. ansible/parsing/mod_args.py +3 -0
  85. ansible/parsing/vault/__init__.py +1 -2
  86. ansible/playbook/base.py +8 -56
  87. ansible/playbook/block.py +0 -60
  88. ansible/playbook/collectionsearch.py +1 -2
  89. ansible/playbook/handler.py +1 -7
  90. ansible/playbook/helpers.py +0 -7
  91. ansible/playbook/included_file.py +1 -1
  92. ansible/playbook/play.py +103 -37
  93. ansible/playbook/play_context.py +4 -0
  94. ansible/playbook/role/__init__.py +10 -65
  95. ansible/playbook/role/definition.py +3 -4
  96. ansible/playbook/role/include.py +2 -3
  97. ansible/playbook/role/metadata.py +1 -12
  98. ansible/playbook/role/requirement.py +1 -2
  99. ansible/playbook/role_include.py +1 -2
  100. ansible/playbook/taggable.py +16 -5
  101. ansible/playbook/task.py +51 -55
  102. ansible/plugins/action/__init__.py +20 -19
  103. ansible/plugins/action/add_host.py +1 -2
  104. ansible/plugins/action/fetch.py +2 -4
  105. ansible/plugins/action/group_by.py +1 -2
  106. ansible/plugins/action/include_vars.py +20 -22
  107. ansible/plugins/action/script.py +1 -3
  108. ansible/plugins/action/template.py +1 -2
  109. ansible/plugins/action/uri.py +4 -2
  110. ansible/plugins/cache/__init__.py +1 -0
  111. ansible/plugins/callback/__init__.py +13 -6
  112. ansible/plugins/connection/__init__.py +3 -7
  113. ansible/plugins/connection/local.py +2 -3
  114. ansible/plugins/connection/psrp.py +0 -2
  115. ansible/plugins/connection/ssh.py +2 -7
  116. ansible/plugins/connection/winrm.py +0 -2
  117. ansible/plugins/doc_fragments/result_format_callback.py +15 -0
  118. ansible/plugins/filter/core.py +4 -5
  119. ansible/plugins/filter/encryption.py +3 -27
  120. ansible/plugins/filter/mathstuff.py +1 -2
  121. ansible/plugins/filter/to_nice_yaml.yml +31 -3
  122. ansible/plugins/filter/to_yaml.yml +29 -12
  123. ansible/plugins/inventory/__init__.py +1 -2
  124. ansible/plugins/inventory/script.py +2 -1
  125. ansible/plugins/inventory/toml.py +3 -6
  126. ansible/plugins/inventory/yaml.py +1 -2
  127. ansible/plugins/list.py +10 -3
  128. ansible/plugins/loader.py +6 -6
  129. ansible/plugins/lookup/password.py +1 -2
  130. ansible/plugins/lookup/subelements.py +2 -3
  131. ansible/plugins/lookup/url.py +1 -1
  132. ansible/plugins/lookup/varnames.py +1 -2
  133. ansible/plugins/shell/__init__.py +9 -4
  134. ansible/plugins/shell/powershell.py +8 -24
  135. ansible/plugins/strategy/__init__.py +6 -3
  136. ansible/plugins/test/core.py +4 -1
  137. ansible/plugins/test/regex.yml +18 -6
  138. ansible/release.py +2 -2
  139. ansible/template/__init__.py +3 -7
  140. ansible/utils/collection_loader/_collection_config.py +5 -0
  141. ansible/utils/collection_loader/_collection_finder.py +11 -14
  142. ansible/utils/context_objects.py +7 -4
  143. ansible/utils/display.py +28 -167
  144. ansible/utils/encrypt.py +0 -5
  145. ansible/utils/helpers.py +6 -2
  146. ansible/utils/jsonrpc.py +7 -3
  147. ansible/utils/plugin_docs.py +49 -38
  148. ansible/utils/ssh_functions.py +0 -19
  149. ansible/utils/unsafe_proxy.py +7 -7
  150. ansible/vars/clean.py +2 -3
  151. ansible/vars/manager.py +27 -20
  152. ansible/vars/plugins.py +1 -31
  153. {ansible_core-2.19.2rc1.dist-info → ansible_core-2.20.0b1.dist-info}/METADATA +3 -3
  154. {ansible_core-2.19.2rc1.dist-info → ansible_core-2.20.0b1.dist-info}/RECORD +200 -200
  155. ansible_test/_data/completion/docker.txt +7 -7
  156. ansible_test/_data/completion/network.txt +0 -1
  157. ansible_test/_data/completion/remote.txt +4 -4
  158. ansible_test/_data/requirements/ansible-test.txt +1 -1
  159. ansible_test/_data/requirements/sanity.changelog.txt +1 -1
  160. ansible_test/_data/requirements/sanity.pep8.txt +1 -1
  161. ansible_test/_data/requirements/sanity.pylint.txt +4 -4
  162. ansible_test/_internal/cache.py +2 -5
  163. ansible_test/_internal/cli/compat.py +1 -1
  164. ansible_test/_internal/commands/coverage/combine.py +1 -3
  165. ansible_test/_internal/commands/integration/__init__.py +3 -7
  166. ansible_test/_internal/commands/integration/cloud/httptester.py +1 -1
  167. ansible_test/_internal/commands/integration/coverage.py +1 -3
  168. ansible_test/_internal/commands/integration/filters.py +5 -10
  169. ansible_test/_internal/commands/sanity/validate_modules.py +1 -5
  170. ansible_test/_internal/commands/units/__init__.py +1 -13
  171. ansible_test/_internal/completion.py +2 -5
  172. ansible_test/_internal/config.py +2 -7
  173. ansible_test/_internal/coverage_util.py +1 -1
  174. ansible_test/_internal/delegation.py +2 -0
  175. ansible_test/_internal/docker_util.py +1 -1
  176. ansible_test/_internal/host_profiles.py +6 -11
  177. ansible_test/_internal/provider/__init__.py +2 -5
  178. ansible_test/_internal/provisioning.py +2 -5
  179. ansible_test/_internal/pypi_proxy.py +1 -1
  180. ansible_test/_internal/target.py +2 -6
  181. ansible_test/_internal/thread.py +1 -4
  182. ansible_test/_internal/util.py +9 -14
  183. ansible_test/_util/controller/sanity/code-smell/runtime-metadata.py +14 -19
  184. ansible_test/_util/controller/sanity/pylint/plugins/unwanted.py +30 -27
  185. ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py +31 -18
  186. ansible_test/_util/controller/sanity/validate-modules/validate_modules/module_args.py +1 -2
  187. ansible_test/_util/controller/sanity/validate-modules/validate_modules/schema.py +59 -71
  188. ansible_test/_util/controller/sanity/validate-modules/validate_modules/utils.py +1 -2
  189. ansible_test/_util/target/cli/ansible_test_cli_stub.py +4 -2
  190. ansible_test/_util/target/common/constants.py +2 -2
  191. ansible_test/_util/target/setup/bootstrap.sh +0 -6
  192. ansible/utils/py3compat.py +0 -27
  193. ansible_test/_data/pytest/config/legacy.ini +0 -4
  194. {ansible_core-2.19.2rc1.dist-info → ansible_core-2.20.0b1.dist-info}/WHEEL +0 -0
  195. {ansible_core-2.19.2rc1.dist-info → ansible_core-2.20.0b1.dist-info}/entry_points.txt +0 -0
  196. {ansible_core-2.19.2rc1.dist-info → ansible_core-2.20.0b1.dist-info}/licenses/COPYING +0 -0
  197. {ansible_core-2.19.2rc1.dist-info → ansible_core-2.20.0b1.dist-info}/licenses/licenses/Apache-License.txt +0 -0
  198. {ansible_core-2.19.2rc1.dist-info → ansible_core-2.20.0b1.dist-info}/licenses/licenses/BSD-3-Clause.txt +0 -0
  199. {ansible_core-2.19.2rc1.dist-info → ansible_core-2.20.0b1.dist-info}/licenses/licenses/MIT-license.txt +0 -0
  200. {ansible_core-2.19.2rc1.dist-info → ansible_core-2.20.0b1.dist-info}/licenses/licenses/PSF-license.txt +0 -0
  201. {ansible_core-2.19.2rc1.dist-info → ansible_core-2.20.0b1.dist-info}/licenses/licenses/simplified_bsd.txt +0 -0
  202. {ansible_core-2.19.2rc1.dist-info → ansible_core-2.20.0b1.dist-info}/top_level.txt +0 -0
@@ -30,10 +30,7 @@ def import_controller_module(module_name: str, /) -> t.Any:
30
30
  return importlib.import_module(module_name)
31
31
 
32
32
 
33
- _T = t.TypeVar('_T')
34
-
35
-
36
- def experimental(obj: _T) -> _T:
33
+ def experimental[T](obj: T) -> T:
37
34
  """
38
35
  Decorator for experimental types and methods outside the `_internal` package which accept or expose internal types.
39
36
  As with internal APIs, these are subject to change at any time without notice.
@@ -9,8 +9,6 @@ from ansible.module_utils._internal._ansiballz import _extensions
9
9
  from ansible.module_utils._internal._ansiballz._extensions import _debugpy, _pydevd, _coverage
10
10
  from ansible.constants import config
11
11
 
12
- _T = t.TypeVar('_T')
13
-
14
12
 
15
13
  class ExtensionManager:
16
14
  """AnsiballZ extension manager."""
@@ -101,7 +99,7 @@ class ExtensionManager:
101
99
  )
102
100
 
103
101
  @classmethod
104
- def _get_options(cls, name: str, config_type: type[_T], task_vars: dict[str, object]) -> _T | None:
102
+ def _get_options[T](cls, name: str, config_type: type[T], task_vars: dict[str, object]) -> T | None:
105
103
  """Parse configuration from the named environment variable as the specified type, or None if not configured."""
106
104
  if (value := config.get_config_value(name, variables=task_vars)) is None:
107
105
  return None
@@ -3,26 +3,24 @@ from __future__ import annotations as _annotations
3
3
  import collections.abc as _c
4
4
  import typing as _t
5
5
 
6
- _T_co = _t.TypeVar('_T_co', covariant=True)
7
6
 
8
-
9
- class SequenceProxy(_c.Sequence[_T_co]):
7
+ class SequenceProxy[T](_c.Sequence[T]):
10
8
  """A read-only sequence proxy."""
11
9
 
12
10
  # DTFIX5: needs unit test coverage
13
11
 
14
12
  __slots__ = ('__value',)
15
13
 
16
- def __init__(self, value: _c.Sequence[_T_co]) -> None:
14
+ def __init__(self, value: _c.Sequence[T]) -> None:
17
15
  self.__value = value
18
16
 
19
17
  @_t.overload
20
- def __getitem__(self, index: int) -> _T_co: ...
18
+ def __getitem__(self, index: int) -> T: ...
21
19
 
22
20
  @_t.overload
23
- def __getitem__(self, index: slice) -> _c.Sequence[_T_co]: ...
21
+ def __getitem__(self, index: slice) -> _c.Sequence[T]: ...
24
22
 
25
- def __getitem__(self, index: int | slice) -> _T_co | _c.Sequence[_T_co]:
23
+ def __getitem__(self, index: int | slice) -> T | _c.Sequence[T]:
26
24
  if isinstance(index, slice):
27
25
  return self.__class__(self.__value[index])
28
26
 
@@ -34,10 +32,10 @@ class SequenceProxy(_c.Sequence[_T_co]):
34
32
  def __contains__(self, item: object) -> bool:
35
33
  return item in self.__value
36
34
 
37
- def __iter__(self) -> _t.Iterator[_T_co]:
35
+ def __iter__(self) -> _t.Iterator[T]:
38
36
  yield from self.__value
39
37
 
40
- def __reversed__(self) -> _c.Iterator[_T_co]:
38
+ def __reversed__(self) -> _c.Iterator[T]:
41
39
  return reversed(self.__value)
42
40
 
43
41
  def index(self, *args) -> int:
@@ -0,0 +1,145 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+
5
+ from ansible.module_utils._internal import _ambient_context, _messages
6
+ from . import _event_formatting
7
+
8
+
9
+ class DeferredWarningContext(_ambient_context.AmbientContextBase):
10
+ """
11
+ Calls to `Display.warning()` and `Display.deprecated()` within this context will cause the resulting warnings to be captured and not displayed.
12
+ The intended use is for task-initiated warnings to be recorded with the task result, which makes them visible to registered results, callbacks, etc.
13
+ The active display callback is responsible for communicating any warnings to the user.
14
+ """
15
+
16
+ # DTFIX-FUTURE: once we start implementing nested scoped contexts for our own bookkeeping, this should be an interface facade that forwards to the nearest
17
+ # context that actually implements the warnings collection capability
18
+
19
+ def __init__(self, *, variables: dict[str, object]) -> None:
20
+ self._variables = variables # DTFIX-FUTURE: move this to an AmbientContext-derived TaskContext (once it exists)
21
+ self._deprecation_warnings: list[_messages.DeprecationSummary] = []
22
+ self._warnings: list[_messages.WarningSummary] = []
23
+ self._seen: set[_messages.WarningSummary] = set()
24
+
25
+ def capture(self, warning: _messages.WarningSummary) -> None:
26
+ """Add the warning/deprecation to the context if it has not already been seen by this context."""
27
+ if warning in self._seen:
28
+ return
29
+
30
+ self._seen.add(warning)
31
+
32
+ if isinstance(warning, _messages.DeprecationSummary):
33
+ self._deprecation_warnings.append(warning)
34
+ else:
35
+ self._warnings.append(warning)
36
+
37
+ def get_warnings(self) -> list[_messages.WarningSummary]:
38
+ """Return a list of the captured non-deprecation warnings."""
39
+ # DTFIX-FUTURE: return a read-only list proxy instead
40
+ return self._warnings
41
+
42
+ def get_deprecation_warnings(self) -> list[_messages.DeprecationSummary]:
43
+ """Return a list of the captured deprecation warnings."""
44
+ # DTFIX-FUTURE: return a read-only list proxy instead
45
+ return self._deprecation_warnings
46
+
47
+
48
+ def format_message(summary: _messages.SummaryBase, include_traceback: bool) -> str:
49
+ if isinstance(summary, _messages.DeprecationSummary):
50
+ deprecation_message = get_deprecation_message_with_plugin_info(
51
+ msg=summary.event.msg,
52
+ version=summary.version,
53
+ date=summary.date,
54
+ deprecator=summary.deprecator,
55
+ )
56
+
57
+ event = dataclasses.replace(summary.event, msg=deprecation_message)
58
+ else:
59
+ event = summary.event
60
+
61
+ return _event_formatting.format_event(event, include_traceback)
62
+
63
+
64
+ def get_deprecation_message_with_plugin_info(
65
+ *,
66
+ msg: str,
67
+ version: str | None,
68
+ removed: bool = False,
69
+ date: str | None,
70
+ deprecator: _messages.PluginInfo | None,
71
+ ) -> str:
72
+ """Internal use only. Return a deprecation message and help text for display."""
73
+ # DTFIX-FUTURE: the logic for omitting date/version doesn't apply to the payload, so it shows up in vars in some cases when it should not
74
+
75
+ if removed:
76
+ removal_fragment = 'This feature was removed'
77
+ else:
78
+ removal_fragment = 'This feature will be removed'
79
+
80
+ if not deprecator or not deprecator.type:
81
+ # indeterminate has no resolved_name or type
82
+ # collections have a resolved_name but no type
83
+ collection = deprecator.resolved_name if deprecator else None
84
+ plugin_fragment = ''
85
+ elif deprecator.resolved_name == 'ansible.builtin':
86
+ # core deprecations from base classes (the API) have no plugin name, only 'ansible.builtin'
87
+ plugin_type_name = str(deprecator.type) if deprecator.type is _messages.PluginType.MODULE else f'{deprecator.type} plugin'
88
+
89
+ collection = deprecator.resolved_name
90
+ plugin_fragment = f'the {plugin_type_name} API'
91
+ else:
92
+ parts = deprecator.resolved_name.split('.')
93
+ plugin_name = parts[-1]
94
+ plugin_type_name = str(deprecator.type) if deprecator.type is _messages.PluginType.MODULE else f'{deprecator.type} plugin'
95
+
96
+ collection = '.'.join(parts[:2]) if len(parts) > 2 else None
97
+ plugin_fragment = f'{plugin_type_name} {plugin_name!r}'
98
+
99
+ if collection and plugin_fragment:
100
+ plugin_fragment += ' in'
101
+
102
+ if collection == 'ansible.builtin':
103
+ collection_fragment = 'ansible-core'
104
+ elif collection:
105
+ collection_fragment = f'collection {collection!r}'
106
+ else:
107
+ collection_fragment = ''
108
+
109
+ if not collection:
110
+ when_fragment = 'in the future' if not removed else ''
111
+ elif date:
112
+ when_fragment = f'in a release after {date}'
113
+ elif version:
114
+ when_fragment = f'version {version}'
115
+ else:
116
+ when_fragment = 'in a future release' if not removed else ''
117
+
118
+ if plugin_fragment or collection_fragment:
119
+ from_fragment = 'from'
120
+ else:
121
+ from_fragment = ''
122
+
123
+ deprecation_msg = ' '.join(f for f in [removal_fragment, from_fragment, plugin_fragment, collection_fragment, when_fragment] if f) + '.'
124
+
125
+ return join_sentences(msg, deprecation_msg)
126
+
127
+
128
+ def join_sentences(first: str | None, second: str | None) -> str:
129
+ """Join two sentences together."""
130
+ first = (first or '').strip()
131
+ second = (second or '').strip()
132
+
133
+ if first and first[-1] not in ('!', '?', '.'):
134
+ first += '.'
135
+
136
+ if second and second[-1] not in ('!', '?', '.'):
137
+ second += '.'
138
+
139
+ if first and not second:
140
+ return first
141
+
142
+ if not first and second:
143
+ return second
144
+
145
+ return ' '.join((first, second))
@@ -24,7 +24,6 @@ from ansible._internal._templating import _transform
24
24
  from ansible.module_utils import _internal
25
25
  from ansible.module_utils._internal import _datatag
26
26
 
27
- _T = t.TypeVar('_T')
28
27
  _sentinel = object()
29
28
 
30
29
 
@@ -115,7 +114,7 @@ class AnsibleVariableVisitor:
115
114
  if func := getattr(super(), '__exit__', None):
116
115
  func(*args, **kwargs)
117
116
 
118
- def visit(self, value: _T) -> _T:
117
+ def visit[T](self, value: T) -> T:
119
118
  """
120
119
  Enforces Ansible's variable type system restrictions before a var is accepted in inventory. Also, conditionally implements template trust
121
120
  compatibility, depending on the plugin's declared understanding (or lack thereof). This always recursively copies inputs to fully isolate
@@ -143,7 +142,7 @@ class AnsibleVariableVisitor:
143
142
 
144
143
  return self._visit(None, key) # key=None prevents state tracking from seeing the key as value
145
144
 
146
- def _visit(self, key: t.Any, value: _T) -> _T:
145
+ def _visit[T](self, key: t.Any, value: T) -> T:
147
146
  """Internal implementation to recursively visit a data structure's contents."""
148
147
  self._current = key # supports StateTrackingMixIn
149
148
 
@@ -168,7 +167,7 @@ class AnsibleVariableVisitor:
168
167
  value = value._native_copy()
169
168
  value_type = type(value)
170
169
 
171
- result: _T
170
+ result: T
172
171
 
173
172
  # DTFIX-FUTURE: Visitor generally ignores dict/mapping keys by default except for debugging and schema-aware checking.
174
173
  # It could be checking keys destined for variable storage to apply more strict rules about key shape and type.
@@ -193,7 +193,7 @@ class TemplateEngine:
193
193
  return self._variables
194
194
 
195
195
  @available_variables.setter
196
- def available_variables(self, variables: dict[str, t.Any]) -> None:
196
+ def available_variables(self, variables: dict[str, t.Any] | ChainMap[str, t.Any]) -> None:
197
197
  self._variables = variables
198
198
 
199
199
  def resolve_variable_expression(
@@ -29,7 +29,6 @@ from ._utils import LazyOptions, TemplateContext
29
29
 
30
30
  _display = Display()
31
31
 
32
- _TCallable = t.TypeVar("_TCallable", bound=t.Callable)
33
32
  _ITERATOR_TYPES: t.Final = (c.Iterator, c.ItemsView, c.KeysView, c.ValuesView, range)
34
33
 
35
34
 
@@ -174,7 +173,7 @@ class _DirectCall:
174
173
  _marker_attr: t.Final[str] = "_directcall"
175
174
 
176
175
  @classmethod
177
- def mark(cls, src: _TCallable) -> _TCallable:
176
+ def mark[T: t.Callable](cls, src: T) -> T:
178
177
  setattr(src, cls._marker_attr, True)
179
178
  return src
180
179