sarj-python-lint 0.86.0__tar.gz → 0.87.0__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.86.0 → sarj_python_lint-0.87.0}/PKG-INFO +2 -2
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/pyproject.toml +4 -4
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_project_index.py +148 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +53 -2
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_delete_statement.py +1 -1
- sarj_python_lint-0.87.0/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +402 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/require_port_for_service.py +107 -32
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +173 -39
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/require_typed_http_test_response.py +97 -1
- sarj_python_lint-0.86.0/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -206
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/.gitignore +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/LICENSE +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/README.md +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.87.0
|
|
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/
|
|
@@ -18,7 +18,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
18
18
|
Classifier: Topic :: Software Development :: Quality Assurance
|
|
19
19
|
Requires-Python: >=3.14
|
|
20
20
|
Requires-Dist: packaging==26.3
|
|
21
|
-
Requires-Dist: ruff>=0.16.
|
|
21
|
+
Requires-Dist: ruff>=0.16.8
|
|
22
22
|
Requires-Dist: sqlglot==30.18.0
|
|
23
23
|
Requires-Dist: typer==0.27.2
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sarj-python-lint"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.87.0"
|
|
4
4
|
description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "sarj-ai" }]
|
|
@@ -15,7 +15,7 @@ classifiers = [
|
|
|
15
15
|
"Programming Language :: Python :: 3.14",
|
|
16
16
|
"Topic :: Software Development :: Quality Assurance",
|
|
17
17
|
]
|
|
18
|
-
dependencies = ["packaging==26.3", "ruff>=0.16.
|
|
18
|
+
dependencies = ["packaging==26.3", "ruff>=0.16.8", "sqlglot==30.18.0", "typer==0.27.2"]
|
|
19
19
|
|
|
20
20
|
[project.scripts]
|
|
21
21
|
sarj-python-lint = "sarj_python_lint.__main__:main"
|
|
@@ -31,8 +31,8 @@ Issues = "https://github.com/sarj-ai/code-standards/issues"
|
|
|
31
31
|
[dependency-groups]
|
|
32
32
|
dev = [
|
|
33
33
|
"pytest>=9.1.1",
|
|
34
|
-
"ruff>=0.16.
|
|
35
|
-
"basedpyright>=1.
|
|
34
|
+
"ruff>=0.16.8",
|
|
35
|
+
"basedpyright>=1.40.1",
|
|
36
36
|
]
|
|
37
37
|
|
|
38
38
|
[build-system]
|
{sarj_python_lint-0.86.0 → sarj_python_lint-0.87.0}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
@@ -162,6 +162,47 @@ class ProjectIndexSet:
|
|
|
162
162
|
consumers.update(_unit_constructor_consumers(candidate, candidate.tree, target))
|
|
163
163
|
return frozenset(consumers)
|
|
164
164
|
|
|
165
|
+
def direct_subclasses(self, unit: SourceUnit, name: str) -> frozenset[tuple[Path, str]]:
|
|
166
|
+
if unit.module is None:
|
|
167
|
+
return frozenset()
|
|
168
|
+
target = SymbolRef(unit.module, name)
|
|
169
|
+
return frozenset(
|
|
170
|
+
(candidate.path, node.name)
|
|
171
|
+
for candidate in self._units.values()
|
|
172
|
+
if candidate.tree is not None and name in candidate.source
|
|
173
|
+
for node in candidate.tree.body
|
|
174
|
+
if isinstance(node, ast.ClassDef)
|
|
175
|
+
and any(_direct_base_symbol(candidate, base) == target for base in node.bases)
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def typed_class_consumers(self, unit: SourceUnit, name: str) -> frozenset[tuple[Path, str]]:
|
|
179
|
+
if unit.module is None:
|
|
180
|
+
return frozenset()
|
|
181
|
+
target = SymbolRef(unit.module, name)
|
|
182
|
+
return frozenset(
|
|
183
|
+
(candidate.path, owner.name)
|
|
184
|
+
for candidate in self._units.values()
|
|
185
|
+
if candidate.tree is not None and name in candidate.source
|
|
186
|
+
for owner in candidate.tree.body
|
|
187
|
+
if isinstance(owner, ast.ClassDef) and _class_has_typed_dependency(candidate, owner, target)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def test_mock_specs(self, unit: SourceUnit, name: str) -> frozenset[Path]:
|
|
191
|
+
if unit.module is None:
|
|
192
|
+
return frozenset()
|
|
193
|
+
target = SymbolRef(unit.module, name)
|
|
194
|
+
return frozenset(
|
|
195
|
+
candidate.path
|
|
196
|
+
for candidate in self._units.values()
|
|
197
|
+
if candidate.tree is not None
|
|
198
|
+
and name in candidate.source
|
|
199
|
+
and any(
|
|
200
|
+
_is_mock_spec_for(candidate, call, target)
|
|
201
|
+
for call in ast.walk(candidate.tree)
|
|
202
|
+
if isinstance(call, ast.Call)
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
|
|
165
206
|
def class_inherits_from(self, unit: SourceUnit, name: str, qualified_bases: frozenset[str]) -> bool:
|
|
166
207
|
if unit.module is None:
|
|
167
208
|
return False
|
|
@@ -213,6 +254,9 @@ def _is_index_candidate(source: str, matched_classes: set[str]) -> bool:
|
|
|
213
254
|
return (
|
|
214
255
|
"NewType(" in source
|
|
215
256
|
or "class " in source
|
|
257
|
+
or "spec=" in source
|
|
258
|
+
or "spec_set=" in source
|
|
259
|
+
or "create_autospec(" in source
|
|
216
260
|
or ("match " in source and "str(" in source)
|
|
217
261
|
or any(f"class {name}" in source for name in matched_classes)
|
|
218
262
|
)
|
|
@@ -223,6 +267,9 @@ def _module_name(path: Path, roots: Sequence[Path]) -> str | None:
|
|
|
223
267
|
root = next((candidate for candidate in roots if resolved == candidate or candidate in resolved.parents), None)
|
|
224
268
|
if root is not None:
|
|
225
269
|
try:
|
|
270
|
+
project_module = _project_module_name(resolved, root)
|
|
271
|
+
if project_module is not None:
|
|
272
|
+
return project_module
|
|
226
273
|
package_dir = _outer_package_directory(resolved, root)
|
|
227
274
|
except OSError:
|
|
228
275
|
return None
|
|
@@ -233,6 +280,23 @@ def _module_name(path: Path, roots: Sequence[Path]) -> str | None:
|
|
|
233
280
|
return _package_module_name(path)
|
|
234
281
|
|
|
235
282
|
|
|
283
|
+
def _project_module_name(resolved: Path, root: Path) -> str | None:
|
|
284
|
+
# PEP 420 namespace directories do not have __init__.py. The closest
|
|
285
|
+
# Python project boundary preserves their complete importable path.
|
|
286
|
+
project = next(
|
|
287
|
+
(ancestor for ancestor in resolved.parents if (ancestor / "pyproject.toml").is_file()),
|
|
288
|
+
None,
|
|
289
|
+
)
|
|
290
|
+
if project is None or not (project == root or root in project.parents):
|
|
291
|
+
return None
|
|
292
|
+
relative = resolved.relative_to(project)
|
|
293
|
+
parts = relative.parts[1:] if relative.parts[0] == "src" else relative.parts
|
|
294
|
+
if not parts:
|
|
295
|
+
return None
|
|
296
|
+
suffix = parts[:-1] if parts[-1] == "__init__.py" else (*parts[:-1], Path(parts[-1]).stem)
|
|
297
|
+
return ".".join(suffix) or None
|
|
298
|
+
|
|
299
|
+
|
|
236
300
|
def _package_module_name(path: Path) -> str | None:
|
|
237
301
|
parts: list[str] = []
|
|
238
302
|
parent = path.parent
|
|
@@ -324,6 +388,21 @@ def _resolve(unit: SourceUnit, expression: ast.expr) -> SymbolRef | None:
|
|
|
324
388
|
return None
|
|
325
389
|
|
|
326
390
|
|
|
391
|
+
def _direct_base_symbol(unit: SourceUnit, expression: ast.expr) -> SymbolRef | None:
|
|
392
|
+
resolved = _resolve(unit, expression)
|
|
393
|
+
if resolved is not None or not isinstance(expression, ast.Name) or unit.tree is None:
|
|
394
|
+
return resolved
|
|
395
|
+
# Test support modules need not be packages. Absolute imports still prove
|
|
396
|
+
# the base identity even when the file itself has no importable module name.
|
|
397
|
+
for statement in _module_import_statements(unit.tree):
|
|
398
|
+
if not isinstance(statement, ast.ImportFrom) or statement.level or statement.module is None:
|
|
399
|
+
continue
|
|
400
|
+
for alias in statement.names:
|
|
401
|
+
if (alias.asname or alias.name) == expression.id:
|
|
402
|
+
return SymbolRef(statement.module, alias.name)
|
|
403
|
+
return None
|
|
404
|
+
|
|
405
|
+
|
|
327
406
|
def _collect_unit_symbols(
|
|
328
407
|
units: Mapping[Path, SourceUnit], classes: dict[SymbolRef, ClassSummary], nominals: dict[str, set[SymbolRef]]
|
|
329
408
|
) -> None:
|
|
@@ -458,6 +537,75 @@ def _unit_constructor_consumers(
|
|
|
458
537
|
yield (candidate.path, owner.name)
|
|
459
538
|
|
|
460
539
|
|
|
540
|
+
def _class_has_typed_dependency(unit: SourceUnit, owner: ast.ClassDef, target: SymbolRef) -> bool:
|
|
541
|
+
for statement in owner.body:
|
|
542
|
+
if isinstance(statement, ast.AnnAssign) and _annotation_contains_symbol(unit, statement.annotation, target):
|
|
543
|
+
return True
|
|
544
|
+
if not isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
545
|
+
continue
|
|
546
|
+
parameters = (*statement.args.posonlyargs, *statement.args.args, *statement.args.kwonlyargs)
|
|
547
|
+
if any(
|
|
548
|
+
parameter.arg not in {"self", "cls"} and _annotation_contains_symbol(unit, parameter.annotation, target)
|
|
549
|
+
for parameter in parameters
|
|
550
|
+
):
|
|
551
|
+
return True
|
|
552
|
+
return False
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _is_mock_spec_for(unit: SourceUnit, call: ast.Call, target: SymbolRef) -> bool:
|
|
556
|
+
mock_name = _mock_function_name(unit, call.func)
|
|
557
|
+
if mock_name is None:
|
|
558
|
+
return False
|
|
559
|
+
return any(
|
|
560
|
+
keyword.arg in {"spec", "spec_set"} and _direct_base_symbol(unit, keyword.value) == target
|
|
561
|
+
for keyword in call.keywords
|
|
562
|
+
) or (mock_name == "create_autospec" and bool(call.args) and _direct_base_symbol(unit, call.args[0]) == target)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _mock_function_name(unit: SourceUnit, function: ast.expr) -> str | None:
|
|
566
|
+
if isinstance(function, ast.Name):
|
|
567
|
+
return _imported_mock_name(unit, function.id)
|
|
568
|
+
if not isinstance(function, ast.Attribute) or not isinstance(function.value, ast.Name):
|
|
569
|
+
return None
|
|
570
|
+
return _module_mock_name(unit, function.value.id, function.attr)
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _imported_mock_name(unit: SourceUnit, name: str) -> str | None:
|
|
574
|
+
imported = _direct_import_symbol(unit, name)
|
|
575
|
+
if imported is None or imported.module != "unittest.mock":
|
|
576
|
+
return None
|
|
577
|
+
return imported.name if imported.name in {"Mock", "MagicMock", "AsyncMock", "create_autospec"} else None
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _module_mock_name(unit: SourceUnit, module_name: str, method_name: str) -> str | None:
|
|
581
|
+
imported = _direct_import_symbol(unit, module_name)
|
|
582
|
+
if imported not in {SymbolRef("unittest", "mock"), SymbolRef("unittest.mock", "")}:
|
|
583
|
+
return None
|
|
584
|
+
return method_name if method_name in {"Mock", "MagicMock", "AsyncMock", "create_autospec"} else None
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _direct_import_symbol(unit: SourceUnit, name: str) -> SymbolRef | None:
|
|
588
|
+
imported = unit.imports.get(name)
|
|
589
|
+
if imported is not None or unit.tree is None:
|
|
590
|
+
return imported
|
|
591
|
+
for statement in _module_import_statements(unit.tree):
|
|
592
|
+
if (matched := _absolute_import_symbol(statement, name)) is not None:
|
|
593
|
+
return matched
|
|
594
|
+
return None
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _absolute_import_symbol(statement: ast.stmt, name: str) -> SymbolRef | None:
|
|
598
|
+
if isinstance(statement, ast.ImportFrom) and statement.level == 0 and statement.module is not None:
|
|
599
|
+
for alias in statement.names:
|
|
600
|
+
if (alias.asname or alias.name) == name:
|
|
601
|
+
return SymbolRef(statement.module, alias.name)
|
|
602
|
+
if isinstance(statement, ast.Import):
|
|
603
|
+
for alias in statement.names:
|
|
604
|
+
if (alias.asname or alias.name.partition(".")[0]) == name:
|
|
605
|
+
return SymbolRef(alias.name if alias.asname else alias.name.partition(".")[0], "")
|
|
606
|
+
return None
|
|
607
|
+
|
|
608
|
+
|
|
461
609
|
def _annotation_contains_symbol(unit: SourceUnit, annotation: ast.expr | None, target: SymbolRef) -> bool:
|
|
462
610
|
if annotation is None:
|
|
463
611
|
return False
|
|
@@ -57,7 +57,8 @@ class NoAnyMappingTypes(Rule):
|
|
|
57
57
|
),
|
|
58
58
|
(
|
|
59
59
|
"Unique unconditional module-level PEP 695 and explicit TypeAlias chains are resolved with cycle "
|
|
60
|
-
"guards
|
|
60
|
+
"guards. Plain module assignments are checked only when they directly define an Any-valued mapping "
|
|
61
|
+
"used in a module-level annotation; imported, conditional, rebound, and ambiguous aliases are excluded."
|
|
61
62
|
),
|
|
62
63
|
"Generated and vendored sources are excluded; exact local suppressions remain auditable.",
|
|
63
64
|
),
|
|
@@ -108,7 +109,8 @@ class NoAnyMappingTypes(Rule):
|
|
|
108
109
|
lines = source.splitlines()
|
|
109
110
|
findings: list[Diagnostic] = []
|
|
110
111
|
seen: set[tuple[int, int]] = set()
|
|
111
|
-
|
|
112
|
+
expressions = [*_type_expressions(tree, imports, parents, scope_cache), *_implicit_mapping_aliases(tree)]
|
|
113
|
+
for expression, owner in expressions:
|
|
112
114
|
for mapping, location in _unshadowed_any_mappings(expression, owner, semantics, parents, scope_cache):
|
|
113
115
|
key = (location.lineno, location.col_offset)
|
|
114
116
|
if key in seen or is_suppressed(lines, location.lineno, self.code):
|
|
@@ -129,6 +131,55 @@ class NoAnyMappingTypes(Rule):
|
|
|
129
131
|
return sorted(findings, key=lambda finding: (finding.line, finding.col))
|
|
130
132
|
|
|
131
133
|
|
|
134
|
+
def _implicit_mapping_aliases(tree: ast.Module) -> list[tuple[ast.expr, ast.Assign]]:
|
|
135
|
+
bindings = [scope_bound_names([statement]) for statement in tree.body]
|
|
136
|
+
aliases: list[tuple[ast.expr, ast.Assign]] = []
|
|
137
|
+
for statement in tree.body:
|
|
138
|
+
if not isinstance(statement, ast.Assign) or len(statement.targets) != 1:
|
|
139
|
+
continue
|
|
140
|
+
target = statement.targets[0]
|
|
141
|
+
if not isinstance(target, ast.Name) or not isinstance(statement.value, ast.Subscript):
|
|
142
|
+
continue
|
|
143
|
+
if sum(target.id in names for names in bindings) != 1:
|
|
144
|
+
continue
|
|
145
|
+
if _used_in_module_annotation(tree, target.id):
|
|
146
|
+
aliases.append((statement.value, statement))
|
|
147
|
+
return aliases
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _used_in_module_annotation(tree: ast.Module, name: str) -> bool:
|
|
151
|
+
return any(
|
|
152
|
+
_annotation_contains_name(annotation, name)
|
|
153
|
+
for statement in tree.body
|
|
154
|
+
for annotation in _module_statement_annotations(statement, name)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _module_statement_annotations(statement: ast.stmt, alias_name: str) -> list[ast.expr]:
|
|
159
|
+
if isinstance(statement, ast.AnnAssign):
|
|
160
|
+
return [statement.annotation]
|
|
161
|
+
if not isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
162
|
+
return []
|
|
163
|
+
arguments = statement.args
|
|
164
|
+
parameters = (*arguments.posonlyargs, *arguments.args, *arguments.kwonlyargs)
|
|
165
|
+
if any(argument.arg == alias_name for argument in parameters):
|
|
166
|
+
return []
|
|
167
|
+
annotations = [argument.annotation for argument in parameters if argument.annotation is not None]
|
|
168
|
+
if statement.returns is not None:
|
|
169
|
+
annotations.append(statement.returns)
|
|
170
|
+
return annotations
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _annotation_contains_name(annotation: ast.expr, name: str) -> bool:
|
|
174
|
+
parsed = annotation
|
|
175
|
+
if isinstance(annotation, ast.Constant) and isinstance(annotation.value, str):
|
|
176
|
+
try:
|
|
177
|
+
parsed = ast.parse(annotation.value, mode="eval").body
|
|
178
|
+
except SyntaxError:
|
|
179
|
+
return False
|
|
180
|
+
return any(isinstance(node, ast.Name) and node.id == name for node in ast.walk(parsed))
|
|
181
|
+
|
|
182
|
+
|
|
132
183
|
def _unshadowed_any_mappings(
|
|
133
184
|
expression: ast.expr,
|
|
134
185
|
owner: ast.AST,
|
|
@@ -93,7 +93,7 @@ class NoDeleteStatement(Rule):
|
|
|
93
93
|
line=node.lineno,
|
|
94
94
|
col=node.col_offset + 1,
|
|
95
95
|
code=self.code,
|
|
96
|
-
severity=Severity.
|
|
96
|
+
severity=Severity.ERROR,
|
|
97
97
|
message=(
|
|
98
98
|
"`del` introduces mutation or hidden lifetime behavior — construct the desired value "
|
|
99
99
|
"immutably, or add an exact SARJ442 suppression explaining why deletion is required."
|