sarj-python-lint 1.47.0__tar.gz → 1.49.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.47.0 → sarj_python_lint-1.49.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/pyproject.toml +1 -1
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_registry.py +8 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_resource_provenance.py +126 -0
- sarj_python_lint-1.49.0/src/sarj_python_lint/rules/_runtime_bindings.py +113 -0
- sarj_python_lint-1.49.0/src/sarj_python_lint/rules/_store_writes.py +319 -0
- sarj_python_lint-1.49.0/src/sarj_python_lint/rules/no_discarded_validation_result.py +453 -0
- sarj_python_lint-1.49.0/src/sarj_python_lint/rules/prefer_bulk_upsert.py +139 -0
- sarj_python_lint-1.49.0/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +139 -0
- sarj_python_lint-1.49.0/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +240 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +13 -2
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/.gitignore +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/LICENSE +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/README.md +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/_file_context.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.47.0 → sarj_python_lint-1.49.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.49.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/
|
|
@@ -43,6 +43,7 @@ from sarj_python_lint.rules.no_cors_wildcard_with_credentials import (
|
|
|
43
43
|
)
|
|
44
44
|
from sarj_python_lint.rules.no_database_programmability import NoDatabaseProgrammability
|
|
45
45
|
from sarj_python_lint.rules.no_delete_statement import NoDeleteStatement
|
|
46
|
+
from sarj_python_lint.rules.no_discarded_validation_result import NoDiscardedValidationResult
|
|
46
47
|
from sarj_python_lint.rules.no_dunder_all import NoDunderAll
|
|
47
48
|
from sarj_python_lint.rules.no_duplicate_dunder_all_entry import NoDuplicateDunderAllEntry
|
|
48
49
|
from sarj_python_lint.rules.no_excessive_cognitive_complexity import NoExcessiveCognitiveComplexity
|
|
@@ -221,14 +222,20 @@ from sarj_python_lint.rules.no_invalid_pydantic_validator_return import NoInvali
|
|
|
221
222
|
from sarj_python_lint.rules.no_partial_type_is import NoPartialTypeIs
|
|
222
223
|
from sarj_python_lint.rules.no_shallow_record_type_guard import NoShallowRecordTypeGuard
|
|
223
224
|
from sarj_python_lint.rules.no_unknown_pydantic_validator_field import NoUnknownPydanticValidatorField
|
|
225
|
+
from sarj_python_lint.rules.prefer_bulk_upsert import PreferBulkUpsert
|
|
224
226
|
from sarj_python_lint.rules.prefer_discriminated_model_union import PreferDiscriminatedModelUnion
|
|
227
|
+
from sarj_python_lint.rules.prefer_psycopg_executemany import PreferPsycopgExecutemany
|
|
225
228
|
from sarj_python_lint.rules.redundant_test_constructor_forwarder import RedundantTestConstructorForwarder
|
|
226
229
|
from sarj_python_lint.rules.repeated_unimplemented_test_contract import RepeatedUnimplementedTestContract
|
|
230
|
+
from sarj_python_lint.rules.store_field_update_reuses_upsert import StoreFieldUpdateReusesUpsert
|
|
227
231
|
from sarj_python_lint.rules.test_without_explicit_oracle import NoExplicitTestOracle
|
|
228
232
|
|
|
229
233
|
|
|
230
234
|
REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
231
235
|
{
|
|
236
|
+
PreferBulkUpsert.id: PreferBulkUpsert,
|
|
237
|
+
PreferPsycopgExecutemany.id: PreferPsycopgExecutemany,
|
|
238
|
+
StoreFieldUpdateReusesUpsert.id: StoreFieldUpdateReusesUpsert,
|
|
232
239
|
NoExplicitTestOracle.id: NoExplicitTestOracle,
|
|
233
240
|
PreferDiscriminatedModelUnion.id: PreferDiscriminatedModelUnion,
|
|
234
241
|
NoInvalidPydanticBypass.id: NoInvalidPydanticBypass,
|
|
@@ -243,6 +250,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
243
250
|
NoFixedSleepInLoop.id: NoFixedSleepInLoop,
|
|
244
251
|
NoInterpreterSourceArguments.id: NoInterpreterSourceArguments,
|
|
245
252
|
NoUnusedCopyResult.id: NoUnusedCopyResult,
|
|
253
|
+
NoDiscardedValidationResult.id: NoDiscardedValidationResult,
|
|
246
254
|
NoBeforeValidatorInputMutation.id: NoBeforeValidatorInputMutation,
|
|
247
255
|
NoInputModelMutation.id: NoInputModelMutation,
|
|
248
256
|
NoDatabaseProgrammability.id: NoDatabaseProgrammability,
|
|
@@ -8,9 +8,12 @@ from typing import TYPE_CHECKING, final
|
|
|
8
8
|
|
|
9
9
|
from sarj_python_lint.rules._ast_index import has_own_yield
|
|
10
10
|
from sarj_python_lint.rules._namespace_access import dynamic_namespace_access
|
|
11
|
+
from sarj_python_lint.rules._runtime_bindings import RuntimeBindings
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
if TYPE_CHECKING:
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
14
17
|
from sarj_python_lint._file_context import PythonFileContext
|
|
15
18
|
|
|
16
19
|
|
|
@@ -76,6 +79,7 @@ class ResourceProvenance:
|
|
|
76
79
|
self.escape_cache: dict[tuple[ast.AST, bool], bool] = {}
|
|
77
80
|
self.async_contract_cache: dict[ast.AsyncFunctionDef, bool] = {}
|
|
78
81
|
self.import_contract_cache: dict[str, bool] = {}
|
|
82
|
+
self.runtime_import_cache: dict[ast.alias, bool] = {}
|
|
79
83
|
self.wildcard = False
|
|
80
84
|
self.mutated: set[str] = set()
|
|
81
85
|
self.external_bindings: set[str] = set()
|
|
@@ -133,6 +137,113 @@ class ResourceProvenance:
|
|
|
133
137
|
scope = self.scope(scope)
|
|
134
138
|
return None
|
|
135
139
|
|
|
140
|
+
@cached_property
|
|
141
|
+
def runtime_bindings(self) -> RuntimeBindings:
|
|
142
|
+
return RuntimeBindings(self.context, self.bindings)
|
|
143
|
+
|
|
144
|
+
def runtime_binding(self, name: str, at: ast.AST) -> ast.AST | None:
|
|
145
|
+
nodes = self.runtime_bindings.lookup(self.scope(at), name)
|
|
146
|
+
if not nodes:
|
|
147
|
+
return None
|
|
148
|
+
return nodes[0] if len(nodes) == 1 or self._same_import_bindings(list(nodes)) else None
|
|
149
|
+
|
|
150
|
+
def runtime_import_name(self, node: ast.expr) -> str | None:
|
|
151
|
+
root = _root_name(node)
|
|
152
|
+
if self.wildcard or self.dynamic_namespace or root is None or root in self.mutated:
|
|
153
|
+
return None
|
|
154
|
+
binding = self.runtime_binding(root, node)
|
|
155
|
+
if not isinstance(binding, ast.alias):
|
|
156
|
+
return None
|
|
157
|
+
qualified = _runtime_import_name(node, binding, self.context.parents.get(binding))
|
|
158
|
+
if qualified is None or not self._owner_namespace_untouched(qualified.partition(".")[0]):
|
|
159
|
+
return None
|
|
160
|
+
if binding not in self.runtime_import_cache:
|
|
161
|
+
self.runtime_import_cache[binding] = all(
|
|
162
|
+
self.runtime_binding(root, read) is not binding or self._safe_import_use(read)
|
|
163
|
+
for read in self.reads.get(root, ())
|
|
164
|
+
)
|
|
165
|
+
return qualified if self.runtime_import_cache[binding] else None
|
|
166
|
+
|
|
167
|
+
@cached_property
|
|
168
|
+
def mock_namespace_patching(self) -> bool:
|
|
169
|
+
return any(
|
|
170
|
+
self.runtime_import_name(node.func)
|
|
171
|
+
in {
|
|
172
|
+
"unittest.mock.patch",
|
|
173
|
+
"unittest.mock.patch.object",
|
|
174
|
+
"unittest.mock.patch.multiple",
|
|
175
|
+
"unittest.mock.patch.dict",
|
|
176
|
+
}
|
|
177
|
+
for node in self.context.nodes(ast.Call)
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
@cached_property
|
|
181
|
+
def imported_namespace_hazards(self) -> frozenset[Path]:
|
|
182
|
+
hazards: set[Path] = set()
|
|
183
|
+
for node in self.context.nodes(ast.Name, ast.Attribute):
|
|
184
|
+
match node:
|
|
185
|
+
case ast.Attribute(value=value, ctx=ast.Store() | ast.Del()):
|
|
186
|
+
hazards.update(self._namespace_paths(value, include_root=True))
|
|
187
|
+
case ast.Name(ctx=ast.Load()) | ast.Attribute(ctx=ast.Load()) if not self._safe_import_use(node):
|
|
188
|
+
hazards.update(self._namespace_paths(node, include_root=False))
|
|
189
|
+
case _:
|
|
190
|
+
pass
|
|
191
|
+
return frozenset(hazards)
|
|
192
|
+
|
|
193
|
+
@cached_property
|
|
194
|
+
def _namespace_aliases(self) -> dict[str, tuple[ast.alias, ...]]:
|
|
195
|
+
aliases: dict[str, list[ast.alias]] = defaultdict(list)
|
|
196
|
+
for binding in self.context.nodes(ast.alias):
|
|
197
|
+
parent = self.context.parents.get(binding)
|
|
198
|
+
name = binding.asname or (
|
|
199
|
+
binding.name.partition(".")[0] if isinstance(parent, ast.Import) else binding.name
|
|
200
|
+
)
|
|
201
|
+
aliases[name].append(binding)
|
|
202
|
+
return {name: tuple(bindings) for name, bindings in aliases.items()}
|
|
203
|
+
|
|
204
|
+
def _namespace_paths(self, expression: ast.expr, *, include_root: bool) -> set[Path]:
|
|
205
|
+
attributes: list[str] = []
|
|
206
|
+
root = expression
|
|
207
|
+
while isinstance(root, ast.Attribute):
|
|
208
|
+
attributes.append(root.attr)
|
|
209
|
+
root = root.value
|
|
210
|
+
if not isinstance(root, ast.Name):
|
|
211
|
+
return set()
|
|
212
|
+
binding = self.runtime_binding(root.id, expression)
|
|
213
|
+
aliases: tuple[ast.alias, ...]
|
|
214
|
+
match binding:
|
|
215
|
+
case ast.alias():
|
|
216
|
+
aliases = (binding,)
|
|
217
|
+
case None:
|
|
218
|
+
aliases = self._namespace_aliases.get(root.id, ())
|
|
219
|
+
case _:
|
|
220
|
+
return set()
|
|
221
|
+
paths: set[Path] = set()
|
|
222
|
+
for alias in aliases:
|
|
223
|
+
path = self._namespace_module_path(alias, tuple(reversed(attributes)))
|
|
224
|
+
if path is not None:
|
|
225
|
+
paths.add(path)
|
|
226
|
+
if include_root and attributes:
|
|
227
|
+
base = self._namespace_module_path(alias, ())
|
|
228
|
+
if base is not None:
|
|
229
|
+
paths.add(base)
|
|
230
|
+
return paths
|
|
231
|
+
|
|
232
|
+
def _namespace_module_path(self, binding: ast.alias, attributes: tuple[str, ...]) -> Path | None:
|
|
233
|
+
parent = self.context.parents.get(binding)
|
|
234
|
+
match parent:
|
|
235
|
+
case ast.Import():
|
|
236
|
+
prefix = binding.name if binding.asname else binding.name.partition(".")[0]
|
|
237
|
+
level = 0
|
|
238
|
+
case ast.ImportFrom(module=module, level=level):
|
|
239
|
+
prefix = ".".join(part for part in (module, binding.name) if part)
|
|
240
|
+
case _:
|
|
241
|
+
return None
|
|
242
|
+
module = ".".join((prefix, *attributes))
|
|
243
|
+
return self.context.session.local_source.resolve_module(
|
|
244
|
+
self.context.path, module, level, facts=self.context.session.first_party
|
|
245
|
+
)
|
|
246
|
+
|
|
136
247
|
def _same_import_bindings(self, nodes: list[ast.AST]) -> bool:
|
|
137
248
|
targets: set[tuple[str, str | None, int, str]] = set()
|
|
138
249
|
for node in nodes:
|
|
@@ -417,6 +528,21 @@ def _root_name(node: ast.expr) -> str | None:
|
|
|
417
528
|
return node.id if isinstance(node, ast.Name) else None
|
|
418
529
|
|
|
419
530
|
|
|
531
|
+
def _runtime_import_name(expression: ast.expr, binding: ast.alias, parent: ast.AST | None) -> str | None:
|
|
532
|
+
attributes: list[str] = []
|
|
533
|
+
while isinstance(expression, ast.Attribute):
|
|
534
|
+
attributes.append(expression.attr)
|
|
535
|
+
expression = expression.value
|
|
536
|
+
match parent:
|
|
537
|
+
case ast.Import():
|
|
538
|
+
prefix = binding.name if binding.asname else binding.name.partition(".")[0]
|
|
539
|
+
case ast.ImportFrom(module=str(module), level=0):
|
|
540
|
+
prefix = f"{module}.{binding.name}"
|
|
541
|
+
case _:
|
|
542
|
+
return None
|
|
543
|
+
return ".".join((prefix, *reversed(attributes)))
|
|
544
|
+
|
|
545
|
+
|
|
420
546
|
def _bound_names(node: ast.AST) -> tuple[str, ...]:
|
|
421
547
|
match node:
|
|
422
548
|
case ast.Name(id=name, ctx=ast.Store() | ast.Del()) | ast.arg(arg=name):
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
import symtable
|
|
6
|
+
from typing import TYPE_CHECKING, final
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Mapping, Sequence
|
|
11
|
+
|
|
12
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@final
|
|
16
|
+
class RuntimeBindings:
|
|
17
|
+
def __init__(
|
|
18
|
+
self, context: PythonFileContext, bindings: Mapping[ast.AST | None, Mapping[str, Sequence[ast.AST]]]
|
|
19
|
+
) -> None:
|
|
20
|
+
self.bindings = bindings
|
|
21
|
+
self.tables: dict[int, symtable.SymbolTable] = {}
|
|
22
|
+
self.parents: dict[int, int | None] = {}
|
|
23
|
+
self.by_scope: dict[ast.AST, int] = {}
|
|
24
|
+
self.scopes: dict[int, ast.AST] = {}
|
|
25
|
+
self.mutable: set[tuple[int, str]] = set()
|
|
26
|
+
self.module: int | None = None
|
|
27
|
+
tree = context.tree
|
|
28
|
+
if tree is None:
|
|
29
|
+
return
|
|
30
|
+
try:
|
|
31
|
+
root = symtable.symtable(context.source, str(context.path), "exec")
|
|
32
|
+
except SyntaxError, UnicodeError, RecursionError:
|
|
33
|
+
return
|
|
34
|
+
self.module = root.get_id()
|
|
35
|
+
self._collect(root)
|
|
36
|
+
self._map_scopes(context, tree, root)
|
|
37
|
+
self._index_writes()
|
|
38
|
+
|
|
39
|
+
def lookup(self, scope: ast.AST | None, name: str) -> Sequence[ast.AST] | None:
|
|
40
|
+
if scope is None or (table_id := self.by_scope.get(scope)) is None:
|
|
41
|
+
return None
|
|
42
|
+
symbol = _symbol(self.tables[table_id], name)
|
|
43
|
+
if symbol is None or (owner := self._owner(table_id, symbol)) is None or (owner, name) in self.mutable:
|
|
44
|
+
return None
|
|
45
|
+
owner_scope = self.scopes.get(owner)
|
|
46
|
+
return self.bindings.get(owner_scope, {}).get(name) if owner_scope is not None else None
|
|
47
|
+
|
|
48
|
+
def _collect(self, root: symtable.SymbolTable) -> None:
|
|
49
|
+
pending: list[tuple[symtable.SymbolTable, int | None]] = [(root, None)]
|
|
50
|
+
while pending:
|
|
51
|
+
table, parent = pending.pop()
|
|
52
|
+
identity = table.get_id()
|
|
53
|
+
self.tables[identity] = table
|
|
54
|
+
self.parents[identity] = parent
|
|
55
|
+
pending.extend((child, identity) for child in table.get_children())
|
|
56
|
+
|
|
57
|
+
def _map_scopes(self, context: PythonFileContext, tree: ast.Module, root: symtable.SymbolTable) -> None:
|
|
58
|
+
candidates: dict[tuple[symtable.SymbolTableType, str, int], list[int]] = defaultdict(list)
|
|
59
|
+
for identity, table in self.tables.items():
|
|
60
|
+
candidates[table.get_type(), table.get_name(), table.get_lineno()].append(identity)
|
|
61
|
+
self.by_scope[tree] = root.get_id()
|
|
62
|
+
self.scopes[root.get_id()] = tree
|
|
63
|
+
for node in context.nodes(ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef):
|
|
64
|
+
kind = (
|
|
65
|
+
symtable.SymbolTableType.CLASS if isinstance(node, ast.ClassDef) else symtable.SymbolTableType.FUNCTION
|
|
66
|
+
)
|
|
67
|
+
matches = candidates.get((kind, node.name, node.lineno), ())
|
|
68
|
+
if len(matches) == 1:
|
|
69
|
+
[identity] = matches
|
|
70
|
+
self.by_scope[node] = identity
|
|
71
|
+
self.scopes[identity] = node
|
|
72
|
+
|
|
73
|
+
def _index_writes(self) -> None:
|
|
74
|
+
for identity, table in self.tables.items():
|
|
75
|
+
if identity == self.module:
|
|
76
|
+
continue
|
|
77
|
+
for symbol in table.get_symbols():
|
|
78
|
+
if not (symbol.is_global() or symbol.is_nonlocal()):
|
|
79
|
+
continue
|
|
80
|
+
if not (symbol.is_assigned() or symbol.is_imported() or symbol.is_namespace()):
|
|
81
|
+
continue
|
|
82
|
+
owner = self._owner(identity, symbol)
|
|
83
|
+
if owner is not None:
|
|
84
|
+
self.mutable.add((owner, symbol.get_name()))
|
|
85
|
+
|
|
86
|
+
def _owner(self, identity: int, symbol: symtable.Symbol) -> int | None:
|
|
87
|
+
if symbol.is_global():
|
|
88
|
+
return self.module
|
|
89
|
+
if symbol.is_local():
|
|
90
|
+
return identity
|
|
91
|
+
if not symbol.is_free():
|
|
92
|
+
return None
|
|
93
|
+
parent = self.parents.get(identity)
|
|
94
|
+
while parent is not None:
|
|
95
|
+
table = self.tables[parent]
|
|
96
|
+
if table.get_type() is symtable.SymbolTableType.MODULE:
|
|
97
|
+
break
|
|
98
|
+
if table.get_type() is symtable.SymbolTableType.CLASS and symbol.get_name() == "__class__":
|
|
99
|
+
# The compiler binds this cell to the lexically defined class.
|
|
100
|
+
return None
|
|
101
|
+
if table.get_type() is not symtable.SymbolTableType.CLASS:
|
|
102
|
+
ancestor = _symbol(table, symbol.get_name())
|
|
103
|
+
if ancestor is not None and ancestor.is_local():
|
|
104
|
+
return parent
|
|
105
|
+
parent = self.parents.get(parent)
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _symbol(table: symtable.SymbolTable, name: str) -> symtable.Symbol | None:
|
|
110
|
+
try:
|
|
111
|
+
return table.lookup(name)
|
|
112
|
+
except KeyError:
|
|
113
|
+
return None
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from types import MappingProxyType
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import sqlglot
|
|
8
|
+
from sqlglot import exp
|
|
9
|
+
from sqlglot.errors import SqlglotError
|
|
10
|
+
|
|
11
|
+
from sarj_python_lint.rules._annotation_semantics import AnnotationSemantics
|
|
12
|
+
from sarj_python_lint.rules._ast_index import walk
|
|
13
|
+
from sarj_python_lint.rules._paths import is_generated
|
|
14
|
+
from sarj_python_lint.rules._project_index import ProjectIndexSet
|
|
15
|
+
from sarj_python_lint.rules._resource_provenance import ResourceProvenance
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from collections.abc import Iterator
|
|
20
|
+
|
|
21
|
+
from sarj_python_lint._file_context import PythonFileContext
|
|
22
|
+
from sarj_python_lint.rules._imports import ImportIndex
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
type Function = ast.FunctionDef | ast.AsyncFunctionDef
|
|
26
|
+
_COLLECTIONS = frozenset(
|
|
27
|
+
{
|
|
28
|
+
"collections.abc.Collection",
|
|
29
|
+
"collections.abc.Iterable",
|
|
30
|
+
"collections.abc.Sequence",
|
|
31
|
+
"typing.Collection",
|
|
32
|
+
"typing.Iterable",
|
|
33
|
+
"typing.Sequence",
|
|
34
|
+
"typing.List",
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
_NEUTRAL_DECORATORS = frozenset({"abc.abstractmethod", "typing.override", "typing_extensions.override"})
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def method_parameters(method: Function) -> list[ast.arg] | None:
|
|
41
|
+
args = method.args
|
|
42
|
+
positional = [*args.posonlyargs, *args.args]
|
|
43
|
+
if not positional or positional[0].arg != "self":
|
|
44
|
+
return None
|
|
45
|
+
if args.vararg or args.kwarg or args.defaults or any(args.kw_defaults):
|
|
46
|
+
return None
|
|
47
|
+
return [*positional[1:], *args.kwonlyargs]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def methods(owner: ast.ClassDef) -> dict[str, Function]:
|
|
51
|
+
result: dict[str, Function] = {}
|
|
52
|
+
ambiguous: set[str] = set()
|
|
53
|
+
for node in owner.body:
|
|
54
|
+
if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
|
|
55
|
+
if node.name in result:
|
|
56
|
+
ambiguous.add(node.name)
|
|
57
|
+
result[node.name] = node
|
|
58
|
+
return {name: method for name, method in result.items() if name not in ambiguous}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def upsert_pair(owner: ast.ClassDef, imports: ImportIndex) -> tuple[Function, Function] | None:
|
|
62
|
+
declared = methods(owner)
|
|
63
|
+
single, bulk = declared.get("upsert"), declared.get("upsert_many")
|
|
64
|
+
if (
|
|
65
|
+
single is None
|
|
66
|
+
or bulk is None
|
|
67
|
+
or isinstance(single, ast.AsyncFunctionDef) != isinstance(bulk, ast.AsyncFunctionDef)
|
|
68
|
+
):
|
|
69
|
+
return None
|
|
70
|
+
if any(
|
|
71
|
+
imports.resolved_qualified_name(decorator) not in _NEUTRAL_DECORATORS
|
|
72
|
+
for method in (single, bulk)
|
|
73
|
+
for decorator in method.decorator_list
|
|
74
|
+
):
|
|
75
|
+
return None
|
|
76
|
+
one, many = method_parameters(single), method_parameters(bulk)
|
|
77
|
+
if one is None or many is None or len(one) != 1 or len(many) != 1:
|
|
78
|
+
return None
|
|
79
|
+
item = collection_item(many[0].annotation, imports)
|
|
80
|
+
result = collection_item(bulk.returns, imports)
|
|
81
|
+
if (
|
|
82
|
+
item is None
|
|
83
|
+
or result is None
|
|
84
|
+
or not same_type(one[0].annotation, item)
|
|
85
|
+
or not same_type(single.returns, result)
|
|
86
|
+
):
|
|
87
|
+
return None
|
|
88
|
+
return single, bulk
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def collection_item(annotation: ast.expr | None, imports: ImportIndex) -> ast.expr | None:
|
|
92
|
+
annotation = AnnotationSemantics(imports, MappingProxyType({})).parse(annotation)
|
|
93
|
+
if not isinstance(annotation, ast.Subscript):
|
|
94
|
+
return None
|
|
95
|
+
container = annotation.value
|
|
96
|
+
if isinstance(container, ast.Name) and container.id == "list" and imports.builtin_is_unshadowed("list"):
|
|
97
|
+
return annotation.slice
|
|
98
|
+
if imports.resolved_qualified_name(container) in _COLLECTIONS:
|
|
99
|
+
return annotation.slice
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def same_type(left: ast.expr | None, right: ast.expr | None) -> bool:
|
|
104
|
+
return (
|
|
105
|
+
left is not None
|
|
106
|
+
and right is not None
|
|
107
|
+
and ast.dump(left, include_attributes=False) == ast.dump(right, include_attributes=False)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def own_nodes(node: ast.AST) -> Iterator[ast.AST]:
|
|
112
|
+
pending = [node]
|
|
113
|
+
while pending:
|
|
114
|
+
current = pending.pop()
|
|
115
|
+
yield current
|
|
116
|
+
pending.extend(
|
|
117
|
+
child
|
|
118
|
+
for child in reversed(list(ast.iter_child_nodes(current)))
|
|
119
|
+
if not isinstance(child, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef | ast.Lambda)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def discarded_call(loop: ast.For) -> tuple[ast.Call, bool] | None:
|
|
124
|
+
if loop.orelse or len(loop.body) != 1 or not isinstance(loop.body[0], ast.Expr):
|
|
125
|
+
return None
|
|
126
|
+
if (
|
|
127
|
+
isinstance(loop.iter, ast.List | ast.Tuple | ast.Set)
|
|
128
|
+
and len(loop.iter.elts) <= 1
|
|
129
|
+
and not any(isinstance(item, ast.Starred) for item in loop.iter.elts)
|
|
130
|
+
):
|
|
131
|
+
return None
|
|
132
|
+
value = loop.body[0].value
|
|
133
|
+
awaited = isinstance(value, ast.Await)
|
|
134
|
+
value = value.value if awaited else value
|
|
135
|
+
return (value, awaited) if isinstance(value, ast.Call) else None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def depends_on_target(expression: ast.expr, target: ast.expr) -> bool:
|
|
139
|
+
names = {node.id for node in walk(target) if isinstance(node, ast.Name)}
|
|
140
|
+
return any(isinstance(node, ast.Name) and node.id in names for node in own_nodes(expression))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class StoreOwners:
|
|
144
|
+
def __init__(self, context: PythonFileContext) -> None:
|
|
145
|
+
self.context = context
|
|
146
|
+
self.provenance = ResourceProvenance(context)
|
|
147
|
+
self.project = context.session.project
|
|
148
|
+
|
|
149
|
+
def receiver(self, expression: ast.expr, at: ast.AST) -> tuple[ast.ClassDef, ImportIndex] | None:
|
|
150
|
+
if isinstance(expression, ast.Name):
|
|
151
|
+
binding = self.provenance.binding(expression.id, at)
|
|
152
|
+
if isinstance(binding, ast.arg) and binding.annotation is not None:
|
|
153
|
+
return self.annotation(binding.annotation)
|
|
154
|
+
return None
|
|
155
|
+
if (
|
|
156
|
+
not isinstance(expression, ast.Attribute)
|
|
157
|
+
or not isinstance(expression.value, ast.Name)
|
|
158
|
+
or expression.value.id != "self"
|
|
159
|
+
):
|
|
160
|
+
return None
|
|
161
|
+
if not isinstance(self.provenance.binding("self", at), ast.arg):
|
|
162
|
+
return None
|
|
163
|
+
annotation = self._injected_annotation(expression.attr, at)
|
|
164
|
+
return self.annotation(annotation) if annotation is not None else None
|
|
165
|
+
|
|
166
|
+
def _injected_annotation(self, attribute: str, at: ast.AST) -> ast.expr | None:
|
|
167
|
+
parent = self.context.parents.get(at)
|
|
168
|
+
while parent is not None and not isinstance(parent, ast.ClassDef):
|
|
169
|
+
parent = self.context.parents.get(parent)
|
|
170
|
+
if parent is None:
|
|
171
|
+
return None
|
|
172
|
+
constructor = methods(parent).get("__init__")
|
|
173
|
+
if constructor is None:
|
|
174
|
+
return None
|
|
175
|
+
assignments = [
|
|
176
|
+
node
|
|
177
|
+
for method in methods(parent).values()
|
|
178
|
+
for node in own_nodes(method)
|
|
179
|
+
if _writes_self_attribute(node, attribute)
|
|
180
|
+
]
|
|
181
|
+
if len(assignments) != 1:
|
|
182
|
+
return None
|
|
183
|
+
for statement in constructor.body:
|
|
184
|
+
if (
|
|
185
|
+
not isinstance(statement, ast.Assign)
|
|
186
|
+
or not isinstance(statement.value, ast.Name)
|
|
187
|
+
or assignments[0] not in statement.targets
|
|
188
|
+
):
|
|
189
|
+
continue
|
|
190
|
+
return _injected_parameter(constructor, statement.value, self.provenance)
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
def annotation(self, expression: ast.expr | None) -> tuple[ast.ClassDef, ImportIndex] | None:
|
|
194
|
+
original = expression
|
|
195
|
+
expression = AnnotationSemantics(self.context.module_imports, MappingProxyType({})).parse(expression)
|
|
196
|
+
if expression is None or self.context.tree is None:
|
|
197
|
+
return None
|
|
198
|
+
root = expression.value if isinstance(expression, ast.Attribute) else expression
|
|
199
|
+
if (
|
|
200
|
+
not isinstance(root, ast.Name)
|
|
201
|
+
or original is None
|
|
202
|
+
or not isinstance(self.provenance.binding(root.id, original), ast.ClassDef | ast.alias)
|
|
203
|
+
):
|
|
204
|
+
return None
|
|
205
|
+
if root.id in self.provenance.mutated:
|
|
206
|
+
return None
|
|
207
|
+
if isinstance(expression, ast.Name):
|
|
208
|
+
declaration = _declared_class(self.context.tree, expression.id)
|
|
209
|
+
if declaration is not None:
|
|
210
|
+
return declaration, self.context.module_imports
|
|
211
|
+
return self._imported_annotation(expression)
|
|
212
|
+
|
|
213
|
+
def _imported_annotation(self, expression: ast.expr) -> tuple[ast.ClassDef, ImportIndex] | None:
|
|
214
|
+
if self.project is None or self.context.tree is None:
|
|
215
|
+
return None
|
|
216
|
+
unit = self.project.unit_or_source(self.context.path, self.context.source, self.context.tree)
|
|
217
|
+
symbol = ProjectIndexSet.resolve(unit, expression) if unit is not None else None
|
|
218
|
+
owner = self.project.source_unit(symbol.module) if symbol is not None else None
|
|
219
|
+
if symbol is None or owner is None or owner.tree is None or is_generated(owner.path, owner.source):
|
|
220
|
+
return None
|
|
221
|
+
declaration = _declared_class(owner.tree, symbol.name)
|
|
222
|
+
if declaration is None:
|
|
223
|
+
return None
|
|
224
|
+
from sarj_python_lint._file_context import PythonFileContext # ruff: ignore[import-outside-top-level] -- defer to avoid the rule registry's context import cycle.
|
|
225
|
+
|
|
226
|
+
definition = PythonFileContext(owner.path, owner.source, self.context.session)
|
|
227
|
+
provenance = ResourceProvenance(definition)
|
|
228
|
+
if provenance.binding(symbol.name, declaration) is not declaration or symbol.name in provenance.mutated:
|
|
229
|
+
return None
|
|
230
|
+
return declaration, definition.module_imports
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _writes_self_attribute(node: ast.AST, attribute: str) -> bool:
|
|
234
|
+
return (
|
|
235
|
+
isinstance(node, ast.Attribute)
|
|
236
|
+
and node.attr == attribute
|
|
237
|
+
and isinstance(node.value, ast.Name)
|
|
238
|
+
and node.value.id == "self"
|
|
239
|
+
and isinstance(node.ctx, ast.Store | ast.Del)
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _injected_parameter(constructor: Function, value: ast.Name, provenance: ResourceProvenance) -> ast.expr | None:
|
|
244
|
+
parameter = next(
|
|
245
|
+
(
|
|
246
|
+
arg
|
|
247
|
+
for arg in (*constructor.args.posonlyargs, *constructor.args.args, *constructor.args.kwonlyargs)
|
|
248
|
+
if arg.arg == value.id
|
|
249
|
+
),
|
|
250
|
+
None,
|
|
251
|
+
)
|
|
252
|
+
if parameter is None or provenance.binding(parameter.arg, value) is not parameter:
|
|
253
|
+
return None
|
|
254
|
+
return parameter.annotation
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _declared_class(tree: ast.Module, name: str) -> ast.ClassDef | None:
|
|
258
|
+
candidates = [node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == name]
|
|
259
|
+
return candidates[0] if len(candidates) == 1 and not candidates[0].decorator_list else None
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def sql_query(node: ast.expr, imports: ImportIndex) -> exp.Expr | None:
|
|
263
|
+
if (
|
|
264
|
+
isinstance(node, ast.Call)
|
|
265
|
+
and imports.resolved_qualified_name(node.func) == "psycopg.sql.SQL"
|
|
266
|
+
and len(node.args) == 1
|
|
267
|
+
and not node.keywords
|
|
268
|
+
):
|
|
269
|
+
node = node.args[0]
|
|
270
|
+
if not isinstance(node, ast.Constant) or not isinstance(node.value, str):
|
|
271
|
+
return None
|
|
272
|
+
try:
|
|
273
|
+
statements = sqlglot.parse(node.value, read="postgres")
|
|
274
|
+
except SqlglotError:
|
|
275
|
+
return None
|
|
276
|
+
return statements[0] if len(statements) == 1 else None
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def executed_queries(method: Function, imports: ImportIndex) -> list[exp.Expr]:
|
|
280
|
+
queries: list[exp.Expr] = []
|
|
281
|
+
calls = [
|
|
282
|
+
node
|
|
283
|
+
for statement in method.body
|
|
284
|
+
if not isinstance(statement, ast.FunctionDef | ast.AsyncFunctionDef | ast.ClassDef)
|
|
285
|
+
for node in own_nodes(statement)
|
|
286
|
+
if isinstance(node, ast.Call)
|
|
287
|
+
and isinstance(node.func, ast.Attribute)
|
|
288
|
+
and node.func.attr in {"execute", "executemany"}
|
|
289
|
+
]
|
|
290
|
+
for call in calls:
|
|
291
|
+
if not call.args or (query := sql_query(call.args[0], imports)) is None:
|
|
292
|
+
return []
|
|
293
|
+
queries.append(query)
|
|
294
|
+
return queries
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def sql_arg(node: exp.Expr, key: str) -> exp.Expr | None:
|
|
298
|
+
return next((child for child in node.iter_expressions() if child.arg_key == key), None)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def sql_args(node: exp.Expr, key: str) -> tuple[exp.Expr, ...]:
|
|
302
|
+
return tuple(child for child in node.iter_expressions() if child.arg_key == key)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def sql_identifier(node: exp.Expr | None) -> str | None:
|
|
306
|
+
if isinstance(node, exp.Column):
|
|
307
|
+
node = sql_arg(node, "this")
|
|
308
|
+
if not isinstance(node, exp.Identifier):
|
|
309
|
+
return None
|
|
310
|
+
return node.name if node.quoted else node.name.lower()
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def sql_table(node: exp.Expr | None) -> tuple[str | None, ...] | None:
|
|
314
|
+
if not isinstance(node, exp.Table) or sql_arg(node, "alias") is not None:
|
|
315
|
+
return None
|
|
316
|
+
return tuple(
|
|
317
|
+
sql_identifier(value) if (value := sql_arg(node, key)) is not None else None
|
|
318
|
+
for key in ("catalog", "db", "this")
|
|
319
|
+
)
|