sarj-python-lint 0.76.0__tar.gz → 0.77.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/PKG-INFO +6 -6
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/README.md +2 -2
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/pyproject.toml +4 -4
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/__main__.py +5 -1
- sarj_python_lint-0.77.1/src/sarj_python_lint/_analysis_session.py +11 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rule_base.py +6 -1
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/_first_party.py +80 -47
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/_paths.py +9 -13
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/_project_index.py +11 -5
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/_registry.py +2 -0
- sarj_python_lint-0.77.1/src/sarj_python_lint/rules/no_fastapi_on_event.py +168 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_first_party_private_import.py +9 -7
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +86 -23
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/.gitignore +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/LICENSE +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/created_at_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/defect_xfail_requires_strict.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/duplicate_test_body.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/duplicated_override_docstring.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/fastapi_openapi_contract.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/get_delegates_to_get_many.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_long_comment.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/prefer_one_for_required_row.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/preserve_declared_nominal_id.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/preserve_enum_types.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/require_validated_row_factory.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/source_coupled_test.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/sql_requires_injected_pool_owner.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.77.1
|
|
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/
|
|
7
|
-
Project-URL: Changelog, https://github.com/sarj-ai/standards/releases
|
|
8
|
-
Project-URL: Repository, https://github.com/sarj-ai/standards
|
|
9
|
-
Project-URL: Issues, https://github.com/sarj-ai/standards/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/sarj-ai/code-standards/releases
|
|
8
|
+
Project-URL: Repository, https://github.com/sarj-ai/code-standards
|
|
9
|
+
Project-URL: Issues, https://github.com/sarj-ai/code-standards/issues
|
|
10
10
|
Author: sarj-ai
|
|
11
11
|
License-Expression: MIT
|
|
12
12
|
License-File: LICENSE
|
|
@@ -19,7 +19,7 @@ Classifier: Topic :: Software Development :: Quality Assurance
|
|
|
19
19
|
Requires-Python: >=3.14
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
|
|
22
|
-
<!-- Generated by `
|
|
22
|
+
<!-- Generated by `code-standards maintain docs sync`; do not edit. -->
|
|
23
23
|
|
|
24
24
|
# sarj-python-lint
|
|
25
25
|
|
|
@@ -33,4 +33,4 @@ uv tool install sarj-python-lint
|
|
|
33
33
|
sarj-python-lint --help
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
[Documentation](https://code-standards.sarj.ai/rules/python/) · [Source](https://github.com/sarj-ai/standards)
|
|
36
|
+
[Documentation](https://code-standards.sarj.ai/rules/python/) · [Source](https://github.com/sarj-ai/code-standards)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- Generated by `
|
|
1
|
+
<!-- Generated by `code-standards maintain docs sync`; do not edit. -->
|
|
2
2
|
|
|
3
3
|
# sarj-python-lint
|
|
4
4
|
|
|
@@ -12,4 +12,4 @@ uv tool install sarj-python-lint
|
|
|
12
12
|
sarj-python-lint --help
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
[Documentation](https://code-standards.sarj.ai/rules/python/) · [Source](https://github.com/sarj-ai/standards)
|
|
15
|
+
[Documentation](https://code-standards.sarj.ai/rules/python/) · [Source](https://github.com/sarj-ai/code-standards)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "sarj-python-lint"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.77.1"
|
|
4
4
|
description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "sarj-ai" }]
|
|
@@ -24,9 +24,9 @@ sarj-ratchet = "sarj_python_lint._ratchet_cli:main"
|
|
|
24
24
|
[project.urls]
|
|
25
25
|
Homepage = "https://code-standards.sarj.ai/rules/python/"
|
|
26
26
|
Documentation = "https://code-standards.sarj.ai/rules/python/"
|
|
27
|
-
Changelog = "https://github.com/sarj-ai/standards/releases"
|
|
28
|
-
Repository = "https://github.com/sarj-ai/standards"
|
|
29
|
-
Issues = "https://github.com/sarj-ai/standards/issues"
|
|
27
|
+
Changelog = "https://github.com/sarj-ai/code-standards/releases"
|
|
28
|
+
Repository = "https://github.com/sarj-ai/code-standards"
|
|
29
|
+
Issues = "https://github.com/sarj-ai/code-standards/issues"
|
|
30
30
|
|
|
31
31
|
[dependency-groups]
|
|
32
32
|
dev = [
|
|
@@ -10,6 +10,7 @@ from types import MappingProxyType
|
|
|
10
10
|
from typing import NamedTuple
|
|
11
11
|
|
|
12
12
|
from sarj_python_lint import __version__
|
|
13
|
+
from sarj_python_lint._analysis_session import AnalysisSession
|
|
13
14
|
from sarj_python_lint._filesystem import atomic_write_text
|
|
14
15
|
from sarj_python_lint.rule_base import Diagnostic, ProjectRule, Severity, is_suppressed
|
|
15
16
|
from sarj_python_lint.rules import REGISTRY
|
|
@@ -77,6 +78,9 @@ def _check(rule_ids: list[str], paths: list[Path]) -> list[Diagnostic]:
|
|
|
77
78
|
sys.stderr.write(f"available: {', '.join(sorted(REGISTRY))}\n")
|
|
78
79
|
raise SystemExit(2)
|
|
79
80
|
rules = [REGISTRY[rid]() for rid in rule_ids]
|
|
81
|
+
session = AnalysisSession()
|
|
82
|
+
for rule in rules:
|
|
83
|
+
rule.prepare_session(session)
|
|
80
84
|
clear_path_caches()
|
|
81
85
|
expanded = _expand_paths(paths)
|
|
82
86
|
loaded: dict[Path, str] = {}
|
|
@@ -87,7 +91,7 @@ def _check(rule_ids: list[str], paths: list[Path]) -> list[Diagnostic]:
|
|
|
87
91
|
continue
|
|
88
92
|
project_rules = [rule for rule in rules if isinstance(rule, ProjectRule)]
|
|
89
93
|
if project_rules:
|
|
90
|
-
indexes = ProjectIndexSet.build(expanded, loaded)
|
|
94
|
+
indexes = ProjectIndexSet.build(expanded, loaded, facts=session.first_party)
|
|
91
95
|
for rule in project_rules:
|
|
92
96
|
rule.prepare(indexes)
|
|
93
97
|
diags: list[Diagnostic] = []
|
|
@@ -12,11 +12,12 @@ from typing import TYPE_CHECKING, ClassVar, Final, Self
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
13
|
from collections.abc import Sequence
|
|
14
14
|
|
|
15
|
+
from sarj_python_lint._analysis_session import AnalysisSession
|
|
15
16
|
from sarj_python_lint.rules._project_index import ProjectIndexSet
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
# Each rule points directly to its executable examples.
|
|
19
|
-
REPO_BLOB: Final = "https://github.com/sarj-ai/standards/blob/main"
|
|
20
|
+
REPO_BLOB: Final = "https://github.com/sarj-ai/code-standards/blob/main"
|
|
20
21
|
TESTS_DIR: Final = "packages/python/tests/rules"
|
|
21
22
|
|
|
22
23
|
|
|
@@ -237,6 +238,10 @@ class Rule(ABC):
|
|
|
237
238
|
code: str
|
|
238
239
|
description: str
|
|
239
240
|
documentation: ClassVar[RuleDocumentation | None] = None
|
|
241
|
+
_analysis_session: AnalysisSession | None = None
|
|
242
|
+
|
|
243
|
+
def prepare_session(self, session: AnalysisSession) -> None:
|
|
244
|
+
self._analysis_session = session
|
|
240
245
|
|
|
241
246
|
@abstractmethod
|
|
242
247
|
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
{sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/_first_party.py
RENAMED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from functools import lru_cache
|
|
6
5
|
import sys
|
|
7
|
-
from typing import TYPE_CHECKING
|
|
6
|
+
from typing import TYPE_CHECKING, final
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
if TYPE_CHECKING:
|
|
@@ -53,22 +52,78 @@ _MAX_DIRS_SCANNED = 3000
|
|
|
53
52
|
_MAX_ANCESTORS = 24
|
|
54
53
|
|
|
55
54
|
|
|
56
|
-
|
|
55
|
+
@final
|
|
56
|
+
class FirstPartyFacts:
|
|
57
|
+
def __init__(self) -> None:
|
|
58
|
+
self._project_roots: dict[Path, Path | None] = {}
|
|
59
|
+
self._roots: dict[Path, frozenset[str]] = {}
|
|
60
|
+
self._packages: dict[Path, tuple[tuple[str, Path], ...]] = {}
|
|
61
|
+
|
|
62
|
+
def project_root(self, path: Path) -> Path | None:
|
|
63
|
+
if path in self._project_roots:
|
|
64
|
+
return self._project_roots[path]
|
|
65
|
+
resolved = _resolved(path)
|
|
66
|
+
if resolved is None:
|
|
67
|
+
self._project_roots[path] = None
|
|
68
|
+
return None
|
|
69
|
+
ancestors = list(resolved.parents)[:_MAX_ANCESTORS]
|
|
70
|
+
for ancestor in ancestors:
|
|
71
|
+
if (ancestor / _GIT_MARKER).exists():
|
|
72
|
+
self._project_roots[path] = ancestor
|
|
73
|
+
return ancestor
|
|
74
|
+
# No VCS boundary: take the OUTERMOST directory of the unbroken run of
|
|
75
|
+
# pyproject.toml ancestors, which is the workspace root in a uv workspace.
|
|
76
|
+
outermost: Path | None = None
|
|
77
|
+
for ancestor in ancestors:
|
|
78
|
+
if not (ancestor / _PROJECT_MARKER).exists():
|
|
79
|
+
if outermost is not None:
|
|
80
|
+
break
|
|
81
|
+
continue
|
|
82
|
+
outermost = ancestor
|
|
83
|
+
self._project_roots[path] = outermost
|
|
84
|
+
return outermost
|
|
85
|
+
|
|
86
|
+
def first_party_roots(self, root: Path) -> frozenset[str]:
|
|
87
|
+
cached = self._roots.get(root)
|
|
88
|
+
if cached is not None:
|
|
89
|
+
return cached
|
|
90
|
+
roots = frozenset(name for name, _ in self.first_party_packages(root))
|
|
91
|
+
self._roots[root] = roots
|
|
92
|
+
return roots
|
|
93
|
+
|
|
94
|
+
def first_party_packages(self, root: Path) -> tuple[tuple[str, Path], ...]:
|
|
95
|
+
cached = self._packages.get(root)
|
|
96
|
+
if cached is not None:
|
|
97
|
+
return cached
|
|
98
|
+
packages = _scan_first_party_packages(root)
|
|
99
|
+
self._packages[root] = packages
|
|
100
|
+
return packages
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _fresh_facts(facts: FirstPartyFacts | None) -> FirstPartyFacts:
|
|
104
|
+
# Compatibility helper calls are independent requests. The CLI supplies one
|
|
105
|
+
# explicit instance so scans are still shared within a full analysis.
|
|
106
|
+
return facts if facts is not None else FirstPartyFacts()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def is_first_party_module(module: str, path: Path, *, facts: FirstPartyFacts | None = None) -> bool:
|
|
110
|
+
facts = _fresh_facts(facts)
|
|
57
111
|
top = module.partition(".")[0]
|
|
58
112
|
if not top or top in sys.stdlib_module_names:
|
|
59
113
|
return False
|
|
60
|
-
root =
|
|
114
|
+
root = facts.project_root(path)
|
|
61
115
|
if root is None:
|
|
62
116
|
# Unresolved ownership biases to third-party to avoid unfixable findings.
|
|
63
117
|
return False
|
|
64
|
-
return top in
|
|
118
|
+
return top in facts.first_party_roots(root)
|
|
65
119
|
|
|
66
120
|
|
|
67
|
-
def own_top_package(path: Path) -> str | None:
|
|
121
|
+
def own_top_package(path: Path, *, facts: FirstPartyFacts | None = None) -> str | None:
|
|
122
|
+
facts = _fresh_facts(facts)
|
|
68
123
|
resolved = _resolved(path)
|
|
69
124
|
if resolved is None:
|
|
70
125
|
return None
|
|
71
|
-
root =
|
|
126
|
+
root = facts.project_root(resolved)
|
|
72
127
|
top: str | None = None
|
|
73
128
|
for ancestor in list(resolved.parents)[:_MAX_ANCESTORS]:
|
|
74
129
|
if (ancestor / "__init__.py").exists():
|
|
@@ -76,30 +131,33 @@ def own_top_package(path: Path) -> str | None:
|
|
|
76
131
|
if ancestor == root:
|
|
77
132
|
break
|
|
78
133
|
if top is None and root is not None:
|
|
79
|
-
roots =
|
|
134
|
+
roots = facts.first_party_roots(root)
|
|
80
135
|
if len(roots) == 1:
|
|
81
136
|
return next(iter(roots))
|
|
82
137
|
return top
|
|
83
138
|
|
|
84
139
|
|
|
85
|
-
def same_distribution(module: str, path: Path) -> bool:
|
|
86
|
-
|
|
140
|
+
def same_distribution(module: str, path: Path, *, facts: FirstPartyFacts | None = None) -> bool:
|
|
141
|
+
facts = _fresh_facts(facts)
|
|
142
|
+
ours = distribution_root(path, facts=facts)
|
|
87
143
|
if ours is None:
|
|
88
144
|
return False
|
|
89
|
-
root =
|
|
145
|
+
root = facts.project_root(path)
|
|
90
146
|
if root is None:
|
|
91
147
|
return False
|
|
92
148
|
top = module.partition(".")[0]
|
|
93
149
|
return any(
|
|
94
|
-
name == top and distribution_root(package_dir) == ours
|
|
150
|
+
name == top and distribution_root(package_dir, facts=facts) == ours
|
|
151
|
+
for name, package_dir in facts.first_party_packages(root)
|
|
95
152
|
)
|
|
96
153
|
|
|
97
154
|
|
|
98
|
-
def distribution_root(path: Path) -> Path | None:
|
|
155
|
+
def distribution_root(path: Path, *, facts: FirstPartyFacts | None = None) -> Path | None:
|
|
156
|
+
facts = _fresh_facts(facts)
|
|
99
157
|
resolved = _resolved(path)
|
|
100
158
|
if resolved is None:
|
|
101
159
|
return None
|
|
102
|
-
root =
|
|
160
|
+
root = facts.project_root(resolved)
|
|
103
161
|
for ancestor in list(resolved.parents)[:_MAX_ANCESTORS]:
|
|
104
162
|
if any((ancestor / marker).exists() for marker in _DISTRIBUTION_MARKERS):
|
|
105
163
|
return ancestor
|
|
@@ -108,19 +166,21 @@ def distribution_root(path: Path) -> Path | None:
|
|
|
108
166
|
return None
|
|
109
167
|
|
|
110
168
|
|
|
111
|
-
def has_first_party_source(module: str, path: Path) -> bool:
|
|
169
|
+
def has_first_party_source(module: str, path: Path, *, facts: FirstPartyFacts | None = None) -> bool:
|
|
170
|
+
facts = _fresh_facts(facts)
|
|
112
171
|
top, _, rest = module.partition(".")
|
|
113
|
-
root =
|
|
172
|
+
root = facts.project_root(path)
|
|
114
173
|
if root is None:
|
|
115
174
|
return False
|
|
116
175
|
segments = rest.split(".") if rest else []
|
|
117
176
|
return any(
|
|
118
|
-
name == top and _declares_module(package_dir, segments)
|
|
177
|
+
name == top and _declares_module(package_dir, segments)
|
|
178
|
+
for name, package_dir in facts.first_party_packages(root)
|
|
119
179
|
)
|
|
120
180
|
|
|
121
181
|
|
|
122
|
-
def project_root(path: Path) -> Path | None:
|
|
123
|
-
return
|
|
182
|
+
def project_root(path: Path, *, facts: FirstPartyFacts | None = None) -> Path | None:
|
|
183
|
+
return _fresh_facts(facts).project_root(path)
|
|
124
184
|
|
|
125
185
|
|
|
126
186
|
def _declares_module(package_dir: Path, segments: list[str]) -> bool:
|
|
@@ -138,34 +198,7 @@ def _resolved(path: Path) -> Path | None:
|
|
|
138
198
|
return None
|
|
139
199
|
|
|
140
200
|
|
|
141
|
-
|
|
142
|
-
def _project_root(path: Path) -> Path | None:
|
|
143
|
-
resolved = _resolved(path)
|
|
144
|
-
if resolved is None:
|
|
145
|
-
return None
|
|
146
|
-
ancestors = list(resolved.parents)[:_MAX_ANCESTORS]
|
|
147
|
-
for ancestor in ancestors:
|
|
148
|
-
if (ancestor / _GIT_MARKER).exists():
|
|
149
|
-
return ancestor
|
|
150
|
-
# No VCS boundary: take the OUTERMOST directory of the unbroken run of
|
|
151
|
-
# pyproject.toml ancestors, which is the workspace root in a uv workspace.
|
|
152
|
-
outermost: Path | None = None
|
|
153
|
-
for ancestor in ancestors:
|
|
154
|
-
if not (ancestor / _PROJECT_MARKER).exists():
|
|
155
|
-
if outermost is not None:
|
|
156
|
-
break
|
|
157
|
-
continue
|
|
158
|
-
outermost = ancestor
|
|
159
|
-
return outermost
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
@lru_cache(maxsize=32)
|
|
163
|
-
def _first_party_roots(root: Path) -> frozenset[str]:
|
|
164
|
-
return frozenset(name for name, _ in _first_party_packages(root))
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
@lru_cache(maxsize=32)
|
|
168
|
-
def _first_party_packages(root: Path) -> tuple[tuple[str, Path], ...]:
|
|
201
|
+
def _scan_first_party_packages(root: Path) -> tuple[tuple[str, Path], ...]:
|
|
169
202
|
found: list[tuple[str, Path]] = []
|
|
170
203
|
queue: list[tuple[Path, int]] = [(root, 0)]
|
|
171
204
|
scanned = 0
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import ast
|
|
4
|
-
from functools import lru_cache
|
|
5
4
|
from io import StringIO
|
|
6
5
|
import re
|
|
7
6
|
import tokenize
|
|
@@ -103,16 +102,6 @@ def _leading_header_bodies(source: str) -> list[str]:
|
|
|
103
102
|
return bodies
|
|
104
103
|
|
|
105
104
|
|
|
106
|
-
@lru_cache(maxsize=256)
|
|
107
|
-
def _is_codegen_root(directory: Path) -> bool:
|
|
108
|
-
return any((directory / name).exists() for name in _CODEGEN_MARKER_NAMES)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
@lru_cache(maxsize=256)
|
|
112
|
-
def _is_repo_root(directory: Path) -> bool:
|
|
113
|
-
return any((directory / name).exists() for name in _REPO_ROOT_MARKERS)
|
|
114
|
-
|
|
115
|
-
|
|
116
105
|
def is_generated_path(path: Path) -> bool:
|
|
117
106
|
if any(part.lower() in _GENERATED_DIR_NAMES for part in path.parts):
|
|
118
107
|
return True
|
|
@@ -128,9 +117,16 @@ def is_generated_path(path: Path) -> bool:
|
|
|
128
117
|
return False
|
|
129
118
|
|
|
130
119
|
|
|
120
|
+
def _is_codegen_root(directory: Path) -> bool:
|
|
121
|
+
return any((directory / name).exists() for name in _CODEGEN_MARKER_NAMES)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _is_repo_root(directory: Path) -> bool:
|
|
125
|
+
return any((directory / name).exists() for name in _REPO_ROOT_MARKERS)
|
|
126
|
+
|
|
127
|
+
|
|
131
128
|
def clear_path_caches() -> None:
|
|
132
|
-
|
|
133
|
-
_is_repo_root.cache_clear()
|
|
129
|
+
"""Compatibility no-op; path topology is intentionally observed fresh."""
|
|
134
130
|
|
|
135
131
|
|
|
136
132
|
def is_generated(path: Path, source: str) -> bool:
|
{sarj_python_lint-0.76.0 → sarj_python_lint-0.77.1}/src/sarj_python_lint/rules/_project_index.py
RENAMED
|
@@ -10,7 +10,7 @@ import re
|
|
|
10
10
|
from types import MappingProxyType
|
|
11
11
|
from typing import TYPE_CHECKING, final
|
|
12
12
|
|
|
13
|
-
from sarj_python_lint.rules._first_party import project_root
|
|
13
|
+
from sarj_python_lint.rules._first_party import FirstPartyFacts, project_root
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
if TYPE_CHECKING:
|
|
@@ -111,8 +111,14 @@ class ProjectIndexSet:
|
|
|
111
111
|
self._nominals = MappingProxyType({key: frozenset(value) for key, value in nominals.items()})
|
|
112
112
|
|
|
113
113
|
@classmethod
|
|
114
|
-
def build(
|
|
115
|
-
|
|
114
|
+
def build(
|
|
115
|
+
cls,
|
|
116
|
+
paths: Sequence[Path],
|
|
117
|
+
loaded: Mapping[Path, str],
|
|
118
|
+
*,
|
|
119
|
+
facts: FirstPartyFacts | None = None,
|
|
120
|
+
) -> Self:
|
|
121
|
+
roots = _project_roots(paths, facts=facts)
|
|
116
122
|
sources: dict[Path, str] = {}
|
|
117
123
|
for path, source in loaded.items():
|
|
118
124
|
try:
|
|
@@ -283,11 +289,11 @@ def _module_name(path: Path, roots: Sequence[Path]) -> str | None:
|
|
|
283
289
|
return ".".join(parts)
|
|
284
290
|
|
|
285
291
|
|
|
286
|
-
def _project_roots(paths: Sequence[Path]) -> tuple[Path, ...]:
|
|
292
|
+
def _project_roots(paths: Sequence[Path], *, facts: FirstPartyFacts | None = None) -> tuple[Path, ...]:
|
|
287
293
|
candidates: set[Path] = set()
|
|
288
294
|
for path in paths:
|
|
289
295
|
try:
|
|
290
|
-
root = project_root(path.resolve())
|
|
296
|
+
root = project_root(path.resolve(), facts=facts)
|
|
291
297
|
except OSError:
|
|
292
298
|
continue
|
|
293
299
|
if root is not None:
|
|
@@ -38,6 +38,7 @@ from sarj_python_lint.rules.no_cors_wildcard_with_credentials import (
|
|
|
38
38
|
NoCorsWildcardWithCredentials,
|
|
39
39
|
)
|
|
40
40
|
from sarj_python_lint.rules.no_duplicate_dunder_all_entry import NoDuplicateDunderAllEntry
|
|
41
|
+
from sarj_python_lint.rules.no_fastapi_on_event import NoFastapiOnEvent
|
|
41
42
|
from sarj_python_lint.rules.no_file_level_escape_hatch_noqa import NoFileLevelEscapeHatchNoqa
|
|
42
43
|
from sarj_python_lint.rules.no_first_party_private_import import (
|
|
43
44
|
NoFirstPartyPrivateImport,
|
|
@@ -205,6 +206,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
205
206
|
NoGenRandomUuidInSql.id: NoGenRandomUuidInSql,
|
|
206
207
|
NoHiddenConstructorFallback.id: NoHiddenConstructorFallback,
|
|
207
208
|
NoFileLevelEscapeHatchNoqa.id: NoFileLevelEscapeHatchNoqa,
|
|
209
|
+
NoFastapiOnEvent.id: NoFastapiOnEvent,
|
|
208
210
|
NoOptionalTenantPredicate.id: NoOptionalTenantPredicate,
|
|
209
211
|
NoTautologicalExpect.id: NoTautologicalExpect,
|
|
210
212
|
PreferLibraryFake.id: PreferLibraryFake,
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
from typing import TYPE_CHECKING, final, override
|
|
6
|
+
|
|
7
|
+
from sarj_python_lint.rule_base import (
|
|
8
|
+
AutofixPolicy,
|
|
9
|
+
Diagnostic,
|
|
10
|
+
ExampleFile,
|
|
11
|
+
ExampleOutcome,
|
|
12
|
+
Rule,
|
|
13
|
+
RuleCategory,
|
|
14
|
+
RuleDocumentation,
|
|
15
|
+
RuleExample,
|
|
16
|
+
parse_or_none,
|
|
17
|
+
)
|
|
18
|
+
from sarj_python_lint.rules._imports import ImportIndex
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_APP_TYPES = frozenset({("fastapi", "FastAPI"), ("starlette.applications", "Starlette")})
|
|
26
|
+
_EVENT_NAMES = frozenset({"startup", "shutdown"})
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@final
|
|
30
|
+
class NoFastapiOnEvent(Rule):
|
|
31
|
+
id = "no-fastapi-on-event"
|
|
32
|
+
code = "SARJ427"
|
|
33
|
+
documentation = RuleDocumentation(
|
|
34
|
+
summary="Deprecated FastAPI or Starlette on_event lifecycle registration.",
|
|
35
|
+
rationale=(
|
|
36
|
+
"The on_event API is deprecated and splits related startup and shutdown state across callbacks; lifespan "
|
|
37
|
+
"keeps acquisition and cleanup in one async context manager and is the supported lifecycle contract."
|
|
38
|
+
),
|
|
39
|
+
remediation="Define an async lifespan context manager and pass it as FastAPI(lifespan=...) or Starlette(lifespan=...).",
|
|
40
|
+
category=RuleCategory.CORRECTNESS,
|
|
41
|
+
autofix=AutofixPolicy.NONE,
|
|
42
|
+
limitations=(
|
|
43
|
+
"Only decorators on a directly constructed FastAPI/Starlette application or a parameter annotated with one are checked.",
|
|
44
|
+
"Factory-returned applications and dynamically selected event names are intentionally not inferred.",
|
|
45
|
+
),
|
|
46
|
+
examples=(
|
|
47
|
+
RuleExample(
|
|
48
|
+
example_id="deprecated-startup-event",
|
|
49
|
+
title="Do not register startup through on_event",
|
|
50
|
+
outcome=ExampleOutcome.MATCH,
|
|
51
|
+
files=(
|
|
52
|
+
ExampleFile.python(
|
|
53
|
+
"app/main.py",
|
|
54
|
+
'from fastapi import FastAPI\napp = FastAPI()\n\n@app.on_event("startup")\nasync def start() -> None:\n pass\n',
|
|
55
|
+
),
|
|
56
|
+
),
|
|
57
|
+
focus_path=PurePosixPath("app/main.py"),
|
|
58
|
+
expected_count=1,
|
|
59
|
+
public=True,
|
|
60
|
+
),
|
|
61
|
+
RuleExample(
|
|
62
|
+
example_id="lifespan-context-manager",
|
|
63
|
+
title="Pair startup and shutdown in lifespan",
|
|
64
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
65
|
+
files=(
|
|
66
|
+
ExampleFile.python(
|
|
67
|
+
"app/main.py",
|
|
68
|
+
"from contextlib import asynccontextmanager\nfrom fastapi import FastAPI\n\n"
|
|
69
|
+
"@asynccontextmanager\nasync def lifespan(app: FastAPI):\n yield\n\napp = FastAPI(lifespan=lifespan)\n",
|
|
70
|
+
),
|
|
71
|
+
),
|
|
72
|
+
focus_path=PurePosixPath("app/main.py"),
|
|
73
|
+
expected_count=0,
|
|
74
|
+
public=True,
|
|
75
|
+
),
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
description = documentation.summary
|
|
79
|
+
|
|
80
|
+
@override
|
|
81
|
+
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
82
|
+
tree = parse_or_none(path, source)
|
|
83
|
+
if tree is None:
|
|
84
|
+
return []
|
|
85
|
+
imports = ImportIndex.from_tree(tree)
|
|
86
|
+
return _scope_diagnostics(path, tree.body, imports)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _scope_diagnostics(
|
|
90
|
+
path: Path,
|
|
91
|
+
statements: list[ast.stmt],
|
|
92
|
+
imports: ImportIndex,
|
|
93
|
+
*,
|
|
94
|
+
extra_app_names: frozenset[str] = frozenset(),
|
|
95
|
+
) -> list[Diagnostic]:
|
|
96
|
+
app_names = _assigned_app_names(statements, imports) | extra_app_names
|
|
97
|
+
findings: list[Diagnostic] = []
|
|
98
|
+
for statement in statements:
|
|
99
|
+
if not isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
100
|
+
continue
|
|
101
|
+
findings.extend(
|
|
102
|
+
Diagnostic(
|
|
103
|
+
path=path,
|
|
104
|
+
line=decorator.lineno,
|
|
105
|
+
col=decorator.col_offset + 1,
|
|
106
|
+
code="SARJ427",
|
|
107
|
+
message=(
|
|
108
|
+
"FastAPI/Starlette on_event is deprecated. Put startup and shutdown work in an async lifespan "
|
|
109
|
+
"context manager and pass it to the application constructor."
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
for decorator in statement.decorator_list
|
|
113
|
+
if _is_on_event_decorator(decorator, app_names)
|
|
114
|
+
)
|
|
115
|
+
findings.extend(
|
|
116
|
+
_scope_diagnostics(
|
|
117
|
+
path,
|
|
118
|
+
statement.body,
|
|
119
|
+
imports,
|
|
120
|
+
extra_app_names=_annotated_app_parameters(statement.args, imports),
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
return findings
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _assigned_app_names(statements: list[ast.stmt], imports: ImportIndex) -> frozenset[str]:
|
|
127
|
+
names: set[str] = set()
|
|
128
|
+
for statement in statements:
|
|
129
|
+
if not isinstance(statement, (ast.Assign, ast.AnnAssign)):
|
|
130
|
+
continue
|
|
131
|
+
value = statement.value
|
|
132
|
+
if not isinstance(value, ast.Call) or not any(
|
|
133
|
+
imports.resolves(value.func, sources=frozenset({source}), symbol=symbol) for source, symbol in _APP_TYPES
|
|
134
|
+
):
|
|
135
|
+
continue
|
|
136
|
+
targets = statement.targets if isinstance(statement, ast.Assign) else [statement.target]
|
|
137
|
+
names.update(target.id for target in targets if isinstance(target, ast.Name))
|
|
138
|
+
return frozenset(names)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _annotated_app_parameters(arguments: ast.arguments, imports: ImportIndex) -> frozenset[str]:
|
|
142
|
+
parameters = [*arguments.posonlyargs, *arguments.args, *arguments.kwonlyargs]
|
|
143
|
+
if arguments.vararg is not None:
|
|
144
|
+
parameters.append(arguments.vararg)
|
|
145
|
+
if arguments.kwarg is not None:
|
|
146
|
+
parameters.append(arguments.kwarg)
|
|
147
|
+
return frozenset(
|
|
148
|
+
parameter.arg
|
|
149
|
+
for parameter in parameters
|
|
150
|
+
if parameter.annotation is not None
|
|
151
|
+
and any(
|
|
152
|
+
imports.resolves(parameter.annotation, sources=frozenset({source}), symbol=symbol)
|
|
153
|
+
for source, symbol in _APP_TYPES
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _is_on_event_decorator(node: ast.expr, app_names: frozenset[str]) -> bool:
|
|
159
|
+
return (
|
|
160
|
+
isinstance(node, ast.Call)
|
|
161
|
+
and isinstance(node.func, ast.Attribute)
|
|
162
|
+
and node.func.attr == "on_event"
|
|
163
|
+
and isinstance(node.func.value, ast.Name)
|
|
164
|
+
and node.func.value.id in app_names
|
|
165
|
+
and len(node.args) == 1
|
|
166
|
+
and isinstance(node.args[0], ast.Constant)
|
|
167
|
+
and node.args[0].value in _EVENT_NAMES
|
|
168
|
+
)
|
|
@@ -17,6 +17,7 @@ from sarj_python_lint.rule_base import (
|
|
|
17
17
|
)
|
|
18
18
|
from sarj_python_lint.rules._ast_index import nodes
|
|
19
19
|
from sarj_python_lint.rules._first_party import (
|
|
20
|
+
FirstPartyFacts,
|
|
20
21
|
has_first_party_source,
|
|
21
22
|
is_first_party_module,
|
|
22
23
|
own_top_package,
|
|
@@ -84,11 +85,12 @@ class NoFirstPartyPrivateImport(Rule):
|
|
|
84
85
|
tree = parse_or_none(path, source)
|
|
85
86
|
if tree is None:
|
|
86
87
|
return []
|
|
87
|
-
|
|
88
|
+
facts = self._analysis_session.first_party if self._analysis_session is not None else FirstPartyFacts()
|
|
89
|
+
own_top = own_top_package(path, facts=facts)
|
|
88
90
|
diags = [
|
|
89
91
|
Diagnostic(path=path, line=hit.line, col=hit.col, code=self.code, message=_message(hit.module, hit.name))
|
|
90
92
|
for hit in _private_imports(tree)
|
|
91
|
-
if _is_ours(hit.module, path, own_top) and not _is_our_own_internals(hit, path)
|
|
93
|
+
if _is_ours(hit.module, path, own_top, facts) and not _is_our_own_internals(hit, path, facts)
|
|
92
94
|
]
|
|
93
95
|
diags.sort(key=lambda d: (d.line, d.col))
|
|
94
96
|
return diags
|
|
@@ -106,12 +108,12 @@ class _PrivateImport:
|
|
|
106
108
|
names_public: bool
|
|
107
109
|
|
|
108
110
|
|
|
109
|
-
def _is_our_own_internals(hit: _PrivateImport, path: Path) -> bool:
|
|
111
|
+
def _is_our_own_internals(hit: _PrivateImport, path: Path, facts: FirstPartyFacts) -> bool:
|
|
110
112
|
if not hit.is_segment:
|
|
111
113
|
return False
|
|
112
|
-
if not has_first_party_source(hit.module, path):
|
|
114
|
+
if not has_first_party_source(hit.module, path, facts=facts):
|
|
113
115
|
return True
|
|
114
|
-
return hit.names_public and same_distribution(hit.module, path)
|
|
116
|
+
return hit.names_public and same_distribution(hit.module, path, facts=facts)
|
|
115
117
|
|
|
116
118
|
|
|
117
119
|
def _message(module: str, name: str) -> str:
|
|
@@ -122,11 +124,11 @@ def _message(module: str, name: str) -> str:
|
|
|
122
124
|
)
|
|
123
125
|
|
|
124
126
|
|
|
125
|
-
def _is_ours(module: str, path: Path, own_top: str | None) -> bool:
|
|
127
|
+
def _is_ours(module: str, path: Path, own_top: str | None, facts: FirstPartyFacts) -> bool:
|
|
126
128
|
top = module.partition(".")[0]
|
|
127
129
|
if own_top is not None and top == own_top:
|
|
128
130
|
return False
|
|
129
|
-
return is_first_party_module(module, path)
|
|
131
|
+
return is_first_party_module(module, path, facts=facts)
|
|
130
132
|
|
|
131
133
|
|
|
132
134
|
def _private_imports(tree: ast.Module) -> list[_PrivateImport]:
|