sarj-python-lint 1.2.4__tar.gz → 1.2.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-1.2.4 → sarj_python_lint-1.2.6}/PKG-INFO +1 -1
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/pyproject.toml +1 -1
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_registry.py +2 -0
- sarj_python_lint-1.2.6/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +227 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +248 -19
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/.gitignore +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/LICENSE +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/README.md +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/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: 1.2.
|
|
3
|
+
Version: 1.2.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/
|
|
@@ -75,6 +75,7 @@ from sarj_python_lint.rules.no_secret_in_log import NoSecretInLog
|
|
|
75
75
|
from sarj_python_lint.rules.no_select_star import NoSelectStar
|
|
76
76
|
from sarj_python_lint.rules.no_service_behavior_in_settings import NoServiceBehaviorInSettings
|
|
77
77
|
from sarj_python_lint.rules.no_string_concat_in_loop import NoStringConcatInLoop
|
|
78
|
+
from sarj_python_lint.rules.no_swallowed_asyncio_cancellation import NoSwallowedAsyncioCancellation
|
|
78
79
|
from sarj_python_lint.rules.no_tautological_expect import NoTautologicalExpect
|
|
79
80
|
from sarj_python_lint.rules.no_typed_doc_sections import NoTypedDocSections
|
|
80
81
|
from sarj_python_lint.rules.no_unique_violation_message_match import (
|
|
@@ -181,6 +182,7 @@ if TYPE_CHECKING:
|
|
|
181
182
|
|
|
182
183
|
REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
183
184
|
{
|
|
185
|
+
NoSwallowedAsyncioCancellation.id: NoSwallowedAsyncioCancellation,
|
|
184
186
|
AsyncMockCallWithoutAwaitAssertion.id: AsyncMockCallWithoutAwaitAssertion,
|
|
185
187
|
ComplexPostgresQueryRequiresArchitectureReview.id: ComplexPostgresQueryRequiresArchitectureReview,
|
|
186
188
|
ExcessiveCommentary.id: ExcessiveCommentary,
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from itertools import pairwise
|
|
5
|
+
from pathlib import PurePosixPath
|
|
6
|
+
from typing import TYPE_CHECKING, ClassVar, final, override
|
|
7
|
+
|
|
8
|
+
from sarj_python_lint.rule_base import (
|
|
9
|
+
AutofixPolicy,
|
|
10
|
+
Diagnostic,
|
|
11
|
+
ExampleFile,
|
|
12
|
+
ExampleOutcome,
|
|
13
|
+
Rule,
|
|
14
|
+
RuleCategory,
|
|
15
|
+
RuleDocumentation,
|
|
16
|
+
RuleExample,
|
|
17
|
+
Severity,
|
|
18
|
+
is_suppressed,
|
|
19
|
+
)
|
|
20
|
+
from sarj_python_lint.rules._ast_index import nodes
|
|
21
|
+
from sarj_python_lint.rules._paths import is_test_path, is_test_support_path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from collections.abc import Iterator
|
|
26
|
+
|
|
27
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
28
|
+
from sarj_python_lint.rules._imports import ImportIndex
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_ASYNCIO = frozenset({"asyncio", "asyncio.exceptions"})
|
|
32
|
+
_SIMPLE_STATEMENTS = (ast.Expr, ast.Assign, ast.AnnAssign, ast.AugAssign, ast.Delete, ast.Pass)
|
|
33
|
+
_MESSAGE = (
|
|
34
|
+
"CancelledError handler completes normally; re-raise caller cancellation after cleanup, "
|
|
35
|
+
"or guard intentional child cancellation with the current task's cancellation state."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@final
|
|
40
|
+
class NoSwallowedAsyncioCancellation(Rule):
|
|
41
|
+
id = "no-swallowed-asyncio-cancellation"
|
|
42
|
+
code = "SARJ458"
|
|
43
|
+
documentation: ClassVar[RuleDocumentation | None] = RuleDocumentation(
|
|
44
|
+
default_level=Severity.WARNING,
|
|
45
|
+
summary="Preserve cancellation when an async operation is cancelled.",
|
|
46
|
+
rationale=(
|
|
47
|
+
"Returning or falling through after catching CancelledError can make cancelled work look successful "
|
|
48
|
+
"and interfere with task groups and timeout shutdown. Child cancellation must be distinguished from "
|
|
49
|
+
"cancellation of the caller."
|
|
50
|
+
),
|
|
51
|
+
remediation=(
|
|
52
|
+
"Re-raise after cleanup. If child cancellation is an expected result, first propagate cancellation "
|
|
53
|
+
"when asyncio.current_task().cancelling() is nonzero. Use an exact suppression for a documented "
|
|
54
|
+
"boundary that deliberately consumes cancellation."
|
|
55
|
+
),
|
|
56
|
+
category=RuleCategory.CORRECTNESS,
|
|
57
|
+
autofix=AutofixPolicy.NONE,
|
|
58
|
+
limitations=(
|
|
59
|
+
"Only explicit, import-proven asyncio.CancelledError handlers in async functions are inspected; broad handlers, contextlib.suppress, tests, support fixtures, and generated files are excluded.",
|
|
60
|
+
"Only straight-line handlers with a return or normal fallthrough are reported. Compound handlers, re-raising, and translated exceptions are excluded. Any attribute call named uncancel is conservatively excluded without proving its receiver, so unrelated methods can hide a finding.",
|
|
61
|
+
"A single-name task created by asyncio.create_task, immediately cancelled before a try that only awaits it, is intentionally excluded as child cleanup; this does not prove concurrent caller cancellation cannot occur.",
|
|
62
|
+
"Try statements with control-transferring or compound finalizers are excluded, including handlers nested beneath them. Runtime helper effects and interprocedural task ownership are not inferred.",
|
|
63
|
+
),
|
|
64
|
+
examples=(
|
|
65
|
+
RuleExample(
|
|
66
|
+
example_id="cancelled-operation-returns-success",
|
|
67
|
+
title="Cancellation becomes a successful return",
|
|
68
|
+
outcome=ExampleOutcome.MATCH,
|
|
69
|
+
files=(
|
|
70
|
+
ExampleFile.python(
|
|
71
|
+
"worker.py",
|
|
72
|
+
"import asyncio\n\nasync def worker():\n try:\n await asyncio.sleep(10)\n except asyncio.CancelledError:\n return None\n",
|
|
73
|
+
),
|
|
74
|
+
),
|
|
75
|
+
focus_path=PurePosixPath("worker.py"),
|
|
76
|
+
expected_count=1,
|
|
77
|
+
public=True,
|
|
78
|
+
),
|
|
79
|
+
RuleExample(
|
|
80
|
+
example_id="cancelled-operation-reraises",
|
|
81
|
+
title="Cleanup preserves cancellation",
|
|
82
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
83
|
+
files=(
|
|
84
|
+
ExampleFile.python(
|
|
85
|
+
"worker.py",
|
|
86
|
+
"import asyncio\n\nasync def worker():\n try:\n await asyncio.sleep(10)\n except asyncio.CancelledError:\n raise\n",
|
|
87
|
+
),
|
|
88
|
+
),
|
|
89
|
+
focus_path=PurePosixPath("worker.py"),
|
|
90
|
+
expected_count=0,
|
|
91
|
+
public=True,
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
description = documentation.summary
|
|
96
|
+
|
|
97
|
+
@override
|
|
98
|
+
def check_context(self, context: PythonFileContext) -> list[Diagnostic]:
|
|
99
|
+
if "CancelledError" not in context.source:
|
|
100
|
+
return []
|
|
101
|
+
if context.generated or is_test_path(context.path) or is_test_support_path(context.path):
|
|
102
|
+
return []
|
|
103
|
+
tree = context.tree
|
|
104
|
+
if tree is None:
|
|
105
|
+
return []
|
|
106
|
+
diagnostics: list[Diagnostic] = []
|
|
107
|
+
for function in nodes(tree, ast.AsyncFunctionDef, index=context.node_index):
|
|
108
|
+
diagnostics.extend(_function_diagnostics(function, context, self.code))
|
|
109
|
+
return sorted(diagnostics, key=lambda item: (item.line, item.col))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _function_diagnostics(function: ast.AsyncFunctionDef, context: PythonFileContext, code: str) -> list[Diagnostic]:
|
|
113
|
+
local = tuple(_local_nodes(function))
|
|
114
|
+
children = _owned_tasks(local, context.imports)
|
|
115
|
+
previous = _preceding_statements(local)
|
|
116
|
+
diagnostics: list[Diagnostic] = []
|
|
117
|
+
for node in local:
|
|
118
|
+
if not isinstance(node, ast.Try) or _owned_child_cleanup(node, previous.get(id(node)), children):
|
|
119
|
+
continue
|
|
120
|
+
for handler in node.handlers:
|
|
121
|
+
if not _catches_cancellation(handler.type, context.imports) or not _normal_exit(handler.body):
|
|
122
|
+
continue
|
|
123
|
+
if is_suppressed(context.source_lines, handler.lineno, code):
|
|
124
|
+
continue
|
|
125
|
+
diagnostics.append(
|
|
126
|
+
Diagnostic(
|
|
127
|
+
path=context.path,
|
|
128
|
+
line=handler.lineno,
|
|
129
|
+
col=handler.col_offset + 1,
|
|
130
|
+
code=code,
|
|
131
|
+
message=_MESSAGE,
|
|
132
|
+
severity=Severity.WARNING,
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
return diagnostics
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _local_nodes(function: ast.AsyncFunctionDef) -> Iterator[ast.AST]:
|
|
139
|
+
stack: list[ast.AST] = list(reversed(function.body))
|
|
140
|
+
yield function
|
|
141
|
+
while stack:
|
|
142
|
+
node = stack.pop()
|
|
143
|
+
if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.Lambda):
|
|
144
|
+
continue
|
|
145
|
+
if isinstance(node, ast.Try | ast.TryStar) and any(
|
|
146
|
+
not isinstance(statement, _SIMPLE_STATEMENTS) for statement in node.finalbody
|
|
147
|
+
):
|
|
148
|
+
continue
|
|
149
|
+
yield node
|
|
150
|
+
stack.extend(reversed(list(ast.iter_child_nodes(node))))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _catches_cancellation(exception: ast.expr | None, imports: ImportIndex) -> bool:
|
|
154
|
+
if isinstance(exception, ast.Tuple):
|
|
155
|
+
return any(_catches_cancellation(item, imports) for item in exception.elts)
|
|
156
|
+
return exception is not None and imports.resolves(exception, sources=_ASYNCIO, symbol="CancelledError")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _normal_exit(statements: list[ast.stmt]) -> bool:
|
|
160
|
+
for statement in statements:
|
|
161
|
+
if any(
|
|
162
|
+
isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == "uncancel"
|
|
163
|
+
for node in ast.walk(statement)
|
|
164
|
+
):
|
|
165
|
+
return False
|
|
166
|
+
if isinstance(statement, ast.Return):
|
|
167
|
+
return True
|
|
168
|
+
if not isinstance(statement, _SIMPLE_STATEMENTS):
|
|
169
|
+
return False
|
|
170
|
+
return True
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _owned_tasks(local: tuple[ast.AST, ...], imports: ImportIndex) -> frozenset[str]:
|
|
174
|
+
bindings: dict[str, list[ast.AST]] = {}
|
|
175
|
+
for node in local:
|
|
176
|
+
if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store | ast.Del):
|
|
177
|
+
bindings.setdefault(node.id, []).append(node)
|
|
178
|
+
if (
|
|
179
|
+
isinstance(node, ast.Attribute)
|
|
180
|
+
and isinstance(node.ctx, ast.Store | ast.Del)
|
|
181
|
+
and isinstance(node.value, ast.Name)
|
|
182
|
+
):
|
|
183
|
+
bindings.setdefault(node.value.id, []).append(node)
|
|
184
|
+
return frozenset(
|
|
185
|
+
node.targets[0].id
|
|
186
|
+
for node in local
|
|
187
|
+
if isinstance(node, ast.Assign)
|
|
188
|
+
and len(node.targets) == 1
|
|
189
|
+
and isinstance(node.targets[0], ast.Name)
|
|
190
|
+
and len(bindings[node.targets[0].id]) == 1
|
|
191
|
+
and isinstance(node.value, ast.Call)
|
|
192
|
+
and imports.resolves(node.value.func, sources=frozenset({"asyncio"}), symbol="create_task")
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _preceding_statements(local: tuple[ast.AST, ...]) -> dict[int, ast.stmt]:
|
|
197
|
+
previous: dict[int, ast.stmt] = {}
|
|
198
|
+
for node in local:
|
|
199
|
+
for suite in _statement_suites(node):
|
|
200
|
+
for first, second in pairwise(suite):
|
|
201
|
+
previous[id(second)] = first
|
|
202
|
+
return previous
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _owned_child_cleanup(node: ast.Try, previous: ast.stmt | None, children: frozenset[str]) -> bool:
|
|
206
|
+
if len(node.body) != 1:
|
|
207
|
+
return False
|
|
208
|
+
match node.body[0], previous:
|
|
209
|
+
case (
|
|
210
|
+
ast.Expr(value=ast.Await(value=ast.Name(id=awaited))),
|
|
211
|
+
ast.Expr(value=ast.Call(func=ast.Attribute(value=ast.Name(id=cancelled), attr="cancel"))),
|
|
212
|
+
):
|
|
213
|
+
return awaited == cancelled and awaited in children
|
|
214
|
+
case _:
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _statement_suites(node: ast.AST) -> tuple[list[ast.stmt], ...]:
|
|
219
|
+
match node:
|
|
220
|
+
case ast.Try() | ast.TryStar():
|
|
221
|
+
return node.body, node.orelse, node.finalbody
|
|
222
|
+
case ast.If() | ast.For() | ast.AsyncFor() | ast.While():
|
|
223
|
+
return node.body, node.orelse
|
|
224
|
+
case ast.AsyncFunctionDef() | ast.With() | ast.AsyncWith() | ast.ExceptHandler() | ast.match_case():
|
|
225
|
+
return (node.body,)
|
|
226
|
+
case _:
|
|
227
|
+
return ()
|
|
@@ -60,8 +60,16 @@ class _ExternalRecordFinding(NamedTuple):
|
|
|
60
60
|
origin: ast.Call
|
|
61
61
|
|
|
62
62
|
|
|
63
|
+
@dataclass(frozen=True, slots=True)
|
|
64
|
+
class _WrapperSummary:
|
|
65
|
+
parameters: tuple[str, ...]
|
|
66
|
+
fields: dict[str, str]
|
|
67
|
+
accessors: dict[str, str]
|
|
68
|
+
|
|
69
|
+
|
|
63
70
|
@dataclass(frozen=True, slots=True)
|
|
64
71
|
class _ModuleSummaries:
|
|
72
|
+
wrappers: dict[str, _WrapperSummary]
|
|
65
73
|
decoder_parameters: dict[str, int]
|
|
66
74
|
record_parameters: dict[str, frozenset[int]]
|
|
67
75
|
local_parameters: dict[str, frozenset[int]]
|
|
@@ -114,6 +122,7 @@ class RequirePydanticForExternalJson(Rule):
|
|
|
114
122
|
autofix=AutofixPolicy.NONE,
|
|
115
123
|
limitations=(
|
|
116
124
|
"The rule follows import-proven JSON decoders, requests/httpx/aiohttp responses and clients, environment or subprocess results, and simple module-local helpers through single-assignment names.",
|
|
125
|
+
"Simple same-module wrappers preserve constructor arguments through direct stored-field accessor methods, including local identity-or-empty-dict narrowing helpers; inheritance, decorators, mutation, nested captures, unknown member escapes/helpers, and wrapper factory returns are excluded.",
|
|
117
126
|
"It diagnoses literal-key subscription and get calls; iteration and other dynamic JSON use remain out of scope.",
|
|
118
127
|
"Unannotated parameters and unknown expressions are not assumed to be external; interprocedural and framework-specific boundaries can remain unreported.",
|
|
119
128
|
"Names bound by loops, comprehensions, context managers, exception handlers, pattern matching, imports, or destructuring are excluded when their provenance is ambiguous; unrelated binders do not suppress the rest of a function.",
|
|
@@ -220,6 +229,7 @@ def _module_summaries(
|
|
|
220
229
|
_summarize_function(function, imports, module_validator_names, source_lines, decoders, records=records)
|
|
221
230
|
response_callables = _response_callables(tree, imports, node_index=node_index)
|
|
222
231
|
return _ModuleSummaries(
|
|
232
|
+
_wrapper_summaries(tree, imports),
|
|
223
233
|
decoders,
|
|
224
234
|
records,
|
|
225
235
|
_locally_sourced_parameters(functions),
|
|
@@ -538,6 +548,7 @@ def _function_findings(
|
|
|
538
548
|
response_names,
|
|
539
549
|
bindings,
|
|
540
550
|
local_bound_names,
|
|
551
|
+
scope,
|
|
541
552
|
)
|
|
542
553
|
validated_names = _validated_names(function, imports, summaries.jsonschema_validator_names)
|
|
543
554
|
findings: list[_ExternalRecordFinding] = []
|
|
@@ -748,6 +759,7 @@ class _OriginResolver:
|
|
|
748
759
|
response_names: frozenset[str]
|
|
749
760
|
bindings: dict[str, ast.expr]
|
|
750
761
|
local_bound_names: frozenset[str]
|
|
762
|
+
scope: tuple[ast.AST, ...]
|
|
751
763
|
|
|
752
764
|
def origins(self, expression: ast.expr, resolving: frozenset[str] = frozenset()) -> frozenset[ast.Call]:
|
|
753
765
|
expression = _unwrap_await(expression)
|
|
@@ -756,30 +768,80 @@ class _OriginResolver:
|
|
|
756
768
|
return frozenset()
|
|
757
769
|
return self.origins(value, resolving | {expression.id})
|
|
758
770
|
if isinstance(expression, ast.Call):
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
if self._is_source(expression):
|
|
762
|
-
return frozenset({expression})
|
|
763
|
-
found_origins: set[ast.Call] = set()
|
|
764
|
-
if (
|
|
765
|
-
isinstance(expression.func, ast.Name)
|
|
766
|
-
and expression.func.id not in self.local_bound_names
|
|
767
|
-
and (position := self.summaries.decoder_parameters.get(expression.func.id)) is not None
|
|
768
|
-
and position < len(expression.args)
|
|
769
|
-
and self._is_external_input(expression.args[position])
|
|
770
|
-
):
|
|
771
|
-
found_origins.add(expression)
|
|
772
|
-
for argument in expression.args:
|
|
773
|
-
found_origins.update(self.origins(argument, resolving))
|
|
774
|
-
for keyword in expression.keywords:
|
|
775
|
-
found_origins.update(self.origins(keyword.value, resolving))
|
|
776
|
-
return frozenset(found_origins)
|
|
777
|
-
found_origins = set()
|
|
771
|
+
return self._call_origins(expression, resolving)
|
|
772
|
+
found_origins: set[ast.Call] = set()
|
|
778
773
|
for child in ast.iter_child_nodes(expression):
|
|
779
774
|
if isinstance(child, ast.expr):
|
|
780
775
|
found_origins.update(self.origins(child, resolving))
|
|
781
776
|
return frozenset(found_origins)
|
|
782
777
|
|
|
778
|
+
def _call_origins(self, call: ast.Call, resolving: frozenset[str]) -> frozenset[ast.Call]:
|
|
779
|
+
if isinstance(call.func, ast.Attribute) and not call.args and not call.keywords:
|
|
780
|
+
wrapped = self._wrapper_value(call.func)
|
|
781
|
+
if wrapped is not None:
|
|
782
|
+
return self.origins(wrapped, resolving)
|
|
783
|
+
if self._is_validation_call(call):
|
|
784
|
+
return frozenset()
|
|
785
|
+
if self._is_source(call):
|
|
786
|
+
return frozenset({call})
|
|
787
|
+
found_origins: set[ast.Call] = set()
|
|
788
|
+
if (
|
|
789
|
+
isinstance(call.func, ast.Name)
|
|
790
|
+
and call.func.id not in self.local_bound_names
|
|
791
|
+
and (position := self.summaries.decoder_parameters.get(call.func.id)) is not None
|
|
792
|
+
and position < len(call.args)
|
|
793
|
+
and self._is_external_input(call.args[position])
|
|
794
|
+
):
|
|
795
|
+
found_origins.add(call)
|
|
796
|
+
for argument in call.args:
|
|
797
|
+
found_origins.update(self.origins(argument, resolving))
|
|
798
|
+
for keyword in call.keywords:
|
|
799
|
+
found_origins.update(self.origins(keyword.value, resolving))
|
|
800
|
+
return frozenset(found_origins)
|
|
801
|
+
|
|
802
|
+
def _wrapper_value(self, access: ast.Attribute) -> ast.expr | None:
|
|
803
|
+
receiver = self._resolved_binding(access.value) if isinstance(access.value, ast.Name) else access.value
|
|
804
|
+
if not isinstance(receiver, ast.Call) or not isinstance(receiver.func, ast.Name):
|
|
805
|
+
return None
|
|
806
|
+
if receiver.func.id in self.local_bound_names:
|
|
807
|
+
return None
|
|
808
|
+
summary = self.summaries.wrappers.get(receiver.func.id)
|
|
809
|
+
if summary is None or not self._wrapper_is_stable(receiver, summary):
|
|
810
|
+
return None
|
|
811
|
+
field = summary.accessors.get(access.attr)
|
|
812
|
+
parameter = summary.fields.get(field) if field is not None else None
|
|
813
|
+
if parameter is None or any(isinstance(argument, ast.Starred) for argument in receiver.args):
|
|
814
|
+
return None
|
|
815
|
+
if any(keyword.arg is None for keyword in receiver.keywords):
|
|
816
|
+
return None
|
|
817
|
+
position = summary.parameters.index(parameter)
|
|
818
|
+
if position < len(receiver.args):
|
|
819
|
+
return receiver.args[position]
|
|
820
|
+
return next((keyword.value for keyword in receiver.keywords if keyword.arg == parameter), None)
|
|
821
|
+
|
|
822
|
+
def _wrapper_is_stable(self, constructor: ast.Call, summary: _WrapperSummary) -> bool:
|
|
823
|
+
def refers_to_wrapper(value: ast.expr) -> bool:
|
|
824
|
+
return isinstance(value, ast.Name) and self._resolved_binding(value) is constructor
|
|
825
|
+
|
|
826
|
+
for node in self.scope:
|
|
827
|
+
if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.Lambda) and any(
|
|
828
|
+
isinstance(child, ast.Name) and refers_to_wrapper(child) for child in ast.walk(node)
|
|
829
|
+
):
|
|
830
|
+
return False
|
|
831
|
+
if (
|
|
832
|
+
isinstance(node, ast.Attribute)
|
|
833
|
+
and refers_to_wrapper(node.value)
|
|
834
|
+
and (isinstance(node.ctx, ast.Store | ast.Del) or node.attr not in summary.accessors | summary.fields)
|
|
835
|
+
):
|
|
836
|
+
return False
|
|
837
|
+
if not isinstance(node, ast.Call):
|
|
838
|
+
continue
|
|
839
|
+
if any(refers_to_wrapper(argument) for argument in node.args) or any(
|
|
840
|
+
refers_to_wrapper(keyword.value) for keyword in node.keywords
|
|
841
|
+
):
|
|
842
|
+
return False
|
|
843
|
+
return True
|
|
844
|
+
|
|
783
845
|
def _is_validation_call(self, call: ast.Call) -> bool:
|
|
784
846
|
if _is_validation_call(
|
|
785
847
|
call,
|
|
@@ -1462,3 +1524,170 @@ def _typed_http_clients(
|
|
|
1462
1524
|
imports.resolves(argument.annotation, sources=_HTTP_MODULES, symbol=symbol) for symbol in _HTTP_CLIENT_TYPES
|
|
1463
1525
|
)
|
|
1464
1526
|
}
|
|
1527
|
+
|
|
1528
|
+
|
|
1529
|
+
def _wrapper_summaries(tree: ast.Module, imports: ImportIndex) -> dict[str, _WrapperSummary]:
|
|
1530
|
+
counts = _suite_binding_counts(tree.body)
|
|
1531
|
+
narrowing_helpers = _narrowing_helpers(tree, imports, counts)
|
|
1532
|
+
mutated_classes = _wrapper_mutated_classes(tree)
|
|
1533
|
+
summaries: dict[str, _WrapperSummary] = {}
|
|
1534
|
+
for node in tree.body:
|
|
1535
|
+
if not isinstance(node, ast.ClassDef) or node.bases or node.keywords or node.decorator_list:
|
|
1536
|
+
continue
|
|
1537
|
+
if counts[node.name] != 1 or node.name in mutated_classes:
|
|
1538
|
+
continue
|
|
1539
|
+
summary = _wrapper_summary(node, narrowing_helpers)
|
|
1540
|
+
if summary is not None:
|
|
1541
|
+
summaries[node.name] = summary
|
|
1542
|
+
return summaries
|
|
1543
|
+
|
|
1544
|
+
|
|
1545
|
+
def _wrapper_summary(node: ast.ClassDef, narrowing_helpers: frozenset[str]) -> _WrapperSummary | None:
|
|
1546
|
+
if any(_wrapper_descriptor(statement) for statement in node.body):
|
|
1547
|
+
return None
|
|
1548
|
+
methods = [statement for statement in node.body if isinstance(statement, ast.FunctionDef)]
|
|
1549
|
+
if any(method.name.startswith("__") and method.name != "__init__" for method in methods):
|
|
1550
|
+
return None
|
|
1551
|
+
constructors = [method for method in methods if method.name == "__init__"]
|
|
1552
|
+
if len(constructors) != 1:
|
|
1553
|
+
return None
|
|
1554
|
+
constructor = constructors[0]
|
|
1555
|
+
fields = _wrapper_fields(constructor)
|
|
1556
|
+
if not fields or fields.keys() & {method.name for method in methods}:
|
|
1557
|
+
return None
|
|
1558
|
+
if any(_wrapper_method_mutates(method) for method in methods if method is not constructor):
|
|
1559
|
+
return None
|
|
1560
|
+
accessors = _wrapper_accessors(node, methods, fields, narrowing_helpers)
|
|
1561
|
+
parameters = tuple(_parameter_positions(constructor))[1:]
|
|
1562
|
+
return _WrapperSummary(parameters, fields, accessors)
|
|
1563
|
+
|
|
1564
|
+
|
|
1565
|
+
def _wrapper_accessors(
|
|
1566
|
+
node: ast.ClassDef,
|
|
1567
|
+
methods: list[ast.FunctionDef],
|
|
1568
|
+
fields: dict[str, str],
|
|
1569
|
+
narrowing_helpers: frozenset[str],
|
|
1570
|
+
) -> dict[str, str]:
|
|
1571
|
+
method_counts = _suite_binding_counts(node.body)
|
|
1572
|
+
return {
|
|
1573
|
+
method.name: field
|
|
1574
|
+
for method in methods
|
|
1575
|
+
if method_counts[method.name] == 1 and (field := _wrapper_accessor(method, narrowing_helpers)) in fields
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
|
|
1579
|
+
def _wrapper_method_mutates(method: ast.FunctionDef) -> bool:
|
|
1580
|
+
return any(
|
|
1581
|
+
isinstance(child, ast.Attribute)
|
|
1582
|
+
and isinstance(child.ctx, ast.Store | ast.Del)
|
|
1583
|
+
and isinstance(child.value, ast.Name)
|
|
1584
|
+
and child.value.id == "self"
|
|
1585
|
+
for child in _own_scope(method)
|
|
1586
|
+
)
|
|
1587
|
+
|
|
1588
|
+
|
|
1589
|
+
def _wrapper_fields(constructor: ast.FunctionDef) -> dict[str, str]:
|
|
1590
|
+
if constructor.decorator_list or constructor.args.vararg or constructor.args.kwarg or constructor.args.kwonlyargs:
|
|
1591
|
+
return {}
|
|
1592
|
+
parameters = _parameter_positions(constructor)
|
|
1593
|
+
if parameters.get("self") != 0:
|
|
1594
|
+
return {}
|
|
1595
|
+
fields: dict[str, str] = {}
|
|
1596
|
+
for statement in constructor.body:
|
|
1597
|
+
if isinstance(statement, ast.Expr) and isinstance(statement.value, ast.Constant):
|
|
1598
|
+
continue
|
|
1599
|
+
if isinstance(statement, ast.Assign) and len(statement.targets) == 1:
|
|
1600
|
+
target, value = statement.targets[0], statement.value
|
|
1601
|
+
elif isinstance(statement, ast.AnnAssign) and statement.value is not None:
|
|
1602
|
+
target, value = statement.target, statement.value
|
|
1603
|
+
else:
|
|
1604
|
+
return {}
|
|
1605
|
+
if not (
|
|
1606
|
+
isinstance(target, ast.Attribute)
|
|
1607
|
+
and isinstance(target.value, ast.Name)
|
|
1608
|
+
and target.value.id == "self"
|
|
1609
|
+
and target.attr not in fields
|
|
1610
|
+
and isinstance(value, ast.Name)
|
|
1611
|
+
and value.id in parameters
|
|
1612
|
+
and parameters[value.id] > 0
|
|
1613
|
+
):
|
|
1614
|
+
return {}
|
|
1615
|
+
fields[target.attr] = value.id
|
|
1616
|
+
return fields
|
|
1617
|
+
|
|
1618
|
+
|
|
1619
|
+
def _wrapper_accessor(method: ast.FunctionDef, narrowing_helpers: frozenset[str]) -> str | None:
|
|
1620
|
+
if (
|
|
1621
|
+
method.decorator_list
|
|
1622
|
+
or len(method.body) != 1
|
|
1623
|
+
or tuple(_parameter_positions(method)) != ("self",)
|
|
1624
|
+
or any((method.args.vararg, method.args.kwarg, method.args.kwonlyargs))
|
|
1625
|
+
):
|
|
1626
|
+
return None
|
|
1627
|
+
statement = method.body[0]
|
|
1628
|
+
if not isinstance(statement, ast.Return):
|
|
1629
|
+
return None
|
|
1630
|
+
value = statement.value
|
|
1631
|
+
if (
|
|
1632
|
+
isinstance(value, ast.Call)
|
|
1633
|
+
and isinstance(value.func, ast.Name)
|
|
1634
|
+
and value.func.id in narrowing_helpers
|
|
1635
|
+
and len(value.args) == 1
|
|
1636
|
+
and not value.keywords
|
|
1637
|
+
):
|
|
1638
|
+
value = value.args[0]
|
|
1639
|
+
return (
|
|
1640
|
+
value.attr
|
|
1641
|
+
if isinstance(value, ast.Attribute) and isinstance(value.value, ast.Name) and value.value.id == "self"
|
|
1642
|
+
else None
|
|
1643
|
+
)
|
|
1644
|
+
|
|
1645
|
+
|
|
1646
|
+
def _narrowing_helpers(tree: ast.Module, imports: ImportIndex, counts: Counter[str]) -> frozenset[str]:
|
|
1647
|
+
if not imports.builtin_is_unshadowed("isinstance") or not imports.builtin_is_unshadowed("dict"):
|
|
1648
|
+
return frozenset()
|
|
1649
|
+
names: set[str] = set()
|
|
1650
|
+
for function in tree.body:
|
|
1651
|
+
if not isinstance(function, ast.FunctionDef) or function.decorator_list or counts[function.name] != 1:
|
|
1652
|
+
continue
|
|
1653
|
+
parameters = tuple(_parameter_positions(function))
|
|
1654
|
+
if len(parameters) != 1 or len(function.body) != 1:
|
|
1655
|
+
continue
|
|
1656
|
+
statement = function.body[0]
|
|
1657
|
+
if not isinstance(statement, ast.Return) or not isinstance(statement.value, ast.IfExp):
|
|
1658
|
+
continue
|
|
1659
|
+
if any((function.args.kwonlyargs, function.args.vararg, function.args.kwarg)):
|
|
1660
|
+
continue
|
|
1661
|
+
if parameters[0] not in {"dict", "isinstance"} and _is_identity_dict_narrowing(statement.value, parameters[0]):
|
|
1662
|
+
names.add(function.name)
|
|
1663
|
+
return frozenset(names)
|
|
1664
|
+
|
|
1665
|
+
|
|
1666
|
+
def _is_identity_dict_narrowing(value: ast.IfExp, parameter: str) -> bool:
|
|
1667
|
+
match value:
|
|
1668
|
+
case ast.IfExp(
|
|
1669
|
+
body=ast.Name(id=returned),
|
|
1670
|
+
orelse=ast.Dict(keys=[]),
|
|
1671
|
+
test=ast.Call(
|
|
1672
|
+
func=ast.Name(id="isinstance"), args=[ast.Name(id=checked), ast.Name(id="dict")], keywords=[]
|
|
1673
|
+
),
|
|
1674
|
+
):
|
|
1675
|
+
return returned == parameter == checked
|
|
1676
|
+
case _:
|
|
1677
|
+
return False
|
|
1678
|
+
|
|
1679
|
+
|
|
1680
|
+
def _wrapper_mutated_classes(tree: ast.Module) -> frozenset[str]:
|
|
1681
|
+
return frozenset(
|
|
1682
|
+
node.value.id
|
|
1683
|
+
for node in walk_ast(tree)
|
|
1684
|
+
if isinstance(node, ast.Attribute)
|
|
1685
|
+
and isinstance(node.ctx, ast.Store | ast.Del)
|
|
1686
|
+
and isinstance(node.value, ast.Name)
|
|
1687
|
+
)
|
|
1688
|
+
|
|
1689
|
+
|
|
1690
|
+
def _wrapper_descriptor(statement: ast.stmt) -> bool:
|
|
1691
|
+
if isinstance(statement, ast.Assign):
|
|
1692
|
+
return not all(isinstance(target, ast.Name) and target.id == "__slots__" for target in statement.targets)
|
|
1693
|
+
return isinstance(statement, ast.AnnAssign | ast.AugAssign)
|
|
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-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_ast_position.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_fixed_record.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_nominal_project.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_prose_budget.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_test_assertions.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/_test_composition.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/excessive_commentary.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/mock_without_spec.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_any_mapping_types.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.2.4 → sarj_python_lint-1.2.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-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_delete_statement.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_dunder_all.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_fastapi_on_event.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-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_offset_pagination.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-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_restated_comment.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/no_secret_in_log.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.2.4 → sarj_python_lint-1.2.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/over_mocked_test.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/phase_label_comment.py
RENAMED
|
File without changes
|
{sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_class_row.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_library_fake.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-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/prefer_or_pattern.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.2.4 → sarj_python_lint-1.2.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
|
{sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/redundant_docstring.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/typed_error_reasons.py
RENAMED
|
File without changes
|
|
File without changes
|
{sarj_python_lint-1.2.4 → sarj_python_lint-1.2.6}/src/sarj_python_lint/rules/unused_mock_setup.py
RENAMED
|
File without changes
|
|
File without changes
|