sarj-python-lint 0.80.5__tar.gz → 0.80.8__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.80.5 → sarj_python_lint-0.80.8}/PKG-INFO +2 -1
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/pyproject.toml +2 -2
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/__main__.py +3 -5
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/_fastapi.py +12 -0
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/_fixed_record.py +151 -0
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/_imports.py +181 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/_prose_budget.py +121 -11
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/_registry.py +15 -29
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/_sql.py +26 -5
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +505 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +144 -54
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/docstring_args_restate_signature.py +215 -0
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +326 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/fakes_in_shared_location.py +64 -13
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +175 -23
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/iac_source_coupled_test.py +27 -10
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +219 -14
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/mock_without_spec.py +255 -35
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +103 -42
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +187 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +33 -21
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +270 -0
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/no_offset_pagination.py +167 -0
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +902 -0
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +804 -0
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +912 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/no_redundant_literal_description.py +124 -49
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/no_repeated_string_literal.py +83 -41
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/no_repeated_test_body.py +41 -50
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/no_restated_comment.py +56 -18
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/no_secret_in_log.py +183 -0
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/no_select_star.py +258 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +222 -206
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/no_tautological_expect.py +218 -0
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/no_typed_doc_sections.py +134 -0
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/no_unique_violation_message_match.py +399 -0
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/no_vague_suppression_description.py +138 -0
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +202 -0
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +298 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/over_mocked_test.py +216 -51
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/prefer_class_row.py +432 -0
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +507 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +246 -87
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +115 -27
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/prefer_library_fake.py +253 -86
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/prefer_match_assert_never.py +436 -0
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +449 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/prefer_module_level_constant.py +254 -110
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +205 -0
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/prefer_nominal_id_types.py +546 -0
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +336 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/prefer_or_pattern.py +72 -34
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +209 -34
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/prefer_self_type_annotation.py +652 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/prefer_str_enum.py +301 -128
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +368 -0
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +265 -0
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +506 -0
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +249 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/production_derived_test_cases.py +151 -44
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/pydantic_at_boundaries.py +313 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +181 -54
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/redundant_class_docstring.py +209 -0
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/redundant_docstring.py +269 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/redundant_module_docstring.py +79 -41
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +273 -0
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/repeated_static_call_cases.py +512 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +219 -18
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +151 -51
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/require_port_for_service.py +250 -51
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +1208 -0
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +614 -0
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/restated_test_docstring.py +545 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +103 -22
- sarj_python_lint-0.80.8/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +315 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +68 -15
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/_imports.py +0 -90
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -140
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -201
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -212
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -299
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/no_long_comment.py +0 -114
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -171
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/no_offset_pagination.py +0 -118
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -202
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -155
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/no_secret_in_log.py +0 -171
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/no_select_star.py +0 -138
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -580
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -167
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/no_tautological_expect.py +0 -279
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -86
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -170
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -107
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -209
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/prefer_class_row.py +0 -231
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -332
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -440
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -978
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -493
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -475
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -383
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/prefer_one_for_required_row.py +0 -175
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -250
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -131
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -669
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -171
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -298
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -149
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/preserve_declared_nominal_id.py +0 -138
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/preserve_enum_types.py +0 -131
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -374
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -121
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/redundant_docstring.py +0 -171
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -206
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -305
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -601
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -166
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/require_validated_row_factory.py +0 -159
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/restated_test_docstring.py +0 -248
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/source_coupled_test.py +0 -503
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/sql_requires_injected_pool_owner.py +0 -202
- sarj_python_lint-0.80.5/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -148
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/.gitignore +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/LICENSE +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/README.md +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/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.80.
|
|
3
|
+
Version: 0.80.8
|
|
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,6 +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: ruff>=0.16.5
|
|
21
|
+
Requires-Dist: sqlglot==30.18.0
|
|
21
22
|
Description-Content-Type: text/markdown
|
|
22
23
|
|
|
23
24
|
<!-- Generated by `code-standards maintain docs sync`; do not edit. -->
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sarj-python-lint"
|
|
3
|
-
version = "0.80.
|
|
3
|
+
version = "0.80.8"
|
|
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 = ["ruff>=0.16.5"]
|
|
18
|
+
dependencies = ["ruff>=0.16.5", "sqlglot==30.18.0"]
|
|
19
19
|
|
|
20
20
|
[project.scripts]
|
|
21
21
|
sarj-python-lint = "sarj_python_lint.__main__:main"
|
|
@@ -125,14 +125,12 @@ def analyze(
|
|
|
125
125
|
|
|
126
126
|
_DIAGNOSTIC_PRECEDENCE = MappingProxyType(
|
|
127
127
|
{
|
|
128
|
-
"SARJ003": frozenset({"SARJ080"}),
|
|
129
128
|
"SARJ050": frozenset({"SARJ420"}),
|
|
130
|
-
"SARJ084": frozenset({"SARJ050", "
|
|
129
|
+
"SARJ084": frozenset({"SARJ050", "SARJ420"}),
|
|
131
130
|
"SARJ085": frozenset({"SARJ420"}),
|
|
132
131
|
"SARJ086": frozenset({"SARJ420"}),
|
|
133
132
|
"SARJ087": frozenset({"SARJ420"}),
|
|
134
|
-
"SARJ088": frozenset({"SARJ050", "SARJ085", "
|
|
135
|
-
"SARJ091": frozenset({"SARJ420"}),
|
|
133
|
+
"SARJ088": frozenset({"SARJ050", "SARJ085", "SARJ420"}),
|
|
136
134
|
"SARJ092": frozenset({"SARJ086", "SARJ087", "SARJ420"}),
|
|
137
135
|
"SARJ093": frozenset({"SARJ034"}),
|
|
138
136
|
"SARJ099": frozenset({"SARJ420"}),
|
|
@@ -140,7 +138,7 @@ _DIAGNOSTIC_PRECEDENCE = MappingProxyType(
|
|
|
140
138
|
)
|
|
141
139
|
|
|
142
140
|
_DOCSTRING_PRECEDENCE_CODES = frozenset(
|
|
143
|
-
{"SARJ050", "SARJ084", "SARJ085", "SARJ086", "SARJ087", "SARJ088", "
|
|
141
|
+
{"SARJ050", "SARJ084", "SARJ085", "SARJ086", "SARJ087", "SARJ088", "SARJ092", "SARJ099"}
|
|
144
142
|
)
|
|
145
143
|
|
|
146
144
|
|
|
@@ -41,6 +41,8 @@ class Route:
|
|
|
41
41
|
path: str | None
|
|
42
42
|
receiver_kind: Literal["FastAPI", "APIRouter"]
|
|
43
43
|
inherited_hidden: bool = False
|
|
44
|
+
inherited_responses: ast.expr | None = None
|
|
45
|
+
inherited_response_class: ast.expr | None = None
|
|
44
46
|
|
|
45
47
|
@property
|
|
46
48
|
def keywords(self) -> dict[str, ast.expr]:
|
|
@@ -119,6 +121,7 @@ class FastapiIndex:
|
|
|
119
121
|
self.receiver_origins: dict[_ReceiverKey, _ReceiverKey] = {}
|
|
120
122
|
self.receiver_kinds: dict[_ReceiverKey, Literal["FastAPI", "APIRouter"]] = {}
|
|
121
123
|
self.hidden_receivers: set[_ReceiverKey] = set()
|
|
124
|
+
self.receiver_defaults: dict[_ReceiverKey, dict[str, ast.expr]] = {}
|
|
122
125
|
self.decorators: dict[_ReceiverKey, _DecoratorBinding] = {}
|
|
123
126
|
self.bound_names: set[_ReceiverKey] = set()
|
|
124
127
|
self._node_scopes: dict[int, int] = {}
|
|
@@ -243,6 +246,12 @@ class FastapiIndex:
|
|
|
243
246
|
self.receivers.add(key)
|
|
244
247
|
self.receiver_origins[key] = key
|
|
245
248
|
self.receiver_kinds[key] = constructor_kind
|
|
249
|
+
if isinstance(value, ast.Call):
|
|
250
|
+
self.receiver_defaults[key] = {
|
|
251
|
+
keyword.arg: keyword.value
|
|
252
|
+
for keyword in value.keywords
|
|
253
|
+
if keyword.arg in {"responses", "default_response_class"}
|
|
254
|
+
}
|
|
246
255
|
if self._constructor_is_hidden(value):
|
|
247
256
|
self.hidden_receivers.add(key)
|
|
248
257
|
changed = True
|
|
@@ -346,6 +355,7 @@ class FastapiIndex:
|
|
|
346
355
|
path = path_node.value if isinstance(path_node, ast.Constant) and isinstance(path_node.value, str) else None
|
|
347
356
|
route_methods = self._route_methods(decorator) if method == "api_route" else (method,)
|
|
348
357
|
origin = self.receiver_origins[receiver_key]
|
|
358
|
+
defaults = self.receiver_defaults.get(origin, {})
|
|
349
359
|
routes.extend(
|
|
350
360
|
Route(
|
|
351
361
|
decorator=decorator,
|
|
@@ -354,6 +364,8 @@ class FastapiIndex:
|
|
|
354
364
|
path=path,
|
|
355
365
|
receiver_kind=self.receiver_kinds[receiver_key],
|
|
356
366
|
inherited_hidden=receiver_key in self.hidden_receivers,
|
|
367
|
+
inherited_responses=defaults.get("responses"),
|
|
368
|
+
inherited_response_class=defaults.get("default_response_class"),
|
|
357
369
|
)
|
|
358
370
|
for route_method in route_methods
|
|
359
371
|
)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
|
|
5
|
+
from sarj_python_lint.rules._ast_index import children
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_RECORD_MUTATOR_METHODS = frozenset({"clear", "pop", "popitem", "setdefault", "update"})
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def builds_fixed_record(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
12
|
+
returned: list[ast.expr] = []
|
|
13
|
+
record_names = {
|
|
14
|
+
target.id
|
|
15
|
+
for current in _owned_nodes(node)
|
|
16
|
+
if isinstance(current, ast.Assign) and _is_record_literal(current.value)
|
|
17
|
+
for target in current.targets
|
|
18
|
+
if isinstance(target, ast.Name)
|
|
19
|
+
}
|
|
20
|
+
record_names.update(
|
|
21
|
+
current.target.id
|
|
22
|
+
for current in _owned_nodes(node)
|
|
23
|
+
if isinstance(current, ast.AnnAssign)
|
|
24
|
+
and isinstance(current.target, ast.Name)
|
|
25
|
+
and current.value is not None
|
|
26
|
+
and _is_record_literal(current.value)
|
|
27
|
+
)
|
|
28
|
+
invalidated_names: set[str] = set()
|
|
29
|
+
aliases: dict[str, str] = {}
|
|
30
|
+
stack: list[ast.AST] = list(node.body)
|
|
31
|
+
while stack:
|
|
32
|
+
current = stack.pop()
|
|
33
|
+
if isinstance(current, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)):
|
|
34
|
+
continue
|
|
35
|
+
if isinstance(current, ast.Return) and current.value is not None:
|
|
36
|
+
returned.append(current.value)
|
|
37
|
+
elif isinstance(current, ast.Assign) and _is_record_literal(current.value):
|
|
38
|
+
for target in current.targets:
|
|
39
|
+
if isinstance(target, ast.Subscript):
|
|
40
|
+
invalidated_names.update(_mutated_record_roots(target))
|
|
41
|
+
elif isinstance(current, ast.Assign):
|
|
42
|
+
if isinstance(current.value, ast.Name):
|
|
43
|
+
for target in current.targets:
|
|
44
|
+
if not isinstance(target, ast.Name):
|
|
45
|
+
continue
|
|
46
|
+
if target.id in record_names:
|
|
47
|
+
invalidated_names.add(target.id)
|
|
48
|
+
else:
|
|
49
|
+
aliases[target.id] = current.value.id
|
|
50
|
+
else:
|
|
51
|
+
invalidated_names.update(
|
|
52
|
+
target.id for target in current.targets if isinstance(target, ast.Name)
|
|
53
|
+
)
|
|
54
|
+
for target in current.targets:
|
|
55
|
+
if isinstance(target, ast.Subscript):
|
|
56
|
+
invalidated_names.update(_mutated_record_roots(target))
|
|
57
|
+
elif (
|
|
58
|
+
isinstance(current, ast.AnnAssign)
|
|
59
|
+
and isinstance(current.target, ast.Name)
|
|
60
|
+
and current.value is not None
|
|
61
|
+
and _is_record_literal(current.value)
|
|
62
|
+
):
|
|
63
|
+
pass
|
|
64
|
+
elif (
|
|
65
|
+
isinstance(current, ast.AnnAssign)
|
|
66
|
+
and isinstance(current.target, ast.Name)
|
|
67
|
+
and isinstance(current.value, ast.Name)
|
|
68
|
+
):
|
|
69
|
+
if current.target.id in record_names:
|
|
70
|
+
invalidated_names.add(current.target.id)
|
|
71
|
+
else:
|
|
72
|
+
aliases[current.target.id] = current.value.id
|
|
73
|
+
elif isinstance(current, (ast.AnnAssign, ast.AugAssign)):
|
|
74
|
+
invalidated_names.update(_mutated_record_roots(current.target))
|
|
75
|
+
elif isinstance(current, ast.Delete):
|
|
76
|
+
for target in current.targets:
|
|
77
|
+
invalidated_names.update(_mutated_record_roots(target))
|
|
78
|
+
elif (
|
|
79
|
+
isinstance(current, ast.Call)
|
|
80
|
+
and isinstance(current.func, ast.Attribute)
|
|
81
|
+
and current.func.attr in _RECORD_MUTATOR_METHODS
|
|
82
|
+
and isinstance(current.func.value, ast.Name)
|
|
83
|
+
):
|
|
84
|
+
invalidated_names.add(current.func.value.id)
|
|
85
|
+
elif isinstance(current, ast.Call):
|
|
86
|
+
invalidated_names.update(
|
|
87
|
+
argument.id
|
|
88
|
+
for argument in (*current.args, *(keyword.value for keyword in current.keywords))
|
|
89
|
+
if isinstance(argument, ast.Name)
|
|
90
|
+
)
|
|
91
|
+
stack.extend(children(current))
|
|
92
|
+
|
|
93
|
+
changed = True
|
|
94
|
+
while changed:
|
|
95
|
+
changed = False
|
|
96
|
+
for alias, original in aliases.items():
|
|
97
|
+
if alias in invalidated_names and original not in invalidated_names:
|
|
98
|
+
invalidated_names.add(original)
|
|
99
|
+
changed = True
|
|
100
|
+
if original in invalidated_names and alias not in invalidated_names:
|
|
101
|
+
invalidated_names.add(alias)
|
|
102
|
+
changed = True
|
|
103
|
+
|
|
104
|
+
def original_name(name: str) -> str:
|
|
105
|
+
seen: set[str] = set()
|
|
106
|
+
while name in aliases and name not in seen:
|
|
107
|
+
seen.add(name)
|
|
108
|
+
name = aliases[name]
|
|
109
|
+
return name
|
|
110
|
+
|
|
111
|
+
intact_record_names = record_names - invalidated_names
|
|
112
|
+
if any(
|
|
113
|
+
isinstance(value, ast.Name) and original_name(value.id) in record_names & invalidated_names
|
|
114
|
+
for value in returned
|
|
115
|
+
):
|
|
116
|
+
return False
|
|
117
|
+
return any(
|
|
118
|
+
_is_record_literal(value)
|
|
119
|
+
or (isinstance(value, ast.Name) and original_name(value.id) in intact_record_names)
|
|
120
|
+
for value in returned
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _owned_nodes(node: ast.FunctionDef | ast.AsyncFunctionDef) -> list[ast.AST]:
|
|
125
|
+
owned: list[ast.AST] = []
|
|
126
|
+
stack: list[ast.AST] = list(node.body)
|
|
127
|
+
while stack:
|
|
128
|
+
current = stack.pop()
|
|
129
|
+
if isinstance(current, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)):
|
|
130
|
+
continue
|
|
131
|
+
owned.append(current)
|
|
132
|
+
stack.extend(children(current))
|
|
133
|
+
return owned
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _is_record_literal(node: ast.expr) -> bool:
|
|
137
|
+
if isinstance(node, ast.List):
|
|
138
|
+
return bool(node.elts) and all(_is_record_literal(element) for element in node.elts)
|
|
139
|
+
if isinstance(node, ast.ListComp):
|
|
140
|
+
return _is_record_literal(node.elt)
|
|
141
|
+
if not isinstance(node, ast.Dict):
|
|
142
|
+
return False
|
|
143
|
+
return bool(node.keys) and all(
|
|
144
|
+
isinstance(key, ast.Constant) and isinstance(key.value, str) for key in node.keys
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _mutated_record_roots(target: ast.AST) -> set[str]:
|
|
149
|
+
while isinstance(target, ast.Subscript):
|
|
150
|
+
target = target.value
|
|
151
|
+
return {target.id} if isinstance(target, ast.Name) else set()
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from types import MappingProxyType
|
|
7
|
+
from typing import Self
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class _ImportTarget:
|
|
12
|
+
module: str
|
|
13
|
+
symbol: str | None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class ImportIndex:
|
|
18
|
+
bindings: Mapping[str, _ImportTarget]
|
|
19
|
+
shadowed_names: frozenset[str]
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_tree(cls, tree: ast.Module, *, module_scope_only: bool = False) -> Self:
|
|
23
|
+
candidates: dict[str, set[_ImportTarget]] = {}
|
|
24
|
+
statements = _module_import_statements(tree) if module_scope_only else tree.body
|
|
25
|
+
for statement in statements:
|
|
26
|
+
match statement:
|
|
27
|
+
case ast.Import(names=names):
|
|
28
|
+
for alias in names:
|
|
29
|
+
local = alias.asname or alias.name.partition(".")[0]
|
|
30
|
+
module = alias.name if alias.asname else alias.name.partition(".")[0]
|
|
31
|
+
candidates.setdefault(local, set()).add(_ImportTarget(module, None))
|
|
32
|
+
case ast.ImportFrom(module=str(module), names=names, level=0):
|
|
33
|
+
for alias in names:
|
|
34
|
+
if alias.name == "*":
|
|
35
|
+
continue
|
|
36
|
+
local = alias.asname or alias.name
|
|
37
|
+
candidates.setdefault(local, set()).add(_ImportTarget(module, alias.name))
|
|
38
|
+
case _:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
imported_names = frozenset(candidates)
|
|
42
|
+
non_import_bindings = (
|
|
43
|
+
_module_non_import_bindings(tree) if module_scope_only else _non_import_bindings(tree)
|
|
44
|
+
)
|
|
45
|
+
rebound = non_import_bindings & imported_names
|
|
46
|
+
bindings = {
|
|
47
|
+
local: next(iter(targets))
|
|
48
|
+
for local, targets in candidates.items()
|
|
49
|
+
if len(targets) == 1 and local not in rebound
|
|
50
|
+
}
|
|
51
|
+
return cls(MappingProxyType(bindings), frozenset(non_import_bindings))
|
|
52
|
+
|
|
53
|
+
def resolves(self, node: ast.expr, *, sources: frozenset[str], symbol: str) -> bool:
|
|
54
|
+
resolved = self._resolve(node)
|
|
55
|
+
return resolved is not None and resolved.module in sources and resolved.symbol == symbol
|
|
56
|
+
|
|
57
|
+
def resolved_symbol(self, node: ast.expr, *, sources: frozenset[str]) -> str | None:
|
|
58
|
+
resolved = self._resolve(node)
|
|
59
|
+
if resolved is None or resolved.module not in sources:
|
|
60
|
+
return None
|
|
61
|
+
return resolved.symbol
|
|
62
|
+
|
|
63
|
+
def builtin_is_unshadowed(self, name: str) -> bool:
|
|
64
|
+
return name not in self.shadowed_names and name not in self.bindings
|
|
65
|
+
|
|
66
|
+
def _resolve(self, node: ast.expr) -> _ImportTarget | None:
|
|
67
|
+
match node:
|
|
68
|
+
case ast.Name(id=local):
|
|
69
|
+
target = self.bindings.get(local)
|
|
70
|
+
return target if target is not None and target.symbol is not None else None
|
|
71
|
+
case ast.Attribute():
|
|
72
|
+
parts: list[str] = []
|
|
73
|
+
current: ast.expr = node
|
|
74
|
+
while isinstance(current, ast.Attribute):
|
|
75
|
+
parts.append(current.attr)
|
|
76
|
+
current = current.value
|
|
77
|
+
if not isinstance(current, ast.Name):
|
|
78
|
+
return None
|
|
79
|
+
target = self.bindings.get(current.id)
|
|
80
|
+
if target is None or target.symbol is not None or not parts:
|
|
81
|
+
return None
|
|
82
|
+
ordered = list(reversed(parts))
|
|
83
|
+
return _ImportTarget(".".join((target.module, *ordered[:-1])), ordered[-1])
|
|
84
|
+
case _:
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _non_import_bindings(tree: ast.Module) -> set[str]:
|
|
89
|
+
names: set[str] = set()
|
|
90
|
+
for node in ast.walk(tree):
|
|
91
|
+
match node:
|
|
92
|
+
case ast.Name(id=name, ctx=(ast.Store() | ast.Del())) | ast.arg(arg=name):
|
|
93
|
+
names.add(name)
|
|
94
|
+
case ast.FunctionDef() | ast.AsyncFunctionDef() | ast.ClassDef():
|
|
95
|
+
names.add(node.name)
|
|
96
|
+
case ast.alias():
|
|
97
|
+
# Imports are the candidate bindings, not shadows of themselves.
|
|
98
|
+
continue
|
|
99
|
+
case _:
|
|
100
|
+
pass
|
|
101
|
+
return names
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _module_non_import_bindings(tree: ast.Module) -> set[str]:
|
|
105
|
+
names: set[str] = set()
|
|
106
|
+
for statement in tree.body:
|
|
107
|
+
match statement:
|
|
108
|
+
case ast.FunctionDef() | ast.AsyncFunctionDef() | ast.ClassDef():
|
|
109
|
+
names.add(statement.name)
|
|
110
|
+
case ast.Assign(targets=targets):
|
|
111
|
+
for target in targets:
|
|
112
|
+
names.update(_target_names(target))
|
|
113
|
+
case ast.AnnAssign(target=target) | ast.AugAssign(target=target):
|
|
114
|
+
names.update(_target_names(target))
|
|
115
|
+
case ast.TypeAlias(name=ast.Name(id=name)):
|
|
116
|
+
names.add(name)
|
|
117
|
+
case ast.Import() | ast.ImportFrom():
|
|
118
|
+
continue
|
|
119
|
+
case _:
|
|
120
|
+
# Module control flow can still bind names in the module namespace.
|
|
121
|
+
_collect_module_bindings(statement, names)
|
|
122
|
+
return names
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _target_names(node: ast.expr) -> set[str]:
|
|
126
|
+
if isinstance(node, ast.Name):
|
|
127
|
+
return {node.id}
|
|
128
|
+
if isinstance(node, (ast.Tuple, ast.List)):
|
|
129
|
+
return {name for element in node.elts for name in _target_names(element)}
|
|
130
|
+
return set()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _collect_module_bindings(node: ast.AST, names: set[str]) -> None:
|
|
134
|
+
match node:
|
|
135
|
+
case ast.FunctionDef() | ast.AsyncFunctionDef() | ast.ClassDef():
|
|
136
|
+
names.add(node.name)
|
|
137
|
+
return
|
|
138
|
+
case ast.ListComp() | ast.SetComp() | ast.GeneratorExp():
|
|
139
|
+
_collect_module_bindings(node.elt, names)
|
|
140
|
+
_collect_comprehension_bindings(node.generators, names)
|
|
141
|
+
return
|
|
142
|
+
case ast.DictComp():
|
|
143
|
+
_collect_module_bindings(node.key, names)
|
|
144
|
+
_collect_module_bindings(node.value, names)
|
|
145
|
+
_collect_comprehension_bindings(node.generators, names)
|
|
146
|
+
return
|
|
147
|
+
case ast.Lambda():
|
|
148
|
+
return
|
|
149
|
+
case ast.Name(id=name, ctx=(ast.Store() | ast.Del())):
|
|
150
|
+
names.add(name)
|
|
151
|
+
case _:
|
|
152
|
+
pass
|
|
153
|
+
for child in ast.iter_child_nodes(node):
|
|
154
|
+
_collect_module_bindings(child, names)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _collect_comprehension_bindings(generators: list[ast.comprehension], names: set[str]) -> None:
|
|
158
|
+
for generator in generators:
|
|
159
|
+
_collect_module_bindings(generator.iter, names)
|
|
160
|
+
for condition in generator.ifs:
|
|
161
|
+
_collect_module_bindings(condition, names)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _module_import_statements(tree: ast.Module) -> list[ast.stmt]:
|
|
165
|
+
statements = list(tree.body)
|
|
166
|
+
for statement in tree.body:
|
|
167
|
+
if not isinstance(statement, ast.If) or not _is_type_checking_guard(statement.test):
|
|
168
|
+
continue
|
|
169
|
+
statements.extend(
|
|
170
|
+
child for child in statement.body if isinstance(child, (ast.Import, ast.ImportFrom))
|
|
171
|
+
)
|
|
172
|
+
return statements
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _is_type_checking_guard(node: ast.expr) -> bool:
|
|
176
|
+
return (isinstance(node, ast.Name) and node.id == "TYPE_CHECKING") or (
|
|
177
|
+
isinstance(node, ast.Attribute)
|
|
178
|
+
and isinstance(node.value, ast.Name)
|
|
179
|
+
and node.value.id in {"typing", "typing_extensions"}
|
|
180
|
+
and node.attr == "TYPE_CHECKING"
|
|
181
|
+
)
|
{sarj_python_lint-0.80.5 → sarj_python_lint-0.80.8}/src/sarj_python_lint/rules/_prose_budget.py
RENAMED
|
@@ -198,7 +198,7 @@ def _docstring_groups(tree: ast.Module) -> list[ProseGroup]:
|
|
|
198
198
|
):
|
|
199
199
|
continue
|
|
200
200
|
if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)) and (
|
|
201
|
-
|
|
201
|
+
_decorator_terminal_names(node) & PROMPT_DECORATOR_MARKERS
|
|
202
202
|
):
|
|
203
203
|
continue
|
|
204
204
|
if isinstance(node, ast.ClassDef) and _is_schema_class(node):
|
|
@@ -244,6 +244,17 @@ def _is_schema_class(node: ast.ClassDef) -> bool:
|
|
|
244
244
|
return bool(bases & _SCHEMA_BASES or decorator_markers(node) & _SCHEMA_DECORATORS)
|
|
245
245
|
|
|
246
246
|
|
|
247
|
+
def _decorator_terminal_names(node: ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef) -> frozenset[str]:
|
|
248
|
+
names: set[str] = set()
|
|
249
|
+
for decorator in node.decorator_list:
|
|
250
|
+
target = decorator.func if isinstance(decorator, ast.Call) else decorator
|
|
251
|
+
if isinstance(target, ast.Name):
|
|
252
|
+
names.add(target.id.casefold())
|
|
253
|
+
elif isinstance(target, ast.Attribute):
|
|
254
|
+
names.add(target.attr.casefold())
|
|
255
|
+
return frozenset(names)
|
|
256
|
+
|
|
257
|
+
|
|
247
258
|
def _fully_typed(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
|
248
259
|
args = node.args
|
|
249
260
|
parameters = (*args.posonlyargs, *args.args, *args.kwonlyargs)
|
|
@@ -270,21 +281,23 @@ def _typed_restatement_lines(
|
|
|
270
281
|
docstring_line: int,
|
|
271
282
|
) -> tuple[int, ...]:
|
|
272
283
|
annotations = {
|
|
273
|
-
arg.arg:
|
|
284
|
+
arg.arg: _annotation_type(arg.annotation)
|
|
274
285
|
for arg in (*node.args.posonlyargs, *node.args.args, *node.args.kwonlyargs)
|
|
275
286
|
if arg.annotation is not None
|
|
276
287
|
}
|
|
277
288
|
if node.args.vararg is not None and node.args.vararg.annotation is not None:
|
|
278
|
-
annotations[f"*{node.args.vararg.arg}"] =
|
|
289
|
+
annotations[f"*{node.args.vararg.arg}"] = _annotation_type(node.args.vararg.annotation)
|
|
279
290
|
if node.args.kwarg is not None and node.args.kwarg.annotation is not None:
|
|
280
|
-
annotations[f"**{node.args.kwarg.arg}"] =
|
|
281
|
-
return_type =
|
|
282
|
-
|
|
291
|
+
annotations[f"**{node.args.kwarg.arg}"] = _annotation_type(node.args.kwarg.annotation)
|
|
292
|
+
return_type = _annotation_type(node.returns) if node.returns is not None else None
|
|
293
|
+
yield_type = _yielded_type(node.returns) if node.returns is not None else None
|
|
294
|
+
visible_doc = _without_literal_examples(doc)
|
|
295
|
+
body_line_counts = _section_body_line_counts(visible_doc)
|
|
283
296
|
|
|
284
297
|
active: str | None = None
|
|
285
298
|
active_header: int | None = None
|
|
286
299
|
result: list[int] = []
|
|
287
|
-
for index, raw in enumerate(
|
|
300
|
+
for index, raw in enumerate(visible_doc.splitlines()):
|
|
288
301
|
if heading := _SECTION_HEADING_RE.match(raw):
|
|
289
302
|
active = heading.group(1)
|
|
290
303
|
active_header = index
|
|
@@ -297,20 +310,63 @@ def _typed_restatement_lines(
|
|
|
297
310
|
continue
|
|
298
311
|
if active in {"Args", "Arguments", "Parameters", "Params", "Keyword Args", "Keyword Arguments"}:
|
|
299
312
|
match = _ARG_TYPE_ENTRY_RE.match(raw) or _NUMPY_ARG_TYPE_ENTRY_RE.match(raw)
|
|
300
|
-
if match is not None and
|
|
313
|
+
if match is not None and _documented_type(match.group(2)) == annotations.get(match.group(1)):
|
|
301
314
|
result.append(docstring_line + index)
|
|
302
315
|
elif match := _RETURN_TYPE_ENTRY_RE.match(raw):
|
|
303
|
-
if
|
|
316
|
+
expected = yield_type if active in {"Yields", "Yield"} else return_type
|
|
317
|
+
if expected is not None and _documented_type(match.group(1)) == expected:
|
|
304
318
|
result.append(docstring_line + index)
|
|
305
319
|
elif (
|
|
306
320
|
active_header is not None
|
|
307
321
|
and body_line_counts.get(active_header) == 1
|
|
308
|
-
and
|
|
322
|
+
and (expected := yield_type if active in {"Yields", "Yield"} else return_type) is not None
|
|
323
|
+
and _documented_type(raw.strip()) == expected
|
|
309
324
|
):
|
|
310
325
|
result.append(docstring_line + index)
|
|
311
326
|
return tuple(result)
|
|
312
327
|
|
|
313
328
|
|
|
329
|
+
def _without_literal_examples(doc: str) -> str:
|
|
330
|
+
visible: list[str] = []
|
|
331
|
+
fence: str | None = None
|
|
332
|
+
literal_indent: int | None = None
|
|
333
|
+
awaiting_literal_block = False
|
|
334
|
+
for raw in doc.splitlines():
|
|
335
|
+
stripped = raw.lstrip()
|
|
336
|
+
indent = len(raw) - len(stripped)
|
|
337
|
+
fence_marker = stripped[:3] if stripped.startswith(("```", "~~~")) else None
|
|
338
|
+
if fence is not None:
|
|
339
|
+
visible.append("")
|
|
340
|
+
if fence_marker == fence:
|
|
341
|
+
fence = None
|
|
342
|
+
continue
|
|
343
|
+
if fence_marker is not None:
|
|
344
|
+
fence = fence_marker
|
|
345
|
+
visible.append("")
|
|
346
|
+
continue
|
|
347
|
+
if stripped.startswith((">>>", "...")):
|
|
348
|
+
visible.append("")
|
|
349
|
+
continue
|
|
350
|
+
if literal_indent is not None:
|
|
351
|
+
if not stripped or indent > literal_indent:
|
|
352
|
+
visible.append("")
|
|
353
|
+
continue
|
|
354
|
+
literal_indent = None
|
|
355
|
+
if awaiting_literal_block:
|
|
356
|
+
if not stripped:
|
|
357
|
+
visible.append(raw)
|
|
358
|
+
continue
|
|
359
|
+
if indent > 0:
|
|
360
|
+
literal_indent = indent - 1
|
|
361
|
+
visible.append("")
|
|
362
|
+
awaiting_literal_block = False
|
|
363
|
+
continue
|
|
364
|
+
awaiting_literal_block = False
|
|
365
|
+
visible.append(raw)
|
|
366
|
+
awaiting_literal_block = stripped.endswith("::")
|
|
367
|
+
return "\n".join(visible)
|
|
368
|
+
|
|
369
|
+
|
|
314
370
|
def _section_body_line_counts(doc: str) -> dict[int, int]:
|
|
315
371
|
counts: dict[int, int] = {}
|
|
316
372
|
active_header: int | None = None
|
|
@@ -325,5 +381,59 @@ def _section_body_line_counts(doc: str) -> dict[int, int]:
|
|
|
325
381
|
return counts
|
|
326
382
|
|
|
327
383
|
|
|
384
|
+
def _annotation_type(node: ast.expr) -> str:
|
|
385
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
386
|
+
parsed = _parse_type_expression(node.value)
|
|
387
|
+
return _normalise_type(ast.unparse(parsed)) if parsed is not None else node.value
|
|
388
|
+
return _normalise_type(ast.unparse(node))
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _documented_type(value: str) -> str | None:
|
|
392
|
+
parsed = _parse_type_expression(value)
|
|
393
|
+
return None if parsed is None else _normalise_type(ast.unparse(parsed))
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _parse_type_expression(value: str) -> ast.expr | None:
|
|
397
|
+
try:
|
|
398
|
+
parsed = ast.parse(value.strip(), mode="eval").body
|
|
399
|
+
except SyntaxError:
|
|
400
|
+
return None
|
|
401
|
+
unsafe = (
|
|
402
|
+
ast.Await,
|
|
403
|
+
ast.BoolOp,
|
|
404
|
+
ast.Call,
|
|
405
|
+
ast.Compare,
|
|
406
|
+
ast.DictComp,
|
|
407
|
+
ast.GeneratorExp,
|
|
408
|
+
ast.IfExp,
|
|
409
|
+
ast.Lambda,
|
|
410
|
+
ast.ListComp,
|
|
411
|
+
ast.NamedExpr,
|
|
412
|
+
ast.SetComp,
|
|
413
|
+
ast.Yield,
|
|
414
|
+
ast.YieldFrom,
|
|
415
|
+
)
|
|
416
|
+
if any(isinstance(part, unsafe) for part in ast.walk(parsed)):
|
|
417
|
+
return None
|
|
418
|
+
if any(isinstance(part, ast.BinOp) and not isinstance(part.op, ast.BitOr) for part in ast.walk(parsed)):
|
|
419
|
+
return None
|
|
420
|
+
return parsed
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _yielded_type(node: ast.expr) -> str | None:
|
|
424
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
425
|
+
parsed = _parse_type_expression(node.value)
|
|
426
|
+
if parsed is None:
|
|
427
|
+
return None
|
|
428
|
+
node = parsed
|
|
429
|
+
if not isinstance(node, ast.Subscript):
|
|
430
|
+
return None
|
|
431
|
+
name = node.value.attr if isinstance(node.value, ast.Attribute) else node.value.id if isinstance(node.value, ast.Name) else ""
|
|
432
|
+
if name not in {"AsyncGenerator", "AsyncIterable", "AsyncIterator", "Generator", "Iterable", "Iterator"}:
|
|
433
|
+
return None
|
|
434
|
+
members = node.slice.elts if isinstance(node.slice, ast.Tuple) else (node.slice,)
|
|
435
|
+
return _annotation_type(members[0]) if members else None
|
|
436
|
+
|
|
437
|
+
|
|
328
438
|
def _normalise_type(value: str) -> str:
|
|
329
|
-
return re.sub(r"\s+", "", value).removeprefix("typing.").
|
|
439
|
+
return re.sub(r"\s+", "", value).removeprefix("typing.").casefold()
|