claude-dev-env 1.50.0 → 1.50.2

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 (82) hide show
  1. package/hooks/blocking/_gh_body_arg_utils.py +67 -11
  2. package/hooks/blocking/_md_to_html_blocker_test_support.py +65 -0
  3. package/hooks/blocking/code_rules_annotations_length.py +167 -0
  4. package/hooks/blocking/code_rules_banned_identifiers.py +385 -0
  5. package/hooks/blocking/code_rules_boolean_mustcheck.py +350 -0
  6. package/hooks/blocking/code_rules_comments.py +337 -0
  7. package/hooks/blocking/code_rules_constants_config.py +252 -0
  8. package/hooks/blocking/code_rules_docstrings.py +308 -0
  9. package/hooks/blocking/code_rules_enforcer.py +98 -5765
  10. package/hooks/blocking/code_rules_imports_logging.py +276 -0
  11. package/hooks/blocking/code_rules_magic_values.py +180 -0
  12. package/hooks/blocking/code_rules_mock_completeness.py +295 -0
  13. package/hooks/blocking/code_rules_naming_collection.py +264 -0
  14. package/hooks/blocking/code_rules_optional_params.py +288 -0
  15. package/hooks/blocking/code_rules_paths_syspath.py +186 -0
  16. package/hooks/blocking/code_rules_probe_chains.py +305 -0
  17. package/hooks/blocking/code_rules_probe_detection.py +257 -0
  18. package/hooks/blocking/code_rules_probe_recording.py +225 -0
  19. package/hooks/blocking/code_rules_scope_binding.py +151 -0
  20. package/hooks/blocking/code_rules_shared.py +301 -0
  21. package/hooks/blocking/code_rules_string_magic.py +207 -0
  22. package/hooks/blocking/code_rules_test_assertions.py +226 -0
  23. package/hooks/blocking/code_rules_test_branching_except.py +181 -0
  24. package/hooks/blocking/code_rules_test_isolation.py +341 -0
  25. package/hooks/blocking/code_rules_type_escape.py +341 -0
  26. package/hooks/blocking/code_rules_typeddict_stub.py +305 -0
  27. package/hooks/blocking/code_rules_unused_imports.py +256 -0
  28. package/hooks/blocking/conftest.py +30 -0
  29. package/hooks/blocking/pr_description_body_audit.py +148 -0
  30. package/hooks/blocking/pr_description_command_parser.py +233 -0
  31. package/hooks/blocking/pr_description_enforcer.py +36 -825
  32. package/hooks/blocking/pr_description_pr_number.py +153 -0
  33. package/hooks/blocking/pr_description_readability.py +366 -0
  34. package/hooks/blocking/tdd_enforcer.py +31 -0
  35. package/hooks/blocking/test_code_rules_constants_config.py +26 -0
  36. package/hooks/blocking/test_code_rules_enforcer_banned_noun_word.py +5 -2
  37. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -5
  38. package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +21 -15
  39. package/hooks/blocking/test_code_rules_enforcer_config_path.py +20 -16
  40. package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +4 -2
  41. package/hooks/blocking/test_code_rules_enforcer_function_length.py +154 -18
  42. package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +1 -2
  43. package/hooks/blocking/test_code_rules_enforcer_ignored_must_check_return.py +22 -12
  44. package/hooks/blocking/test_code_rules_enforcer_split_annotations_length.py +55 -0
  45. package/hooks/blocking/test_code_rules_enforcer_split_banned.py +170 -0
  46. package/hooks/blocking/test_code_rules_enforcer_split_comments.py +60 -0
  47. package/hooks/blocking/test_code_rules_enforcer_split_config_path.py +52 -0
  48. package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +236 -0
  49. package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +296 -0
  50. package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +238 -0
  51. package/hooks/blocking/test_code_rules_enforcer_split_isolation_1.py +271 -0
  52. package/hooks/blocking/test_code_rules_enforcer_split_isolation_2.py +283 -0
  53. package/hooks/blocking/test_code_rules_enforcer_split_isolation_3.py +268 -0
  54. package/hooks/blocking/test_code_rules_enforcer_split_isolation_4.py +85 -0
  55. package/hooks/blocking/test_code_rules_enforcer_split_mocks_1.py +303 -0
  56. package/hooks/blocking/test_code_rules_enforcer_split_mocks_2.py +111 -0
  57. package/hooks/blocking/test_code_rules_enforcer_split_mustcheck.py +87 -0
  58. package/hooks/blocking/test_code_rules_enforcer_split_naming.py +107 -0
  59. package/hooks/blocking/test_code_rules_enforcer_split_optional_params.py +325 -0
  60. package/hooks/blocking/test_code_rules_enforcer_split_paths_syspath.py +110 -0
  61. package/hooks/blocking/test_code_rules_enforcer_split_shared.py +44 -0
  62. package/hooks/blocking/test_code_rules_enforcer_split_string_magic.py +55 -0
  63. package/hooks/blocking/test_code_rules_enforcer_split_test_assertions.py +56 -0
  64. package/hooks/blocking/test_code_rules_enforcer_todo_markers.py +21 -15
  65. package/hooks/blocking/test_code_rules_paths_syspath.py +26 -0
  66. package/hooks/blocking/test_md_to_html_blocker_exemptions.py +368 -0
  67. package/hooks/blocking/test_md_to_html_blocker_extensions.py +157 -0
  68. package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +336 -0
  69. package/hooks/blocking/test_pr_description_enforcer.py +13 -1499
  70. package/hooks/blocking/test_pr_description_enforcer_body_audit.py +247 -0
  71. package/hooks/blocking/test_pr_description_enforcer_body_rules.py +493 -0
  72. package/hooks/blocking/test_pr_description_enforcer_command_parser.py +366 -0
  73. package/hooks/blocking/test_pr_description_enforcer_pr_number.py +159 -0
  74. package/hooks/blocking/test_pr_description_enforcer_readability.py +443 -0
  75. package/hooks/blocking/test_tdd_enforcer.py +116 -0
  76. package/hooks/hooks_constants/blocking_check_limits.py +3 -0
  77. package/hooks/hooks_constants/code_rules_enforcer_constants.py +8 -0
  78. package/hooks/hooks_constants/pr_description_enforcer_constants.py +7 -0
  79. package/hooks/hooks_constants/sys_path_insert_constants.py +1 -0
  80. package/package.json +1 -1
  81. package/hooks/blocking/test_code_rules_enforcer.py +0 -2669
  82. package/hooks/blocking/test_md_to_html_blocker.py +0 -810
