sarj-python-lint 1.67.0__tar.gz → 1.69.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/pyproject.toml +1 -1
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/__main__.py +19 -5
- sarj_python_lint-1.69.0/src/sarj_python_lint/_parallel.py +102 -0
- sarj_python_lint-1.69.0/src/sarj_python_lint/rules/_declared_contracts.py +243 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_local_source.py +13 -3
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_project_index.py +38 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_registry.py +10 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_test_composition.py +8 -2
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/excessive_commentary.py +36 -14
- sarj_python_lint-1.69.0/src/sarj_python_lint/rules/prefer_direct_import_module.py +83 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +7 -3
- sarj_python_lint-1.69.0/src/sarj_python_lint/rules/prefer_precise_object_contract.py +220 -0
- sarj_python_lint-1.69.0/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py +192 -0
- sarj_python_lint-1.69.0/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py +154 -0
- sarj_python_lint-1.69.0/src/sarj_python_lint/rules/prefer_typed_settings_field.py +199 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +7 -7
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/.gitignore +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/LICENSE +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/README.md +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/cast_access.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_ast_names.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/pytest_fixture_returns_closed_resource.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
- {sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/suppression_policy.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.69.0
|
|
4
4
|
Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
|
|
5
5
|
Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
|
|
6
6
|
Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
|
|
@@ -96,7 +96,7 @@ def _canonical_rule_ids(rule_ids: list[str]) -> list[str]:
|
|
|
96
96
|
return list(dict.fromkeys(rid if rid in REGISTRY else aliases.get(rid, rid) for rid in rule_ids))
|
|
97
97
|
|
|
98
98
|
|
|
99
|
-
def _check(rule_ids: list[str], paths: list[Path]) -> list[Diagnostic]:
|
|
99
|
+
def _check(rule_ids: list[str], paths: list[Path], *, jobs: int = 1) -> list[Diagnostic]:
|
|
100
100
|
rule_ids = _canonical_rule_ids(rule_ids)
|
|
101
101
|
unknown = [rid for rid in rule_ids if rid not in REGISTRY]
|
|
102
102
|
if unknown:
|
|
@@ -116,6 +116,12 @@ def _check(rule_ids: list[str], paths: list[Path]) -> list[Diagnostic]:
|
|
|
116
116
|
session.project = indexes
|
|
117
117
|
for rule in project_rules:
|
|
118
118
|
rule.prepare(indexes)
|
|
119
|
+
if jobs > 1 and len(loaded) > 1:
|
|
120
|
+
from sarj_python_lint._parallel import check_parallel # ruff: ignore[import-outside-top-level] -- serial API users do not need multiprocessing.
|
|
121
|
+
|
|
122
|
+
parallel = check_parallel([type(rule) for rule in rules], loaded, session.project, jobs=jobs)
|
|
123
|
+
if parallel is not None:
|
|
124
|
+
return parallel
|
|
119
125
|
diags: list[Diagnostic] = []
|
|
120
126
|
for p, source in loaded.items():
|
|
121
127
|
diags.extend(check_source(rules, p, source, session=session))
|
|
@@ -144,8 +150,12 @@ def analyze(
|
|
|
144
150
|
*,
|
|
145
151
|
baseline: Path | None = None,
|
|
146
152
|
root: Path | None = None,
|
|
153
|
+
jobs: int = 1,
|
|
147
154
|
) -> list[Diagnostic]:
|
|
148
|
-
|
|
155
|
+
if jobs < 1:
|
|
156
|
+
msg = "jobs must be at least 1"
|
|
157
|
+
raise ValueError(msg)
|
|
158
|
+
diagnostics = _check(rule_ids, paths, jobs=jobs)
|
|
149
159
|
return diagnostics if baseline is None else _apply_baseline(diagnostics, _read_baseline(baseline), root=root)
|
|
150
160
|
|
|
151
161
|
|
|
@@ -481,9 +491,11 @@ def _apply_baseline(
|
|
|
481
491
|
return out
|
|
482
492
|
|
|
483
493
|
|
|
484
|
-
def _run_check(
|
|
494
|
+
def _run_check(
|
|
495
|
+
rule: list[str], files: list[Path], baseline: Path | None, update_baseline: Path | None, *, jobs: int = 1
|
|
496
|
+
) -> int:
|
|
485
497
|
try:
|
|
486
|
-
diags = analyze(rule, files)
|
|
498
|
+
diags = analyze(rule, files, jobs=jobs)
|
|
487
499
|
except (OSError, ValueError, SyntaxError) as exc:
|
|
488
500
|
sys.stderr.write(f"error: {exc}\n")
|
|
489
501
|
return 2
|
|
@@ -554,14 +566,16 @@ def _check_command(
|
|
|
554
566
|
context: typer.Context,
|
|
555
567
|
files: Annotated[list[Path], typer.Argument()],
|
|
556
568
|
rule: Annotated[list[str], typer.Option("--rule", help="Rule ID (repeat for multiple).")],
|
|
569
|
+
*,
|
|
557
570
|
baseline: Annotated[
|
|
558
571
|
Path | None, typer.Option(help="Per-file shrink-only baseline JSON: {path: {CODE: count}}.")
|
|
559
572
|
] = None,
|
|
560
573
|
update_baseline: Annotated[
|
|
561
574
|
Path | None, typer.Option(help="Write current per-file diagnostic counts to JSON and exit 0.")
|
|
562
575
|
] = None,
|
|
576
|
+
jobs: Annotated[int, typer.Option(min=1, help="Analysis worker budget (up to 2); default is serial.")] = 1,
|
|
563
577
|
) -> None:
|
|
564
|
-
context.ensure_object(_Result).code = _run_check(rule, files, baseline, update_baseline)
|
|
578
|
+
context.ensure_object(_Result).code = _run_check(rule, files, baseline, update_baseline, jobs=jobs)
|
|
565
579
|
|
|
566
580
|
|
|
567
581
|
def main(argv: list[str] | None = None) -> int:
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
import multiprocessing
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from sarj_python_lint._analysis_session import AnalysisSession
|
|
10
|
+
from sarj_python_lint.rule_base import Diagnostic, ProjectRule, Rule
|
|
11
|
+
from sarj_python_lint.rules._local_source import LOCAL_SOURCE_CACHE_LIMIT
|
|
12
|
+
from sarj_python_lint.rules._paths import clear_path_caches
|
|
13
|
+
from sarj_python_lint.rules._project_index import ProjectIndexSet
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from sarj_python_lint.rules._project_index import ProjectSourceSnapshot
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
type _ResolutionKey = tuple[Path, str | None, int]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class _WorkerState:
|
|
25
|
+
rules: list[Rule]
|
|
26
|
+
session: AnalysisSession
|
|
27
|
+
resolutions: frozenset[_ResolutionKey] = frozenset()
|
|
28
|
+
modules: frozenset[Path] = frozenset()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class _FileResult:
|
|
33
|
+
diagnostics: list[Diagnostic]
|
|
34
|
+
resolutions: frozenset[_ResolutionKey]
|
|
35
|
+
modules: frozenset[Path]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
_state: _WorkerState | None = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _initialize(rule_types: list[type[Rule]], snapshot: ProjectSourceSnapshot | None) -> None:
|
|
42
|
+
global _state # ruff: ignore[global-statement] -- each spawned process owns one initialized session.
|
|
43
|
+
rules = [rule_type() for rule_type in rule_types]
|
|
44
|
+
session = AnalysisSession()
|
|
45
|
+
for rule in rules:
|
|
46
|
+
rule.prepare_session(session)
|
|
47
|
+
clear_path_caches()
|
|
48
|
+
project = ProjectIndexSet.from_source_snapshot(snapshot) if snapshot is not None else None
|
|
49
|
+
session.project = project
|
|
50
|
+
for rule in rules:
|
|
51
|
+
if isinstance(rule, ProjectRule) and project is not None:
|
|
52
|
+
rule.prepare(project)
|
|
53
|
+
_state = _WorkerState(rules, session)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _check_file(item: tuple[Path, str]) -> _FileResult:
|
|
57
|
+
from sarj_python_lint.__main__ import check_source # ruff: ignore[import-outside-top-level] -- keep spawn targets in an importable module.
|
|
58
|
+
|
|
59
|
+
if _state is None:
|
|
60
|
+
msg = "Python analysis worker was not initialized"
|
|
61
|
+
raise RuntimeError(msg)
|
|
62
|
+
path, source = item
|
|
63
|
+
diagnostics = check_source(_state.rules, path, source, session=_state.session)
|
|
64
|
+
keys = _state.session.local_source.cache_keys()
|
|
65
|
+
result = _FileResult(diagnostics, keys.resolutions - _state.resolutions, keys.modules - _state.modules)
|
|
66
|
+
_state.resolutions, _state.modules = keys.resolutions, keys.modules
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def check_parallel(
|
|
71
|
+
rule_types: list[type[Rule]],
|
|
72
|
+
loaded: dict[Path, str],
|
|
73
|
+
project: ProjectIndexSet | None,
|
|
74
|
+
*,
|
|
75
|
+
jobs: int,
|
|
76
|
+
) -> list[Diagnostic] | None:
|
|
77
|
+
# The untouched parent rules/session remain the serial fallback. Workers
|
|
78
|
+
# receive the same complete project snapshot and selected source strings.
|
|
79
|
+
workers = min(jobs, 2, len(loaded))
|
|
80
|
+
pool = ProcessPoolExecutor(
|
|
81
|
+
max_workers=workers,
|
|
82
|
+
mp_context=multiprocessing.get_context("spawn"),
|
|
83
|
+
initializer=_initialize,
|
|
84
|
+
initargs=(rule_types, project.source_snapshot() if project is not None else None),
|
|
85
|
+
)
|
|
86
|
+
diagnostics: list[Diagnostic] = []
|
|
87
|
+
resolutions: set[_ResolutionKey] = set()
|
|
88
|
+
modules: set[Path] = set()
|
|
89
|
+
try:
|
|
90
|
+
for result in pool.map(_check_file, loaded.items(), chunksize=1, buffersize=workers):
|
|
91
|
+
resolutions.update(result.resolutions)
|
|
92
|
+
modules.update(result.modules)
|
|
93
|
+
# A worker cannot hit either order-dependent limit when the whole
|
|
94
|
+
# union stays strictly below it. At the boundary, discard all output.
|
|
95
|
+
if len(resolutions) >= LOCAL_SOURCE_CACHE_LIMIT or len(modules) >= LOCAL_SOURCE_CACHE_LIMIT:
|
|
96
|
+
return None
|
|
97
|
+
diagnostics.extend(result.diagnostics)
|
|
98
|
+
return diagnostics
|
|
99
|
+
finally:
|
|
100
|
+
# Bound queued work and reap before fallback/error return. Running file
|
|
101
|
+
# checks finish normally; group cancellation belongs to the supervisor.
|
|
102
|
+
pool.shutdown(wait=True, cancel_futures=True)
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics
|
|
8
|
+
from sarj_python_lint.rules._imports import ImportIndex
|
|
9
|
+
from sarj_python_lint.rules._project_index import ProjectIndexSet, SourceUnit, SymbolRef
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
MODEL_BASES = frozenset({"pydantic.BaseModel", "pydantic.main.BaseModel"})
|
|
19
|
+
SETTINGS_BASES = frozenset({"pydantic_settings.BaseSettings", "pydantic_settings.main.BaseSettings"})
|
|
20
|
+
_MAX_DEPTH = 8
|
|
21
|
+
_TYPING_SOURCES = frozenset({"typing", "typing_extensions"})
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class DeclaredClass:
|
|
26
|
+
unit: SourceUnit
|
|
27
|
+
declaration: ast.ClassDef
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _class_metadata_annotation(annotation: ast.expr, semantics: AnnotationSemantics) -> bool:
|
|
31
|
+
pending = [annotation]
|
|
32
|
+
seen: set[str] = set()
|
|
33
|
+
while pending:
|
|
34
|
+
node = semantics.parse(pending.pop())
|
|
35
|
+
if node is None:
|
|
36
|
+
return True
|
|
37
|
+
for member in ast.walk(node):
|
|
38
|
+
if isinstance(member, ast.Subscript) and semantics.imports.resolved_symbol(
|
|
39
|
+
member.value, sources=_TYPING_SOURCES
|
|
40
|
+
) in {"ClassVar", "Final"}:
|
|
41
|
+
return True
|
|
42
|
+
if isinstance(member, ast.Name) and member.id in semantics.aliases and member.id not in seen:
|
|
43
|
+
seen.add(member.id)
|
|
44
|
+
pending.append(semantics.aliases[member.id])
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DeclaredContracts:
|
|
49
|
+
def __init__(self, context: PythonFileContext) -> None:
|
|
50
|
+
self.context = context
|
|
51
|
+
self._field_cache: dict[ast.ClassDef, dict[str, ast.AnnAssign]] = {}
|
|
52
|
+
self._module_annotations: dict[Path, AnnotationSemantics] = {}
|
|
53
|
+
self.project = context.session.project or ProjectIndexSet.single(context.path, context.source)
|
|
54
|
+
self.unit = self.project.unit(context.path)
|
|
55
|
+
if self.unit is not None and self.unit.tree is not context.tree:
|
|
56
|
+
self.unit = SourceUnit(self.unit.path, self.unit.module, self.unit.source, context.tree, self.unit.imports)
|
|
57
|
+
self.project = self.project.with_unit(self.unit)
|
|
58
|
+
if self.unit is not None and self.unit.module is None:
|
|
59
|
+
self.unit = SourceUnit(
|
|
60
|
+
context.path,
|
|
61
|
+
"__snippet__",
|
|
62
|
+
context.source,
|
|
63
|
+
context.tree,
|
|
64
|
+
{
|
|
65
|
+
name: SymbolRef(target.module, target.symbol or "")
|
|
66
|
+
for name, target in context.module_imports.bindings.items()
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
self.project = self.project.with_unit(self.unit)
|
|
70
|
+
|
|
71
|
+
def class_type(
|
|
72
|
+
self, expression: ast.expr, unit: SourceUnit | None = None, *, at: ast.AST | None = None
|
|
73
|
+
) -> DeclaredClass | None:
|
|
74
|
+
owner = unit or self.unit
|
|
75
|
+
if owner is None or not self._lookup_allowed(expression, owner, at):
|
|
76
|
+
return None
|
|
77
|
+
summary = self.project.class_for(owner, expression)
|
|
78
|
+
source = self.project.source_unit(summary.symbol.module) if summary is not None else None
|
|
79
|
+
if source is None or source.tree is None or summary is None:
|
|
80
|
+
return None
|
|
81
|
+
declaration = self._stable_declaration(source, summary.symbol.name)
|
|
82
|
+
return DeclaredClass(source, declaration) if declaration is not None else None
|
|
83
|
+
|
|
84
|
+
def _lookup_allowed(self, expression: ast.expr, owner: SourceUnit, at: ast.AST | None) -> bool:
|
|
85
|
+
if owner is not self.unit or (at or expression) not in self.context.parents:
|
|
86
|
+
return True
|
|
87
|
+
root = expression
|
|
88
|
+
while isinstance(root, ast.Attribute):
|
|
89
|
+
root = root.value
|
|
90
|
+
if not isinstance(root, ast.Name):
|
|
91
|
+
return True
|
|
92
|
+
binding = self.context.static_resource_provenance.binding(root.id, at or expression)
|
|
93
|
+
return (
|
|
94
|
+
isinstance(binding, ast.alias | ast.ClassDef)
|
|
95
|
+
and root.id not in self.context.static_resource_provenance.mutated
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def _stable_declaration(self, source: SourceUnit, name: str) -> ast.ClassDef | None:
|
|
99
|
+
if source.tree is None:
|
|
100
|
+
return None
|
|
101
|
+
declarations = [node for node in source.tree.body if isinstance(node, ast.ClassDef) and node.name == name]
|
|
102
|
+
if len(declarations) != 1 or declarations[0].keywords:
|
|
103
|
+
return None
|
|
104
|
+
if not self._identity_decorators(source, declarations[0]):
|
|
105
|
+
return None
|
|
106
|
+
if any(
|
|
107
|
+
isinstance(node, ast.Name) and node.id == name and isinstance(node.ctx, ast.Store | ast.Del)
|
|
108
|
+
for statement in source.tree.body
|
|
109
|
+
if not isinstance(statement, ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef)
|
|
110
|
+
for node in ast.walk(statement)
|
|
111
|
+
):
|
|
112
|
+
return None
|
|
113
|
+
return declarations[0]
|
|
114
|
+
|
|
115
|
+
def _identity_decorators(self, source: SourceUnit, declaration: ast.ClassDef) -> bool:
|
|
116
|
+
if not declaration.decorator_list:
|
|
117
|
+
return True
|
|
118
|
+
if source.tree is None:
|
|
119
|
+
return False
|
|
120
|
+
imports = (
|
|
121
|
+
self.context.module_imports
|
|
122
|
+
if source.path == self.context.path
|
|
123
|
+
else ImportIndex.from_tree(source.tree, module_scope_only=True)
|
|
124
|
+
)
|
|
125
|
+
return all(
|
|
126
|
+
imports.resolves(decorator, sources=_TYPING_SOURCES, symbol="final")
|
|
127
|
+
for decorator in declaration.decorator_list
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def inherits(self, model: DeclaredClass, bases: frozenset[str]) -> bool:
|
|
131
|
+
return self.project.class_inherits_from(model.unit, model.declaration.name, bases)
|
|
132
|
+
|
|
133
|
+
def model_fields(self, model: DeclaredClass) -> dict[str, ast.AnnAssign]:
|
|
134
|
+
if model.unit.tree is None:
|
|
135
|
+
return {}
|
|
136
|
+
cached = self._field_cache.get(model.declaration)
|
|
137
|
+
if cached is not None:
|
|
138
|
+
return cached
|
|
139
|
+
semantics = self._module_semantics(model.unit)
|
|
140
|
+
fields = {
|
|
141
|
+
node.target.id: node
|
|
142
|
+
for node in model.declaration.body
|
|
143
|
+
if isinstance(node, ast.AnnAssign)
|
|
144
|
+
and isinstance(node.target, ast.Name)
|
|
145
|
+
and not node.target.id.startswith("_")
|
|
146
|
+
and node.target.id != "model_config"
|
|
147
|
+
and not _class_metadata_annotation(node.annotation, semantics)
|
|
148
|
+
}
|
|
149
|
+
self._field_cache[model.declaration] = fields
|
|
150
|
+
return fields
|
|
151
|
+
|
|
152
|
+
def _module_semantics(self, unit: SourceUnit) -> AnnotationSemantics:
|
|
153
|
+
if unit.tree is None:
|
|
154
|
+
return self.context.annotation_semantics
|
|
155
|
+
semantics = self._module_annotations.get(unit.path)
|
|
156
|
+
if semantics is None:
|
|
157
|
+
imports = (
|
|
158
|
+
self.context.module_imports
|
|
159
|
+
if unit.path == self.context.path
|
|
160
|
+
else ImportIndex.from_tree(unit.tree, module_scope_only=True)
|
|
161
|
+
)
|
|
162
|
+
semantics = AnnotationSemantics.from_imports(unit.tree, imports)
|
|
163
|
+
self._module_annotations[unit.path] = semantics
|
|
164
|
+
return semantics
|
|
165
|
+
|
|
166
|
+
def annotation(self, expression: ast.expr, unit: SourceUnit | None = None) -> DeclaredClass | None:
|
|
167
|
+
semantics = self.context.annotation_semantics
|
|
168
|
+
if unit is not None and unit is not self.unit and unit.tree is not None:
|
|
169
|
+
semantics = self._module_semantics(unit)
|
|
170
|
+
candidates = [
|
|
171
|
+
model
|
|
172
|
+
for member in semantics.transparent_members(expression)
|
|
173
|
+
if (model := self.class_type(member, unit, at=expression)) is not None
|
|
174
|
+
]
|
|
175
|
+
return candidates[0] if len(candidates) == 1 else None
|
|
176
|
+
|
|
177
|
+
def value(self, expression: ast.expr, *, depth: int = 0) -> DeclaredClass | None:
|
|
178
|
+
if depth > _MAX_DEPTH:
|
|
179
|
+
return None
|
|
180
|
+
match expression:
|
|
181
|
+
case ast.Await():
|
|
182
|
+
return self.value(expression.value, depth=depth + 1)
|
|
183
|
+
case ast.Name():
|
|
184
|
+
return self._local_value(expression, depth)
|
|
185
|
+
case ast.Call():
|
|
186
|
+
return self.class_type(expression.func) or self._call_result(expression, depth)
|
|
187
|
+
case _:
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
def _local_value(self, expression: ast.Name, depth: int) -> DeclaredClass | None:
|
|
191
|
+
binding = self.context.static_resource_provenance.binding(expression.id, expression)
|
|
192
|
+
if isinstance(binding, ast.arg) and binding.annotation is not None:
|
|
193
|
+
return self.annotation(binding.annotation)
|
|
194
|
+
parent = self.context.parents.get(binding) if binding is not None else None
|
|
195
|
+
if isinstance(parent, ast.AnnAssign):
|
|
196
|
+
return self.annotation(parent.annotation)
|
|
197
|
+
if isinstance(binding, ast.Name):
|
|
198
|
+
value = self.context.static_resource_provenance.assignment_value(binding, expression)
|
|
199
|
+
return self.value(value, depth=depth + 1) if value is not None else None
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
def _call_result(self, expression: ast.Call, depth: int) -> DeclaredClass | None:
|
|
203
|
+
match expression.func:
|
|
204
|
+
case ast.Name():
|
|
205
|
+
binding = self.context.static_resource_provenance.binding(expression.func.id, expression)
|
|
206
|
+
if isinstance(binding, ast.FunctionDef | ast.AsyncFunctionDef) and binding.returns is not None:
|
|
207
|
+
return self.annotation(binding.returns)
|
|
208
|
+
return None
|
|
209
|
+
case ast.Attribute():
|
|
210
|
+
return self._method_result(expression.func, depth)
|
|
211
|
+
case _:
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
def _method_result(self, target: ast.Attribute, depth: int) -> DeclaredClass | None:
|
|
215
|
+
model = self.class_type(target.value) or self.value(target.value, depth=depth + 1)
|
|
216
|
+
if model is None:
|
|
217
|
+
return None
|
|
218
|
+
if target.attr in {"model_validate", "model_validate_json", "model_copy"} and self.inherits(
|
|
219
|
+
model, MODEL_BASES | SETTINGS_BASES
|
|
220
|
+
):
|
|
221
|
+
return model
|
|
222
|
+
methods = [
|
|
223
|
+
node
|
|
224
|
+
for node in model.declaration.body
|
|
225
|
+
if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef) and node.name == target.attr
|
|
226
|
+
]
|
|
227
|
+
if len(methods) == 1 and methods[0].returns is not None and not methods[0].decorator_list:
|
|
228
|
+
return self.annotation(methods[0].returns, model.unit)
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def ordinary_pytest_test(function: ast.FunctionDef | ast.AsyncFunctionDef, context: PythonFileContext) -> bool:
|
|
233
|
+
if not function.name.startswith("test_"):
|
|
234
|
+
return False
|
|
235
|
+
owner = context.parents.get(function)
|
|
236
|
+
if isinstance(owner, ast.ClassDef) and (owner.bases or owner.decorator_list or owner.keywords):
|
|
237
|
+
return False
|
|
238
|
+
for decorator in function.decorator_list:
|
|
239
|
+
target = decorator.func if isinstance(decorator, ast.Call) else decorator
|
|
240
|
+
qualified = context.imports.resolved_qualified_name(target)
|
|
241
|
+
if qualified is None or not qualified.startswith("pytest.mark."):
|
|
242
|
+
return False
|
|
243
|
+
return True
|
{sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_local_source.py
RENAMED
|
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
|
|
|
15
15
|
|
|
16
16
|
_MAX_ANCESTORS = 24
|
|
17
17
|
_MAX_MODULE_BYTES = 256_000
|
|
18
|
-
|
|
18
|
+
LOCAL_SOURCE_CACHE_LIMIT = 512
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
@dataclass(frozen=True, slots=True)
|
|
@@ -26,6 +26,12 @@ class LocalModule:
|
|
|
26
26
|
runtime_imports: ImportIndex
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class LocalSourceKeys:
|
|
31
|
+
resolutions: frozenset[tuple[Path, str | None, int]]
|
|
32
|
+
modules: frozenset[Path]
|
|
33
|
+
|
|
34
|
+
|
|
29
35
|
@final
|
|
30
36
|
class LocalSourceFacts:
|
|
31
37
|
def __init__(self) -> None:
|
|
@@ -40,7 +46,7 @@ class LocalSourceFacts:
|
|
|
40
46
|
key = (current, module, level)
|
|
41
47
|
if key in self._resolutions:
|
|
42
48
|
return self._resolutions[key]
|
|
43
|
-
if len(self._resolutions) >=
|
|
49
|
+
if len(self._resolutions) >= LOCAL_SOURCE_CACHE_LIMIT:
|
|
44
50
|
return None
|
|
45
51
|
result = _resolve_module(current, module, level, facts=facts)
|
|
46
52
|
self._resolutions[key] = result
|
|
@@ -49,12 +55,16 @@ class LocalSourceFacts:
|
|
|
49
55
|
def read_module(self, path: Path) -> LocalModule | None:
|
|
50
56
|
if path in self._modules:
|
|
51
57
|
return self._modules[path]
|
|
52
|
-
if len(self._modules) >=
|
|
58
|
+
if len(self._modules) >= LOCAL_SOURCE_CACHE_LIMIT:
|
|
53
59
|
return None
|
|
54
60
|
module = _read_module(path)
|
|
55
61
|
self._modules[path] = module
|
|
56
62
|
return module
|
|
57
63
|
|
|
64
|
+
def cache_keys(self) -> LocalSourceKeys:
|
|
65
|
+
# Failed lookups consume the same semantic budgets as successful ones.
|
|
66
|
+
return LocalSourceKeys(frozenset(self._resolutions), frozenset(self._modules))
|
|
67
|
+
|
|
58
68
|
|
|
59
69
|
def _resolve_module(current: Path, module: str | None, level: int, *, facts: FirstPartyFacts) -> Path | None:
|
|
60
70
|
if ".." in current.parts or level > _MAX_ANCESTORS:
|
{sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
@@ -154,6 +154,20 @@ class LoadedSource:
|
|
|
154
154
|
source: str
|
|
155
155
|
|
|
156
156
|
|
|
157
|
+
@dataclass(frozen=True, slots=True)
|
|
158
|
+
class _IndexedSource:
|
|
159
|
+
key: Path
|
|
160
|
+
path: Path
|
|
161
|
+
module: str | None
|
|
162
|
+
source: str
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass(frozen=True, slots=True)
|
|
166
|
+
class ProjectSourceSnapshot:
|
|
167
|
+
roots: tuple[Path, ...]
|
|
168
|
+
units: tuple[_IndexedSource, ...]
|
|
169
|
+
|
|
170
|
+
|
|
157
171
|
@dataclass(frozen=True, slots=True)
|
|
158
172
|
class _ClassValueUse:
|
|
159
173
|
reference: ast.Name | ast.Attribute
|
|
@@ -196,6 +210,30 @@ class ProjectIndexSet:
|
|
|
196
210
|
self._classes = MappingProxyType(classes)
|
|
197
211
|
self._nominals = MappingProxyType({key: frozenset(value) for key, value in nominals.items()})
|
|
198
212
|
|
|
213
|
+
def source_snapshot(self) -> ProjectSourceSnapshot:
|
|
214
|
+
# Rebuild memoized facts without rescanning roots or changing the
|
|
215
|
+
# module identities/order of the complete prepared source graph.
|
|
216
|
+
return ProjectSourceSnapshot(
|
|
217
|
+
self._roots,
|
|
218
|
+
tuple(_IndexedSource(key, unit.path, unit.module, unit.source) for key, unit in self._units.items()),
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
@classmethod
|
|
222
|
+
def from_source_snapshot(cls, snapshot: ProjectSourceSnapshot) -> Self:
|
|
223
|
+
units: dict[Path, SourceUnit] = {}
|
|
224
|
+
for source in snapshot.units:
|
|
225
|
+
tree: ast.Module | None = None
|
|
226
|
+
with suppress(SyntaxError):
|
|
227
|
+
tree = ast.parse(source.source, filename=str(source.path))
|
|
228
|
+
units[source.key] = SourceUnit(
|
|
229
|
+
path=source.path,
|
|
230
|
+
module=source.module,
|
|
231
|
+
source=source.source,
|
|
232
|
+
tree=tree,
|
|
233
|
+
imports=MappingProxyType(_imports(source.module, tree, is_package=source.path.name == "__init__.py")),
|
|
234
|
+
)
|
|
235
|
+
return cls(units, snapshot.roots)
|
|
236
|
+
|
|
199
237
|
@classmethod
|
|
200
238
|
def build(
|
|
201
239
|
cls,
|
|
@@ -237,8 +237,13 @@ from sarj_python_lint.rules.no_slash_collection_root_under_prefix import NoSlash
|
|
|
237
237
|
from sarj_python_lint.rules.no_unknown_pydantic_validator_field import NoUnknownPydanticValidatorField
|
|
238
238
|
from sarj_python_lint.rules.no_work_verb_get_route import NoWorkVerbGetRoute
|
|
239
239
|
from sarj_python_lint.rules.prefer_bulk_upsert import PreferBulkUpsert
|
|
240
|
+
from sarj_python_lint.rules.prefer_direct_import_module import PreferDirectImportModule
|
|
240
241
|
from sarj_python_lint.rules.prefer_discriminated_model_union import PreferDiscriminatedModelUnion
|
|
242
|
+
from sarj_python_lint.rules.prefer_precise_object_contract import PreferPreciseObjectContract
|
|
241
243
|
from sarj_python_lint.rules.prefer_psycopg_executemany import PreferPsycopgExecutemany
|
|
244
|
+
from sarj_python_lint.rules.prefer_pydantic_model_assertion import PreferPydanticModelAssertion
|
|
245
|
+
from sarj_python_lint.rules.prefer_pytest_dependency_fixtures import PreferPytestDependencyFixtures
|
|
246
|
+
from sarj_python_lint.rules.prefer_typed_settings_field import PreferTypedSettingsField
|
|
242
247
|
from sarj_python_lint.rules.pytest_fixture_returns_closed_resource import PytestFixtureReturnsClosedResource
|
|
243
248
|
from sarj_python_lint.rules.redundant_test_constructor_forwarder import RedundantTestConstructorForwarder
|
|
244
249
|
from sarj_python_lint.rules.repeated_unimplemented_test_contract import RepeatedUnimplementedTestContract
|
|
@@ -251,6 +256,11 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
251
256
|
{
|
|
252
257
|
PytestFixtureReturnsClosedResource.id: PytestFixtureReturnsClosedResource,
|
|
253
258
|
DuplicatePytestFixtureParams.id: DuplicatePytestFixtureParams,
|
|
259
|
+
PreferTypedSettingsField.id: PreferTypedSettingsField,
|
|
260
|
+
PreferDirectImportModule.id: PreferDirectImportModule,
|
|
261
|
+
PreferPreciseObjectContract.id: PreferPreciseObjectContract,
|
|
262
|
+
PreferPydanticModelAssertion.id: PreferPydanticModelAssertion,
|
|
263
|
+
PreferPytestDependencyFixtures.id: PreferPytestDependencyFixtures,
|
|
254
264
|
PreferBulkUpsert.id: PreferBulkUpsert,
|
|
255
265
|
PreferPsycopgExecutemany.id: PreferPsycopgExecutemany,
|
|
256
266
|
StoreFieldUpdateReusesUpsert.id: StoreFieldUpdateReusesUpsert,
|
{sarj_python_lint-1.67.0 → sarj_python_lint-1.69.0}/src/sarj_python_lint/rules/_test_composition.py
RENAMED
|
@@ -65,7 +65,13 @@ def _class_owner(
|
|
|
65
65
|
return unit, owner
|
|
66
66
|
|
|
67
67
|
|
|
68
|
-
def constructor(
|
|
68
|
+
def constructor(
|
|
69
|
+
context: PythonFileContext,
|
|
70
|
+
expression: ast.expr,
|
|
71
|
+
project: ProjectIndexSet,
|
|
72
|
+
*,
|
|
73
|
+
minimum_collaborators: int = _MIN_COLLABORATORS,
|
|
74
|
+
) -> Constructor | None:
|
|
69
75
|
resolved = _class_owner(context, expression, project)
|
|
70
76
|
if resolved is None:
|
|
71
77
|
return None
|
|
@@ -87,7 +93,7 @@ def constructor(context: PythonFileContext, expression: ast.expr, project: Proje
|
|
|
87
93
|
and _is_port(project, unit, parameter.annotation)
|
|
88
94
|
and stored.get(parameter.arg) in invoked
|
|
89
95
|
)
|
|
90
|
-
if len(collaborators) <
|
|
96
|
+
if len(collaborators) < minimum_collaborators:
|
|
91
97
|
return None
|
|
92
98
|
return Constructor(
|
|
93
99
|
SymbolRef(unit.module, owner.name),
|