sarj-python-lint 0.81.4__tar.gz → 0.81.6__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.81.4 → sarj_python_lint-0.81.6}/PKG-INFO +1 -1
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/pyproject.toml +1 -1
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +1 -1
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +1 -1
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_typed_doc_sections.py +1 -1
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +1 -1
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_class_row.py +85 -25
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +4 -4
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +1 -1
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +60 -6
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/.gitignore +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/LICENSE +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/README.md +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 0.81.
|
|
3
|
+
Version: 0.81.6
|
|
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/
|
|
@@ -200,7 +200,7 @@ def _query_argument(call: ast.Call) -> ast.expr | None:
|
|
|
200
200
|
tokens = frozenset(part for part in receiver.split("_") if part)
|
|
201
201
|
if (
|
|
202
202
|
receiver not in _QUERY_RECEIVER_TOKENS
|
|
203
|
-
and
|
|
203
|
+
and tokens.isdisjoint(_QUERY_RECEIVER_TOKENS)
|
|
204
204
|
and not any(receiver.endswith(suffix) for suffix in ("conn", "cur", "cursor", "session"))
|
|
205
205
|
):
|
|
206
206
|
return None
|
|
@@ -761,7 +761,7 @@ def _representation_fixture_path(node: ast.AST) -> bool:
|
|
|
761
761
|
if not isinstance(child, ast.Constant) or not isinstance(child.value, str):
|
|
762
762
|
continue
|
|
763
763
|
parts = {part.lower() for part in child.value.replace("\\", "/").split("/")}
|
|
764
|
-
if parts
|
|
764
|
+
if not parts.isdisjoint(_REPRESENTATION_DIRS):
|
|
765
765
|
return True
|
|
766
766
|
return False
|
|
767
767
|
|
|
@@ -121,7 +121,7 @@ def _public_contract_docstring_lines(path: Path, source: str) -> frozenset[int]:
|
|
|
121
121
|
protocol_method = isinstance(owner, ast.ClassDef) and any(
|
|
122
122
|
_terminal_name(base) in _PUBLIC_CONTRACT_BASES for base in owner.bases
|
|
123
123
|
)
|
|
124
|
-
if decorators
|
|
124
|
+
if not decorators.isdisjoint(_PUBLIC_CONTRACT_DECORATORS) or protocol_method:
|
|
125
125
|
excluded.add(first.lineno)
|
|
126
126
|
return frozenset(excluded)
|
|
127
127
|
|
|
@@ -647,7 +647,7 @@ def _schema_docstring_consumers(tree: ast.Module, imports: dict[str, str]) -> fr
|
|
|
647
647
|
_resolve_imported_name(base.value if isinstance(base, ast.Subscript) else base, imports)
|
|
648
648
|
for base in class_node.bases
|
|
649
649
|
}
|
|
650
|
-
if base_names
|
|
650
|
+
if not base_names.isdisjoint(_KNOWN_SCHEMA_BASES | consumed):
|
|
651
651
|
consumed.add(name)
|
|
652
652
|
changed = True
|
|
653
653
|
return frozenset(id(classes[name]) for name in consumed)
|
{sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_class_row.py
RENAMED
|
@@ -36,6 +36,7 @@ _SCOPE_NODES = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)
|
|
|
36
36
|
class _BoundCursor(NamedTuple):
|
|
37
37
|
variable: str
|
|
38
38
|
factory: ast.expr
|
|
39
|
+
active_nodes: tuple[ast.AST, ...]
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
class _FetchedValue(NamedTuple):
|
|
@@ -73,7 +74,7 @@ class PreferClassRow(Rule):
|
|
|
73
74
|
files=(
|
|
74
75
|
ExampleFile.python(
|
|
75
76
|
"app/task_store.py",
|
|
76
|
-
|
|
77
|
+
'from psycopg.rows import dict_row\n\nasync def load(conn):\n async with conn.cursor(row_factory=dict_row) as cursor:\n await cursor.execute("SELECT id, state FROM task")\n row = await cursor.fetchone()\n return Task.model_validate(row)\n',
|
|
77
78
|
),
|
|
78
79
|
),
|
|
79
80
|
focus_path=PurePosixPath("app/task_store.py"),
|
|
@@ -112,7 +113,7 @@ class PreferClassRow(Rule):
|
|
|
112
113
|
for cursor in _bound_cursors(function):
|
|
113
114
|
if not imports.is_dict_row(cursor.factory, shadowed):
|
|
114
115
|
continue
|
|
115
|
-
models = _models_built_from_cursor(
|
|
116
|
+
models = _models_built_from_cursor(cursor)
|
|
116
117
|
if len(models) != 1:
|
|
117
118
|
continue
|
|
118
119
|
model = next(iter(models))
|
|
@@ -247,37 +248,78 @@ def _functions(tree: ast.Module) -> Iterator[ast.FunctionDef | ast.AsyncFunction
|
|
|
247
248
|
return (node for node in ast.walk(tree) if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)))
|
|
248
249
|
|
|
249
250
|
|
|
250
|
-
def _scope_nodes(function: ast.FunctionDef | ast.AsyncFunctionDef) -> Iterator[ast.AST]:
|
|
251
|
-
pending: list[ast.AST] = list(reversed(function.body))
|
|
252
|
-
while pending:
|
|
253
|
-
node = pending.pop()
|
|
254
|
-
yield node
|
|
255
|
-
if isinstance(node, _SCOPE_NODES):
|
|
256
|
-
continue
|
|
257
|
-
pending.extend(reversed(list(ast.iter_child_nodes(node))))
|
|
258
|
-
|
|
259
|
-
|
|
260
251
|
def _bound_cursors(function: ast.FunctionDef | ast.AsyncFunctionDef) -> list[_BoundCursor]:
|
|
252
|
+
scoped_nodes = tuple(_nodes_in(function.body))
|
|
261
253
|
cursors: list[_BoundCursor] = []
|
|
262
|
-
for node in
|
|
254
|
+
for node in scoped_nodes:
|
|
263
255
|
if isinstance(node, (ast.With, ast.AsyncWith)):
|
|
264
256
|
for item in node.items:
|
|
265
257
|
if isinstance(item.optional_vars, ast.Name):
|
|
266
|
-
|
|
258
|
+
active_nodes = _active_nodes(tuple(_nodes_in(node.body)), item.optional_vars.id, node.lineno)
|
|
259
|
+
cursor = _cursor(item.context_expr, item.optional_vars.id, active_nodes)
|
|
267
260
|
if cursor is not None:
|
|
268
261
|
cursors.append(cursor)
|
|
269
262
|
elif isinstance(node, ast.Assign) and len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
|
|
270
|
-
|
|
263
|
+
variable = node.targets[0].id
|
|
264
|
+
cursor = _cursor(node.value, variable, _active_nodes(scoped_nodes, variable, node.lineno))
|
|
271
265
|
if cursor is not None:
|
|
272
266
|
cursors.append(cursor)
|
|
273
267
|
elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name) and node.value is not None:
|
|
274
|
-
cursor = _cursor(
|
|
268
|
+
cursor = _cursor(
|
|
269
|
+
node.value,
|
|
270
|
+
node.target.id,
|
|
271
|
+
_active_nodes(scoped_nodes, node.target.id, node.lineno),
|
|
272
|
+
)
|
|
275
273
|
if cursor is not None:
|
|
276
274
|
cursors.append(cursor)
|
|
277
275
|
return cursors
|
|
278
276
|
|
|
279
277
|
|
|
280
|
-
def
|
|
278
|
+
def _nodes_in(statements: Iterable[ast.stmt]) -> Iterator[ast.AST]:
|
|
279
|
+
pending: list[ast.AST] = list(reversed(tuple(statements)))
|
|
280
|
+
while pending:
|
|
281
|
+
node = pending.pop()
|
|
282
|
+
yield node
|
|
283
|
+
if isinstance(node, _SCOPE_NODES):
|
|
284
|
+
continue
|
|
285
|
+
pending.extend(reversed(list(ast.iter_child_nodes(node))))
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _active_nodes(nodes: tuple[ast.AST, ...], variable: str, start: int) -> tuple[ast.AST, ...]:
|
|
289
|
+
rebound_lines = [
|
|
290
|
+
node.lineno
|
|
291
|
+
for node in nodes
|
|
292
|
+
if isinstance(node, ast.stmt) and node.lineno > start and _binds_name(node, variable)
|
|
293
|
+
]
|
|
294
|
+
end = min(rebound_lines, default=None)
|
|
295
|
+
return tuple(
|
|
296
|
+
node
|
|
297
|
+
for node in nodes
|
|
298
|
+
if getattr(node, "lineno", start) > start and (end is None or getattr(node, "lineno", end) < end)
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _binds_name(node: ast.stmt, name: str) -> bool:
|
|
303
|
+
if _writes_name(node, name):
|
|
304
|
+
return True
|
|
305
|
+
match node:
|
|
306
|
+
case ast.FunctionDef() | ast.AsyncFunctionDef() | ast.ClassDef():
|
|
307
|
+
return node.name == name
|
|
308
|
+
case ast.For() | ast.AsyncFor():
|
|
309
|
+
return _target_root(node.target) == name
|
|
310
|
+
case ast.With() | ast.AsyncWith():
|
|
311
|
+
return any(
|
|
312
|
+
item.optional_vars is not None and _target_root(item.optional_vars) == name for item in node.items
|
|
313
|
+
)
|
|
314
|
+
case ast.Import():
|
|
315
|
+
return any((alias.asname or alias.name.partition(".")[0]) == name for alias in node.names)
|
|
316
|
+
case ast.ImportFrom():
|
|
317
|
+
return any((alias.asname or alias.name) == name for alias in node.names)
|
|
318
|
+
case _:
|
|
319
|
+
return False
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _cursor(expression: ast.expr, variable: str, active_nodes: tuple[ast.AST, ...]) -> _BoundCursor | None:
|
|
281
323
|
if not (
|
|
282
324
|
isinstance(expression, ast.Call)
|
|
283
325
|
and isinstance(expression.func, ast.Attribute)
|
|
@@ -285,18 +327,15 @@ def _cursor(expression: ast.expr, variable: str) -> _BoundCursor | None:
|
|
|
285
327
|
):
|
|
286
328
|
return None
|
|
287
329
|
factory = next((keyword.value for keyword in expression.keywords if keyword.arg == _ROW_FACTORY), None)
|
|
288
|
-
return None if factory is None else _BoundCursor(variable, factory)
|
|
330
|
+
return None if factory is None else _BoundCursor(variable, factory, active_nodes)
|
|
289
331
|
|
|
290
332
|
|
|
291
333
|
def _models_built_from_cursor(
|
|
292
|
-
|
|
293
|
-
cursor: str,
|
|
334
|
+
cursor: _BoundCursor,
|
|
294
335
|
) -> frozenset[str]:
|
|
295
|
-
scoped_nodes = list(
|
|
296
|
-
fetched_values = _fetched_values(scoped_nodes, cursor)
|
|
297
|
-
|
|
298
|
-
return frozenset()
|
|
299
|
-
models: set[str] = set()
|
|
336
|
+
scoped_nodes = list(cursor.active_nodes)
|
|
337
|
+
fetched_values = _fetched_values(scoped_nodes, cursor.variable)
|
|
338
|
+
models = _models_from_direct_fetch(scoped_nodes, cursor.variable)
|
|
300
339
|
for fetched in fetched_values:
|
|
301
340
|
if fetched.many:
|
|
302
341
|
converted = _models_from_collection(scoped_nodes, fetched)
|
|
@@ -308,6 +347,27 @@ def _models_built_from_cursor(
|
|
|
308
347
|
return frozenset(models)
|
|
309
348
|
|
|
310
349
|
|
|
350
|
+
def _models_from_direct_fetch(scoped_nodes: list[ast.AST], cursor: str) -> set[str]:
|
|
351
|
+
models: set[str] = set()
|
|
352
|
+
for node in scoped_nodes:
|
|
353
|
+
if not isinstance(node, ast.Call):
|
|
354
|
+
continue
|
|
355
|
+
if isinstance(node.func, ast.Attribute) and node.func.attr == _MODEL_VALIDATE:
|
|
356
|
+
if len(node.args) != 1 or node.keywords or _fetch_method(node.args[0], cursor) != "fetchone":
|
|
357
|
+
continue
|
|
358
|
+
model = _model_name(node.func.value)
|
|
359
|
+
else:
|
|
360
|
+
direct_rows = [
|
|
361
|
+
keyword.value
|
|
362
|
+
for keyword in node.keywords
|
|
363
|
+
if keyword.arg is None and _fetch_method(keyword.value, cursor) == "fetchone"
|
|
364
|
+
]
|
|
365
|
+
model = _model_name(node.func) if len(direct_rows) == 1 else None
|
|
366
|
+
if model is not None:
|
|
367
|
+
models.add(model)
|
|
368
|
+
return models
|
|
369
|
+
|
|
370
|
+
|
|
311
371
|
def _fetched_values(scoped_nodes: list[ast.AST], cursor: str) -> list[_FetchedValue]:
|
|
312
372
|
fetched: list[_FetchedValue] = []
|
|
313
373
|
for node in scoped_nodes:
|
|
@@ -299,15 +299,15 @@ def _name_role(identifier: str) -> _Role:
|
|
|
299
299
|
def _is_authenticator_name(identifier: str) -> bool:
|
|
300
300
|
tokens = identifier_tokens(identifier)
|
|
301
301
|
token_set = set(tokens)
|
|
302
|
-
is_auth_header = "header" in token_set and
|
|
302
|
+
is_auth_header = "header" in token_set and not token_set.isdisjoint({"auth", "authorization"})
|
|
303
303
|
if not is_secret_name(identifier) and not is_auth_header:
|
|
304
304
|
return False
|
|
305
305
|
if (token_set == {"token"} and not identifier.isupper()) or not tokens:
|
|
306
306
|
return False
|
|
307
|
-
if tokens[-1] in _DESCRIPTOR_WORDS or token_set
|
|
307
|
+
if tokens[-1] in _DESCRIPTOR_WORDS or not token_set.isdisjoint(_CATEGORY_WORDS):
|
|
308
308
|
return False
|
|
309
|
-
return
|
|
310
|
-
token_set
|
|
309
|
+
return (
|
|
310
|
+
not token_set.isdisjoint(_AUTH_WORDS)
|
|
311
311
|
or is_auth_header
|
|
312
312
|
or any(left == "api" and right == "key" for left, right in pairwise(tokens))
|
|
313
313
|
)
|
|
@@ -431,7 +431,7 @@ def _missing_unit(statement: ast.Assign | ast.AnnAssign, name: str, comment: str
|
|
|
431
431
|
# A name carrying a different unit is contradictory, but appending a second
|
|
432
432
|
# suffix would worsen it; leave the conflict to a dedicated future rule.
|
|
433
433
|
mentioned_name_tokens = frozenset(token for unit in mentioned for token in unit.name_tokens)
|
|
434
|
-
if declaration_tokens
|
|
434
|
+
if not declaration_tokens.isdisjoint(_CONFLICT_UNIT_NAME_TOKENS - mentioned_name_tokens):
|
|
435
435
|
return None
|
|
436
436
|
# A rationale can mention several quantities; once the name identifies one,
|
|
437
437
|
# guessing that another quantity is the assigned value is unsafe.
|
|
@@ -23,6 +23,24 @@ if TYPE_CHECKING:
|
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
_TRACKED_BUILTINS = frozenset(
|
|
27
|
+
{
|
|
28
|
+
"bool",
|
|
29
|
+
"dict",
|
|
30
|
+
"enumerate",
|
|
31
|
+
"filter",
|
|
32
|
+
"frozenset",
|
|
33
|
+
"list",
|
|
34
|
+
"map",
|
|
35
|
+
"range",
|
|
36
|
+
"reversed",
|
|
37
|
+
"set",
|
|
38
|
+
"tuple",
|
|
39
|
+
"zip",
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
26
44
|
@final
|
|
27
45
|
class PreferSetIsdisjoint(Rule):
|
|
28
46
|
id = "prefer-set-isdisjoint"
|
|
@@ -35,7 +53,7 @@ class PreferSetIsdisjoint(Rule):
|
|
|
35
53
|
autofix=AutofixPolicy.SUGGESTION,
|
|
36
54
|
limitations=(
|
|
37
55
|
"Built-in set identity must be proven from a literal, comprehension, constructor, or one dominating local assignment.",
|
|
38
|
-
"
|
|
56
|
+
"For binary `&`, the left operand must be a proven built-in set and the right operand must be a literal, comprehension, built-in iterable constructor, standard no-argument collection view, or proven set; annotations, parameters, attributes, subclasses, branch-merged bindings, stored intersections, and generated files are excluded.",
|
|
39
57
|
"The suggestion is intentionally not an autofix because short-circuiting may make custom element equality or hashing side effects observable.",
|
|
40
58
|
),
|
|
41
59
|
examples=(
|
|
@@ -185,6 +203,8 @@ class _Scanner:
|
|
|
185
203
|
if isinstance(expression, ast.UnaryOp) and isinstance(expression.op, ast.Not):
|
|
186
204
|
negated = True
|
|
187
205
|
candidate = expression.operand
|
|
206
|
+
if isinstance(candidate, ast.Call) and _is_builtin_bool_call(candidate, self.shadowed):
|
|
207
|
+
candidate = candidate.args[0]
|
|
188
208
|
if _is_intersection(candidate, safe_exact, self.shadowed):
|
|
189
209
|
self._report(candidate, negated=negated)
|
|
190
210
|
|
|
@@ -246,7 +266,7 @@ class _Scanner:
|
|
|
246
266
|
|
|
247
267
|
def _is_intersection(node: ast.expr, exact: set[str], shadowed: frozenset[str]) -> bool:
|
|
248
268
|
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitAnd):
|
|
249
|
-
return _is_exact_set(node.left, exact, shadowed) and
|
|
269
|
+
return _is_exact_set(node.left, exact, shadowed) and _is_proven_iterable(node.right, exact, shadowed)
|
|
250
270
|
return (
|
|
251
271
|
isinstance(node, ast.Call)
|
|
252
272
|
and isinstance(node.func, ast.Attribute)
|
|
@@ -254,7 +274,17 @@ def _is_intersection(node: ast.expr, exact: set[str], shadowed: frozenset[str])
|
|
|
254
274
|
and len(node.args) == 1
|
|
255
275
|
and not node.keywords
|
|
256
276
|
and _is_exact_set(node.func.value, exact, shadowed)
|
|
257
|
-
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _is_builtin_bool_call(node: ast.expr, shadowed: frozenset[str]) -> bool:
|
|
281
|
+
return (
|
|
282
|
+
isinstance(node, ast.Call)
|
|
283
|
+
and isinstance(node.func, ast.Name)
|
|
284
|
+
and node.func.id == "bool"
|
|
285
|
+
and node.func.id not in shadowed
|
|
286
|
+
and len(node.args) == 1
|
|
287
|
+
and not node.keywords
|
|
258
288
|
)
|
|
259
289
|
|
|
260
290
|
|
|
@@ -273,6 +303,30 @@ def _is_exact_set(node: ast.expr, exact: set[str], shadowed: frozenset[str]) ->
|
|
|
273
303
|
)
|
|
274
304
|
|
|
275
305
|
|
|
306
|
+
def _is_proven_iterable(node: ast.expr, exact: set[str], shadowed: frozenset[str]) -> bool:
|
|
307
|
+
if _is_exact_set(node, exact, shadowed):
|
|
308
|
+
return True
|
|
309
|
+
if isinstance(
|
|
310
|
+
node,
|
|
311
|
+
ast.List | ast.Tuple | ast.Dict | ast.ListComp | ast.DictComp | ast.GeneratorExp | ast.Constant,
|
|
312
|
+
):
|
|
313
|
+
return not isinstance(node, ast.Constant) or isinstance(node.value, str | bytes)
|
|
314
|
+
if (
|
|
315
|
+
isinstance(node, ast.Call)
|
|
316
|
+
and isinstance(node.func, ast.Attribute)
|
|
317
|
+
and node.func.attr in {"items", "keys", "values"}
|
|
318
|
+
and not node.args
|
|
319
|
+
and not node.keywords
|
|
320
|
+
):
|
|
321
|
+
return True
|
|
322
|
+
return (
|
|
323
|
+
isinstance(node, ast.Call)
|
|
324
|
+
and isinstance(node.func, ast.Name)
|
|
325
|
+
and node.func.id in {"dict", "enumerate", "filter", "list", "map", "range", "reversed", "tuple", "zip"}
|
|
326
|
+
and node.func.id not in shadowed
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
|
|
276
330
|
def _stored_names(node: ast.AST) -> set[str]:
|
|
277
331
|
return {child.id for child in ast.walk(node) if isinstance(child, ast.Name) and isinstance(child.ctx, ast.Store)}
|
|
278
332
|
|
|
@@ -297,10 +351,10 @@ def _argument_names(arguments: ast.arguments) -> set[str]:
|
|
|
297
351
|
def _shadowed_builtins(tree: ast.Module) -> frozenset[str]:
|
|
298
352
|
shadowed: set[str] = set()
|
|
299
353
|
for node in ast.walk(tree):
|
|
300
|
-
if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store) and node.id in
|
|
354
|
+
if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store) and node.id in _TRACKED_BUILTINS:
|
|
301
355
|
shadowed.add(node.id)
|
|
302
|
-
elif isinstance(node, ast.arg) and node.arg in
|
|
356
|
+
elif isinstance(node, ast.arg) and node.arg in _TRACKED_BUILTINS:
|
|
303
357
|
shadowed.add(node.arg)
|
|
304
|
-
elif isinstance(node, ast.alias) and (node.asname or node.name.split(".")[0]) in
|
|
358
|
+
elif isinstance(node, ast.alias) and (node.asname or node.name.split(".")[0]) in _TRACKED_BUILTINS:
|
|
305
359
|
shadowed.add(node.asname or node.name.split(".")[0])
|
|
306
360
|
return frozenset(shadowed)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/_analysis_session.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.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_ast_index.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_docstrings.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_fixed_record.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/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.81.4 → sarj_python_lint-0.81.6}/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
|
{sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/mock_without_spec.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_comment_cruft.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_dunder_all.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_secret_in_log.py
RENAMED
|
File without changes
|
{sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/no_select_star.py
RENAMED
|
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.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/over_mocked_test.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
|
{sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/prefer_or_pattern.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-0.81.4 → sarj_python_lint-0.81.6}/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
|
|
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.81.4 → sarj_python_lint-0.81.6}/src/sarj_python_lint/rules/unused_mock_setup.py
RENAMED
|
File without changes
|