wexample-filestate-python 0.0.48__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 (80) hide show
  1. wexample_filestate_python/__init__.py +0 -0
  2. wexample_filestate_python/__pycache__/__init__.py +0 -0
  3. wexample_filestate_python/common/__init__.py +0 -0
  4. wexample_filestate_python/common/__pycache__/__init__.py +0 -0
  5. wexample_filestate_python/common/pipy_gateway.py +20 -0
  6. wexample_filestate_python/config_option/__init__.py +0 -0
  7. wexample_filestate_python/config_option/__pycache__/__init__.py +0 -0
  8. wexample_filestate_python/config_option/mixin/__init__.py +0 -0
  9. wexample_filestate_python/config_option/mixin/__pycache__/__init__.py +0 -0
  10. wexample_filestate_python/config_option/mixin/with_stdout_wrapping_mixin.py +46 -0
  11. wexample_filestate_python/config_value/__init__.py +0 -0
  12. wexample_filestate_python/config_value/__pycache__/__init__.py +0 -0
  13. wexample_filestate_python/config_value/python_config_value.py +195 -0
  14. wexample_filestate_python/const/__init__.py +0 -0
  15. wexample_filestate_python/const/__pycache__/__init__.py +0 -0
  16. wexample_filestate_python/const/name_pattern.py +4 -0
  17. wexample_filestate_python/const/python_file.py +5 -0
  18. wexample_filestate_python/file/__init__.py +0 -0
  19. wexample_filestate_python/file/__pycache__/__init__.py +0 -0
  20. wexample_filestate_python/file/python_file.py +12 -0
  21. wexample_filestate_python/helpers/__init__.py +0 -0
  22. wexample_filestate_python/helpers/__pycache__/__init__.py +0 -0
  23. wexample_filestate_python/helpers/package.py +122 -0
  24. wexample_filestate_python/helpers/toml.py +116 -0
  25. wexample_filestate_python/option/__init__.py +0 -0
  26. wexample_filestate_python/option/__pycache__/__init__.py +0 -0
  27. wexample_filestate_python/option/abstract_python_file_content_option.py +45 -0
  28. wexample_filestate_python/option/add_future_annotations_option.py +79 -0
  29. wexample_filestate_python/option/add_return_types_option.py +265 -0
  30. wexample_filestate_python/option/fix_attrs_option.py +37 -0
  31. wexample_filestate_python/option/fix_blank_lines_option.py +47 -0
  32. wexample_filestate_python/option/format_option.py +34 -0
  33. wexample_filestate_python/option/fstringify_option.py +34 -0
  34. wexample_filestate_python/option/modernize_typing_option.py +25 -0
  35. wexample_filestate_python/option/order_class_attributes_option.py +34 -0
  36. wexample_filestate_python/option/order_class_docstring_option.py +36 -0
  37. wexample_filestate_python/option/order_class_methods_option.py +37 -0
  38. wexample_filestate_python/option/order_constants_option.py +35 -0
  39. wexample_filestate_python/option/order_iterable_items_option.py +31 -0
  40. wexample_filestate_python/option/order_main_guard_option.py +44 -0
  41. wexample_filestate_python/option/order_module_docstring_option.py +73 -0
  42. wexample_filestate_python/option/order_module_functions_option.py +42 -0
  43. wexample_filestate_python/option/order_module_metadata_option.py +62 -0
  44. wexample_filestate_python/option/order_type_checking_block_option.py +51 -0
  45. wexample_filestate_python/option/python_option.py +164 -0
  46. wexample_filestate_python/option/relocate_imports_option.py +189 -0
  47. wexample_filestate_python/option/remove_unused_option.py +45 -0
  48. wexample_filestate_python/option/sort_imports_option.py +26 -0
  49. wexample_filestate_python/option/unquote_annotations_option.py +85 -0
  50. wexample_filestate_python/options_provider/__init__.py +0 -0
  51. wexample_filestate_python/options_provider/__pycache__/__init__.py +0 -0
  52. wexample_filestate_python/options_provider/python_options_provider.py +24 -0
  53. wexample_filestate_python/py.typed +0 -0
  54. wexample_filestate_python/utils/__init__.py +0 -0
  55. wexample_filestate_python/utils/__pycache__/__init__.py +0 -0
  56. wexample_filestate_python/utils/python_attrs_utils.py +112 -0
  57. wexample_filestate_python/utils/python_blank_lines_utils.py +568 -0
  58. wexample_filestate_python/utils/python_class_attributes_utils.py +275 -0
  59. wexample_filestate_python/utils/python_class_docstring_utils.py +85 -0
  60. wexample_filestate_python/utils/python_class_methods_utils.py +230 -0
  61. wexample_filestate_python/utils/python_constants_utils.py +302 -0
  62. wexample_filestate_python/utils/python_docstring_utils.py +117 -0
  63. wexample_filestate_python/utils/python_functions_utils.py +212 -0
  64. wexample_filestate_python/utils/python_iterable_utils.py +131 -0
  65. wexample_filestate_python/utils/python_main_guard_utils.py +80 -0
  66. wexample_filestate_python/utils/python_module_metadata_utils.py +147 -0
  67. wexample_filestate_python/utils/python_type_checking_utils.py +113 -0
  68. wexample_filestate_python/utils/relocate_imports/__init__.py +7 -0
  69. wexample_filestate_python/utils/relocate_imports/__pycache__/__init__.py +0 -0
  70. wexample_filestate_python/utils/relocate_imports/python_import_rewriter.py +413 -0
  71. wexample_filestate_python/utils/relocate_imports/python_localize_runtime_imports.py +324 -0
  72. wexample_filestate_python/utils/relocate_imports/python_parser_import_index.py +80 -0
  73. wexample_filestate_python/utils/relocate_imports/python_runtime_symbol_collector.py +33 -0
  74. wexample_filestate_python/utils/relocate_imports/python_usage_collector.py +410 -0
  75. wexample_filestate_python/workdir/__init__.py +0 -0
  76. wexample_filestate_python/workdir/__pycache__/__init__.py +0 -0
  77. wexample_filestate_python-0.0.48.dist-info/METADATA +191 -0
  78. wexample_filestate_python-0.0.48.dist-info/RECORD +80 -0
  79. wexample_filestate_python-0.0.48.dist-info/WHEEL +4 -0
  80. wexample_filestate_python-0.0.48.dist-info/entry_points.txt +4 -0
