ansible-core 2.19.0b1__py3-none-any.whl → 2.19.0b3__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.
- ansible/_internal/_ansiballz.py +1 -4
- ansible/_internal/_collection_proxy.py +47 -0
- ansible/_internal/_errors/_handler.py +4 -4
- ansible/_internal/_json/__init__.py +47 -4
- ansible/_internal/_json/_profiles/_legacy.py +2 -3
- ansible/_internal/_templating/_datatag.py +3 -4
- ansible/_internal/_templating/_engine.py +6 -1
- ansible/_internal/_templating/_jinja_bits.py +4 -4
- ansible/_internal/_templating/_jinja_plugins.py +7 -17
- ansible/cli/__init__.py +12 -5
- ansible/cli/arguments/option_helpers.py +4 -1
- ansible/cli/doc.py +14 -8
- ansible/config/base.yml +17 -20
- ansible/config/manager.py +2 -2
- ansible/constants.py +0 -62
- ansible/errors/__init__.py +6 -2
- ansible/executor/module_common.py +11 -7
- ansible/executor/process/worker.py +31 -26
- ansible/executor/task_executor.py +38 -31
- ansible/executor/task_queue_manager.py +62 -52
- ansible/executor/task_result.py +168 -72
- ansible/galaxy/api.py +1 -1
- ansible/galaxy/collection/__init__.py +3 -3
- ansible/inventory/manager.py +2 -1
- ansible/module_utils/_internal/_ansiballz.py +4 -30
- ansible/module_utils/_internal/_datatag/_tags.py +3 -25
- ansible/module_utils/_internal/_deprecator.py +134 -0
- ansible/module_utils/_internal/_plugin_info.py +25 -0
- ansible/module_utils/_internal/_validation.py +14 -0
- ansible/module_utils/ansible_release.py +1 -1
- ansible/module_utils/basic.py +68 -23
- ansible/module_utils/common/arg_spec.py +8 -3
- ansible/module_utils/common/messages.py +40 -23
- ansible/module_utils/common/process.py +0 -1
- ansible/module_utils/common/respawn.py +0 -7
- ansible/module_utils/common/warnings.py +13 -13
- ansible/module_utils/datatag.py +13 -13
- ansible/modules/async_status.py +1 -1
- ansible/modules/dnf5.py +1 -1
- ansible/modules/get_url.py +1 -1
- ansible/parsing/utils/jsonify.py +40 -0
- ansible/parsing/yaml/objects.py +16 -5
- ansible/playbook/included_file.py +25 -12
- ansible/playbook/task.py +0 -2
- ansible/plugins/__init__.py +18 -8
- ansible/plugins/action/__init__.py +6 -14
- ansible/plugins/action/gather_facts.py +2 -4
- ansible/plugins/callback/__init__.py +173 -86
- ansible/plugins/callback/default.py +79 -79
- ansible/plugins/callback/junit.py +20 -19
- ansible/plugins/callback/minimal.py +17 -17
- ansible/plugins/callback/oneline.py +23 -16
- ansible/plugins/callback/tree.py +13 -6
- ansible/plugins/connection/local.py +1 -1
- ansible/plugins/connection/paramiko_ssh.py +9 -2
- ansible/plugins/doc_fragments/action_core.py +1 -1
- ansible/plugins/filter/core.py +12 -2
- ansible/plugins/inventory/__init__.py +2 -2
- ansible/plugins/loader.py +194 -130
- ansible/plugins/lookup/url.py +2 -2
- ansible/plugins/strategy/__init__.py +76 -82
- ansible/plugins/strategy/free.py +4 -4
- ansible/plugins/strategy/linear.py +11 -9
- ansible/plugins/test/core.py +1 -1
- ansible/release.py +1 -1
- ansible/template/__init__.py +8 -6
- ansible/utils/collection_loader/_collection_meta.py +5 -3
- ansible/utils/display.py +141 -79
- ansible/utils/py3compat.py +1 -7
- ansible/utils/ssh_functions.py +4 -1
- ansible/utils/vars.py +23 -0
- ansible/vars/clean.py +1 -1
- ansible/vars/manager.py +18 -27
- ansible/vars/plugins.py +4 -4
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/METADATA +1 -1
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/RECORD +89 -85
- ansible_test/_internal/commands/sanity/pylint.py +1 -0
- ansible_test/_internal/docker_util.py +4 -3
- ansible_test/_util/controller/sanity/pylint/plugins/deprecated_calls.py +475 -0
- ansible_test/_util/controller/sanity/pylint/plugins/deprecated_comment.py +137 -0
- ansible/module_utils/_internal/_dataclass_annotation_patch.py +0 -64
- ansible/module_utils/_internal/_plugin_exec_context.py +0 -49
- ansible_test/_util/controller/sanity/pylint/plugins/deprecated.py +0 -399
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/Apache-License.txt +0 -0
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/BSD-3-Clause.txt +0 -0
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/COPYING +0 -0
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/MIT-license.txt +0 -0
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/PSF-license.txt +0 -0
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/WHEEL +0 -0
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/entry_points.txt +0 -0
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/simplified_bsd.txt +0 -0
- {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,475 @@
|
|
1
|
+
"""Ansible-specific pylint plugin for checking deprecation calls."""
|
2
|
+
|
3
|
+
# (c) 2018, Matt Martz <matt@sivel.net>
|
4
|
+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
5
|
+
|
6
|
+
from __future__ import annotations
|
7
|
+
|
8
|
+
import dataclasses
|
9
|
+
import datetime
|
10
|
+
import functools
|
11
|
+
import pathlib
|
12
|
+
|
13
|
+
import astroid
|
14
|
+
import astroid.context
|
15
|
+
import astroid.typing
|
16
|
+
|
17
|
+
import pylint.lint
|
18
|
+
import pylint.checkers
|
19
|
+
import pylint.checkers.utils
|
20
|
+
|
21
|
+
import ansible.release
|
22
|
+
|
23
|
+
from ansible.module_utils._internal._deprecator import INDETERMINATE_DEPRECATOR, _path_as_collection_plugininfo
|
24
|
+
from ansible.module_utils.compat.version import StrictVersion
|
25
|
+
from ansible.utils.version import SemanticVersion
|
26
|
+
|
27
|
+
|
28
|
+
@dataclasses.dataclass(frozen=True, kw_only=True)
|
29
|
+
class DeprecationCallArgs:
|
30
|
+
"""Arguments passed to a deprecation function."""
|
31
|
+
|
32
|
+
msg: object = None
|
33
|
+
version: object = None
|
34
|
+
date: object = None
|
35
|
+
collection_name: object = None
|
36
|
+
deprecator: object = None
|
37
|
+
help_text: object = None # only on Display.deprecated, warnings.deprecate and deprecate_value
|
38
|
+
obj: object = None # only on Display.deprecated and warnings.deprecate
|
39
|
+
removed: object = None # only on Display.deprecated
|
40
|
+
value: object = None # only on deprecate_value
|
41
|
+
|
42
|
+
|
43
|
+
class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
|
44
|
+
"""Checks for deprecated calls to ensure proper usage."""
|
45
|
+
|
46
|
+
name = 'deprecated-calls'
|
47
|
+
msgs = {
|
48
|
+
'E9501': (
|
49
|
+
"Deprecated version %r found in call to %r",
|
50
|
+
"ansible-deprecated-version",
|
51
|
+
None,
|
52
|
+
),
|
53
|
+
'E9502': (
|
54
|
+
"Found %r call without a version or date",
|
55
|
+
"ansible-deprecated-no-version",
|
56
|
+
None,
|
57
|
+
),
|
58
|
+
'E9503': (
|
59
|
+
"Invalid deprecated version %r found in call to %r",
|
60
|
+
"ansible-invalid-deprecated-version",
|
61
|
+
None,
|
62
|
+
),
|
63
|
+
'E9504': (
|
64
|
+
"Deprecated version %r found in call to %r",
|
65
|
+
"collection-deprecated-version",
|
66
|
+
None,
|
67
|
+
),
|
68
|
+
'E9505': (
|
69
|
+
"Invalid deprecated version %r found in call to %r",
|
70
|
+
"collection-invalid-deprecated-version",
|
71
|
+
None,
|
72
|
+
),
|
73
|
+
'E9506': (
|
74
|
+
"No collection_name or deprecator found in call to %r",
|
75
|
+
"ansible-deprecated-no-collection-name",
|
76
|
+
None,
|
77
|
+
),
|
78
|
+
'E9507': (
|
79
|
+
"Wrong collection_name %r found in call to %r",
|
80
|
+
"wrong-collection-deprecated",
|
81
|
+
None,
|
82
|
+
),
|
83
|
+
'E9508': (
|
84
|
+
"Expired date %r found in call to %r",
|
85
|
+
"ansible-expired-deprecated-date",
|
86
|
+
None,
|
87
|
+
),
|
88
|
+
'E9509': (
|
89
|
+
"Invalid date %r found in call to %r",
|
90
|
+
"ansible-invalid-deprecated-date",
|
91
|
+
None,
|
92
|
+
),
|
93
|
+
'E9510': (
|
94
|
+
"Both version and date found in call to %r",
|
95
|
+
"ansible-deprecated-both-version-and-date",
|
96
|
+
None,
|
97
|
+
),
|
98
|
+
'E9511': (
|
99
|
+
"Removal version %r must be a major release, not a minor or patch release, see https://semver.org/",
|
100
|
+
"removal-version-must-be-major",
|
101
|
+
None,
|
102
|
+
),
|
103
|
+
'E9512': (
|
104
|
+
"Passing date is not permitted in call to %r for ansible-core, use a version instead",
|
105
|
+
"ansible-deprecated-date-not-permitted",
|
106
|
+
None,
|
107
|
+
),
|
108
|
+
'E9513': (
|
109
|
+
"Unnecessary %r found in call to %r",
|
110
|
+
"ansible-deprecated-unnecessary-collection-name",
|
111
|
+
None,
|
112
|
+
),
|
113
|
+
'E9514': (
|
114
|
+
"Passing collection_name not permitted in call to %r for ansible-core, use deprecator instead",
|
115
|
+
"ansible-deprecated-collection-name-not-permitted",
|
116
|
+
None,
|
117
|
+
),
|
118
|
+
'E9515': (
|
119
|
+
"Both collection_name and deprecator found in call to %r",
|
120
|
+
"ansible-deprecated-both-collection-name-and-deprecator",
|
121
|
+
None,
|
122
|
+
),
|
123
|
+
}
|
124
|
+
|
125
|
+
options = (
|
126
|
+
(
|
127
|
+
'collection-name',
|
128
|
+
dict(
|
129
|
+
default=None,
|
130
|
+
type='string',
|
131
|
+
metavar='<name>',
|
132
|
+
help="The name of the collection to check.",
|
133
|
+
),
|
134
|
+
),
|
135
|
+
(
|
136
|
+
'collection-version',
|
137
|
+
dict(
|
138
|
+
default=None,
|
139
|
+
type='string',
|
140
|
+
metavar='<version>',
|
141
|
+
help="The version of the collection to check.",
|
142
|
+
),
|
143
|
+
),
|
144
|
+
(
|
145
|
+
'collection-path',
|
146
|
+
dict(
|
147
|
+
default=None,
|
148
|
+
type='string',
|
149
|
+
metavar='<path>',
|
150
|
+
help="The path of the collection to check.",
|
151
|
+
),
|
152
|
+
),
|
153
|
+
)
|
154
|
+
|
155
|
+
ANSIBLE_VERSION = StrictVersion('.'.join(ansible.release.__version__.split('.')[:3]))
|
156
|
+
"""The current ansible-core X.Y.Z version."""
|
157
|
+
|
158
|
+
DEPRECATION_MODULE_FUNCTIONS: dict[tuple[str, str], tuple[str, ...]] = {
|
159
|
+
('ansible.module_utils.common.warnings', 'deprecate'): ('msg', 'version', 'date', 'collection_name'),
|
160
|
+
('ansible.module_utils.datatag', 'deprecate_value'): ('value', 'msg'),
|
161
|
+
('ansible.module_utils.basic', 'AnsibleModule.deprecate'): ('msg', 'version', 'date', 'collection_name'),
|
162
|
+
('ansible.utils.display', 'Display.deprecated'): ('msg', 'version', 'removed', 'date', 'collection_name'),
|
163
|
+
}
|
164
|
+
"""Mapping of deprecation module+function and their positional arguments."""
|
165
|
+
|
166
|
+
DEPRECATION_MODULES = frozenset(key[0] for key in DEPRECATION_MODULE_FUNCTIONS)
|
167
|
+
"""Modules which contain deprecation functions."""
|
168
|
+
|
169
|
+
DEPRECATION_FUNCTIONS = {'.'.join(key): value for key, value in DEPRECATION_MODULE_FUNCTIONS.items()}
|
170
|
+
"""Mapping of deprecation functions and their positional arguments."""
|
171
|
+
|
172
|
+
def __init__(self, *args, **kwargs) -> None:
|
173
|
+
super().__init__(*args, **kwargs)
|
174
|
+
|
175
|
+
self.inference_context = astroid.context.InferenceContext()
|
176
|
+
self.module_cache: dict[str, astroid.Module] = {}
|
177
|
+
|
178
|
+
@functools.cached_property
|
179
|
+
def collection_name(self) -> str | None:
|
180
|
+
"""Return the collection name, or None if ansible-core is being tested."""
|
181
|
+
return self.linter.config.collection_name or None
|
182
|
+
|
183
|
+
@functools.cached_property
|
184
|
+
def collection_path(self) -> pathlib.Path:
|
185
|
+
"""Return the collection path. Not valid when ansible-core is being tested."""
|
186
|
+
return pathlib.Path(self.linter.config.collection_path)
|
187
|
+
|
188
|
+
@functools.cached_property
|
189
|
+
def collection_version(self) -> SemanticVersion | None:
|
190
|
+
"""Return the collection version, or None if ansible-core is being tested."""
|
191
|
+
if not self.linter.config.collection_version:
|
192
|
+
return None
|
193
|
+
|
194
|
+
sem_ver = SemanticVersion(self.linter.config.collection_version)
|
195
|
+
sem_ver.prerelease = () # ignore pre-release for version comparison to catch issues before the final release is cut
|
196
|
+
|
197
|
+
return sem_ver
|
198
|
+
|
199
|
+
@functools.cached_property
|
200
|
+
def is_ansible_core(self) -> bool:
|
201
|
+
"""True if ansible-core is being tested."""
|
202
|
+
return not self.collection_name
|
203
|
+
|
204
|
+
@functools.cached_property
|
205
|
+
def today_utc(self) -> datetime.date:
|
206
|
+
"""Today's date in UTC."""
|
207
|
+
return datetime.datetime.now(tz=datetime.timezone.utc).date()
|
208
|
+
|
209
|
+
def is_deprecator_required(self) -> bool | None:
|
210
|
+
"""Determine is a `collection_name` or `deprecator` is required (True), unnecessary (False) or optional (None)."""
|
211
|
+
if self.is_ansible_core:
|
212
|
+
return False # in ansible-core, never provide the deprecator -- if it really is needed, disable the sanity test inline for that line of code
|
213
|
+
|
214
|
+
plugin_info = _path_as_collection_plugininfo(self.linter.current_file)
|
215
|
+
|
216
|
+
if plugin_info is INDETERMINATE_DEPRECATOR:
|
217
|
+
return True # deprecator cannot be detected, caller must provide deprecator
|
218
|
+
|
219
|
+
# deprecation: description='deprecate collection_name/deprecator now that detection is widely available' core_version='2.23'
|
220
|
+
# When this deprecation triggers, change the return type here to False.
|
221
|
+
# At that point, callers should be able to omit the collection_name/deprecator in all but a few cases (inline ignores can be used for those cases)
|
222
|
+
return None
|
223
|
+
|
224
|
+
@pylint.checkers.utils.only_required_for_messages(*(msgs.keys()))
|
225
|
+
def visit_call(self, node: astroid.Call) -> None:
|
226
|
+
"""Visit a call node."""
|
227
|
+
if inferred := self.infer(node.func):
|
228
|
+
name = self.get_fully_qualified_name(inferred)
|
229
|
+
|
230
|
+
if args := self.DEPRECATION_FUNCTIONS.get(name):
|
231
|
+
self.check_call(node, name, args)
|
232
|
+
|
233
|
+
def infer(self, node: astroid.NodeNG) -> astroid.NodeNG | None:
|
234
|
+
"""Return the inferred node from the given node, or `None` if it cannot be unambiguously inferred."""
|
235
|
+
names: list[str] = []
|
236
|
+
target: astroid.NodeNG | None = node
|
237
|
+
inferred: astroid.typing.InferenceResult | None = None
|
238
|
+
|
239
|
+
while target:
|
240
|
+
if inferred := astroid.util.safe_infer(target, self.inference_context):
|
241
|
+
break
|
242
|
+
|
243
|
+
if isinstance(target, astroid.Call):
|
244
|
+
inferred = self.infer(target.func)
|
245
|
+
break
|
246
|
+
|
247
|
+
if isinstance(target, astroid.FunctionDef):
|
248
|
+
inferred = target
|
249
|
+
break
|
250
|
+
|
251
|
+
if isinstance(target, astroid.Name):
|
252
|
+
target = self.infer_name(target)
|
253
|
+
elif isinstance(target, astroid.AssignName) and isinstance(target.parent, astroid.Assign):
|
254
|
+
target = target.parent.value
|
255
|
+
elif isinstance(target, astroid.Attribute):
|
256
|
+
names.append(target.attrname)
|
257
|
+
target = target.expr
|
258
|
+
else:
|
259
|
+
break
|
260
|
+
|
261
|
+
for name in reversed(names):
|
262
|
+
if not isinstance(inferred, (astroid.Module, astroid.ClassDef)):
|
263
|
+
inferred = None
|
264
|
+
break
|
265
|
+
|
266
|
+
try:
|
267
|
+
inferred = inferred[name]
|
268
|
+
except KeyError:
|
269
|
+
inferred = None
|
270
|
+
else:
|
271
|
+
inferred = self.infer(inferred)
|
272
|
+
|
273
|
+
if isinstance(inferred, astroid.FunctionDef) and isinstance(inferred.parent, astroid.ClassDef):
|
274
|
+
inferred = astroid.BoundMethod(inferred, inferred.parent)
|
275
|
+
|
276
|
+
return inferred
|
277
|
+
|
278
|
+
def infer_name(self, node: astroid.Name) -> astroid.NodeNG | None:
|
279
|
+
"""Infer the node referenced by the given name, or `None` if it cannot be unambiguously inferred."""
|
280
|
+
scope = node.scope()
|
281
|
+
name = None
|
282
|
+
|
283
|
+
while scope:
|
284
|
+
try:
|
285
|
+
assignment = scope[node.name]
|
286
|
+
except KeyError:
|
287
|
+
scope = scope.parent.scope() if scope.parent else None
|
288
|
+
continue
|
289
|
+
|
290
|
+
if isinstance(assignment, astroid.AssignName) and isinstance(assignment.parent, astroid.Assign):
|
291
|
+
name = assignment.parent.value
|
292
|
+
elif isinstance(assignment, astroid.ImportFrom):
|
293
|
+
if module := self.get_module(assignment):
|
294
|
+
scope = module.scope()
|
295
|
+
continue
|
296
|
+
|
297
|
+
break
|
298
|
+
|
299
|
+
return name
|
300
|
+
|
301
|
+
def get_module(self, node: astroid.ImportFrom) -> astroid.Module | None:
|
302
|
+
"""Import the requested module if possible and cache the result."""
|
303
|
+
module_name = pylint.checkers.utils.get_import_name(node, node.modname)
|
304
|
+
|
305
|
+
if module_name not in self.DEPRECATION_MODULES:
|
306
|
+
return None # avoid unnecessary import overhead
|
307
|
+
|
308
|
+
if module := self.module_cache.get(module_name):
|
309
|
+
return module
|
310
|
+
|
311
|
+
module = node.do_import_module()
|
312
|
+
|
313
|
+
if module.name != module_name:
|
314
|
+
raise RuntimeError(f'Attempted to import {module_name!r} but found {module.name!r} instead.')
|
315
|
+
|
316
|
+
self.module_cache[module_name] = module
|
317
|
+
|
318
|
+
return module
|
319
|
+
|
320
|
+
@staticmethod
|
321
|
+
def get_fully_qualified_name(node: astroid.NodeNG) -> str | None:
|
322
|
+
"""Return the fully qualified name of the given inferred node."""
|
323
|
+
parent = node.parent
|
324
|
+
parts: tuple[str, ...] | None
|
325
|
+
|
326
|
+
if isinstance(node, astroid.FunctionDef) and isinstance(parent, astroid.Module):
|
327
|
+
parts = (parent.name, node.name)
|
328
|
+
elif isinstance(node, astroid.BoundMethod) and isinstance(parent, astroid.ClassDef) and isinstance(parent.parent, astroid.Module):
|
329
|
+
parts = (parent.parent.name, parent.name, node.name)
|
330
|
+
else:
|
331
|
+
parts = None
|
332
|
+
|
333
|
+
return '.'.join(parts) if parts else None
|
334
|
+
|
335
|
+
def check_call(self, node: astroid.Call, name: str, args: tuple[str, ...]) -> None:
|
336
|
+
"""Check the given deprecation call node for valid arguments."""
|
337
|
+
call_args = self.get_deprecation_call_args(node, args)
|
338
|
+
|
339
|
+
self.check_collection_name(node, name, call_args)
|
340
|
+
|
341
|
+
if not call_args.version and not call_args.date:
|
342
|
+
self.add_message('ansible-deprecated-no-version', node=node, args=(name,))
|
343
|
+
return
|
344
|
+
|
345
|
+
if call_args.date and self.is_ansible_core:
|
346
|
+
self.add_message('ansible-deprecated-date-not-permitted', node=node, args=(name,))
|
347
|
+
return
|
348
|
+
|
349
|
+
if call_args.version and call_args.date:
|
350
|
+
self.add_message('ansible-deprecated-both-version-and-date', node=node, args=(name,))
|
351
|
+
return
|
352
|
+
|
353
|
+
if call_args.date:
|
354
|
+
self.check_date(node, name, call_args)
|
355
|
+
|
356
|
+
if call_args.version:
|
357
|
+
self.check_version(node, name, call_args)
|
358
|
+
|
359
|
+
@staticmethod
|
360
|
+
def get_deprecation_call_args(node: astroid.Call, args: tuple[str, ...]) -> DeprecationCallArgs:
|
361
|
+
"""Get the deprecation call arguments from the given node."""
|
362
|
+
fields: dict[str, object] = {}
|
363
|
+
|
364
|
+
for idx, arg in enumerate(node.args):
|
365
|
+
field = args[idx]
|
366
|
+
fields[field] = arg
|
367
|
+
|
368
|
+
for keyword in node.keywords:
|
369
|
+
if keyword.arg is not None:
|
370
|
+
fields[keyword.arg] = keyword.value
|
371
|
+
|
372
|
+
for key, value in fields.items():
|
373
|
+
if isinstance(value, astroid.Const):
|
374
|
+
fields[key] = value.value
|
375
|
+
|
376
|
+
return DeprecationCallArgs(**fields)
|
377
|
+
|
378
|
+
def check_collection_name(self, node: astroid.Call, name: str, args: DeprecationCallArgs) -> None:
|
379
|
+
"""Check the collection name provided to the given call node."""
|
380
|
+
deprecator_requirement = self.is_deprecator_required()
|
381
|
+
|
382
|
+
if self.is_ansible_core and args.collection_name:
|
383
|
+
self.add_message('ansible-deprecated-collection-name-not-permitted', node=node, args=(name,))
|
384
|
+
return
|
385
|
+
|
386
|
+
if args.collection_name and args.deprecator:
|
387
|
+
self.add_message('ansible-deprecated-both-collection-name-and-deprecator', node=node, args=(name,))
|
388
|
+
|
389
|
+
if deprecator_requirement is True:
|
390
|
+
if not args.collection_name and not args.deprecator:
|
391
|
+
self.add_message('ansible-deprecated-no-collection-name', node=node, args=(name,))
|
392
|
+
return
|
393
|
+
elif deprecator_requirement is False:
|
394
|
+
if args.collection_name:
|
395
|
+
self.add_message('ansible-deprecated-unnecessary-collection-name', node=node, args=('collection_name', name,))
|
396
|
+
return
|
397
|
+
|
398
|
+
if args.deprecator:
|
399
|
+
self.add_message('ansible-deprecated-unnecessary-collection-name', node=node, args=('deprecator', name,))
|
400
|
+
return
|
401
|
+
else:
|
402
|
+
# collection_name may be needed for backward compat with 2.18 and earlier, since it is only detected in 2.19 and later
|
403
|
+
|
404
|
+
if args.deprecator:
|
405
|
+
# Unlike collection_name, which is needed for backward compat, deprecator is generally not needed by collections.
|
406
|
+
# For the very rare cases where this is needed by collections, an inline pylint ignore can be used to silence it.
|
407
|
+
self.add_message('ansible-deprecated-unnecessary-collection-name', node=node, args=('deprecator', name,))
|
408
|
+
return
|
409
|
+
|
410
|
+
expected_collection_name = 'ansible.builtin' if self.is_ansible_core else self.collection_name
|
411
|
+
|
412
|
+
if args.collection_name and args.collection_name != expected_collection_name:
|
413
|
+
# if collection_name is provided and a constant, report when it does not match the expected name
|
414
|
+
self.add_message('wrong-collection-deprecated', node=node, args=(args.collection_name, name))
|
415
|
+
|
416
|
+
def check_version(self, node: astroid.Call, name: str, args: DeprecationCallArgs) -> None:
|
417
|
+
"""Check the version provided to the given call node."""
|
418
|
+
if self.collection_name:
|
419
|
+
self.check_collection_version(node, name, args)
|
420
|
+
else:
|
421
|
+
self.check_core_version(node, name, args)
|
422
|
+
|
423
|
+
def check_core_version(self, node: astroid.Call, name: str, args: DeprecationCallArgs) -> None:
|
424
|
+
"""Check the core version provided to the given call node."""
|
425
|
+
try:
|
426
|
+
if not isinstance(args.version, str) or not args.version:
|
427
|
+
raise ValueError()
|
428
|
+
|
429
|
+
strict_version = StrictVersion(args.version)
|
430
|
+
except ValueError:
|
431
|
+
self.add_message('ansible-invalid-deprecated-version', node=node, args=(args.version, name))
|
432
|
+
return
|
433
|
+
|
434
|
+
if self.ANSIBLE_VERSION >= strict_version:
|
435
|
+
self.add_message('ansible-deprecated-version', node=node, args=(args.version, name))
|
436
|
+
|
437
|
+
def check_collection_version(self, node: astroid.Call, name: str, args: DeprecationCallArgs) -> None:
|
438
|
+
"""Check the collection version provided to the given call node."""
|
439
|
+
try:
|
440
|
+
if not isinstance(args.version, str) or not args.version:
|
441
|
+
raise ValueError()
|
442
|
+
|
443
|
+
semantic_version = SemanticVersion(args.version)
|
444
|
+
except ValueError:
|
445
|
+
self.add_message('collection-invalid-deprecated-version', node=node, args=(args.version, name))
|
446
|
+
return
|
447
|
+
|
448
|
+
if self.collection_version >= semantic_version:
|
449
|
+
self.add_message('collection-deprecated-version', node=node, args=(args.version, name))
|
450
|
+
|
451
|
+
if semantic_version.major != 0 and (semantic_version.minor != 0 or semantic_version.patch != 0):
|
452
|
+
self.add_message('removal-version-must-be-major', node=node, args=(args.version,))
|
453
|
+
|
454
|
+
def check_date(self, node: astroid.Call, name: str, args: DeprecationCallArgs) -> None:
|
455
|
+
"""Check the date provided to the given call node."""
|
456
|
+
try:
|
457
|
+
date_parsed = self.parse_isodate(args.date)
|
458
|
+
except (ValueError, TypeError):
|
459
|
+
self.add_message('ansible-invalid-deprecated-date', node=node, args=(args.date, name))
|
460
|
+
else:
|
461
|
+
if date_parsed < self.today_utc:
|
462
|
+
self.add_message('ansible-expired-deprecated-date', node=node, args=(args.date, name))
|
463
|
+
|
464
|
+
@staticmethod
|
465
|
+
def parse_isodate(value: object) -> datetime.date:
|
466
|
+
"""Parse an ISO 8601 date string."""
|
467
|
+
if isinstance(value, str):
|
468
|
+
return datetime.date.fromisoformat(value)
|
469
|
+
|
470
|
+
raise TypeError(type(value))
|
471
|
+
|
472
|
+
|
473
|
+
def register(linter: pylint.lint.PyLinter) -> None:
|
474
|
+
"""Required method to auto-register this checker."""
|
475
|
+
linter.register_checker(AnsibleDeprecatedChecker(linter))
|
@@ -0,0 +1,137 @@
|
|
1
|
+
"""Ansible-specific pylint plugin for checking deprecation comments."""
|
2
|
+
|
3
|
+
# (c) 2018, Matt Martz <matt@sivel.net>
|
4
|
+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
5
|
+
|
6
|
+
from __future__ import annotations
|
7
|
+
|
8
|
+
import shlex
|
9
|
+
import tokenize
|
10
|
+
|
11
|
+
import pylint.checkers
|
12
|
+
import pylint.lint
|
13
|
+
|
14
|
+
import ansible.release
|
15
|
+
|
16
|
+
from ansible.module_utils.compat.version import LooseVersion
|
17
|
+
|
18
|
+
|
19
|
+
class AnsibleDeprecatedCommentChecker(pylint.checkers.BaseTokenChecker):
|
20
|
+
"""Checks for ``# deprecated:`` comments to ensure that the ``version`` has not passed or met the time for removal."""
|
21
|
+
|
22
|
+
name = 'deprecated-comment'
|
23
|
+
msgs = {
|
24
|
+
'E9601': (
|
25
|
+
"Deprecated core version (%r) found: %s",
|
26
|
+
"ansible-deprecated-version-comment",
|
27
|
+
None,
|
28
|
+
),
|
29
|
+
'E9602': (
|
30
|
+
"Deprecated comment contains invalid keys %r",
|
31
|
+
"ansible-deprecated-version-comment-invalid-key",
|
32
|
+
None,
|
33
|
+
),
|
34
|
+
'E9603': (
|
35
|
+
"Deprecated comment missing version",
|
36
|
+
"ansible-deprecated-version-comment-missing-version",
|
37
|
+
None,
|
38
|
+
),
|
39
|
+
'E9604': (
|
40
|
+
"Deprecated python version (%r) found: %s",
|
41
|
+
"ansible-deprecated-python-version-comment",
|
42
|
+
None,
|
43
|
+
),
|
44
|
+
'E9605': (
|
45
|
+
"Deprecated comment contains invalid version %r: %s",
|
46
|
+
"ansible-deprecated-version-comment-invalid-version",
|
47
|
+
None,
|
48
|
+
),
|
49
|
+
}
|
50
|
+
|
51
|
+
ANSIBLE_VERSION = LooseVersion('.'.join(ansible.release.__version__.split('.')[:3]))
|
52
|
+
"""The current ansible-core X.Y.Z version."""
|
53
|
+
|
54
|
+
def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None:
|
55
|
+
for token in tokens:
|
56
|
+
if token.type == tokenize.COMMENT:
|
57
|
+
self._process_comment(token)
|
58
|
+
|
59
|
+
def _deprecated_string_to_dict(self, token: tokenize.TokenInfo, string: str) -> dict[str, str]:
|
60
|
+
valid_keys = {'description', 'core_version', 'python_version'}
|
61
|
+
data = dict.fromkeys(valid_keys)
|
62
|
+
for opt in shlex.split(string):
|
63
|
+
if '=' not in opt:
|
64
|
+
data[opt] = None
|
65
|
+
continue
|
66
|
+
key, _sep, value = opt.partition('=')
|
67
|
+
data[key] = value
|
68
|
+
if not any((data['core_version'], data['python_version'])):
|
69
|
+
self.add_message(
|
70
|
+
'ansible-deprecated-version-comment-missing-version',
|
71
|
+
line=token.start[0],
|
72
|
+
col_offset=token.start[1],
|
73
|
+
)
|
74
|
+
bad = set(data).difference(valid_keys)
|
75
|
+
if bad:
|
76
|
+
self.add_message(
|
77
|
+
'ansible-deprecated-version-comment-invalid-key',
|
78
|
+
line=token.start[0],
|
79
|
+
col_offset=token.start[1],
|
80
|
+
args=(','.join(bad),),
|
81
|
+
)
|
82
|
+
return data
|
83
|
+
|
84
|
+
def _process_python_version(self, token: tokenize.TokenInfo, data: dict[str, str]) -> None:
|
85
|
+
check_version = '.'.join(map(str, self.linter.config.py_version)) # minimum supported Python version provided by ansible-test
|
86
|
+
|
87
|
+
try:
|
88
|
+
if LooseVersion(check_version) > LooseVersion(data['python_version']):
|
89
|
+
self.add_message(
|
90
|
+
'ansible-deprecated-python-version-comment',
|
91
|
+
line=token.start[0],
|
92
|
+
col_offset=token.start[1],
|
93
|
+
args=(
|
94
|
+
data['python_version'],
|
95
|
+
data['description'] or 'description not provided',
|
96
|
+
),
|
97
|
+
)
|
98
|
+
except (ValueError, TypeError) as exc:
|
99
|
+
self.add_message(
|
100
|
+
'ansible-deprecated-version-comment-invalid-version',
|
101
|
+
line=token.start[0],
|
102
|
+
col_offset=token.start[1],
|
103
|
+
args=(data['python_version'], exc),
|
104
|
+
)
|
105
|
+
|
106
|
+
def _process_core_version(self, token: tokenize.TokenInfo, data: dict[str, str]) -> None:
|
107
|
+
try:
|
108
|
+
if self.ANSIBLE_VERSION >= LooseVersion(data['core_version']):
|
109
|
+
self.add_message(
|
110
|
+
'ansible-deprecated-version-comment',
|
111
|
+
line=token.start[0],
|
112
|
+
col_offset=token.start[1],
|
113
|
+
args=(
|
114
|
+
data['core_version'],
|
115
|
+
data['description'] or 'description not provided',
|
116
|
+
),
|
117
|
+
)
|
118
|
+
except (ValueError, TypeError) as exc:
|
119
|
+
self.add_message(
|
120
|
+
'ansible-deprecated-version-comment-invalid-version',
|
121
|
+
line=token.start[0],
|
122
|
+
col_offset=token.start[1],
|
123
|
+
args=(data['core_version'], exc),
|
124
|
+
)
|
125
|
+
|
126
|
+
def _process_comment(self, token: tokenize.TokenInfo) -> None:
|
127
|
+
if token.string.startswith('# deprecated:'):
|
128
|
+
data = self._deprecated_string_to_dict(token, token.string[13:].strip())
|
129
|
+
if data['core_version']:
|
130
|
+
self._process_core_version(token, data)
|
131
|
+
if data['python_version']:
|
132
|
+
self._process_python_version(token, data)
|
133
|
+
|
134
|
+
|
135
|
+
def register(linter: pylint.lint.PyLinter) -> None:
|
136
|
+
"""Required method to auto-register this checker."""
|
137
|
+
linter.register_checker(AnsibleDeprecatedCommentChecker(linter))
|
@@ -1,64 +0,0 @@
|
|
1
|
-
"""Patch broken ClassVar support in dataclasses when ClassVar is accessed via a module other than `typing`."""
|
2
|
-
|
3
|
-
# deprecated: description='verify ClassVar support in dataclasses has been fixed in Python before removing this patching code', python_version='3.12'
|
4
|
-
|
5
|
-
from __future__ import annotations
|
6
|
-
|
7
|
-
import dataclasses
|
8
|
-
import sys
|
9
|
-
import typing as t
|
10
|
-
|
11
|
-
# trigger the bug by exposing typing.ClassVar via a module reference that is not `typing`
|
12
|
-
_ts = sys.modules[__name__]
|
13
|
-
ClassVar = t.ClassVar
|
14
|
-
|
15
|
-
|
16
|
-
def patch_dataclasses_is_type() -> None:
|
17
|
-
if not _is_patch_needed():
|
18
|
-
return # pragma: nocover
|
19
|
-
|
20
|
-
try:
|
21
|
-
real_is_type = dataclasses._is_type # type: ignore[attr-defined]
|
22
|
-
except AttributeError: # pragma: nocover
|
23
|
-
raise RuntimeError("unable to patch broken dataclasses ClassVar support") from None
|
24
|
-
|
25
|
-
# patch dataclasses._is_type - impl from https://github.com/python/cpython/blob/4c6d4f5cb33e48519922d635894eef356faddba2/Lib/dataclasses.py#L709-L765
|
26
|
-
def _is_type(annotation, cls, a_module, a_type, is_type_predicate):
|
27
|
-
match = dataclasses._MODULE_IDENTIFIER_RE.match(annotation) # type: ignore[attr-defined]
|
28
|
-
if match:
|
29
|
-
ns = None
|
30
|
-
module_name = match.group(1)
|
31
|
-
if not module_name:
|
32
|
-
# No module name, assume the class's module did
|
33
|
-
# "from dataclasses import InitVar".
|
34
|
-
ns = sys.modules.get(cls.__module__).__dict__
|
35
|
-
else:
|
36
|
-
# Look up module_name in the class's module.
|
37
|
-
module = sys.modules.get(cls.__module__)
|
38
|
-
if module and module.__dict__.get(module_name): # this is the patched line; removed `is a_module`
|
39
|
-
ns = sys.modules.get(a_type.__module__).__dict__
|
40
|
-
if ns and is_type_predicate(ns.get(match.group(2)), a_module):
|
41
|
-
return True
|
42
|
-
return False
|
43
|
-
|
44
|
-
_is_type._orig_impl = real_is_type # type: ignore[attr-defined] # stash this away to allow unit tests to undo the patch
|
45
|
-
|
46
|
-
dataclasses._is_type = _is_type # type: ignore[attr-defined]
|
47
|
-
|
48
|
-
try:
|
49
|
-
if _is_patch_needed():
|
50
|
-
raise RuntimeError("patching had no effect") # pragma: nocover
|
51
|
-
except Exception as ex: # pragma: nocover
|
52
|
-
dataclasses._is_type = real_is_type # type: ignore[attr-defined]
|
53
|
-
raise RuntimeError("dataclasses ClassVar support is still broken after patching") from ex
|
54
|
-
|
55
|
-
|
56
|
-
def _is_patch_needed() -> bool:
|
57
|
-
@dataclasses.dataclass
|
58
|
-
class CheckClassVar:
|
59
|
-
# this is the broken case requiring patching: ClassVar dot-referenced from a module that is not `typing` is treated as an instance field
|
60
|
-
# DTFIX-RELEASE: add link to CPython bug report to-be-filed (or update associated deprecation comments if we don't)
|
61
|
-
a_classvar: _ts.ClassVar[int] # type: ignore[name-defined]
|
62
|
-
a_field: int
|
63
|
-
|
64
|
-
return len(dataclasses.fields(CheckClassVar)) != 1
|