sarj-python-lint 0.67.2__tar.gz → 0.67.4__tar.gz
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.
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/PKG-INFO +1 -1
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/pyproject.toml +1 -1
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/_fastapi.py +36 -1
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +9 -3
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/prefer_module_level_constant.py +10 -2
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +144 -1
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/.gitignore +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/LICENSE +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/README.md +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/defect_xfail_requires_strict.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/duplicate_test_body.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/duplicated_override_docstring.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/fastapi_openapi_contract.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_file_level_suppression.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_long_comment.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
- {sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 0.67.
|
|
3
|
+
Version: 0.67.4
|
|
4
4
|
Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
|
|
5
5
|
Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
|
|
6
6
|
Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
|
|
@@ -570,7 +570,7 @@ def _resolve_module_path(current: Path, reference: _ImportedReference) -> Path |
|
|
|
570
570
|
current = current.resolve()
|
|
571
571
|
checkout = _checkout_root(current)
|
|
572
572
|
if checkout is None:
|
|
573
|
-
return
|
|
573
|
+
return _resolve_module_path_without_checkout(current, reference)
|
|
574
574
|
module_parts = tuple(part for part in reference.module.split(".") if part)
|
|
575
575
|
candidates: set[Path] = set()
|
|
576
576
|
if 0 < reference.level <= _MAX_PATH_ANCESTORS:
|
|
@@ -593,6 +593,41 @@ def _resolve_module_path(current: Path, reference: _ImportedReference) -> Path |
|
|
|
593
593
|
return next(iter(candidates)) if len(candidates) == 1 else None
|
|
594
594
|
|
|
595
595
|
|
|
596
|
+
def _resolve_module_path_without_checkout(current: Path, reference: _ImportedReference) -> Path | None:
|
|
597
|
+
"""Resolve an explicitly local import when a detached source tree has no VCS root.
|
|
598
|
+
|
|
599
|
+
Relative imports must stay inside a real package chain. Absolute imports are
|
|
600
|
+
considered only at an ancestor matching their first component, or as a
|
|
601
|
+
direct sibling module. These exact, bounded anchors avoid treating an
|
|
602
|
+
arbitrary filesystem ancestor as an import search path.
|
|
603
|
+
"""
|
|
604
|
+
module_parts = tuple(part for part in reference.module.split(".") if part)
|
|
605
|
+
candidates: set[Path] = set()
|
|
606
|
+
if 0 < reference.level <= _MAX_PATH_ANCESTORS:
|
|
607
|
+
package = current.parent
|
|
608
|
+
if not (package / "__init__.py").is_file():
|
|
609
|
+
return None
|
|
610
|
+
for _ in range(reference.level - 1):
|
|
611
|
+
parent = package.parent
|
|
612
|
+
if not (parent / "__init__.py").is_file():
|
|
613
|
+
return None
|
|
614
|
+
package = parent
|
|
615
|
+
candidates.update(_existing_module_files(package.joinpath(*module_parts), package))
|
|
616
|
+
elif module_parts:
|
|
617
|
+
first = module_parts[0]
|
|
618
|
+
for depth, ancestor in enumerate(current.parent.parents):
|
|
619
|
+
if depth >= _MAX_PATH_ANCESTORS:
|
|
620
|
+
break
|
|
621
|
+
package = ancestor / first
|
|
622
|
+
if package != current.parent and not current.is_relative_to(package):
|
|
623
|
+
continue
|
|
624
|
+
candidates.update(_existing_module_files(ancestor.joinpath(*module_parts), ancestor))
|
|
625
|
+
# A detached directory of modules is also a valid, deliberately narrow
|
|
626
|
+
# import root. Do not search any of its parents for an unrelated module.
|
|
627
|
+
candidates.update(_existing_module_files(current.parent.joinpath(*module_parts), current.parent))
|
|
628
|
+
return next(iter(candidates)) if len(candidates) == 1 else None
|
|
629
|
+
|
|
630
|
+
|
|
596
631
|
def _checkout_root(current: Path) -> Path | None:
|
|
597
632
|
for depth, ancestor in enumerate(current.parents):
|
|
598
633
|
if depth >= _MAX_PATH_ANCESTORS:
|
|
@@ -202,6 +202,7 @@ class _MutationVisitor(ast.NodeVisitor):
|
|
|
202
202
|
self.mutated: set[str] = mutated
|
|
203
203
|
self._scopes: list[tuple[set[str], set[str]]] = []
|
|
204
204
|
self._local_container_roots: list[dict[str, set[str]]] = []
|
|
205
|
+
self._module_container_roots: dict[str, set[str]] = {}
|
|
205
206
|
|
|
206
207
|
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
207
208
|
self._visit_function(node)
|
|
@@ -278,11 +279,14 @@ class _MutationVisitor(ast.NodeVisitor):
|
|
|
278
279
|
self.generic_visit(node)
|
|
279
280
|
|
|
280
281
|
def _remember_local_container_roots(self, targets: tuple[ast.expr, ...] | list[ast.expr], value: ast.expr) -> None:
|
|
281
|
-
if not self._local_container_roots:
|
|
282
|
-
return
|
|
283
282
|
roots = self._module_roots(value)
|
|
284
283
|
if not roots:
|
|
285
284
|
return
|
|
285
|
+
if not self._local_container_roots:
|
|
286
|
+
for target in targets:
|
|
287
|
+
if isinstance(target, ast.Name):
|
|
288
|
+
self._module_container_roots.setdefault(target.id, set()).update(roots)
|
|
289
|
+
return
|
|
286
290
|
locals_ = self._scopes[-1][0]
|
|
287
291
|
aliases = self._local_container_roots[-1]
|
|
288
292
|
for target in targets:
|
|
@@ -298,9 +302,11 @@ class _MutationVisitor(ast.NodeVisitor):
|
|
|
298
302
|
(scope[name] for scope in reversed(self._local_container_roots) if name in scope),
|
|
299
303
|
None,
|
|
300
304
|
)
|
|
305
|
+
if alias is None:
|
|
306
|
+
alias = self._module_container_roots.get(name)
|
|
301
307
|
if alias is not None:
|
|
302
308
|
roots.update(alias)
|
|
303
|
-
|
|
309
|
+
if self._is_module_reference(name):
|
|
304
310
|
roots.add(name)
|
|
305
311
|
return roots
|
|
306
312
|
|
|
@@ -432,12 +432,20 @@ def _is_safe_read(node: ast.Name, parents: dict[int, ast.AST], safe_methods: fro
|
|
|
432
432
|
return _is_safe_method_call(parent, parents, safe_methods)
|
|
433
433
|
case ast.Subscript(value=value, ctx=ctx):
|
|
434
434
|
# `x[k]` reads; `x[k] = v` / `del x[k]` mutate.
|
|
435
|
-
|
|
435
|
+
# A binding used as `frame[x]` is not representation-neutral:
|
|
436
|
+
# pandas and other selector APIs distinguish a list from a tuple.
|
|
437
|
+
return value is node and isinstance(ctx, ast.Load)
|
|
436
438
|
case ast.Call(args=args, func=callee):
|
|
437
439
|
return any(arg is node for arg in args) and _is_safe_callee(callee)
|
|
438
440
|
case ast.keyword(arg=str(), value=value) if value is node:
|
|
439
441
|
grandparent = parents.get(id(parent))
|
|
440
|
-
|
|
442
|
+
# `dict(label=x)` retains `x` as a nested value; unlike
|
|
443
|
+
# `dict(x)`, it does not copy the candidate collection itself.
|
|
444
|
+
return (
|
|
445
|
+
isinstance(grandparent, ast.Call)
|
|
446
|
+
and _is_safe_callee(grandparent.func)
|
|
447
|
+
and _dotted_name(grandparent.func) != "dict"
|
|
448
|
+
)
|
|
441
449
|
case ast.Compare():
|
|
442
450
|
return True
|
|
443
451
|
case ast.For() | ast.AsyncFor() | ast.comprehension():
|
|
@@ -143,6 +143,7 @@ class PreferTimedeltaForDurations(Rule):
|
|
|
143
143
|
exempt_fields = (
|
|
144
144
|
_settings_field_ids(tree) | _pydantic_model_field_ids(tree, imports) | _typed_dict_field_ids(tree, imports)
|
|
145
145
|
)
|
|
146
|
+
serialized_constants = _exclusively_serialized_duration_constants(tree, imports)
|
|
146
147
|
diags: list[Diagnostic] = []
|
|
147
148
|
for node in nodes(tree, ast.FunctionDef, ast.AsyncFunctionDef, ast.AnnAssign):
|
|
148
149
|
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
@@ -163,7 +164,7 @@ class PreferTimedeltaForDurations(Rule):
|
|
|
163
164
|
for statement in tree.body:
|
|
164
165
|
match statement:
|
|
165
166
|
case ast.Assign(targets=[ast.Name(id=name)], value=value) if _is_numeric_expression(value):
|
|
166
|
-
if _is_constant_reference(name):
|
|
167
|
+
if _is_constant_reference(name) and name not in serialized_constants:
|
|
167
168
|
numeric = "float" if _contains_float(value) else "int"
|
|
168
169
|
self._consider(name, ast.Name(id=numeric), statement, diags, path)
|
|
169
170
|
case _:
|
|
@@ -238,6 +239,148 @@ def _contains_float(node: ast.AST) -> bool:
|
|
|
238
239
|
return any(isinstance(child, ast.Constant) and isinstance(child.value, float) for child in ast.walk(node))
|
|
239
240
|
|
|
240
241
|
|
|
242
|
+
def _exclusively_serialized_duration_constants(tree: ast.Module, imports: ImportIndex) -> frozenset[str]:
|
|
243
|
+
"""Find numeric constants whose reads are proven wire-model construction only."""
|
|
244
|
+
definitions = {
|
|
245
|
+
statement.targets[0].id: statement.targets[0]
|
|
246
|
+
for statement in tree.body
|
|
247
|
+
if isinstance(statement, ast.Assign)
|
|
248
|
+
and len(statement.targets) == 1
|
|
249
|
+
and isinstance(statement.targets[0], ast.Name)
|
|
250
|
+
and _is_numeric_expression(statement.value)
|
|
251
|
+
and _is_constant_reference(statement.targets[0].id)
|
|
252
|
+
}
|
|
253
|
+
if not definitions:
|
|
254
|
+
return frozenset()
|
|
255
|
+
wire_models = _wire_model_class_names(tree, imports)
|
|
256
|
+
if not wire_models:
|
|
257
|
+
return frozenset()
|
|
258
|
+
parent = {id(child): owner for owner in ast.walk(tree) for child in ast.iter_child_nodes(owner)}
|
|
259
|
+
serialized: set[str] = set()
|
|
260
|
+
for name, target in definitions.items():
|
|
261
|
+
if _has_competing_binding(tree, name, target):
|
|
262
|
+
continue
|
|
263
|
+
loads = [
|
|
264
|
+
node
|
|
265
|
+
for node in ast.walk(tree)
|
|
266
|
+
if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load) and node.id == name
|
|
267
|
+
]
|
|
268
|
+
if loads and all(_is_wire_model_value(load, parent, wire_models) for load in loads):
|
|
269
|
+
serialized.add(name)
|
|
270
|
+
return frozenset(serialized)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _has_competing_binding(tree: ast.Module, name: str, definition: ast.Name) -> bool:
|
|
274
|
+
"""Reject lexical ambiguity rather than attributing shadowed reads to a constant."""
|
|
275
|
+
for node in ast.walk(tree):
|
|
276
|
+
match node:
|
|
277
|
+
case ast.Name(id=identifier, ctx=ast.Store()) if identifier == name and node is not definition:
|
|
278
|
+
return True
|
|
279
|
+
case ast.arg(arg=identifier) if identifier == name:
|
|
280
|
+
return True
|
|
281
|
+
case (
|
|
282
|
+
ast.FunctionDef(name=identifier) | ast.AsyncFunctionDef(name=identifier) | ast.ClassDef(name=identifier)
|
|
283
|
+
) if identifier == name:
|
|
284
|
+
return True
|
|
285
|
+
case ast.alias(name=imported, asname=alias) if (alias or imported.split(".")[0]) == name:
|
|
286
|
+
return True
|
|
287
|
+
case ast.ExceptHandler(name=identifier) if identifier == name:
|
|
288
|
+
return True
|
|
289
|
+
case ast.MatchAs(name=identifier) | ast.MatchStar(name=identifier) if identifier == name:
|
|
290
|
+
return True
|
|
291
|
+
case ast.MatchMapping(rest=identifier) if identifier == name:
|
|
292
|
+
return True
|
|
293
|
+
case _:
|
|
294
|
+
pass
|
|
295
|
+
return False
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _is_wire_model_value(
|
|
299
|
+
load: ast.Name,
|
|
300
|
+
parents: dict[int, ast.AST],
|
|
301
|
+
wire_models: frozenset[str],
|
|
302
|
+
) -> bool:
|
|
303
|
+
parent = parents.get(id(load))
|
|
304
|
+
if isinstance(parent, ast.keyword) and parent.value is load:
|
|
305
|
+
call = parents.get(id(parent))
|
|
306
|
+
return isinstance(call, ast.Call) and _trailing_name(call.func) in wire_models
|
|
307
|
+
if not (isinstance(parent, ast.Dict) and any(value is load for value in parent.values)):
|
|
308
|
+
return False
|
|
309
|
+
returned = parents.get(id(parent))
|
|
310
|
+
if not (isinstance(returned, ast.Return) and returned.value is parent):
|
|
311
|
+
return False
|
|
312
|
+
owner = parents.get(id(returned))
|
|
313
|
+
while owner is not None and not isinstance(owner, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
314
|
+
owner = parents.get(id(owner))
|
|
315
|
+
return owner is not None and bool(_annotation_names(owner.returns) & wire_models)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _annotation_names(node: ast.expr | None) -> frozenset[str]:
|
|
319
|
+
if node is None:
|
|
320
|
+
return frozenset()
|
|
321
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
322
|
+
try:
|
|
323
|
+
node = ast.parse(node.value.strip(), mode="eval").body
|
|
324
|
+
except SyntaxError:
|
|
325
|
+
return frozenset()
|
|
326
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
|
|
327
|
+
return _annotation_names(node.left) | _annotation_names(node.right)
|
|
328
|
+
if isinstance(node, ast.Subscript) and _trailing_name(node.value) in {"Annotated", "Optional"}:
|
|
329
|
+
inner = node.slice.elts[0] if isinstance(node.slice, ast.Tuple) and node.slice.elts else node.slice
|
|
330
|
+
return _annotation_names(inner)
|
|
331
|
+
name = _trailing_name(node)
|
|
332
|
+
return frozenset({name}) if name is not None else frozenset()
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _wire_model_class_names(tree: ast.Module, imports: ImportIndex) -> frozenset[str]:
|
|
336
|
+
"""Resolve locally declared Pydantic and TypedDict wire models."""
|
|
337
|
+
classes = nodes(tree, ast.ClassDef)
|
|
338
|
+
by_name: dict[str, list[ast.ClassDef]] = {}
|
|
339
|
+
for node in classes:
|
|
340
|
+
by_name.setdefault(node.name, []).append(node)
|
|
341
|
+
resolved: dict[int, bool] = {}
|
|
342
|
+
|
|
343
|
+
def is_wire_model(node: ast.ClassDef, seen: frozenset[int]) -> bool:
|
|
344
|
+
cached = resolved.get(id(node))
|
|
345
|
+
if cached is not None:
|
|
346
|
+
return cached
|
|
347
|
+
if id(node) in seen:
|
|
348
|
+
return False
|
|
349
|
+
result = any(
|
|
350
|
+
imports.resolves(
|
|
351
|
+
base,
|
|
352
|
+
sources=frozenset(
|
|
353
|
+
{
|
|
354
|
+
"pydantic",
|
|
355
|
+
"pydantic.main",
|
|
356
|
+
"pydantic.v1",
|
|
357
|
+
"pydantic.v1.main",
|
|
358
|
+
}
|
|
359
|
+
),
|
|
360
|
+
symbol="BaseModel",
|
|
361
|
+
)
|
|
362
|
+
or imports.resolves(
|
|
363
|
+
base,
|
|
364
|
+
sources=frozenset({"typing", "typing_extensions"}),
|
|
365
|
+
symbol="TypedDict",
|
|
366
|
+
)
|
|
367
|
+
or (
|
|
368
|
+
(base_name := _trailing_name(base)) is not None
|
|
369
|
+
and len(parents := by_name.get(base_name, ())) == 1
|
|
370
|
+
and is_wire_model(parents[0], seen | {id(node)})
|
|
371
|
+
)
|
|
372
|
+
for base in node.bases
|
|
373
|
+
)
|
|
374
|
+
resolved[id(node)] = result
|
|
375
|
+
return result
|
|
376
|
+
|
|
377
|
+
return frozenset(
|
|
378
|
+
name
|
|
379
|
+
for name, definitions in by_name.items()
|
|
380
|
+
if len(definitions) == 1 and is_wire_model(definitions[0], frozenset())
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
|
|
241
384
|
def _has_cli_decorator(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
242
385
|
"""Report whether the function is a `click` / `typer` command entry point."""
|
|
243
386
|
for dec in node.decorator_list:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/_docstrings.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/_prose_budget.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/_test_assertions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/mock_without_spec.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_comment_cruft.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_long_comment.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_secret_in_log.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_select_star.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/no_stdlib_logging.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/over_mocked_test.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/prefer_class_row.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/prefer_or_pattern.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/prefer_str_enum.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.67.2 → sarj_python_lint-0.67.4}/src/sarj_python_lint/rules/unused_mock_setup.py
RENAMED
|
File without changes
|