@@ -0,0 +1,410 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import ClassVar, DefaultDict
4
+
5
+ import libcst as cst
6
+
7
+ from wexample_filestate_python.utils.relocate_imports import PythonParserImportIndex
8
+
9
+
10
+ class PythonUsageCollector(cst.CSTVisitor):
11
+ """Collect usage of imported names across three categories:
12
+ - A: runtime usages inside function/method bodies (class calls, typing.cast target)
13
+ - B: class-level property type annotations
14
+ - C: type-only annotations in params/returns/module-level AnnAssign
15
+
16
+ The collector mutates the provided buckets so the caller can reuse shared storage.
17
+ """
18
+
19
+ DEFAULT_CAST_FUNCTION_CANDIDATES: ClassVar[set[str]] = {"cast"}
20
+
21
+ def __init__(
22
+ self,
23
+ imported_value_names: set[str],
24
+ functions_needing_local: DefaultDict[str, set[str]],
25
+ used_in_B: set[str],
26
+ used_in_C_annot: set[str],
27
+ cast_type_names_anywhere: set[str],
28
+ cast_function_candidates: set[str] | None = None,
29
+ future_annotations_enabled: bool = True,
30
+ idx: PythonParserImportIndex | None = None,
31
+ ) -> None:
32
+ super().__init__()
33
+ self.imported_value_names = imported_value_names
34
+ self.functions_needing_local = functions_needing_local
35
+ self.used_in_B = used_in_B
36
+ self.used_in_C_annot = used_in_C_annot
37
+ self.cast_type_names_anywhere = cast_type_names_anywhere
38
+ self.cast_function_candidates = (
39
+ set(self.DEFAULT_CAST_FUNCTION_CANDIDATES)
40
+ if cast_function_candidates is None
41
+ else cast_function_candidates
42
+ )
43
+ self.future_annotations_enabled = future_annotations_enabled
44
+ self.idx = idx
45
+
46
+ self.class_stack: list[str] = []
47
+ self.func_stack: list[str] = []
48
+ self._in_annotation_stack: list[bool] = []
49
+ self._in_decorator_stack: list[bool] = []
50
+ self._in_param_default_stack: list[bool] = []
51
+ self._in_param_annot_stack: list[bool] = []
52
+
53
+ def leave_Annotation(self, node: cst.Annotation) -> None: # type: ignore[override]
54
+ if self._in_annotation_stack:
55
+ self._in_annotation_stack.pop()
56
+
57
+ def leave_AsyncFunctionDef(self, node: cst.AsyncFunctionDef) -> None: # type: ignore[override]
58
+ self.func_stack.pop()
59
+
60
+ def leave_ClassDef(self, node: cst.ClassDef) -> None: # type: ignore[override]
61
+ self.class_stack.pop()
62
+
63
+ def leave_Decorator(self, node: cst.Decorator) -> None: # type: ignore[override]
64
+ if self._in_decorator_stack:
65
+ self._in_decorator_stack.pop()
66
+
67
+ def leave_FunctionDef(self, node: cst.FunctionDef) -> None: # type: ignore[override]
68
+ self.func_stack.pop()
69
+
70
+ def leave_Param(self, node: cst.Param) -> None: # type: ignore[override]
71
+ if self._in_param_default_stack:
72
+ self._in_param_default_stack.pop()
73
+ if self._in_param_annot_stack:
74
+ self._in_param_annot_stack.pop()
75
+
76
+ # ----- B: class-level property annotations -----
77
+ def visit_AnnAssign(self, node: cst.AnnAssign) -> None: # type: ignore[override]
78
+ if not self.class_stack:
79
+ # module-level annotated assignment -> C
80
+ self._record_type_names(node.annotation.annotation, self.used_in_C_annot)
81
+ return
82
+ # class-level annotated assignment: if future annotations are enabled, treat as type-only (C)
83
+ # otherwise, require availability at class definition time (B)
84
+ if self.future_annotations_enabled:
85
+ self._record_type_names(node.annotation.annotation, self.used_in_C_annot)
86
+ else:
87
+ self._record_type_names(node.annotation.annotation, self.used_in_B)
88
+ # Additionally, if the annotated assignment has a default value at class level,
89
+ # any imported names referenced in that value are needed at class definition time.
90
+ try:
91
+ if node.value is not None:
92
+ self._walk_expr_for_names(node.value, self.used_in_B)
93
+ except Exception:
94
+ pass
95
+
96
+ # Track annotation context to avoid misclassifying annotation names as runtime
97
+ def visit_Annotation(self, node: cst.Annotation) -> bool: # type: ignore[override]
98
+ self._in_annotation_stack.append(True)
99
+ return True
100
+
101
+ # Prevent visiting keyword argument names in function calls
102
+ def visit_Arg(self, node: cst.Arg) -> bool: # type: ignore[override]
103
+ # Only visit the value, not the keyword name
104
+ # e.g., in foo(verbosity=123), visit 123 but not 'verbosity'
105
+ if node.value:
106
+ node.value.visit(self)
107
+ return False # Don't visit children (especially node.keyword)
108
+
109
+ # Also treat class-level simple assignments where RHS references imported names as B.
110
+ # Example: SERVICE_CLASS = GithubRemote
111
+ def visit_Assign(self, node: cst.Assign) -> None: # type: ignore[override]
112
+ if not self.class_stack:
113
+ return
114
+ # Record any imported names appearing in the value expression as B
115
+ try:
116
+ value = node.value
117
+ except Exception:
118
+ return
119
+ self._walk_expr_for_names(value, self.used_in_B)
120
+
121
+ def visit_AsyncFunctionDef(self, node: cst.AsyncFunctionDef) -> bool: # type: ignore[override]
122
+ self.func_stack.append(self._qualified_func_name(node.name.value))
123
+ # Record return annotation for C
124
+ if node.returns is not None:
125
+ self._record_type_names(node.returns.annotation, self.used_in_C_annot)
126
+ return True
127
+
128
+ # Track when we're inside an Attribute to avoid treating attribute names as bare names
129
+ def visit_Attribute(self, node: cst.Attribute) -> bool: # type: ignore[override]
130
+ # Visit the value (left side) but not the attr (right side)
131
+ # This prevents visit_Name from being called on the attribute name itself
132
+ # e.g., in self.verbosity, we visit 'self' but not 'verbosity'
133
+ if isinstance(node.value, cst.BaseExpression):
134
+ node.value.visit(self)
135
+ return False # Don't visit children (especially node.attr)
136
+
137
+ # ----- A: runtime usages inside functions -----
138
+ def visit_Call(self, node: cst.Call) -> None: # type: ignore[override]
139
+ if not self.func_stack:
140
+ return
141
+ func = node.func
142
+ if isinstance(func, cst.Name):
143
+ callee = func.value
144
+ # class constructor call
145
+ if callee in self.imported_value_names and callee[:1].isupper():
146
+ self.functions_needing_local[self.func_stack[-1]].add(callee)
147
+ return
148
+ # typing.cast(x, MyClass) when used as bare `cast(...)`
149
+ if (
150
+ (callee in self.cast_function_candidates or "cast" in callee.lower())
151
+ and node.args
152
+ and len(node.args) >= 1
153
+ ):
154
+ type_arg = node.args[0].value
155
+ # Collect any imported names appearing anywhere inside the type expression
156
+ names = self._collect_names_from_type_expr(type_arg)
157
+ for n in names:
158
+ # Always record for module-wide exclusion from TYPE_CHECKING
159
+ self.cast_type_names_anywhere.add(n)
160
+ # Always schedule local import for this function; localizer will filter if module cannot be resolved
161
+ if self.func_stack:
162
+ self.functions_needing_local[self.func_stack[-1]].add(n)
163
+ return
164
+ elif isinstance(func, cst.Attribute):
165
+ # typing.cast(...) or pkg.cast(...)
166
+ if (
167
+ isinstance(func.attr, cst.Name)
168
+ and (
169
+ func.attr.value in self.cast_function_candidates
170
+ or "cast" in func.attr.value.lower()
171
+ )
172
+ and node.args
173
+ and len(node.args) >= 1
174
+ ):
175
+ type_arg = node.args[0].value
176
+ names = self._collect_names_from_type_expr(type_arg)
177
+ for n in names:
178
+ self.cast_type_names_anywhere.add(n)
179
+ if self.func_stack:
180
+ self.functions_needing_local[self.func_stack[-1]].add(n)
181
+ return
182
+
183
+ # ----- Stack management -----
184
+ def visit_ClassDef(self, node: cst.ClassDef) -> bool: # type: ignore[override]
185
+ self.class_stack.append(node.name.value)
186
+ # Treat symbols in the inheritance list as B (must be available at class creation)
187
+ for base in node.bases:
188
+ try:
189
+ self._walk_expr_for_names(base.value, self.used_in_B)
190
+ except Exception:
191
+ pass
192
+ return True
193
+
194
+ # Track decorator context and record decorator names as module-level usage (B)
195
+ # Decorators are evaluated at function/class definition time, not at runtime
196
+ def visit_Decorator(self, node: cst.Decorator) -> bool: # type: ignore[override]
197
+ self._in_decorator_stack.append(True)
198
+ # Walk the decorator expression to find imported names
199
+ # e.g., @command, @option(...), @middleware(middleware=PackageSuiteMiddleware)
200
+ try:
201
+ self._walk_expr_for_names(node.decorator, self.used_in_B)
202
+ except Exception:
203
+ pass
204
+ return True
205
+
206
+ def visit_FunctionDef(self, node: cst.FunctionDef) -> bool: # type: ignore[override]
207
+ self.func_stack.append(self._qualified_func_name(node.name.value))
208
+ # Record return annotation for C
209
+ if node.returns is not None:
210
+ self._record_type_names(node.returns.annotation, self.used_in_C_annot)
211
+ return True
212
+
213
+ # Treat bare Name usage inside function bodies as runtime usage (A)
214
+ def visit_Name(self, node: cst.Name) -> None: # type: ignore[override]
215
+ if not self.func_stack:
216
+ return
217
+ if (
218
+ self._in_annotation_stack
219
+ or self._in_decorator_stack
220
+ or any(self._in_param_default_stack)
221
+ or any(self._in_param_annot_stack)
222
+ ):
223
+ return
224
+ val = node.value
225
+ # Do not treat 'cast' identifier as runtime usage; keep typing at module level
226
+ if val == "cast":
227
+ return
228
+ if val in self.imported_value_names:
229
+ # Resolve module to avoid misclassifying annotation-only names like Mapping
230
+ resolved_mod = None
231
+ try:
232
+ if hasattr(self, "idx") and getattr(self, "idx") is not None: # type: ignore[attr-defined]
233
+ resolved_mod = getattr(self, "idx").name_to_from.get(val, (None, None))[0] # type: ignore[index]
234
+ except Exception:
235
+ resolved_mod = None
236
+ # Skip if module is unknown or belongs to typing/collections.abc
237
+ # Also skip bare imports (import os.path) which cannot be localized
238
+ if resolved_mod in (None, "typing", "collections", "collections.abc"):
239
+ # For bare imports (resolved_mod is None), mark as module-level usage
240
+ # since we cannot relocate them (can't do "from os import path" locally)
241
+ if resolved_mod is None and val in self.imported_value_names:
242
+ self.used_in_B.add(val)
243
+ return
244
+ self.functions_needing_local[self.func_stack[-1]].add(val)
245
+
246
+ # Track param default context; defaults are evaluated at definition time (module init), not runtime
247
+ def visit_Param(self, node: cst.Param) -> bool: # type: ignore[override]
248
+ has_default = node.default is not None
249
+ self._in_param_default_stack.append(has_default)
250
+ # Mark that we are in a parameter annotation while traversing children
251
+ in_annot = node.annotation is not None
252
+ self._in_param_annot_stack.append(in_annot)
253
+ # Record parameter annotation types into C (annotation usage)
254
+ if node.annotation is not None:
255
+ self._record_type_names(node.annotation.annotation, self.used_in_C_annot)
256
+ # Names in defaults are needed at definition time: treat as B
257
+ if has_default:
258
+ try:
259
+ # Collect base identifiers even if not recognized as imported yet
260
+ collected: set[str] = set()
261
+
262
+ def _collect_base_names(expr: cst.BaseExpression) -> None:
263
+ if isinstance(expr, cst.Name):
264
+ collected.add(expr.value)
265
+ elif isinstance(expr, cst.Attribute):
266
+ _collect_base_names(expr.value)
267
+ elif isinstance(expr, cst.Subscript):
268
+ _collect_base_names(expr.value)
269
+ for e in expr.slice:
270
+ if isinstance(e, cst.SubscriptElement) and isinstance(
271
+ e.slice, cst.Index
272
+ ):
273
+ _collect_base_names(e.slice.value)
274
+
275
+ _collect_base_names(node.default)
276
+ self.used_in_B.update(collected)
277
+ except Exception:
278
+ pass
279
+ # We still record annotation as C elsewhere
280
+ return True
281
+
282
+ # ----- C: function param annotations -----
283
+ def visit_Param(self, node: cst.Param) -> None: # type: ignore[override]
284
+ if node.annotation is not None:
285
+ self._record_type_names(node.annotation.annotation, self.used_in_C_annot)
286
+
287
+ # Fallback: explicitly scan Parameters node for defaults (some environments may not trigger visit_Param)
288
+ def visit_Parameters(self, node: cst.Parameters) -> bool: # type: ignore[override]
289
+ try:
290
+ self.func_stack[-1] if self.func_stack else "<module>"
291
+ # Aggregate all parameter-like collections
292
+ all_params: list[cst.Param] = []
293
+ all_params.extend(list(node.params))
294
+ all_params.extend(list(node.posonly_params))
295
+ all_params.extend(list(node.kwonly_params))
296
+ if node.star_arg is not None and isinstance(node.star_arg, cst.Param):
297
+ all_params.append(node.star_arg)
298
+ if node.star_kwarg is not None and isinstance(node.star_kwarg, cst.Param):
299
+ all_params.append(node.star_kwarg)
300
+
301
+ for p in all_params:
302
+ has_default = p.default is not None
303
+ if not has_default:
304
+ continue
305
+ collected: set[str] = set()
306
+
307
+ def _collect_base_names(expr: cst.BaseExpression) -> None:
308
+ if isinstance(expr, cst.Name):
309
+ collected.add(expr.value)
310
+ elif isinstance(expr, cst.Attribute):
311
+ _collect_base_names(expr.value)
312
+ elif isinstance(expr, cst.Subscript):
313
+ _collect_base_names(expr.value)
314
+ for e in expr.slice:
315
+ if isinstance(e, cst.SubscriptElement) and isinstance(
316
+ e.slice, cst.Index
317
+ ):
318
+ _collect_base_names(e.slice.value)
319
+
320
+ _collect_base_names(p.default) # type: ignore[arg-type]
321
+ self.used_in_B.update(collected)
322
+ except Exception:
323
+ pass
324
+ return True
325
+
326
+ # Collect names inside a type expression (Name, Attribute tail, Subscript args)
327
+ def _collect_names_from_type_expr(self, expr: cst.BaseExpression) -> set[str]:
328
+ acc: set[str] = set()
329
+ # Handle forward-ref style: cast("Foo", x) or cast("dict[str, Foo]", x)
330
+ if isinstance(expr, cst.SimpleString):
331
+ try:
332
+ # Remove surrounding quotes; libcst handles raw string quotes
333
+ text = expr.evaluated_value
334
+ parsed = cst.parse_expression(text)
335
+ self._walk_expr_for_names(parsed, acc)
336
+ return acc
337
+ except Exception:
338
+ return acc
339
+ self._walk_expr_for_names(expr, acc)
340
+ return acc
341
+
342
+ # ----- internals -----
343
+ def _qualified_func_name(self, base: str) -> str:
344
+ return ".".join(self.class_stack + [base]) if self.class_stack else base
345
+
346
+ def _record_type_names(self, ann: cst.BaseExpression, bucket: set[str]) -> None:
347
+ if isinstance(ann, cst.Name):
348
+ if ann.value in self.imported_value_names:
349
+ bucket.add(ann.value)
350
+ elif isinstance(ann, cst.Subscript):
351
+ self._walk_expr_for_names(ann.value, bucket)
352
+ for e in ann.slice:
353
+ if isinstance(e, cst.SubscriptElement) and isinstance(
354
+ e.slice, cst.Index
355
+ ):
356
+ self._walk_expr_for_names(e.slice.value, bucket)
357
+ else:
358
+ self._walk_expr_for_names(ann, bucket)
359
+
360
+ def _walk_expr_for_names(self, expr: cst.BaseExpression, bucket: set[str]) -> None:
361
+ if isinstance(expr, cst.Name):
362
+ if expr.value in self.imported_value_names:
363
+ bucket.add(expr.value)
364
+ elif isinstance(expr, cst.Attribute):
365
+ # Walk attribute chain: only record the base name (e.g., TerminalColor in TerminalColor.WHITE)
366
+ # Recurse into value (left side) only
367
+ self._walk_expr_for_names(expr.value, bucket)
368
+ # DO NOT consider the attr name itself (right side) as an imported name to avoid
369
+ # confusion with instance/class attributes like self.verbosity vs imported verbosity
370
+ elif isinstance(expr, cst.BinaryOperation):
371
+ # Handle PEP 604 union types: e.g., Path | None
372
+ # Traverse both sides of the binary operation
373
+ try:
374
+ self._walk_expr_for_names(expr.left, bucket)
375
+ except Exception:
376
+ pass
377
+ try:
378
+ self._walk_expr_for_names(expr.right, bucket)
379
+ except Exception:
380
+ pass
381
+ elif isinstance(expr, cst.Subscript):
382
+ self._walk_expr_for_names(expr.value, bucket)
383
+ for e in expr.slice:
384
+ if isinstance(e, cst.SubscriptElement) and isinstance(
385
+ e.slice, cst.Index
386
+ ):
387
+ self._walk_expr_for_names(e.slice.value, bucket)
388
+ elif isinstance(expr, cst.Call):
389
+ # Walk into function calls to detect names in arguments
390
+ # e.g., private_field(default=VerbosityLevel.DEFAULT)
391
+ self._walk_expr_for_names(expr.func, bucket)
392
+ for arg in expr.args:
393
+ self._walk_expr_for_names(arg.value, bucket)
394
+ elif isinstance(expr, cst.List):
395
+ # Walk into list literals to detect names in elements
396
+ # e.g., validators=[RegexValidator(...)]
397
+ for element in expr.elements:
398
+ if isinstance(element, cst.Element):
399
+ self._walk_expr_for_names(element.value, bucket)
400
+ elif isinstance(expr, cst.Tuple):
401
+ # Walk into tuple literals to detect names in elements
402
+ for element in expr.elements:
403
+ if isinstance(element, cst.Element):
404
+ self._walk_expr_for_names(element.value, bucket)
405
+ elif isinstance(expr, cst.Dict):
406
+ # Walk into dict literals to detect names in keys and values
407
+ for element in expr.elements:
408
+ if isinstance(element, cst.DictElement):
409
+ self._walk_expr_for_names(element.key, bucket)
410
+ self._walk_expr_for_names(element.value, bucket)
File without changes
@@ -0,0 +1,191 @@
1
+ Metadata-Version: 2.1
2
+ Name: wexample-filestate-python
3
+ Version: 0.0.48
4
+ Summary: Helpers for Python.
5
+ Author-Email: weeger <contact@wexample.com>
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Project-URL: homepage, https://github.com/wexample/python-filestate-python
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: attrs>=23.1.0
13
+ Requires-Dist: autoflake
14
+ Requires-Dist: cattrs>=23.1.0
15
+ Requires-Dist: libcst
16
+ Requires-Dist: packaging
17
+ Requires-Dist: tomli
18
+ Requires-Dist: wexample-filestate==0.0.61
19
+ Requires-Dist: wexample-helpers-api==0.0.71
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest; extra == "dev"
22
+ Requires-Dist: pytest-cov; extra == "dev"
23
+ Description-Content-Type: text/markdown
24
+
25
+ # wexample-filestate-python
26
+
27
+ Version: 0.0.48
28
+
29
+ Helpers for Python.
30
+
31
+ ## Tests
32
+
33
+ This project uses `pytest` for testing and `pytest-cov` for code coverage analysis.
34
+
35
+ ### Installation
36
+
37
+ First, install the required testing dependencies:
38
+ ```bash
39
+ .venv/bin/python -m pip install pytest pytest-cov
40
+ ```
41
+
42
+ ### Basic Usage
43
+
44
+ Run all tests with coverage:
45
+ ```bash
46
+ .venv/bin/python -m pytest --cov
47
+ ```
48
+
49
+ ### Common Commands
50
+ ```bash
51
+ # Run tests with coverage for a specific module
52
+ .venv/bin/python -m pytest --cov=your_module
53
+
54
+ # Show which lines are not covered
55
+ .venv/bin/python -m pytest --cov=your_module --cov-report=term-missing
56
+
57
+ # Generate an HTML coverage report
58
+ .venv/bin/python -m pytest --cov=your_module --cov-report=html
59
+
60
+ # Combine terminal and HTML reports
61
+ .venv/bin/python -m pytest --cov=your_module --cov-report=term-missing --cov-report=html
62
+
63
+ # Run specific test file with coverage
64
+ .venv/bin/python -m pytest tests/test_file.py --cov=your_module --cov-report=term-missing
65
+ ```
66
+
67
+ ### Viewing HTML Reports
68
+
69
+ After generating an HTML report, open `htmlcov/index.html` in your browser to view detailed line-by-line coverage information.
70
+
71
+ ### Coverage Threshold
72
+
73
+ To enforce a minimum coverage percentage:
74
+ ```bash
75
+ .venv/bin/python -m pytest --cov=your_module --cov-fail-under=80
76
+ ```
77
+
78
+ This will cause the test suite to fail if coverage drops below 80%.
79
+
80
+ ## Code Quality & Typing
81
+
82
+ All the suite packages follow strict quality standards:
83
+
84
+ - **Type hints**: Full type coverage with mypy validation
85
+ - **Code formatting**: Enforced with black and isort
86
+ - **Linting**: Comprehensive checks with custom scripts and tools
87
+ - **Testing**: High test coverage requirements
88
+
89
+ These standards ensure reliability and maintainability across the suite.
90
+
91
+ ## Versioning & Compatibility Policy
92
+
93
+ Wexample packages follow **Semantic Versioning** (SemVer):
94
+
95
+ - **MAJOR**: Breaking changes
96
+ - **MINOR**: New features, backward compatible
97
+ - **PATCH**: Bug fixes, backward compatible
98
+
99
+ We maintain backward compatibility within major versions and provide clear migration guides for breaking changes.
100
+
101
+ ## Changelog
102
+
103
+ See [CHANGELOG.md](CHANGELOG.md) for detailed version history and release notes.
104
+
105
+ Major changes are documented with migration guides when applicable.
106
+
107
+ ## Migration Notes
108
+
109
+ When upgrading between major versions, refer to the migration guides in the documentation.
110
+
111
+ Breaking changes are clearly documented with upgrade paths and examples.
112
+
113
+ ## Known Limitations & Roadmap
114
+
115
+ Current limitations and planned features are tracked in the GitHub issues.
116
+
117
+ See the [project roadmap](https://github.com/wexample/python-filestate-python/issues) for upcoming features and improvements.
118
+
119
+ ## Security Policy
120
+
121
+ ### Reporting Vulnerabilities
122
+
123
+ If you discover a security vulnerability, please email security@wexample.com.
124
+
125
+ **Do not** open public issues for security vulnerabilities.
126
+
127
+ We take security seriously and will respond promptly to verified reports.
128
+
129
+ ## Privacy & Telemetry
130
+
131
+ This package does **not** collect any telemetry or usage data.
132
+
133
+ Your privacy is respected — no data is transmitted to external services.
134
+
135
+ ## Support Channels
136
+
137
+ - **GitHub Issues**: Bug reports and feature requests
138
+ - **GitHub Discussions**: Questions and community support
139
+ - **Documentation**: Comprehensive guides and API reference
140
+ - **Email**: contact@wexample.com for general inquiries
141
+
142
+ Community support is available through GitHub Discussions.
143
+
144
+ ## Contribution Guidelines
145
+
146
+ We welcome contributions to the Wexample suite!
147
+
148
+ ### How to Contribute
149
+
150
+ 1. **Fork** the repository
151
+ 2. **Create** a feature branch
152
+ 3. **Make** your changes
153
+ 4. **Test** thoroughly
154
+ 5. **Submit** a pull request
155
+
156
+ ## Maintainers & Authors
157
+
158
+ Maintained by the Wexample team and community contributors.
159
+
160
+ See [CONTRIBUTORS.md](CONTRIBUTORS.md) for the full list of contributors.
161
+
162
+ ## License
163
+
164
+ MIT
165
+
166
+ ## Useful Links
167
+
168
+ - **Homepage**: https://github.com/wexample/python-filestate-python
169
+ - **Documentation**: [docs.wexample.com](https://docs.wexample.com)
170
+ - **Issue Tracker**: https://github.com/wexample/python-filestate-python/issues
171
+ - **Discussions**: https://github.com/wexample/python-filestate-python/discussions
172
+ - **PyPI**: [pypi.org/project/wexample-filestate-python](https://pypi.org/project/wexample-filestate-python/)
173
+
174
+ ## Integration in the Suite
175
+
176
+ This package is part of the **Wexample Suite** — a collection of high-quality Python packages designed to work seamlessly together.
177
+
178
+ ### Related Packages
179
+
180
+ The suite includes packages for configuration management, file handling, prompts, and more. Each package can be used independently or as part of the integrated suite.
181
+
182
+ Visit the [Wexample Suite documentation](https://docs.wexample.com) for the complete package ecosystem.
183
+
184
+ # About us
185
+
186
+ Wexample stands as a cornerstone of the digital ecosystem — a collective of seasoned engineers, researchers, and creators driven by a relentless pursuit of technological excellence. More than a media platform, it has grown into a vibrant community where innovation meets craftsmanship, and where every line of code reflects a commitment to clarity, durability, and shared intelligence.
187
+
188
+ This packages suite embodies this spirit. Trusted by professionals and enthusiasts alike, it delivers a consistent, high-quality foundation for modern development — open, elegant, and battle-tested. Its reputation is built on years of collaboration, refinement, and rigorous attention to detail, making it a natural choice for those who demand both robustness and beauty in their tools.
189
+
190
+ Wexample cultivates a culture of mastery. Each package, each contribution carries the mark of a community that values precision, ethics, and innovation — a community proud to shape the future of digital craftsmanship.
191
+