@@ -0,0 +1,305 @@
1
+ """Attribute-chain and os.environ alias-resolution primitives for the test-isolation check."""
2
+
3
+ import ast
4
+ import sys
5
+ from collections.abc import Iterator
6
+ from pathlib import Path
7
+
8
+ _blocking_directory = str(Path(__file__).resolve().parent)
9
+ _hooks_directory = str(Path(__file__).resolve().parent.parent)
10
+ if _blocking_directory not in sys.path:
11
+ sys.path.insert(0, _blocking_directory)
12
+ if _hooks_directory not in sys.path:
13
+ sys.path.insert(0, _hooks_directory)
14
+
15
+ from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
16
+ ALL_CANONICAL_DOTTED_NAMES_BY_BARE_IMPORT,
17
+ ALL_ENVIRONMENT_GETTER_DOTTED_NAMES,
18
+ ALL_PROBE_ALIASABLE_CANONICAL_PREFIXES,
19
+ ALL_PROBE_RELEVANT_MODULE_CANONICAL_NAMES,
20
+ ENVIRON_GET_METHOD_NAME,
21
+ OS_ENVIRON_DOTTED_NAME,
22
+ )
23
+
24
+
25
+ def _dotted_call_attribute_chain(call_node: ast.Call) -> str | None:
26
+ """Return the dotted name path of *call_node*'s callee, or None.
27
+
28
+ For ``pathlib.Path.home()`` returns ``"pathlib.Path.home"``; for
29
+ ``Path.home()`` returns ``"Path.home"``; for ``tempfile.gettempdir()``
30
+ returns ``"tempfile.gettempdir"``. Returns ``None`` when the call target
31
+ is not a pure attribute chain rooted at an ``ast.Name`` (for example,
32
+ ``obj.method()`` where ``obj`` is the result of another expression).
33
+ """
34
+ chain_parts: list[str] = []
35
+ walker: ast.expr = call_node.func
36
+ while isinstance(walker, ast.Attribute):
37
+ chain_parts.append(walker.attr)
38
+ walker = walker.value
39
+ if not isinstance(walker, ast.Name):
40
+ return None
41
+ chain_parts.append(walker.id)
42
+ chain_parts.reverse()
43
+ return ".".join(chain_parts)
44
+
45
+
46
+ def _record_probe_import_aliases(
47
+ import_node: ast.Import | ast.ImportFrom,
48
+ all_canonical_names_by_alias: dict[str, str],
49
+ ) -> None:
50
+ """Record the probe-relevant alias entries from a single import statement.
51
+
52
+ Module aliases are recorded only for the probe-relevant modules in
53
+ ``ALL_PROBE_RELEVANT_MODULE_CANONICAL_NAMES``. Bare-imported names are
54
+ recorded only for the ``(module, name)`` pairs in
55
+ ``ALL_CANONICAL_DOTTED_NAMES_BY_BARE_IMPORT``. Imports outside those sets are
56
+ ignored so unrelated bindings never rewrite a chain.
57
+
58
+ Args:
59
+ import_node: A single ``ast.Import`` or ``ast.ImportFrom`` statement.
60
+ all_canonical_names_by_alias: The alias map to mutate in place with any
61
+ probe-relevant local-name to canonical-dotted-prefix entries.
62
+ """
63
+ if isinstance(import_node, ast.Import):
64
+ for each_alias in import_node.names:
65
+ if each_alias.name not in ALL_PROBE_RELEVANT_MODULE_CANONICAL_NAMES:
66
+ continue
67
+ local_name = each_alias.asname or each_alias.name
68
+ all_canonical_names_by_alias[local_name] = each_alias.name
69
+ return
70
+ for each_alias in import_node.names:
71
+ canonical_dotted = ALL_CANONICAL_DOTTED_NAMES_BY_BARE_IMPORT.get(
72
+ (import_node.module or "", each_alias.name)
73
+ )
74
+ if canonical_dotted is None:
75
+ continue
76
+ local_name = each_alias.asname or each_alias.name
77
+ all_canonical_names_by_alias[local_name] = canonical_dotted
78
+
79
+
80
+ def _node_is_lexically_inside_function_or_class(
81
+ node: ast.AST, parent_by_child_id: dict[int, ast.AST],
82
+ ) -> bool:
83
+ """Return True when *node* is nested inside a function or class body.
84
+
85
+ Walks ancestors via *parent_by_child_id*. A node nested only inside
86
+ module-level ``try``/``if``/``with`` blocks has no enclosing function or
87
+ class and is module-scoped; a node inside a
88
+ ``FunctionDef``/``AsyncFunctionDef``/``ClassDef`` body is scoped to that
89
+ enclosing definition and is not module-scoped. A class-body import binds
90
+ its alias only within the class namespace, so it must not enter the
91
+ module-wide alias map any more than a function-local import does.
92
+
93
+ Args:
94
+ node: The node whose lexical scope is being classified.
95
+ parent_by_child_id: Child-``id()``-to-parent map from
96
+ ``_build_parent_map``.
97
+
98
+ Returns:
99
+ True when an enclosing
100
+ ``FunctionDef``/``AsyncFunctionDef``/``ClassDef`` exists.
101
+ """
102
+ current_ancestor = parent_by_child_id.get(id(node))
103
+ while current_ancestor is not None:
104
+ if isinstance(
105
+ current_ancestor,
106
+ (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef),
107
+ ):
108
+ return True
109
+ current_ancestor = parent_by_child_id.get(id(current_ancestor))
110
+ return False
111
+
112
+
113
+ def _canonical_probe_prefix_for_value(
114
+ node: ast.expr, all_canonical_names_by_alias: dict[str, str],
115
+ ) -> str | None:
116
+ if isinstance(node, ast.Name):
117
+ candidate_prefix = all_canonical_names_by_alias.get(node.id, node.id)
118
+ elif isinstance(node, ast.Attribute):
119
+ attribute_chain = _dotted_attribute_chain(node)
120
+ if attribute_chain is None:
121
+ return None
122
+ candidate_prefix = _resolve_chain_through_aliases(
123
+ attribute_chain, all_canonical_names_by_alias
124
+ )
125
+ else:
126
+ return None
127
+ if candidate_prefix in ALL_PROBE_ALIASABLE_CANONICAL_PREFIXES:
128
+ return candidate_prefix
129
+ return None
130
+
131
+
132
+ def _attribute_chain_resolves_to_os_environ(
133
+ node: ast.expr, all_canonical_names_by_alias: dict[str, str],
134
+ ) -> bool:
135
+ if not isinstance(node, ast.Attribute):
136
+ return False
137
+ chain = _dotted_attribute_chain(node)
138
+ if chain is None:
139
+ return False
140
+ canonical_chain = _resolve_chain_through_aliases(
141
+ chain, all_canonical_names_by_alias
142
+ )
143
+ return canonical_chain == OS_ENVIRON_DOTTED_NAME
144
+
145
+
146
+ def _dotted_attribute_chain(attribute_node: ast.Attribute) -> str | None:
147
+ chain_parts: list[str] = []
148
+ walker: ast.expr = attribute_node
149
+ while isinstance(walker, ast.Attribute):
150
+ chain_parts.append(walker.attr)
151
+ walker = walker.value
152
+ if not isinstance(walker, ast.Name):
153
+ return None
154
+ chain_parts.append(walker.id)
155
+ chain_parts.reverse()
156
+ return ".".join(chain_parts)
157
+
158
+
159
+ def _resolve_chain_through_aliases(
160
+ chain: str, all_canonical_names_by_alias: dict[str, str],
161
+ ) -> str:
162
+ """Rewrite the leading segment of *chain* through the alias map.
163
+
164
+ Args:
165
+ chain: A dotted callee chain such as ``"P.home"``,
166
+ ``"op.expanduser"``, or ``"o.path.expanduser"``.
167
+ all_canonical_names_by_alias: Local-binding-to-canonical-prefix
168
+ mapping from ``_build_alias_canonicalization_map``.
169
+
170
+ Returns:
171
+ The chain with its leading segment replaced by the canonical
172
+ (possibly multi-segment) prefix when a binding matches; otherwise
173
+ the chain unchanged.
174
+ """
175
+ first_segment, separator, remainder = chain.partition(".")
176
+ canonical_prefix = all_canonical_names_by_alias.get(first_segment)
177
+ if canonical_prefix is None:
178
+ return chain
179
+ if not separator:
180
+ return canonical_prefix
181
+ return f"{canonical_prefix}{separator}{remainder}"
182
+
183
+
184
+ def _environ_key_string_from_call(
185
+ call_node: ast.Call,
186
+ all_canonical_names_by_alias: dict[str, str],
187
+ all_environ_local_bindings: set[str],
188
+ ) -> str | None:
189
+ if not _call_is_environment_getter(call_node, all_canonical_names_by_alias, all_environ_local_bindings):
190
+ return None
191
+ if not call_node.args:
192
+ return None
193
+ first_argument = call_node.args[0]
194
+ if isinstance(first_argument, ast.Constant) and isinstance(first_argument.value, str):
195
+ return first_argument.value
196
+ return None
197
+
198
+
199
+ def _call_is_environment_getter(
200
+ call_node: ast.Call,
201
+ all_canonical_names_by_alias: dict[str, str],
202
+ all_environ_local_bindings: set[str],
203
+ ) -> bool:
204
+ """Return True when *call_node* reads an env var via a recognized getter.
205
+
206
+ Recognizes the canonical ``os.getenv(...)`` / ``os.environ.get(...)``
207
+ chains and the local-alias ``e.get(...)`` form where ``e`` is a name in
208
+ *all_environ_local_bindings* (a binding to ``os.environ`` collected from
209
+ the same test function).
210
+
211
+ Args:
212
+ call_node: The call to inspect.
213
+ all_canonical_names_by_alias: Import-alias map from
214
+ ``_build_alias_canonicalization_map``.
215
+ all_environ_local_bindings: Local names bound to ``os.environ`` within
216
+ the test function being analyzed.
217
+
218
+ Returns:
219
+ True when the call is an environment getter whose key argument is
220
+ worth inspecting.
221
+ """
222
+ if _call_targets_local_environ_get(call_node, all_environ_local_bindings):
223
+ return True
224
+ raw_chain = _dotted_call_attribute_chain(call_node)
225
+ if raw_chain is None:
226
+ return False
227
+ canonical_chain = _resolve_chain_through_aliases(raw_chain, all_canonical_names_by_alias)
228
+ return canonical_chain in ALL_ENVIRONMENT_GETTER_DOTTED_NAMES
229
+
230
+
231
+ def _call_targets_local_environ_get(
232
+ call_node: ast.Call, all_environ_local_bindings: set[str],
233
+ ) -> bool:
234
+ callee = call_node.func
235
+ if not isinstance(callee, ast.Attribute):
236
+ return False
237
+ if callee.attr != ENVIRON_GET_METHOD_NAME:
238
+ return False
239
+ receiver = callee.value
240
+ return isinstance(receiver, ast.Name) and receiver.id in all_environ_local_bindings
241
+
242
+
243
+ def _environ_key_string_from_subscript(
244
+ subscript_node: ast.Subscript,
245
+ all_canonical_names_by_alias: dict[str, str],
246
+ all_environ_local_bindings: set[str],
247
+ ) -> str | None:
248
+ if not _subscript_target_is_os_environ(
249
+ subscript_node.value, all_canonical_names_by_alias, all_environ_local_bindings
250
+ ):
251
+ return None
252
+ key_node = subscript_node.slice
253
+ if isinstance(key_node, ast.Constant) and isinstance(key_node.value, str):
254
+ return key_node.value
255
+ return None
256
+
257
+
258
+ def _subscript_target_is_os_environ(
259
+ target_node: ast.expr,
260
+ all_canonical_names_by_alias: dict[str, str],
261
+ all_environ_local_bindings: set[str],
262
+ ) -> bool:
263
+ if isinstance(target_node, ast.Name):
264
+ if target_node.id in all_environ_local_bindings:
265
+ return True
266
+ return all_canonical_names_by_alias.get(target_node.id) == OS_ENVIRON_DOTTED_NAME
267
+ if isinstance(target_node, ast.Attribute):
268
+ return _attribute_chain_resolves_to_os_environ(target_node, all_canonical_names_by_alias)
269
+ return False
270
+
271
+
272
+ def _children_to_descend_into(node: ast.AST) -> list[ast.AST]:
273
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda)):
274
+ return []
275
+ if isinstance(node, ast.ClassDef):
276
+ return list(node.body)
277
+ return list(ast.iter_child_nodes(node))
278
+
279
+
280
+ def _descend_within_test_scope(
281
+ function_node: ast.FunctionDef | ast.AsyncFunctionDef,
282
+ ) -> Iterator[ast.AST]:
283
+ """Yield every descendant of *function_node* on the test's own runtime path.
284
+
285
+ Bounded traversal that shares ``_children_to_descend_into`` so every caller
286
+ treats the same nodes as in scope. Nested function definitions, methods, and
287
+ lambdas are scope boundaries — Python does not run a callable's body just
288
+ because the callable (or its enclosing class) is defined, so a binding or
289
+ probe inside one does not leak onto the test's runtime path. Nested
290
+ ``ClassDef`` bodies stay in scope because their class-creation statements
291
+ (class attribute initializers) run as the ``class`` statement executes
292
+ during the test; descent stops at the methods declared in that class body.
293
+
294
+ Args:
295
+ function_node: The test function whose in-scope descendants to yield.
296
+
297
+ Yields:
298
+ Each descendant node within the test's bounded scope, in stack-pop
299
+ order.
300
+ """
301
+ nodes_to_visit: list[ast.AST] = list(ast.iter_child_nodes(function_node))
302
+ while nodes_to_visit:
303
+ each_descendant = nodes_to_visit.pop()
304
+ yield each_descendant
305
+ nodes_to_visit.extend(_children_to_descend_into(each_descendant))
@@ -0,0 +1,257 @@
1
+ """Home-directory and shared-temp value-reference detection for the test-isolation check."""
2
+
3
+ import ast
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ _blocking_directory = str(Path(__file__).resolve().parent)
8
+ _hooks_directory = str(Path(__file__).resolve().parent.parent)
9
+ if _blocking_directory not in sys.path:
10
+ sys.path.insert(0, _blocking_directory)
11
+ if _hooks_directory not in sys.path:
12
+ sys.path.insert(0, _hooks_directory)
13
+
14
+ from code_rules_probe_chains import ( # noqa: E402
15
+ _dotted_call_attribute_chain,
16
+ _environ_key_string_from_call,
17
+ _environ_key_string_from_subscript,
18
+ _resolve_chain_through_aliases,
19
+ )
20
+
21
+ from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
22
+ ALL_HOME_DIRECTORY_ENV_VAR_NAMES,
23
+ ALL_PATHLIB_PATH_CONSTRUCTOR_CANONICAL_NAMES,
24
+ ALL_SHARED_TEMP_SOURCE_PROBE_DOTTED_NAMES,
25
+ ENVIRONMENT_VARIABLE_REFERENCE_PATTERN,
26
+ HOME_DIRECTORY_TILDE_PREFIX,
27
+ PATHLIB_EXPANDUSER_METHOD_NAME,
28
+ TEMPFILE_FACTORY_ISOLATION_DIRECTORY_KEYWORD,
29
+ WINDOWS_PERCENT_VARIABLE_REFERENCE_PATTERN,
30
+ )
31
+
32
+
33
+ def _pathlib_path_construction_uses_home_tilde(
34
+ node: ast.expr, all_canonical_names_by_alias: dict[str, str],
35
+ ) -> bool:
36
+ """Return True for a ``pathlib.Path('~...')`` construction with a home tilde.
37
+
38
+ The node is a Path construction when its callee chain resolves (directly,
39
+ aliased, or fully qualified) to a member of
40
+ ``ALL_PATHLIB_PATH_CONSTRUCTOR_CANONICAL_NAMES``. It uses the home tilde
41
+ when its first argument is a literal string beginning with ``~``. A
42
+ tilde-free or dynamic first argument expands no home directory and returns
43
+ False, mirroring ``_expanduser_argument_references_home``.
44
+
45
+ Args:
46
+ node: The candidate ``Path(...)`` construction expression.
47
+ all_canonical_names_by_alias: Import-alias map from
48
+ ``_build_alias_canonicalization_map``.
49
+
50
+ Returns:
51
+ True when *node* constructs a ``pathlib.Path`` from a leading-tilde
52
+ literal string.
53
+ """
54
+ if not isinstance(node, ast.Call):
55
+ return False
56
+ constructor_chain = _dotted_call_attribute_chain(node)
57
+ if constructor_chain is None:
58
+ return False
59
+ canonical_chain = _resolve_chain_through_aliases(
60
+ constructor_chain, all_canonical_names_by_alias
61
+ )
62
+ if canonical_chain not in ALL_PATHLIB_PATH_CONSTRUCTOR_CANONICAL_NAMES:
63
+ return False
64
+ return _expanduser_argument_references_home(node)
65
+
66
+
67
+ def _expanduser_method_call_targets_pathlib_path(
68
+ call_node: ast.Call,
69
+ all_canonical_names_by_alias: dict[str, str],
70
+ all_path_local_bindings: set[str],
71
+ ) -> bool:
72
+ """Return True for a ``.expanduser()`` call on a home-tilde ``pathlib.Path``.
73
+
74
+ ``Path.expanduser`` expands the ``~`` bound into the receiver Path, so the
75
+ call resolves the home directory only when that receiver carries a leading
76
+ tilde. The receiver carries a tilde when it is a ``pathlib.Path('~...')``
77
+ construction (directly, aliased, or fully qualified) or a local variable
78
+ previously bound to such a construction. A tilde-free or dynamic receiver
79
+ (``Path('/tmp/x').expanduser()`` / ``Path(some_path).expanduser()``)
80
+ expands no home directory and is not flagged, keeping the form symmetric
81
+ with ``os.path.expanduser`` argument inspection.
82
+
83
+ Args:
84
+ call_node: The call whose callee attribute is ``expanduser``.
85
+ all_canonical_names_by_alias: Import-alias map from
86
+ ``_build_alias_canonicalization_map``.
87
+ all_path_local_bindings: Local names bound to a home-tilde
88
+ ``pathlib.Path`` construction from
89
+ ``_collect_pathlib_path_local_binding_names``.
90
+
91
+ Returns:
92
+ True when the ``expanduser`` receiver resolves to a home-tilde
93
+ ``pathlib.Path``.
94
+ """
95
+ callee = call_node.func
96
+ if not isinstance(callee, ast.Attribute):
97
+ return False
98
+ if callee.attr != PATHLIB_EXPANDUSER_METHOD_NAME:
99
+ return False
100
+ receiver = callee.value
101
+ if isinstance(receiver, ast.Name):
102
+ return receiver.id in all_path_local_bindings
103
+ return _pathlib_path_construction_uses_home_tilde(receiver, all_canonical_names_by_alias)
104
+
105
+
106
+ def _expandvars_argument_references_home_or_temp(call_node: ast.Call) -> bool:
107
+ """Return True when an ``expandvars`` call expands a home/temp env var.
108
+
109
+ Inspects the first string argument for dollar-style ``$NAME`` / ``${NAME}``
110
+ references and Windows percent-style ``%NAME%`` references, then reports
111
+ whether any referenced name is a home/temp env var. ``os.path.expandvars``
112
+ expands percent syntax on Windows, so both forms reach the same home/temp
113
+ env-var name set. A non-constant or absent argument is treated as not
114
+ referencing a home/temp variable, mirroring the conservative env-name
115
+ filtering applied to ``os.getenv``.
116
+
117
+ Args:
118
+ call_node: The ``os.path.expandvars(...)`` call node.
119
+
120
+ Returns:
121
+ True when at least one expanded variable name is in
122
+ ``ALL_HOME_DIRECTORY_ENV_VAR_NAMES``.
123
+ """
124
+ if not call_node.args:
125
+ return False
126
+ first_argument = call_node.args[0]
127
+ if not (
128
+ isinstance(first_argument, ast.Constant)
129
+ and isinstance(first_argument.value, str)
130
+ ):
131
+ return False
132
+ dollar_style_names = ENVIRONMENT_VARIABLE_REFERENCE_PATTERN.findall(
133
+ first_argument.value
134
+ )
135
+ percent_style_names = WINDOWS_PERCENT_VARIABLE_REFERENCE_PATTERN.findall(
136
+ first_argument.value
137
+ )
138
+ all_referenced_names = dollar_style_names + percent_style_names
139
+ return any(
140
+ each_name in ALL_HOME_DIRECTORY_ENV_VAR_NAMES
141
+ for each_name in all_referenced_names
142
+ )
143
+
144
+
145
+ def _expanduser_argument_references_home(call_node: ast.Call) -> bool:
146
+ """Return True when an ``expanduser`` call expands the home directory.
147
+
148
+ ``os.path.expanduser`` only substitutes a leading ``~`` (``~`` alone or
149
+ ``~user``); a string without a leading tilde is returned unchanged and
150
+ never touches HOME. A non-constant or absent argument is treated as not
151
+ referencing home, mirroring the conservative argument inspection applied
152
+ to ``expandvars``.
153
+
154
+ Args:
155
+ call_node: The ``os.path.expanduser(...)`` call node.
156
+
157
+ Returns:
158
+ True when the first string argument begins with the home-directory
159
+ tilde prefix.
160
+ """
161
+ if not call_node.args:
162
+ return False
163
+ first_argument = call_node.args[0]
164
+ if not (
165
+ isinstance(first_argument, ast.Constant)
166
+ and isinstance(first_argument.value, str)
167
+ ):
168
+ return False
169
+ return first_argument.value.startswith(HOME_DIRECTORY_TILDE_PREFIX)
170
+
171
+
172
+ def _tempfile_factory_call_is_isolated_by_dir(
173
+ call_node: ast.Call,
174
+ all_canonical_names_by_alias: dict[str, str],
175
+ all_environ_local_bindings: set[str],
176
+ ) -> bool:
177
+ """Return True when a tempfile factory's ``dir=`` sandboxes the allocation.
178
+
179
+ A ``dir=`` keyword sandboxes the allocation only when its value is a
180
+ plausibly isolated path (typically the pytest ``tmp_path`` fixture). A
181
+ ``dir=`` value that resolves to the shared temp directory does not isolate
182
+ the call and is treated as absent:
183
+
184
+ - a constant ``None`` selects the default shared temp directory; and
185
+ - a shared-temp source — ``os.getenv('TMPDIR'|'TEMP'|'TMP')`` /
186
+ ``os.environ['TMPDIR'|...]`` / ``os.environ.get('TMPDIR'|...)``, or
187
+ ``tempfile.gettempdir()`` / ``tempfile.gettempprefix()`` — returns the
188
+ shared temp directory.
189
+
190
+ Only an explicit ``dir=`` keyword counts; a ``**kwargs`` ``dir`` cannot be
191
+ resolved statically and is treated as absent, mirroring the conservative
192
+ argument inspection applied to ``expandvars`` and ``expanduser``.
193
+
194
+ Args:
195
+ call_node: The tempfile factory call node.
196
+ all_canonical_names_by_alias: Import-alias map used to resolve aliased
197
+ shared-temp sources passed as the ``dir=`` value.
198
+ all_environ_local_bindings: Local names bound to ``os.environ`` within
199
+ the test function, used to recognize aliased ``os.environ`` reads.
200
+
201
+ Returns:
202
+ True when an explicit ``dir=`` keyword is present and its value is not
203
+ a recognized shared-temp source.
204
+ """
205
+ for each_keyword in call_node.keywords:
206
+ if each_keyword.arg != TEMPFILE_FACTORY_ISOLATION_DIRECTORY_KEYWORD:
207
+ continue
208
+ return not _dir_value_resolves_to_shared_temp(
209
+ each_keyword.value,
210
+ all_canonical_names_by_alias,
211
+ all_environ_local_bindings,
212
+ )
213
+ return False
214
+
215
+
216
+ def _dir_value_resolves_to_shared_temp(
217
+ dir_value: ast.expr,
218
+ all_canonical_names_by_alias: dict[str, str],
219
+ all_environ_local_bindings: set[str],
220
+ ) -> bool:
221
+ """Return True when a tempfile ``dir=`` value points at the shared temp dir.
222
+
223
+ Args:
224
+ dir_value: The expression supplied as the factory's ``dir=`` value.
225
+ all_canonical_names_by_alias: Import-alias map used to resolve aliased
226
+ ``os.getenv`` / ``os.environ`` / ``tempfile`` references.
227
+ all_environ_local_bindings: Local names bound to ``os.environ`` within
228
+ the test function.
229
+
230
+ Returns:
231
+ True when the value is a constant ``None``, a call or subscript that
232
+ reads a home or temp environment variable named in
233
+ ``ALL_HOME_DIRECTORY_ENV_VAR_NAMES``, or a call whose dotted chain is
234
+ a recognized shared-temp source in
235
+ ``ALL_SHARED_TEMP_SOURCE_PROBE_DOTTED_NAMES``.
236
+ """
237
+ if isinstance(dir_value, ast.Constant) and dir_value.value is None:
238
+ return True
239
+ if isinstance(dir_value, ast.Call):
240
+ environ_key = _environ_key_string_from_call(
241
+ dir_value, all_canonical_names_by_alias, all_environ_local_bindings
242
+ )
243
+ if environ_key in ALL_HOME_DIRECTORY_ENV_VAR_NAMES:
244
+ return True
245
+ raw_chain = _dotted_call_attribute_chain(dir_value)
246
+ if raw_chain is None:
247
+ return False
248
+ canonical_chain = _resolve_chain_through_aliases(
249
+ raw_chain, all_canonical_names_by_alias
250
+ )
251
+ return canonical_chain in ALL_SHARED_TEMP_SOURCE_PROBE_DOTTED_NAMES
252
+ if isinstance(dir_value, ast.Subscript):
253
+ environ_key = _environ_key_string_from_subscript(
254
+ dir_value, all_canonical_names_by_alias, all_environ_local_bindings
255
+ )
256
+ return environ_key in ALL_HOME_DIRECTORY_ENV_VAR_NAMES
257
+ return False