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.
- wexample_filestate_python/__init__.py +0 -0
- wexample_filestate_python/__pycache__/__init__.py +0 -0
- wexample_filestate_python/common/__init__.py +0 -0
- wexample_filestate_python/common/__pycache__/__init__.py +0 -0
- wexample_filestate_python/common/pipy_gateway.py +20 -0
- wexample_filestate_python/config_option/__init__.py +0 -0
- wexample_filestate_python/config_option/__pycache__/__init__.py +0 -0
- wexample_filestate_python/config_option/mixin/__init__.py +0 -0
- wexample_filestate_python/config_option/mixin/__pycache__/__init__.py +0 -0
- wexample_filestate_python/config_option/mixin/with_stdout_wrapping_mixin.py +46 -0
- wexample_filestate_python/config_value/__init__.py +0 -0
- wexample_filestate_python/config_value/__pycache__/__init__.py +0 -0
- wexample_filestate_python/config_value/python_config_value.py +195 -0
- wexample_filestate_python/const/__init__.py +0 -0
- wexample_filestate_python/const/__pycache__/__init__.py +0 -0
- wexample_filestate_python/const/name_pattern.py +4 -0
- wexample_filestate_python/const/python_file.py +5 -0
- wexample_filestate_python/file/__init__.py +0 -0
- wexample_filestate_python/file/__pycache__/__init__.py +0 -0
- wexample_filestate_python/file/python_file.py +12 -0
- wexample_filestate_python/helpers/__init__.py +0 -0
- wexample_filestate_python/helpers/__pycache__/__init__.py +0 -0
- wexample_filestate_python/helpers/package.py +122 -0
- wexample_filestate_python/helpers/toml.py +116 -0
- wexample_filestate_python/option/__init__.py +0 -0
- wexample_filestate_python/option/__pycache__/__init__.py +0 -0
- wexample_filestate_python/option/abstract_python_file_content_option.py +45 -0
- wexample_filestate_python/option/add_future_annotations_option.py +79 -0
- wexample_filestate_python/option/add_return_types_option.py +265 -0
- wexample_filestate_python/option/fix_attrs_option.py +37 -0
- wexample_filestate_python/option/fix_blank_lines_option.py +47 -0
- wexample_filestate_python/option/format_option.py +34 -0
- wexample_filestate_python/option/fstringify_option.py +34 -0
- wexample_filestate_python/option/modernize_typing_option.py +25 -0
- wexample_filestate_python/option/order_class_attributes_option.py +34 -0
- wexample_filestate_python/option/order_class_docstring_option.py +36 -0
- wexample_filestate_python/option/order_class_methods_option.py +37 -0
- wexample_filestate_python/option/order_constants_option.py +35 -0
- wexample_filestate_python/option/order_iterable_items_option.py +31 -0
- wexample_filestate_python/option/order_main_guard_option.py +44 -0
- wexample_filestate_python/option/order_module_docstring_option.py +73 -0
- wexample_filestate_python/option/order_module_functions_option.py +42 -0
- wexample_filestate_python/option/order_module_metadata_option.py +62 -0
- wexample_filestate_python/option/order_type_checking_block_option.py +51 -0
- wexample_filestate_python/option/python_option.py +164 -0
- wexample_filestate_python/option/relocate_imports_option.py +189 -0
- wexample_filestate_python/option/remove_unused_option.py +45 -0
- wexample_filestate_python/option/sort_imports_option.py +26 -0
- wexample_filestate_python/option/unquote_annotations_option.py +85 -0
- wexample_filestate_python/options_provider/__init__.py +0 -0
- wexample_filestate_python/options_provider/__pycache__/__init__.py +0 -0
- wexample_filestate_python/options_provider/python_options_provider.py +24 -0
- wexample_filestate_python/py.typed +0 -0
- wexample_filestate_python/utils/__init__.py +0 -0
- wexample_filestate_python/utils/__pycache__/__init__.py +0 -0
- wexample_filestate_python/utils/python_attrs_utils.py +112 -0
- wexample_filestate_python/utils/python_blank_lines_utils.py +568 -0
- wexample_filestate_python/utils/python_class_attributes_utils.py +275 -0
- wexample_filestate_python/utils/python_class_docstring_utils.py +85 -0
- wexample_filestate_python/utils/python_class_methods_utils.py +230 -0
- wexample_filestate_python/utils/python_constants_utils.py +302 -0
- wexample_filestate_python/utils/python_docstring_utils.py +117 -0
- wexample_filestate_python/utils/python_functions_utils.py +212 -0
- wexample_filestate_python/utils/python_iterable_utils.py +131 -0
- wexample_filestate_python/utils/python_main_guard_utils.py +80 -0
- wexample_filestate_python/utils/python_module_metadata_utils.py +147 -0
- wexample_filestate_python/utils/python_type_checking_utils.py +113 -0
- wexample_filestate_python/utils/relocate_imports/__init__.py +7 -0
- wexample_filestate_python/utils/relocate_imports/__pycache__/__init__.py +0 -0
- wexample_filestate_python/utils/relocate_imports/python_import_rewriter.py +413 -0
- wexample_filestate_python/utils/relocate_imports/python_localize_runtime_imports.py +324 -0
- wexample_filestate_python/utils/relocate_imports/python_parser_import_index.py +80 -0
- wexample_filestate_python/utils/relocate_imports/python_runtime_symbol_collector.py +33 -0
- wexample_filestate_python/utils/relocate_imports/python_usage_collector.py +410 -0
- wexample_filestate_python/workdir/__init__.py +0 -0
- wexample_filestate_python/workdir/__pycache__/__init__.py +0 -0
- wexample_filestate_python-0.0.48.dist-info/METADATA +191 -0
- wexample_filestate_python-0.0.48.dist-info/RECORD +80 -0
- wexample_filestate_python-0.0.48.dist-info/WHEEL +4 -0
- 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
|
|
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
|
+
|