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.
Files changed (92) hide show
  1. ansible/_internal/_ansiballz.py +1 -4
  2. ansible/_internal/_collection_proxy.py +47 -0
  3. ansible/_internal/_errors/_handler.py +4 -4
  4. ansible/_internal/_json/__init__.py +47 -4
  5. ansible/_internal/_json/_profiles/_legacy.py +2 -3
  6. ansible/_internal/_templating/_datatag.py +3 -4
  7. ansible/_internal/_templating/_engine.py +6 -1
  8. ansible/_internal/_templating/_jinja_bits.py +4 -4
  9. ansible/_internal/_templating/_jinja_plugins.py +7 -17
  10. ansible/cli/__init__.py +12 -5
  11. ansible/cli/arguments/option_helpers.py +4 -1
  12. ansible/cli/doc.py +14 -8
  13. ansible/config/base.yml +17 -20
  14. ansible/config/manager.py +2 -2
  15. ansible/constants.py +0 -62
  16. ansible/errors/__init__.py +6 -2
  17. ansible/executor/module_common.py +11 -7
  18. ansible/executor/process/worker.py +31 -26
  19. ansible/executor/task_executor.py +38 -31
  20. ansible/executor/task_queue_manager.py +62 -52
  21. ansible/executor/task_result.py +168 -72
  22. ansible/galaxy/api.py +1 -1
  23. ansible/galaxy/collection/__init__.py +3 -3
  24. ansible/inventory/manager.py +2 -1
  25. ansible/module_utils/_internal/_ansiballz.py +4 -30
  26. ansible/module_utils/_internal/_datatag/_tags.py +3 -25
  27. ansible/module_utils/_internal/_deprecator.py +134 -0
  28. ansible/module_utils/_internal/_plugin_info.py +25 -0
  29. ansible/module_utils/_internal/_validation.py +14 -0
  30. ansible/module_utils/ansible_release.py +1 -1
  31. ansible/module_utils/basic.py +68 -23
  32. ansible/module_utils/common/arg_spec.py +8 -3
  33. ansible/module_utils/common/messages.py +40 -23
  34. ansible/module_utils/common/process.py +0 -1
  35. ansible/module_utils/common/respawn.py +0 -7
  36. ansible/module_utils/common/warnings.py +13 -13
  37. ansible/module_utils/datatag.py +13 -13
  38. ansible/modules/async_status.py +1 -1
  39. ansible/modules/dnf5.py +1 -1
  40. ansible/modules/get_url.py +1 -1
  41. ansible/parsing/utils/jsonify.py +40 -0
  42. ansible/parsing/yaml/objects.py +16 -5
  43. ansible/playbook/included_file.py +25 -12
  44. ansible/playbook/task.py +0 -2
  45. ansible/plugins/__init__.py +18 -8
  46. ansible/plugins/action/__init__.py +6 -14
  47. ansible/plugins/action/gather_facts.py +2 -4
  48. ansible/plugins/callback/__init__.py +173 -86
  49. ansible/plugins/callback/default.py +79 -79
  50. ansible/plugins/callback/junit.py +20 -19
  51. ansible/plugins/callback/minimal.py +17 -17
  52. ansible/plugins/callback/oneline.py +23 -16
  53. ansible/plugins/callback/tree.py +13 -6
  54. ansible/plugins/connection/local.py +1 -1
  55. ansible/plugins/connection/paramiko_ssh.py +9 -2
  56. ansible/plugins/doc_fragments/action_core.py +1 -1
  57. ansible/plugins/filter/core.py +12 -2
  58. ansible/plugins/inventory/__init__.py +2 -2
  59. ansible/plugins/loader.py +194 -130
  60. ansible/plugins/lookup/url.py +2 -2
  61. ansible/plugins/strategy/__init__.py +76 -82
  62. ansible/plugins/strategy/free.py +4 -4
  63. ansible/plugins/strategy/linear.py +11 -9
  64. ansible/plugins/test/core.py +1 -1
  65. ansible/release.py +1 -1
  66. ansible/template/__init__.py +8 -6
  67. ansible/utils/collection_loader/_collection_meta.py +5 -3
  68. ansible/utils/display.py +141 -79
  69. ansible/utils/py3compat.py +1 -7
  70. ansible/utils/ssh_functions.py +4 -1
  71. ansible/utils/vars.py +23 -0
  72. ansible/vars/clean.py +1 -1
  73. ansible/vars/manager.py +18 -27
  74. ansible/vars/plugins.py +4 -4
  75. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/METADATA +1 -1
  76. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/RECORD +89 -85
  77. ansible_test/_internal/commands/sanity/pylint.py +1 -0
  78. ansible_test/_internal/docker_util.py +4 -3
  79. ansible_test/_util/controller/sanity/pylint/plugins/deprecated_calls.py +475 -0
  80. ansible_test/_util/controller/sanity/pylint/plugins/deprecated_comment.py +137 -0
  81. ansible/module_utils/_internal/_dataclass_annotation_patch.py +0 -64
  82. ansible/module_utils/_internal/_plugin_exec_context.py +0 -49
  83. ansible_test/_util/controller/sanity/pylint/plugins/deprecated.py +0 -399
  84. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/Apache-License.txt +0 -0
  85. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/BSD-3-Clause.txt +0 -0
  86. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/COPYING +0 -0
  87. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/MIT-license.txt +0 -0
  88. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/PSF-license.txt +0 -0
  89. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/WHEEL +0 -0
  90. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/entry_points.txt +0 -0
  91. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/simplified_bsd.txt +0 -0
  92. {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