sarj-python-lint 1.8.2__tar.gz → 1.8.3__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.8.2 → sarj_python_lint-1.8.3}/PKG-INFO +1 -1
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/pyproject.toml +1 -1
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_project_index.py +177 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/require_port_for_service.py +339 -1
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/.gitignore +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/LICENSE +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/README.md +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_registry.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/discourage_nullable_constructor_parameters.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/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.8.
|
|
3
|
+
Version: 1.8.3
|
|
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/
|
{sarj_python_lint-1.8.2 → sarj_python_lint-1.8.3}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
@@ -4,6 +4,7 @@ import ast
|
|
|
4
4
|
from collections.abc import Mapping
|
|
5
5
|
from contextlib import suppress
|
|
6
6
|
from dataclasses import dataclass
|
|
7
|
+
from functools import cached_property
|
|
7
8
|
import os
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
import re
|
|
@@ -12,6 +13,7 @@ from typing import TYPE_CHECKING, final
|
|
|
12
13
|
|
|
13
14
|
from sarj_python_lint.rules._ast_index import walk as walk_ast
|
|
14
15
|
from sarj_python_lint.rules._first_party import FirstPartyFacts, project_root
|
|
16
|
+
from sarj_python_lint.rules._paths import is_generated
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
if TYPE_CHECKING:
|
|
@@ -179,6 +181,27 @@ class ProjectIndexSet:
|
|
|
179
181
|
consumers.update(_unit_constructor_consumers(candidate, candidate.tree, target))
|
|
180
182
|
return frozenset(consumers)
|
|
181
183
|
|
|
184
|
+
def constructor_operations(self, unit: SourceUnit, name: str) -> frozenset[tuple[Path, str]]:
|
|
185
|
+
if unit.module is None:
|
|
186
|
+
return frozenset()
|
|
187
|
+
return self._constructor_operations.get(SymbolRef(unit.module, name), frozenset())
|
|
188
|
+
|
|
189
|
+
@cached_property
|
|
190
|
+
def _constructor_operations(self) -> Mapping[SymbolRef, frozenset[tuple[Path, str]]]:
|
|
191
|
+
module_counts: dict[str, int] = {}
|
|
192
|
+
for unit in self._units.values():
|
|
193
|
+
if unit.module is not None:
|
|
194
|
+
module_counts[unit.module] = module_counts.get(unit.module, 0) + 1
|
|
195
|
+
owned_modules = frozenset(module for module, count in module_counts.items() if count == 1)
|
|
196
|
+
operations: dict[SymbolRef, set[tuple[Path, str]]] = {}
|
|
197
|
+
for unit in self._units.values():
|
|
198
|
+
if unit.tree is None or unit.module not in owned_modules or is_generated(unit.path, unit.source):
|
|
199
|
+
continue
|
|
200
|
+
for symbol, operation in _unit_constructor_operations(unit, unit.tree, owned_modules):
|
|
201
|
+
if symbol.module in owned_modules and symbol in self._classes:
|
|
202
|
+
operations.setdefault(symbol, set()).add((unit.path, operation))
|
|
203
|
+
return MappingProxyType({symbol: frozenset(calls) for symbol, calls in operations.items()})
|
|
204
|
+
|
|
182
205
|
def direct_subclasses(self, unit: SourceUnit, name: str) -> frozenset[tuple[Path, str]]:
|
|
183
206
|
if unit.module is None:
|
|
184
207
|
return frozenset()
|
|
@@ -554,6 +577,160 @@ def _unit_constructor_consumers(
|
|
|
554
577
|
yield (candidate.path, owner.name)
|
|
555
578
|
|
|
556
579
|
|
|
580
|
+
def _unit_constructor_operations(
|
|
581
|
+
unit: SourceUnit, tree: ast.Module, owned_modules: frozenset[str]
|
|
582
|
+
) -> Iterator[tuple[SymbolRef, str]]:
|
|
583
|
+
if any(
|
|
584
|
+
isinstance(item, ast.ImportFrom) and any(alias.name == "*" for alias in item.names) for item in walk_ast(tree)
|
|
585
|
+
):
|
|
586
|
+
return
|
|
587
|
+
rebound = _module_constructor_rebindings(tree)
|
|
588
|
+
for owner in tree.body:
|
|
589
|
+
self_symbol = SymbolRef(unit.module or "", owner.name) if isinstance(owner, ast.ClassDef) else None
|
|
590
|
+
for method in _direct_functions(owner):
|
|
591
|
+
for symbol, operation in _function_constructor_operations(unit, method, rebound, owned_modules):
|
|
592
|
+
if symbol != self_symbol:
|
|
593
|
+
yield symbol, operation
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def _module_constructor_rebindings(tree: ast.Module) -> frozenset[str]:
|
|
597
|
+
names: set[str] = set()
|
|
598
|
+
declarations: dict[str, int] = {}
|
|
599
|
+
for statement in tree.body:
|
|
600
|
+
for name in _module_constructor_declarations(statement):
|
|
601
|
+
declarations[name] = declarations.get(name, 0) + 1
|
|
602
|
+
if isinstance(statement, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
603
|
+
names.add(statement.name)
|
|
604
|
+
elif not isinstance(statement, ast.ClassDef | ast.Import | ast.ImportFrom):
|
|
605
|
+
names.update(_bound_constructor_names(statement))
|
|
606
|
+
names.update(name for name, count in declarations.items() if count > 1)
|
|
607
|
+
return frozenset(names)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def _module_constructor_declarations(statement: ast.stmt) -> frozenset[str]:
|
|
611
|
+
if isinstance(statement, ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef):
|
|
612
|
+
return frozenset({statement.name})
|
|
613
|
+
return _bound_constructor_names(statement)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _direct_functions(owner: ast.stmt) -> tuple[ast.FunctionDef | ast.AsyncFunctionDef, ...]:
|
|
617
|
+
if isinstance(owner, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
618
|
+
return (owner,)
|
|
619
|
+
if isinstance(owner, ast.ClassDef):
|
|
620
|
+
return tuple(item for item in owner.body if isinstance(item, ast.FunctionDef | ast.AsyncFunctionDef))
|
|
621
|
+
return ()
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _function_constructor_operations(
|
|
625
|
+
unit: SourceUnit,
|
|
626
|
+
method: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
627
|
+
rebound: frozenset[str],
|
|
628
|
+
owned_modules: frozenset[str],
|
|
629
|
+
) -> Iterator[tuple[SymbolRef, str]]:
|
|
630
|
+
shadowed = _bound_constructor_names(method) | rebound
|
|
631
|
+
for statement in method.body:
|
|
632
|
+
if isinstance(statement, ast.Expr | ast.Assign | ast.AnnAssign | ast.Return):
|
|
633
|
+
operation = _direct_constructor_operation(unit, statement.value, shadowed, owned_modules)
|
|
634
|
+
if operation is not None:
|
|
635
|
+
yield operation
|
|
636
|
+
if isinstance(
|
|
637
|
+
statement,
|
|
638
|
+
ast.Return
|
|
639
|
+
| ast.Raise
|
|
640
|
+
| ast.If
|
|
641
|
+
| ast.For
|
|
642
|
+
| ast.AsyncFor
|
|
643
|
+
| ast.While
|
|
644
|
+
| ast.Try
|
|
645
|
+
| ast.TryStar
|
|
646
|
+
| ast.With
|
|
647
|
+
| ast.AsyncWith
|
|
648
|
+
| ast.Match,
|
|
649
|
+
):
|
|
650
|
+
break
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def _direct_constructor_operation(
|
|
654
|
+
unit: SourceUnit, value: ast.expr | None, shadowed: frozenset[str], owned_modules: frozenset[str]
|
|
655
|
+
) -> tuple[SymbolRef, str] | None:
|
|
656
|
+
if isinstance(value, ast.Await):
|
|
657
|
+
value = value.value
|
|
658
|
+
if not isinstance(value, ast.Call) or not isinstance(value.func, ast.Attribute):
|
|
659
|
+
return None
|
|
660
|
+
if not isinstance(value.func.value, ast.Call):
|
|
661
|
+
return None
|
|
662
|
+
constructor = value.func.value.func
|
|
663
|
+
root = constructor
|
|
664
|
+
while isinstance(root, ast.Attribute):
|
|
665
|
+
root = root.value
|
|
666
|
+
if not isinstance(root, ast.Name) or root.id in shadowed:
|
|
667
|
+
return None
|
|
668
|
+
symbol = _owned_constructor_symbol(unit, constructor, owned_modules)
|
|
669
|
+
return (symbol, value.func.attr) if symbol is not None else None
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def _owned_constructor_symbol(
|
|
673
|
+
unit: SourceUnit, expression: ast.expr, owned_modules: frozenset[str]
|
|
674
|
+
) -> SymbolRef | None:
|
|
675
|
+
if isinstance(expression, ast.Name):
|
|
676
|
+
return _resolve(unit, expression)
|
|
677
|
+
parts: list[str] = []
|
|
678
|
+
current = expression
|
|
679
|
+
while isinstance(current, ast.Attribute):
|
|
680
|
+
parts.append(current.attr)
|
|
681
|
+
current = current.value
|
|
682
|
+
if not isinstance(current, ast.Name) or not parts:
|
|
683
|
+
return None
|
|
684
|
+
imported = unit.imports.get(current.id)
|
|
685
|
+
if imported is None or imported.name:
|
|
686
|
+
return None
|
|
687
|
+
ordered = list(reversed(parts))
|
|
688
|
+
module = ".".join((imported.module, *ordered[:-1]))
|
|
689
|
+
if len(ordered) > 1 and not _explicit_constructor_module(unit, module):
|
|
690
|
+
return None
|
|
691
|
+
return SymbolRef(module, ordered[-1]) if module in owned_modules else None
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def _explicit_constructor_module(unit: SourceUnit, module: str) -> bool:
|
|
695
|
+
if unit.tree is None:
|
|
696
|
+
return False
|
|
697
|
+
return any(
|
|
698
|
+
alias.name == module and alias.asname is None
|
|
699
|
+
for statement in _module_import_statements(unit.tree)
|
|
700
|
+
if isinstance(statement, ast.Import)
|
|
701
|
+
for alias in statement.names
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def _bound_constructor_names(node: ast.AST) -> frozenset[str]:
|
|
706
|
+
names: set[str] = set()
|
|
707
|
+
for item in walk_ast(node):
|
|
708
|
+
match item:
|
|
709
|
+
case (
|
|
710
|
+
ast.Name(id=name, ctx=(ast.Store() | ast.Del()))
|
|
711
|
+
| ast.arg(arg=name)
|
|
712
|
+
| ast.ClassDef(name=name)
|
|
713
|
+
| ast.FunctionDef(name=name)
|
|
714
|
+
| ast.AsyncFunctionDef(name=name)
|
|
715
|
+
| ast.ExceptHandler(name=str(name))
|
|
716
|
+
| ast.MatchAs(name=str(name))
|
|
717
|
+
| ast.MatchStar(name=str(name))
|
|
718
|
+
| ast.MatchMapping(rest=str(name))
|
|
719
|
+
):
|
|
720
|
+
names.add(name)
|
|
721
|
+
case ast.Import(names=aliases) | ast.ImportFrom(names=aliases):
|
|
722
|
+
names.update(alias.asname or alias.name.split(".", 1)[0] for alias in aliases)
|
|
723
|
+
case ast.Attribute(ctx=(ast.Store() | ast.Del())):
|
|
724
|
+
root = item.value
|
|
725
|
+
while isinstance(root, ast.Attribute):
|
|
726
|
+
root = root.value
|
|
727
|
+
if isinstance(root, ast.Name):
|
|
728
|
+
names.add(root.id)
|
|
729
|
+
case _:
|
|
730
|
+
pass
|
|
731
|
+
return frozenset(names)
|
|
732
|
+
|
|
733
|
+
|
|
557
734
|
def _class_has_typed_dependency(unit: SourceUnit, owner: ast.ClassDef, target: SymbolRef) -> bool:
|
|
558
735
|
for statement in owner.body:
|
|
559
736
|
if isinstance(statement, ast.AnnAssign) and _annotation_contains_symbol(unit, statement.annotation, target):
|
|
@@ -20,6 +20,7 @@ from sarj_python_lint.rule_base import (
|
|
|
20
20
|
is_suppressed,
|
|
21
21
|
)
|
|
22
22
|
from sarj_python_lint.rules._ast_index import walk as walk_ast
|
|
23
|
+
from sarj_python_lint.rules._imports import ABC_SOURCES, TYPING_SOURCES
|
|
23
24
|
from sarj_python_lint.rules._paths import is_test_path, is_test_support_path
|
|
24
25
|
|
|
25
26
|
|
|
@@ -45,6 +46,13 @@ class _StoredParameters:
|
|
|
45
46
|
fallback_stored: frozenset[str]
|
|
46
47
|
|
|
47
48
|
|
|
49
|
+
@dataclass(frozen=True, slots=True)
|
|
50
|
+
class _FactoryClassFacts:
|
|
51
|
+
owned_names: frozenset[str]
|
|
52
|
+
excluded_owners: frozenset[str]
|
|
53
|
+
excluded_collaborators: frozenset[str]
|
|
54
|
+
|
|
55
|
+
|
|
48
56
|
# Classes named as the base of a family are the port being asked for, not a missing one.
|
|
49
57
|
_BASE_NAME_RE = re.compile(r"^(?:Base|Abstract)[A-Z_]")
|
|
50
58
|
|
|
@@ -211,9 +219,84 @@ class RequirePortForService(ProjectRule):
|
|
|
211
219
|
limitations=(
|
|
212
220
|
"This advisory uses service-family names, constructor annotations, collaborator calls, and public-method counts as heuristics.",
|
|
213
221
|
"Only direct module classes are checked; tests, generated code, scripts, framework callbacks, Store/Repository persistence dependencies, and external or interface-like bases are excluded.",
|
|
214
|
-
"A
|
|
222
|
+
"A suffixless class can also qualify when it retains a locally typed factory-created collaborator driving multiple public operations and production code constructs it directly to call at least two distinct operations. Constructor aliases and explicit module imports are resolved within the bounded project index; package-export indirection, wildcard imports, duplicate module identities, variable-held instances, factory indirection, and ambiguous control flow are not inferred. Top-level functions are analyzed in indexed modules; ordinary function-only modules may be excluded by the shared type index's lexical admission.",
|
|
223
|
+
"Otherwise, a one-method, suffixless, or store-backed class requires a concrete production type dependency and a test subclass or typed mock, or at least two production consumers for a multi-method class. A port owned in another module may require an exact suppression on the implementation.",
|
|
215
224
|
),
|
|
216
225
|
examples=(
|
|
226
|
+
RuleExample(
|
|
227
|
+
example_id="factory-created-boundary",
|
|
228
|
+
scenario="factory-created-boundary",
|
|
229
|
+
title="A native collaborator is hidden behind a directly constructed concrete boundary",
|
|
230
|
+
outcome=ExampleOutcome.MATCH,
|
|
231
|
+
files=(
|
|
232
|
+
ExampleFile(
|
|
233
|
+
path=PurePosixPath("pyproject.toml"),
|
|
234
|
+
source='[project]\nname = "boundary-example"\nversion = "0.1.0"\n',
|
|
235
|
+
),
|
|
236
|
+
ExampleFile.python(
|
|
237
|
+
"app/native.py",
|
|
238
|
+
"from typing import Protocol\n\n"
|
|
239
|
+
"class Backend(Protocol):\n"
|
|
240
|
+
" def read(self) -> str: ...\n"
|
|
241
|
+
" def write(self, value: str) -> None: ...\n\n"
|
|
242
|
+
"class Coordinator:\n"
|
|
243
|
+
" def __init__(self) -> None:\n"
|
|
244
|
+
" self.backend: Backend = native_backend()\n"
|
|
245
|
+
" def read(self) -> str:\n"
|
|
246
|
+
" return self.backend.read()\n"
|
|
247
|
+
" def write(self, value: str) -> None:\n"
|
|
248
|
+
" self.backend.write(value)\n\n"
|
|
249
|
+
"class Consumer:\n"
|
|
250
|
+
" def receive(self) -> str:\n"
|
|
251
|
+
" return Coordinator().read()\n"
|
|
252
|
+
" def send(self, value: str) -> None:\n"
|
|
253
|
+
" Coordinator().write(value)\n",
|
|
254
|
+
),
|
|
255
|
+
),
|
|
256
|
+
focus_path=PurePosixPath("app/native.py"),
|
|
257
|
+
expected_count=1,
|
|
258
|
+
public=True,
|
|
259
|
+
),
|
|
260
|
+
RuleExample(
|
|
261
|
+
example_id="factory-created-public-port",
|
|
262
|
+
scenario="factory-created-boundary",
|
|
263
|
+
title="A typed factory exposes the consumer's public Protocol",
|
|
264
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
265
|
+
files=(
|
|
266
|
+
ExampleFile(
|
|
267
|
+
path=PurePosixPath("pyproject.toml"),
|
|
268
|
+
source='[project]\nname = "boundary-example"\nversion = "0.1.0"\n',
|
|
269
|
+
),
|
|
270
|
+
ExampleFile.python(
|
|
271
|
+
"app/native.py",
|
|
272
|
+
"from typing import Protocol, final\n\n"
|
|
273
|
+
"class Backend(Protocol):\n"
|
|
274
|
+
" def read(self) -> str: ...\n"
|
|
275
|
+
" def write(self, value: str) -> None: ...\n\n"
|
|
276
|
+
"class Operations(Protocol):\n"
|
|
277
|
+
" def read(self) -> str: ...\n"
|
|
278
|
+
" def write(self, value: str) -> None: ...\n\n"
|
|
279
|
+
"@final\n"
|
|
280
|
+
"class Coordinator:\n"
|
|
281
|
+
" def __init__(self) -> None:\n"
|
|
282
|
+
" self.backend: Backend = native_backend()\n"
|
|
283
|
+
" def read(self) -> str:\n"
|
|
284
|
+
" return self.backend.read()\n"
|
|
285
|
+
" def write(self, value: str) -> None:\n"
|
|
286
|
+
" self.backend.write(value)\n\n"
|
|
287
|
+
"def coordinator() -> Operations:\n"
|
|
288
|
+
" return Coordinator()\n\n"
|
|
289
|
+
"class Consumer:\n"
|
|
290
|
+
" def receive(self) -> str:\n"
|
|
291
|
+
" return coordinator().read()\n"
|
|
292
|
+
" def send(self, value: str) -> None:\n"
|
|
293
|
+
" coordinator().write(value)\n",
|
|
294
|
+
),
|
|
295
|
+
),
|
|
296
|
+
focus_path=PurePosixPath("app/native.py"),
|
|
297
|
+
expected_count=0,
|
|
298
|
+
public=True,
|
|
299
|
+
),
|
|
217
300
|
RuleExample(
|
|
218
301
|
example_id="concrete-service-boundary",
|
|
219
302
|
title="Concrete service directly exposes an injected collaborator",
|
|
@@ -285,6 +368,7 @@ class RequirePortForService(ProjectRule):
|
|
|
285
368
|
data_names = {node.name for node in classes if is_data_type(node)}
|
|
286
369
|
local_class_names = {node.name for node in classes}
|
|
287
370
|
local_port_names = _local_port_names(classes)
|
|
371
|
+
factory_facts = _factory_class_facts(tree, classes, context.module_imports)
|
|
288
372
|
|
|
289
373
|
diags: list[Diagnostic] = []
|
|
290
374
|
for node in classes:
|
|
@@ -298,6 +382,7 @@ class RequirePortForService(ProjectRule):
|
|
|
298
382
|
local_class_names=local_class_names,
|
|
299
383
|
local_port_names=local_port_names,
|
|
300
384
|
imports=imports,
|
|
385
|
+
factory_facts=factory_facts,
|
|
301
386
|
)
|
|
302
387
|
if diagnostic is not None:
|
|
303
388
|
diags.append(diagnostic)
|
|
@@ -316,6 +401,7 @@ class RequirePortForService(ProjectRule):
|
|
|
316
401
|
local_class_names: set[str],
|
|
317
402
|
local_port_names: set[str],
|
|
318
403
|
imports: ImportIndex,
|
|
404
|
+
factory_facts: _FactoryClassFacts,
|
|
319
405
|
) -> Diagnostic | None:
|
|
320
406
|
if _class_is_suppressed(node, source_lines, self.code):
|
|
321
407
|
return None
|
|
@@ -329,14 +415,22 @@ class RequirePortForService(ProjectRule):
|
|
|
329
415
|
test_subclass_count = self._test_subclass_count(path, node, indexes=indexes) if typed_consumer_count else 0
|
|
330
416
|
test_mock_count = self._test_mock_count(path, node, indexes=indexes) if typed_consumer_count else 0
|
|
331
417
|
substituted_boundary = typed_consumer_count >= 1 and (test_subclass_count + test_mock_count) >= 1
|
|
418
|
+
factory_boundary = (
|
|
419
|
+
self._factory_boundary(path, node, local_class_names, local_port_names, factory_facts, indexes=indexes)
|
|
420
|
+
if collaborator is None
|
|
421
|
+
else None
|
|
422
|
+
)
|
|
332
423
|
if (
|
|
333
424
|
collaborator is None
|
|
334
425
|
and (consumer_count < _MIN_PROJECT_CONSUMERS or _public_method_count(node) < _MIN_PUBLIC_METHODS)
|
|
335
426
|
and not substituted_boundary
|
|
427
|
+
and factory_boundary is None
|
|
336
428
|
):
|
|
337
429
|
return None
|
|
338
430
|
if collaborator is not None:
|
|
339
431
|
evidence = f"injects `{collaborator}` and exposes {_public_method_count(node)} public methods"
|
|
432
|
+
elif factory_boundary is not None:
|
|
433
|
+
evidence = f"retains factory-created `{factory_boundary}` and is constructed directly to invoke multiple public operations"
|
|
340
434
|
elif substituted_boundary:
|
|
341
435
|
evidence = (
|
|
342
436
|
f"is typed directly by {typed_consumer_count} production classes and has "
|
|
@@ -357,6 +451,49 @@ class RequirePortForService(ProjectRule):
|
|
|
357
451
|
),
|
|
358
452
|
)
|
|
359
453
|
|
|
454
|
+
def _factory_boundary(
|
|
455
|
+
self,
|
|
456
|
+
path: Path,
|
|
457
|
+
node: ast.ClassDef,
|
|
458
|
+
local_class_names: set[str],
|
|
459
|
+
local_port_names: set[str],
|
|
460
|
+
factory_facts: _FactoryClassFacts,
|
|
461
|
+
*,
|
|
462
|
+
indexes: ProjectIndexSet | None,
|
|
463
|
+
) -> str | None:
|
|
464
|
+
if (
|
|
465
|
+
indexes is None
|
|
466
|
+
or node.name not in factory_facts.owned_names
|
|
467
|
+
or node.name in factory_facts.excluded_owners
|
|
468
|
+
or not _project_boundary_candidate(node, local_class_names, local_port_names)
|
|
469
|
+
):
|
|
470
|
+
return None
|
|
471
|
+
collaborator = _factory_collaborator(node, factory_facts)
|
|
472
|
+
if collaborator is None:
|
|
473
|
+
return None
|
|
474
|
+
unit = indexes.unit(path)
|
|
475
|
+
if unit is None:
|
|
476
|
+
return None
|
|
477
|
+
public_methods = {
|
|
478
|
+
method.name
|
|
479
|
+
for method in class_methods(node)
|
|
480
|
+
if not method.name.startswith("_")
|
|
481
|
+
and not any(_dotted_tail(decorator) in _NON_METHOD_DECORATORS for decorator in method.decorator_list)
|
|
482
|
+
}
|
|
483
|
+
operations = {
|
|
484
|
+
operation
|
|
485
|
+
for consumer_path, operation in indexes.constructor_operations(unit, node.name)
|
|
486
|
+
if operation in public_methods and self._factory_library_consumer(consumer_path, indexes)
|
|
487
|
+
}
|
|
488
|
+
return collaborator if len(operations) >= _MIN_COLLABORATOR_METHODS else None
|
|
489
|
+
|
|
490
|
+
@staticmethod
|
|
491
|
+
def _factory_library_consumer(path: Path, indexes: ProjectIndexSet) -> bool:
|
|
492
|
+
unit = indexes.unit(path)
|
|
493
|
+
return (
|
|
494
|
+
_is_library_source(path) and unit is not None and unit.tree is not None and not _has_main_guard(unit.tree)
|
|
495
|
+
)
|
|
496
|
+
|
|
360
497
|
def _concrete_consumer_count(
|
|
361
498
|
self,
|
|
362
499
|
path: Path,
|
|
@@ -649,6 +786,207 @@ def _injected_collaborator(
|
|
|
649
786
|
return _behavioral_collaborator(node, candidates)
|
|
650
787
|
|
|
651
788
|
|
|
789
|
+
def _factory_class_facts(tree: ast.Module, classes: list[ast.ClassDef], imports: ImportIndex) -> _FactoryClassFacts:
|
|
790
|
+
local_names = {node.name for node in classes}
|
|
791
|
+
declarations: dict[str, int] = {}
|
|
792
|
+
for statement in tree.body:
|
|
793
|
+
for name in _factory_statement_bindings(statement):
|
|
794
|
+
declarations[name] = declarations.get(name, 0) + 1
|
|
795
|
+
owned_names: frozenset[str] = frozenset(name for name in local_names if declarations.get(name) == 1)
|
|
796
|
+
if any(
|
|
797
|
+
isinstance(item, ast.ImportFrom) and any(alias.name == "*" for alias in item.names) for item in walk_ast(tree)
|
|
798
|
+
):
|
|
799
|
+
owned_names = frozenset()
|
|
800
|
+
interfaces = {node.name for node in classes if _factory_interface(node, imports)}
|
|
801
|
+
values = {node.name for node in classes if _factory_value_class(node, imports, local_names)}
|
|
802
|
+
_inherit_factory_exclusions(classes, interfaces)
|
|
803
|
+
_inherit_factory_exclusions(classes, values)
|
|
804
|
+
return _FactoryClassFacts(owned_names, frozenset(interfaces | values), frozenset(values))
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def _factory_interface(node: ast.ClassDef, imports: ImportIndex) -> bool:
|
|
808
|
+
return any(
|
|
809
|
+
imports.resolves(base, sources=TYPING_SOURCES, symbol="Protocol")
|
|
810
|
+
or imports.resolves(base, sources=ABC_SOURCES, symbol="ABC")
|
|
811
|
+
for base in node.bases
|
|
812
|
+
) or any(
|
|
813
|
+
imports.resolved_symbol(decorator, sources=ABC_SOURCES) in _INTERFACE_DECORATORS
|
|
814
|
+
for method in class_methods(node)
|
|
815
|
+
for decorator in method.decorator_list
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
def _factory_value_class(node: ast.ClassDef, imports: ImportIndex, local_names: set[str]) -> bool:
|
|
820
|
+
if is_data_type(node):
|
|
821
|
+
return True
|
|
822
|
+
if any(
|
|
823
|
+
imports.resolved_symbol(
|
|
824
|
+
decorator.func if isinstance(decorator, ast.Call) else decorator,
|
|
825
|
+
sources=frozenset({"dataclasses", "attrs", "attr"}),
|
|
826
|
+
)
|
|
827
|
+
in _DATA_DECORATORS
|
|
828
|
+
for decorator in node.decorator_list
|
|
829
|
+
):
|
|
830
|
+
return True
|
|
831
|
+
return any(_factory_value_base(base, imports, local_names) for base in node.bases)
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def _factory_value_base(base: ast.expr, imports: ImportIndex, local_names: set[str]) -> bool:
|
|
835
|
+
if (
|
|
836
|
+
imports.resolved_symbol(
|
|
837
|
+
base, sources=frozenset({"pydantic", "typing", "typing_extensions", "enum", "msgspec", "builtins"})
|
|
838
|
+
)
|
|
839
|
+
in _DATA_BASES
|
|
840
|
+
):
|
|
841
|
+
return True
|
|
842
|
+
if any(imports.resolves(base, sources=TYPING_SOURCES, symbol=name) for name in ("Protocol", "Generic")):
|
|
843
|
+
return False
|
|
844
|
+
if imports.resolves(base, sources=ABC_SOURCES, symbol="ABC"):
|
|
845
|
+
return False
|
|
846
|
+
return _dotted_tail(base) not in local_names | {"object", "Generic", "Protocol", "ABC", None}
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def _inherit_factory_exclusions(classes: list[ast.ClassDef], excluded: set[str]) -> None:
|
|
850
|
+
while grown := {
|
|
851
|
+
node.name
|
|
852
|
+
for node in classes
|
|
853
|
+
if node.name not in excluded and any(_dotted_tail(base) in excluded for base in node.bases)
|
|
854
|
+
}:
|
|
855
|
+
excluded.update(grown)
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def _factory_statement_bindings(statement: ast.stmt) -> frozenset[str]:
|
|
859
|
+
names: set[str] = set()
|
|
860
|
+
stack: list[ast.AST] = [statement]
|
|
861
|
+
while stack:
|
|
862
|
+
node = stack.pop()
|
|
863
|
+
match node:
|
|
864
|
+
case ast.FunctionDef(name=name) | ast.AsyncFunctionDef(name=name) | ast.ClassDef(name=name):
|
|
865
|
+
names.add(name)
|
|
866
|
+
continue
|
|
867
|
+
case ast.AnnAssign(value=None):
|
|
868
|
+
continue
|
|
869
|
+
case (
|
|
870
|
+
ast.Name(id=name, ctx=(ast.Store() | ast.Del()))
|
|
871
|
+
| ast.ExceptHandler(name=str(name))
|
|
872
|
+
| ast.MatchAs(name=str(name))
|
|
873
|
+
| ast.MatchStar(name=str(name))
|
|
874
|
+
| ast.MatchMapping(rest=str(name))
|
|
875
|
+
):
|
|
876
|
+
names.add(name)
|
|
877
|
+
case ast.Import(names=aliases) | ast.ImportFrom(names=aliases):
|
|
878
|
+
names.update(alias.asname or alias.name.partition(".")[0] for alias in aliases)
|
|
879
|
+
case _:
|
|
880
|
+
pass
|
|
881
|
+
stack.extend(ast.iter_child_nodes(node))
|
|
882
|
+
return frozenset(names)
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
def _factory_collaborator(node: ast.ClassDef, facts: _FactoryClassFacts) -> str | None:
|
|
886
|
+
init = next((method for method in class_methods(node) if method.name == "__init__"), None)
|
|
887
|
+
if init is None:
|
|
888
|
+
return None
|
|
889
|
+
origins: dict[str, ast.expr | None] = {}
|
|
890
|
+
candidates: list[tuple[str, frozenset[str]]] = []
|
|
891
|
+
writes = _constructor_field_writes(init)
|
|
892
|
+
available_names = set(facts.owned_names)
|
|
893
|
+
available_names.difference_update(
|
|
894
|
+
parameter.arg for parameter in (*init.args.posonlyargs, *init.args.args, *init.args.kwonlyargs)
|
|
895
|
+
)
|
|
896
|
+
for statement in init.body:
|
|
897
|
+
if isinstance(statement, ast.Raise):
|
|
898
|
+
return None
|
|
899
|
+
if isinstance(statement, ast.Return):
|
|
900
|
+
break
|
|
901
|
+
if not _factory_falls_through(statement):
|
|
902
|
+
return None
|
|
903
|
+
available_names.difference_update(_factory_statement_bindings(statement))
|
|
904
|
+
if not _update_factory_origins(statement, origins):
|
|
905
|
+
return None
|
|
906
|
+
candidate = _factory_field_candidate(statement, origins, writes, facts.excluded_collaborators, available_names)
|
|
907
|
+
if candidate is not None:
|
|
908
|
+
candidates.append(candidate)
|
|
909
|
+
return _behavioral_collaborator(node, candidates)
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
def _factory_falls_through(statement: ast.stmt) -> bool:
|
|
913
|
+
if isinstance(statement, (ast.Return, ast.Raise, ast.While, *_UNSUPPORTED_COMPOUND_STATEMENTS)):
|
|
914
|
+
return False
|
|
915
|
+
if not isinstance(statement, ast.If):
|
|
916
|
+
return True
|
|
917
|
+
truth = _static_truth(statement.test)
|
|
918
|
+
if truth is not None:
|
|
919
|
+
return _factory_statements_fall_through(statement.body if truth else statement.orelse)
|
|
920
|
+
return _factory_statements_fall_through(statement.body) or _factory_statements_fall_through(statement.orelse)
|
|
921
|
+
|
|
922
|
+
|
|
923
|
+
def _factory_statements_fall_through(statements: list[ast.stmt]) -> bool:
|
|
924
|
+
return all(_factory_falls_through(statement) for statement in statements)
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
def _constructor_field_writes(init: ast.FunctionDef | ast.AsyncFunctionDef) -> dict[str, int]:
|
|
928
|
+
writes: dict[str, int] = {}
|
|
929
|
+
for statement in walk_ast(init):
|
|
930
|
+
if (
|
|
931
|
+
isinstance(statement, ast.Attribute)
|
|
932
|
+
and isinstance(statement.ctx, ast.Store | ast.Del)
|
|
933
|
+
and isinstance(statement.value, ast.Name)
|
|
934
|
+
and statement.value.id == "self"
|
|
935
|
+
):
|
|
936
|
+
writes[statement.attr] = writes.get(statement.attr, 0) + 1
|
|
937
|
+
return writes
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
def _update_factory_origins(statement: ast.stmt, origins: dict[str, ast.expr | None]) -> bool:
|
|
941
|
+
value = statement.value if isinstance(statement, ast.Assign | ast.AnnAssign) else None
|
|
942
|
+
snapshot = origins.get(value.id) if isinstance(value, ast.Name) else value
|
|
943
|
+
for name in _factory_statement_bindings(statement):
|
|
944
|
+
origins.pop(name, None)
|
|
945
|
+
if isinstance(statement, ast.Assign | ast.AnnAssign):
|
|
946
|
+
if statement.value is None:
|
|
947
|
+
return True
|
|
948
|
+
targets = statement.targets if isinstance(statement, ast.Assign) else [statement.target]
|
|
949
|
+
for target in targets:
|
|
950
|
+
if not isinstance(target, ast.Name):
|
|
951
|
+
continue
|
|
952
|
+
origins[target.id] = snapshot
|
|
953
|
+
elif isinstance(statement, (ast.If, ast.While, *_UNSUPPORTED_COMPOUND_STATEMENTS)):
|
|
954
|
+
for item in walk_ast(statement):
|
|
955
|
+
if isinstance(item, ast.Return):
|
|
956
|
+
return False
|
|
957
|
+
return True
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
def _factory_field_candidate(
|
|
961
|
+
statement: ast.stmt,
|
|
962
|
+
origins: dict[str, ast.expr | None],
|
|
963
|
+
writes: dict[str, int],
|
|
964
|
+
data_names: frozenset[str],
|
|
965
|
+
local_class_names: set[str],
|
|
966
|
+
) -> tuple[str, frozenset[str]] | None:
|
|
967
|
+
if not isinstance(statement, ast.AnnAssign):
|
|
968
|
+
return None
|
|
969
|
+
assignment = _self_field_assignment(statement)
|
|
970
|
+
annotation = annotation_tail(statement.annotation)
|
|
971
|
+
if assignment is None or annotation is None or annotation not in local_class_names:
|
|
972
|
+
return None
|
|
973
|
+
if any(isinstance(item, ast.Attribute) and item.attr == annotation for item in walk_ast(statement.annotation)):
|
|
974
|
+
return None
|
|
975
|
+
if (
|
|
976
|
+
annotation in data_names
|
|
977
|
+
or annotation in _DRIVER_HANDLE_ANNOTATIONS
|
|
978
|
+
or _PERSISTENCE_DEPENDENCY_RE.search(annotation)
|
|
979
|
+
or _WEAK_COLLABORATOR_RE.search(annotation)
|
|
980
|
+
or _annotation_allows_none(statement.annotation)
|
|
981
|
+
):
|
|
982
|
+
return None
|
|
983
|
+
fields, value = assignment
|
|
984
|
+
origin = value if isinstance(value, ast.Call) else origins.get(value.id) if isinstance(value, ast.Name) else None
|
|
985
|
+
if not isinstance(origin, ast.Call) or any(writes[field] != 1 for field in fields):
|
|
986
|
+
return None
|
|
987
|
+
return annotation, frozenset(fields)
|
|
988
|
+
|
|
989
|
+
|
|
652
990
|
def self_stored_parameters(
|
|
653
991
|
init: ast.FunctionDef | ast.AsyncFunctionDef,
|
|
654
992
|
imports: ImportIndex,
|
|
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
|