sarj-python-lint 0.43.2__tar.gz → 0.45.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-0.43.2 → sarj_python_lint-0.45.0}/PKG-INFO +44 -2
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/README.md +43 -1
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/pyproject.toml +1 -1
- sarj_python_lint-0.45.0/src/sarj_python_lint/rules/_fastapi.py +398 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/_registry.py +6 -0
- sarj_python_lint-0.45.0/src/sarj_python_lint/rules/fastapi_openapi_contract.py +557 -0
- sarj_python_lint-0.45.0/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +701 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +8 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/.gitignore +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/conditional_assertion_in_test.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/duplicate_test_body.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/duplicated_override_docstring.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/inefficient_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/interaction_only_test.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/kwonly_same_type_params.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/no_fat_try_blocks.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/no_file_level_suppression.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/no_fstring_in_log.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/no_long_comment.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/no_raw_sql_in_tests.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/no_sequential_await.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/no_sleep_in_test_body.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/no_unreachable_after_terminal.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/prefer_real_store_in_tests.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/prefer_single_sentence_comment.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/single_public_export.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/sleep_with_computed_arg_in_test.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/tautological_mock_assertion.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/test_loops_over_literal_cases.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/xfail_requires_strict.py +0 -0
- {sarj_python_lint-0.43.2 → sarj_python_lint-0.45.0}/src/sarj_python_lint/rules/zero_assertion_test.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.45.0
|
|
4
4
|
Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
|
|
5
5
|
Project-URL: Homepage, https://github.com/sarj-ai/standards/tree/main/packages/python
|
|
6
6
|
Project-URL: Repository, https://github.com/sarj-ai/standards
|
|
@@ -28,13 +28,15 @@ uv tool install sarj-python-lint
|
|
|
28
28
|
|
|
29
29
|
```yaml
|
|
30
30
|
- repo: https://github.com/sarj-ai/standards
|
|
31
|
-
rev: python-v0.
|
|
31
|
+
rev: python-v0.45.0
|
|
32
32
|
hooks:
|
|
33
33
|
- id: sarj-no-sequential-await
|
|
34
34
|
- id: sarj-inefficient-string-concat-in-loop
|
|
35
35
|
- id: sarj-prefer-str-enum
|
|
36
36
|
- id: sarj-no-fat-try-blocks
|
|
37
37
|
- id: sarj-pydantic-at-boundaries
|
|
38
|
+
- id: sarj-fastapi-openapi-contract # SARJ094
|
|
39
|
+
- id: sarj-no-hidden-constructor-fallback # SARJ095 (warning)
|
|
38
40
|
- id: sarj-prefer-class-row
|
|
39
41
|
- id: sarj-prefer-timedelta-for-durations
|
|
40
42
|
- id: sarj-prefer-struct-over-namedtuple
|
|
@@ -43,6 +45,46 @@ uv tool install sarj-python-lint
|
|
|
43
45
|
- id: sarj-prefer-non-nullable-collection # SARJ074
|
|
44
46
|
```
|
|
45
47
|
|
|
48
|
+
### FastAPI OpenAPI contracts (0.44.0)
|
|
49
|
+
|
|
50
|
+
`SARJ094` complements Ruff's `ANN*` and `FAST001`-`FAST003` checks. It requires
|
|
51
|
+
schema-visible operations to declare their summary, description and status;
|
|
52
|
+
uses described `Annotated` request markers; rejects schema-erasing response
|
|
53
|
+
shapes and response projections; requires explicit content schemas for direct
|
|
54
|
+
response objects; and keeps direct errors, custom responses, bodyless statuses,
|
|
55
|
+
GET/HEAD inputs and local route ordering honest in OpenAPI. Missing Python
|
|
56
|
+
annotations remain owned by Ruff's `ANN*` rules, so enable both policies.
|
|
57
|
+
|
|
58
|
+
The rule resolves module-level FastAPI imports and locally constructed or
|
|
59
|
+
aliased routers without guessing from names. Dynamic decorator mappings,
|
|
60
|
+
function-local framework imports, imported router instances and the assembled
|
|
61
|
+
`app.openapi()` document remain application-level integration-test concerns.
|
|
62
|
+
Existing projects can adopt the default-enabled rule with `--update-baseline`
|
|
63
|
+
and then shrink that baseline as endpoint contracts are repaired.
|
|
64
|
+
|
|
65
|
+
### Hidden constructor settings fallback (0.45.0)
|
|
66
|
+
|
|
67
|
+
`SARJ095` warns when a keyword-only constructor parameter defaults to `None`
|
|
68
|
+
and the constructor silently replaces it with a proven pydantic-settings value.
|
|
69
|
+
The effective dependency is invisible at the call site, and `value or
|
|
70
|
+
settings.VALUE` also treats an explicit falsey value as omitted. Make the
|
|
71
|
+
argument required and resolve the setting at the application composition root;
|
|
72
|
+
the annotation may remain nullable when `None` is still a valid explicit value.
|
|
73
|
+
|
|
74
|
+
The rule resolves same-module settings objects, imports, aliases and re-exports
|
|
75
|
+
back to an instance of a `pydantic_settings.BaseSettings` subclass. Literal and
|
|
76
|
+
enum defaults, mutable-container initialization, arbitrary factories and
|
|
77
|
+
clients, module constants, environment-variable APIs, other parameters and
|
|
78
|
+
instance state are deliberately outside v1. No autofix is offered because
|
|
79
|
+
changing constructor optionality requires coordinated call-site edits.
|
|
80
|
+
|
|
81
|
+
Measured over 4,638 tracked Python files in 33 first-party repositories: three
|
|
82
|
+
constructor warnings across two repositories, all three actionable. A pinned
|
|
83
|
+
15-repository OSS sweep covered 29,203 files and produced zero reports, which is
|
|
84
|
+
compatibility evidence rather than a precision claim. An environment-variable
|
|
85
|
+
arm was rejected before shipping: it reported a public first-party library and
|
|
86
|
+
two intentional LiteLLM integration constructors, all non-actionable.
|
|
87
|
+
|
|
46
88
|
### Test-quality rules (0.15.0)
|
|
47
89
|
|
|
48
90
|
Mined from an AST audit of ~7,500 test functions across two production repos.
|
|
@@ -10,13 +10,15 @@ uv tool install sarj-python-lint
|
|
|
10
10
|
|
|
11
11
|
```yaml
|
|
12
12
|
- repo: https://github.com/sarj-ai/standards
|
|
13
|
-
rev: python-v0.
|
|
13
|
+
rev: python-v0.45.0
|
|
14
14
|
hooks:
|
|
15
15
|
- id: sarj-no-sequential-await
|
|
16
16
|
- id: sarj-inefficient-string-concat-in-loop
|
|
17
17
|
- id: sarj-prefer-str-enum
|
|
18
18
|
- id: sarj-no-fat-try-blocks
|
|
19
19
|
- id: sarj-pydantic-at-boundaries
|
|
20
|
+
- id: sarj-fastapi-openapi-contract # SARJ094
|
|
21
|
+
- id: sarj-no-hidden-constructor-fallback # SARJ095 (warning)
|
|
20
22
|
- id: sarj-prefer-class-row
|
|
21
23
|
- id: sarj-prefer-timedelta-for-durations
|
|
22
24
|
- id: sarj-prefer-struct-over-namedtuple
|
|
@@ -25,6 +27,46 @@ uv tool install sarj-python-lint
|
|
|
25
27
|
- id: sarj-prefer-non-nullable-collection # SARJ074
|
|
26
28
|
```
|
|
27
29
|
|
|
30
|
+
### FastAPI OpenAPI contracts (0.44.0)
|
|
31
|
+
|
|
32
|
+
`SARJ094` complements Ruff's `ANN*` and `FAST001`-`FAST003` checks. It requires
|
|
33
|
+
schema-visible operations to declare their summary, description and status;
|
|
34
|
+
uses described `Annotated` request markers; rejects schema-erasing response
|
|
35
|
+
shapes and response projections; requires explicit content schemas for direct
|
|
36
|
+
response objects; and keeps direct errors, custom responses, bodyless statuses,
|
|
37
|
+
GET/HEAD inputs and local route ordering honest in OpenAPI. Missing Python
|
|
38
|
+
annotations remain owned by Ruff's `ANN*` rules, so enable both policies.
|
|
39
|
+
|
|
40
|
+
The rule resolves module-level FastAPI imports and locally constructed or
|
|
41
|
+
aliased routers without guessing from names. Dynamic decorator mappings,
|
|
42
|
+
function-local framework imports, imported router instances and the assembled
|
|
43
|
+
`app.openapi()` document remain application-level integration-test concerns.
|
|
44
|
+
Existing projects can adopt the default-enabled rule with `--update-baseline`
|
|
45
|
+
and then shrink that baseline as endpoint contracts are repaired.
|
|
46
|
+
|
|
47
|
+
### Hidden constructor settings fallback (0.45.0)
|
|
48
|
+
|
|
49
|
+
`SARJ095` warns when a keyword-only constructor parameter defaults to `None`
|
|
50
|
+
and the constructor silently replaces it with a proven pydantic-settings value.
|
|
51
|
+
The effective dependency is invisible at the call site, and `value or
|
|
52
|
+
settings.VALUE` also treats an explicit falsey value as omitted. Make the
|
|
53
|
+
argument required and resolve the setting at the application composition root;
|
|
54
|
+
the annotation may remain nullable when `None` is still a valid explicit value.
|
|
55
|
+
|
|
56
|
+
The rule resolves same-module settings objects, imports, aliases and re-exports
|
|
57
|
+
back to an instance of a `pydantic_settings.BaseSettings` subclass. Literal and
|
|
58
|
+
enum defaults, mutable-container initialization, arbitrary factories and
|
|
59
|
+
clients, module constants, environment-variable APIs, other parameters and
|
|
60
|
+
instance state are deliberately outside v1. No autofix is offered because
|
|
61
|
+
changing constructor optionality requires coordinated call-site edits.
|
|
62
|
+
|
|
63
|
+
Measured over 4,638 tracked Python files in 33 first-party repositories: three
|
|
64
|
+
constructor warnings across two repositories, all three actionable. A pinned
|
|
65
|
+
15-repository OSS sweep covered 29,203 files and produced zero reports, which is
|
|
66
|
+
compatibility evidence rather than a precision claim. An environment-variable
|
|
67
|
+
arm was rejected before shipping: it reported a public first-party library and
|
|
68
|
+
two intentional LiteLLM integration constructors, all non-actionable.
|
|
69
|
+
|
|
28
70
|
### Test-quality rules (0.15.0)
|
|
29
71
|
|
|
30
72
|
Mined from an AST audit of ~7,500 test functions across two production repos.
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"""Deterministic FastAPI syntax resolution shared by route-aware rules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
from collections import Counter
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
HTTP_METHODS = frozenset({"get", "post", "put", "patch", "delete", "options", "head", "trace", "api_route"})
|
|
11
|
+
PARAM_MARKERS = frozenset({"Path", "Query", "Header", "Cookie", "Body", "Form", "File", "Depends", "Security"})
|
|
12
|
+
SCHEMA_MARKERS = PARAM_MARKERS - {"Depends", "Security"}
|
|
13
|
+
INJECTION_TYPES = frozenset({"Request", "Response", "WebSocket", "HTTPConnection", "BackgroundTasks", "SecurityScopes"})
|
|
14
|
+
RESPONSE_TYPES = frozenset(
|
|
15
|
+
{
|
|
16
|
+
"Response",
|
|
17
|
+
"JSONResponse",
|
|
18
|
+
"HTMLResponse",
|
|
19
|
+
"PlainTextResponse",
|
|
20
|
+
"RedirectResponse",
|
|
21
|
+
"StreamingResponse",
|
|
22
|
+
"FileResponse",
|
|
23
|
+
"ORJSONResponse",
|
|
24
|
+
"UJSONResponse",
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class Route:
|
|
31
|
+
decorator: ast.Call
|
|
32
|
+
receiver: str
|
|
33
|
+
method: str
|
|
34
|
+
path: str | None
|
|
35
|
+
inherited_hidden: bool = False
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def keywords(self) -> dict[str, ast.expr]:
|
|
39
|
+
return {keyword.arg: keyword.value for keyword in self.decorator.keywords if keyword.arg is not None}
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def has_unpack(self) -> bool:
|
|
43
|
+
return any(keyword.arg is None for keyword in self.decorator.keywords)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def is_hidden(self) -> bool:
|
|
47
|
+
if self.inherited_hidden:
|
|
48
|
+
return True
|
|
49
|
+
value = self.keywords.get("include_in_schema")
|
|
50
|
+
return isinstance(value, ast.Constant) and value.value is False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def flat_name(node: ast.expr) -> str:
|
|
54
|
+
if isinstance(node, ast.Name):
|
|
55
|
+
return node.id
|
|
56
|
+
if isinstance(node, ast.Attribute):
|
|
57
|
+
return node.attr
|
|
58
|
+
return ""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _binding_name(node: ast.expr) -> str:
|
|
62
|
+
if isinstance(node, ast.Name):
|
|
63
|
+
return node.id
|
|
64
|
+
if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name) and node.value.id == "self":
|
|
65
|
+
return f"self.{node.attr}"
|
|
66
|
+
return ""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class FastapiIndex:
|
|
70
|
+
"""Resolve only FastAPI bindings whose provenance is visible in one module."""
|
|
71
|
+
|
|
72
|
+
def __init__(self, tree: ast.Module) -> None:
|
|
73
|
+
self.modules: set[str] = set()
|
|
74
|
+
self.module_symbols: dict[str, str] = {}
|
|
75
|
+
self.http_modules: set[str] = set()
|
|
76
|
+
self.annotation_modules: set[str] = set()
|
|
77
|
+
self.constructors: set[str] = set()
|
|
78
|
+
self.annotated: set[str] = set()
|
|
79
|
+
self.symbols: dict[str, str] = {}
|
|
80
|
+
self.type_aliases: dict[str, ast.expr] = {}
|
|
81
|
+
self.receivers: set[tuple[int, str]] = set()
|
|
82
|
+
self.receiver_origins: dict[tuple[int, str], tuple[int, str]] = {}
|
|
83
|
+
self.hidden_receivers: set[tuple[int, str]] = set()
|
|
84
|
+
self.decorators: dict[tuple[int, str], tuple[str, str]] = {}
|
|
85
|
+
self.bound_names: set[tuple[int, str]] = set()
|
|
86
|
+
self._node_scopes: dict[int, int] = {}
|
|
87
|
+
self._node_classes: dict[int, int | None] = {}
|
|
88
|
+
self._route_scopes: dict[int, int] = {}
|
|
89
|
+
self._scope_parents: dict[int, int | None] = {0: None}
|
|
90
|
+
self._index_scopes(tree)
|
|
91
|
+
self._read_imports(tree)
|
|
92
|
+
self._read_aliases(tree)
|
|
93
|
+
|
|
94
|
+
def _index_scopes(self, tree: ast.Module) -> None:
|
|
95
|
+
def visit(node: ast.AST, scope: int, class_owner: int | None) -> None:
|
|
96
|
+
self._node_scopes[id(node)] = scope
|
|
97
|
+
if isinstance(node, ast.ClassDef):
|
|
98
|
+
class_owner = node.lineno
|
|
99
|
+
self._scope_parents[node.lineno] = scope
|
|
100
|
+
scope = node.lineno
|
|
101
|
+
self._node_classes[id(node)] = class_owner
|
|
102
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
103
|
+
self._route_scopes[id(node)] = scope
|
|
104
|
+
self._scope_parents[node.lineno] = scope
|
|
105
|
+
scope = node.lineno
|
|
106
|
+
for child in ast.iter_child_nodes(node):
|
|
107
|
+
visit(child, scope, class_owner)
|
|
108
|
+
|
|
109
|
+
visit(tree, 0, None)
|
|
110
|
+
|
|
111
|
+
def _read_imports(self, tree: ast.Module) -> None:
|
|
112
|
+
for node in ast.walk(tree):
|
|
113
|
+
if self._node_scopes[id(node)] != 0:
|
|
114
|
+
continue
|
|
115
|
+
if isinstance(node, ast.Import):
|
|
116
|
+
for alias in node.names:
|
|
117
|
+
local = alias.asname or alias.name.split(".")[0]
|
|
118
|
+
if alias.name == "fastapi" or alias.name.startswith(("fastapi.", "starlette.")):
|
|
119
|
+
self.modules.add(local)
|
|
120
|
+
self.module_symbols[local] = alias.name.rsplit(".", 1)[-1]
|
|
121
|
+
if alias.name in {"typing", "typing_extensions"}:
|
|
122
|
+
self.annotation_modules.add(local)
|
|
123
|
+
if alias.name == "http":
|
|
124
|
+
self.http_modules.add(local)
|
|
125
|
+
elif isinstance(node, ast.ImportFrom):
|
|
126
|
+
module = node.module or ""
|
|
127
|
+
if module in {"typing", "typing_extensions"}:
|
|
128
|
+
for alias in node.names:
|
|
129
|
+
if alias.name == "Annotated":
|
|
130
|
+
self.annotated.add(alias.asname or alias.name)
|
|
131
|
+
continue
|
|
132
|
+
if module == "http":
|
|
133
|
+
for alias in node.names:
|
|
134
|
+
if alias.name == "HTTPStatus":
|
|
135
|
+
self.symbols[alias.asname or alias.name] = "HTTPStatus"
|
|
136
|
+
continue
|
|
137
|
+
if module != "fastapi" and not module.startswith(("fastapi.", "starlette.")):
|
|
138
|
+
continue
|
|
139
|
+
for alias in node.names:
|
|
140
|
+
local = alias.asname or alias.name
|
|
141
|
+
self.symbols[local] = alias.name
|
|
142
|
+
if alias.name in {"FastAPI", "APIRouter"}:
|
|
143
|
+
self.constructors.add(local)
|
|
144
|
+
|
|
145
|
+
def _read_aliases(self, tree: ast.Module) -> None:
|
|
146
|
+
assignments: list[tuple[int, str, ast.expr]] = []
|
|
147
|
+
for node in ast.walk(tree):
|
|
148
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
149
|
+
self.bound_names.add((self._node_scopes[id(node)], node.name))
|
|
150
|
+
arguments = [*node.args.posonlyargs, *node.args.args, *node.args.kwonlyargs]
|
|
151
|
+
if node.args.vararg is not None:
|
|
152
|
+
arguments.append(node.args.vararg)
|
|
153
|
+
if node.args.kwarg is not None:
|
|
154
|
+
arguments.append(node.args.kwarg)
|
|
155
|
+
self.bound_names.update((node.lineno, argument.arg) for argument in arguments)
|
|
156
|
+
elif isinstance(node, ast.ClassDef):
|
|
157
|
+
self.bound_names.add((self._node_scopes[id(node)], node.name))
|
|
158
|
+
elif isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
159
|
+
self.bound_names.update(
|
|
160
|
+
(self._node_scopes[id(node)], alias.asname or alias.name.split(".")[0]) for alias in node.names
|
|
161
|
+
)
|
|
162
|
+
elif isinstance(node, (ast.For, ast.AsyncFor)):
|
|
163
|
+
self.bound_names.update((self._node_scopes[id(node)], name) for name in self._target_names(node.target))
|
|
164
|
+
elif isinstance(node, ast.With):
|
|
165
|
+
for item in node.items:
|
|
166
|
+
if item.optional_vars is not None:
|
|
167
|
+
self.bound_names.update(
|
|
168
|
+
(self._node_scopes[id(node)], name) for name in self._target_names(item.optional_vars)
|
|
169
|
+
)
|
|
170
|
+
elif isinstance(node, ast.ExceptHandler) and node.name is not None:
|
|
171
|
+
self.bound_names.add((self._node_scopes[id(node)], node.name))
|
|
172
|
+
elif isinstance(node, ast.NamedExpr):
|
|
173
|
+
self.bound_names.update((self._node_scopes[id(node)], name) for name in self._target_names(node.target))
|
|
174
|
+
if isinstance(node, ast.Assign) and len(node.targets) == 1:
|
|
175
|
+
name = _binding_name(node.targets[0])
|
|
176
|
+
if name:
|
|
177
|
+
assignments.append((self._assignment_scope(node, name), name, node.value))
|
|
178
|
+
elif isinstance(node, ast.AnnAssign) and node.value is not None:
|
|
179
|
+
name = _binding_name(node.target)
|
|
180
|
+
if name:
|
|
181
|
+
assignments.append((self._assignment_scope(node, name), name, node.value))
|
|
182
|
+
elif isinstance(node, ast.TypeAlias):
|
|
183
|
+
self.type_aliases[node.name.id] = node.value
|
|
184
|
+
|
|
185
|
+
changed = True
|
|
186
|
+
counts = Counter((scope, name) for scope, name, _value in assignments)
|
|
187
|
+
self.bound_names.update(counts)
|
|
188
|
+
while changed:
|
|
189
|
+
changed = False
|
|
190
|
+
for scope, name, value in assignments:
|
|
191
|
+
key = (scope, name)
|
|
192
|
+
if counts[key] != 1:
|
|
193
|
+
continue
|
|
194
|
+
if key in self.receivers or key in self.decorators:
|
|
195
|
+
continue
|
|
196
|
+
source = self._receiver_key(scope, _binding_name(value))
|
|
197
|
+
if self._is_constructor_call(value) or source is not None:
|
|
198
|
+
self.receivers.add(key)
|
|
199
|
+
self.receiver_origins[key] = key if source is None else self.receiver_origins[source]
|
|
200
|
+
if self._constructor_is_hidden(value) or (source is not None and source in self.hidden_receivers):
|
|
201
|
+
self.hidden_receivers.add(key)
|
|
202
|
+
changed = True
|
|
203
|
+
continue
|
|
204
|
+
if isinstance(value, ast.Attribute) and value.attr in HTTP_METHODS:
|
|
205
|
+
receiver = _binding_name(value.value)
|
|
206
|
+
if self._receiver_key(scope, receiver) is not None:
|
|
207
|
+
self.decorators[key] = (receiver, value.attr)
|
|
208
|
+
changed = True
|
|
209
|
+
continue
|
|
210
|
+
if self._is_annotated(value):
|
|
211
|
+
self.type_aliases[name] = value
|
|
212
|
+
|
|
213
|
+
def _target_names(self, node: ast.expr) -> tuple[str, ...]:
|
|
214
|
+
if isinstance(node, ast.Name):
|
|
215
|
+
return (node.id,)
|
|
216
|
+
if isinstance(node, (ast.Tuple, ast.List)):
|
|
217
|
+
return tuple(name for item in node.elts for name in self._target_names(item))
|
|
218
|
+
return ()
|
|
219
|
+
|
|
220
|
+
def _assignment_scope(self, node: ast.Assign | ast.AnnAssign, name: str) -> int:
|
|
221
|
+
if name.startswith("self."):
|
|
222
|
+
return self._node_classes[id(node)] or self._node_scopes[id(node)]
|
|
223
|
+
return self._node_scopes[id(node)]
|
|
224
|
+
|
|
225
|
+
def _receiver_key(self, scope: int, name: str) -> tuple[int, str] | None:
|
|
226
|
+
if not name:
|
|
227
|
+
return None
|
|
228
|
+
current: int | None = scope
|
|
229
|
+
while current is not None:
|
|
230
|
+
binding = (current, name)
|
|
231
|
+
if binding in self.receivers:
|
|
232
|
+
return binding
|
|
233
|
+
if binding in self.bound_names:
|
|
234
|
+
return None
|
|
235
|
+
current = self._scope_parents[current]
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
def _decorator(self, scope: int, name: str) -> tuple[str, str] | None:
|
|
239
|
+
current: int | None = scope
|
|
240
|
+
while current is not None:
|
|
241
|
+
binding = (current, name)
|
|
242
|
+
if binding in self.decorators:
|
|
243
|
+
return self.decorators[binding]
|
|
244
|
+
if binding in self.bound_names:
|
|
245
|
+
return None
|
|
246
|
+
current = self._scope_parents[current]
|
|
247
|
+
return None
|
|
248
|
+
|
|
249
|
+
def _constructor_is_hidden(self, node: ast.expr) -> bool:
|
|
250
|
+
if not self._is_constructor_call(node) or not isinstance(node, ast.Call):
|
|
251
|
+
return False
|
|
252
|
+
value = next((keyword.value for keyword in node.keywords if keyword.arg == "include_in_schema"), None)
|
|
253
|
+
return isinstance(value, ast.Constant) and value.value is False
|
|
254
|
+
|
|
255
|
+
def _is_constructor_call(self, node: ast.expr) -> bool:
|
|
256
|
+
if not isinstance(node, ast.Call):
|
|
257
|
+
return False
|
|
258
|
+
if isinstance(node.func, ast.Name):
|
|
259
|
+
return node.func.id in self.constructors
|
|
260
|
+
return (
|
|
261
|
+
isinstance(node.func, ast.Attribute)
|
|
262
|
+
and isinstance(node.func.value, ast.Name)
|
|
263
|
+
and node.func.value.id in self.modules
|
|
264
|
+
and node.func.attr in {"FastAPI", "APIRouter"}
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
def routes(self, function: ast.FunctionDef | ast.AsyncFunctionDef) -> tuple[Route, ...]:
|
|
268
|
+
routes: list[Route] = []
|
|
269
|
+
scope = self._route_scopes.get(id(function), 0)
|
|
270
|
+
for decorator in function.decorator_list:
|
|
271
|
+
if not isinstance(decorator, ast.Call):
|
|
272
|
+
continue
|
|
273
|
+
method = ""
|
|
274
|
+
receiver = ""
|
|
275
|
+
receiver_key = None
|
|
276
|
+
if isinstance(decorator.func, ast.Attribute):
|
|
277
|
+
receiver = _binding_name(decorator.func.value)
|
|
278
|
+
lookup_scope = self._node_classes[id(function)] if receiver.startswith("self.") else scope
|
|
279
|
+
receiver_key = self._receiver_key(lookup_scope or scope, receiver)
|
|
280
|
+
if receiver_key is not None:
|
|
281
|
+
method = decorator.func.attr
|
|
282
|
+
elif isinstance(decorator.func, ast.Name) and (resolved := self._decorator(scope, decorator.func.id)):
|
|
283
|
+
receiver, method = resolved
|
|
284
|
+
receiver_key = self._receiver_key(scope, receiver)
|
|
285
|
+
if method not in HTTP_METHODS or receiver_key is None:
|
|
286
|
+
continue
|
|
287
|
+
path_node = (
|
|
288
|
+
decorator.args[0]
|
|
289
|
+
if decorator.args
|
|
290
|
+
else next((keyword.value for keyword in decorator.keywords if keyword.arg == "path"), None)
|
|
291
|
+
)
|
|
292
|
+
path = path_node.value if isinstance(path_node, ast.Constant) and isinstance(path_node.value, str) else None
|
|
293
|
+
route_methods = self._route_methods(decorator) if method == "api_route" else (method,)
|
|
294
|
+
origin_scope, origin_name = self.receiver_origins[receiver_key]
|
|
295
|
+
routes.extend(
|
|
296
|
+
Route(
|
|
297
|
+
decorator=decorator,
|
|
298
|
+
receiver=f"{origin_scope}:{origin_name}",
|
|
299
|
+
method=route_method,
|
|
300
|
+
path=path,
|
|
301
|
+
inherited_hidden=receiver_key in self.hidden_receivers,
|
|
302
|
+
)
|
|
303
|
+
for route_method in route_methods
|
|
304
|
+
)
|
|
305
|
+
return tuple(routes)
|
|
306
|
+
|
|
307
|
+
@staticmethod
|
|
308
|
+
def _route_methods(decorator: ast.Call) -> tuple[str, ...]:
|
|
309
|
+
value = next((keyword.value for keyword in decorator.keywords if keyword.arg == "methods"), None)
|
|
310
|
+
if value is None:
|
|
311
|
+
return ("get",)
|
|
312
|
+
if not isinstance(value, (ast.List, ast.Tuple, ast.Set)):
|
|
313
|
+
return ("*",)
|
|
314
|
+
methods = {
|
|
315
|
+
item.value.lower()
|
|
316
|
+
for item in value.elts
|
|
317
|
+
if isinstance(item, ast.Constant)
|
|
318
|
+
and isinstance(item.value, str)
|
|
319
|
+
and item.value.lower() in HTTP_METHODS - {"api_route"}
|
|
320
|
+
}
|
|
321
|
+
return tuple(sorted(methods)) or ("*",)
|
|
322
|
+
|
|
323
|
+
def canonical(self, node: ast.expr) -> str:
|
|
324
|
+
if isinstance(node, ast.Name):
|
|
325
|
+
if node.id in self.modules:
|
|
326
|
+
return self.module_symbols[node.id]
|
|
327
|
+
return self.symbols.get(node.id, "")
|
|
328
|
+
if isinstance(node, ast.Attribute) and self._root_name(node) in self.modules:
|
|
329
|
+
return node.attr
|
|
330
|
+
if isinstance(node, ast.Attribute) and self._root_name(node) in self.http_modules:
|
|
331
|
+
return node.attr
|
|
332
|
+
return ""
|
|
333
|
+
|
|
334
|
+
@staticmethod
|
|
335
|
+
def _root_name(node: ast.Attribute) -> str:
|
|
336
|
+
value: ast.expr = node
|
|
337
|
+
while isinstance(value, ast.Attribute):
|
|
338
|
+
value = value.value
|
|
339
|
+
return value.id if isinstance(value, ast.Name) else ""
|
|
340
|
+
|
|
341
|
+
def resolve_annotation(self, node: ast.expr | None) -> ast.expr | None:
|
|
342
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
343
|
+
try:
|
|
344
|
+
node = ast.parse(node.value.strip(), mode="eval").body
|
|
345
|
+
except SyntaxError:
|
|
346
|
+
return None
|
|
347
|
+
seen: set[str] = set()
|
|
348
|
+
while isinstance(node, ast.Name) and node.id in self.type_aliases and node.id not in seen:
|
|
349
|
+
seen.add(node.id)
|
|
350
|
+
node = self.type_aliases[node.id]
|
|
351
|
+
return node
|
|
352
|
+
|
|
353
|
+
def annotated_parts(self, node: ast.expr | None) -> tuple[ast.expr, tuple[ast.expr, ...]] | None:
|
|
354
|
+
resolved = self.resolve_annotation(node)
|
|
355
|
+
if not self._is_annotated(resolved) or not isinstance(resolved, ast.Subscript):
|
|
356
|
+
return None
|
|
357
|
+
elements = resolved.slice.elts if isinstance(resolved.slice, ast.Tuple) else [resolved.slice]
|
|
358
|
+
if not elements:
|
|
359
|
+
return None
|
|
360
|
+
return elements[0], tuple(elements[1:])
|
|
361
|
+
|
|
362
|
+
def _is_annotated(self, node: ast.expr | None) -> bool:
|
|
363
|
+
if not isinstance(node, ast.Subscript):
|
|
364
|
+
return False
|
|
365
|
+
value = node.value
|
|
366
|
+
if isinstance(value, ast.Name):
|
|
367
|
+
return value.id in self.annotated
|
|
368
|
+
return (
|
|
369
|
+
isinstance(value, ast.Attribute)
|
|
370
|
+
and value.attr == "Annotated"
|
|
371
|
+
and self._root_name(value) in self.annotation_modules
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
def marker(self, node: ast.expr) -> tuple[str, ast.Call] | None:
|
|
375
|
+
if not isinstance(node, ast.Call):
|
|
376
|
+
return None
|
|
377
|
+
canonical = self.canonical(node.func)
|
|
378
|
+
if canonical not in PARAM_MARKERS:
|
|
379
|
+
return None
|
|
380
|
+
return canonical, node
|
|
381
|
+
|
|
382
|
+
def is_injection(self, node: ast.expr | None) -> bool:
|
|
383
|
+
resolved = self.resolve_annotation(node)
|
|
384
|
+
return isinstance(resolved, (ast.Name, ast.Attribute)) and self.canonical(resolved) in INJECTION_TYPES
|
|
385
|
+
|
|
386
|
+
def is_response(self, node: ast.expr | None) -> bool:
|
|
387
|
+
resolved = self.resolve_annotation(node)
|
|
388
|
+
return isinstance(resolved, (ast.Name, ast.Attribute)) and self.canonical(resolved) in RESPONSE_TYPES
|
|
389
|
+
|
|
390
|
+
def response_name(self, node: ast.expr | None) -> str:
|
|
391
|
+
resolved = self.resolve_annotation(node)
|
|
392
|
+
if not isinstance(resolved, (ast.Name, ast.Attribute)):
|
|
393
|
+
return ""
|
|
394
|
+
name = self.canonical(resolved)
|
|
395
|
+
return name if name in RESPONSE_TYPES else ""
|
|
396
|
+
|
|
397
|
+
def is_http_exception(self, node: ast.expr) -> bool:
|
|
398
|
+
return self.canonical(node) == "HTTPException"
|
|
@@ -13,6 +13,7 @@ from sarj_python_lint.rules.duplicate_test_body import DuplicateTestBody
|
|
|
13
13
|
from sarj_python_lint.rules.duplicated_override_docstring import (
|
|
14
14
|
DuplicatedOverrideDocstring,
|
|
15
15
|
)
|
|
16
|
+
from sarj_python_lint.rules.fastapi_openapi_contract import FastapiOpenapiContract
|
|
16
17
|
from sarj_python_lint.rules.fixture_returns_bare_tuple import FixtureReturnsBareTuple
|
|
17
18
|
from sarj_python_lint.rules.inefficient_string_concat_in_loop import (
|
|
18
19
|
InefficientStringConcatInLoop,
|
|
@@ -36,6 +37,9 @@ from sarj_python_lint.rules.no_first_party_private_import import (
|
|
|
36
37
|
)
|
|
37
38
|
from sarj_python_lint.rules.no_fstring_in_log import NoFstringInLog
|
|
38
39
|
from sarj_python_lint.rules.no_gen_random_uuid_in_sql import NoGenRandomUuidInSql
|
|
40
|
+
from sarj_python_lint.rules.no_hidden_constructor_fallback import (
|
|
41
|
+
NoHiddenConstructorFallback,
|
|
42
|
+
)
|
|
39
43
|
from sarj_python_lint.rules.no_isinstance_union_chain import NoIsinstanceUnionChain
|
|
40
44
|
from sarj_python_lint.rules.no_long_comment import NoLongComment
|
|
41
45
|
from sarj_python_lint.rules.no_offset_pagination import NoOffsetPagination
|
|
@@ -131,6 +135,7 @@ REGISTRY: dict[str, type[Rule]] = {
|
|
|
131
135
|
NoCorsWildcardWithCredentials.id: NoCorsWildcardWithCredentials,
|
|
132
136
|
NoSleepInTestBody.id: NoSleepInTestBody,
|
|
133
137
|
PydanticAtBoundaries.id: PydanticAtBoundaries,
|
|
138
|
+
FastapiOpenapiContract.id: FastapiOpenapiContract,
|
|
134
139
|
NoSentinelReturnOnExcept.id: NoSentinelReturnOnExcept,
|
|
135
140
|
NoUnreachableAfterTerminal.id: NoUnreachableAfterTerminal,
|
|
136
141
|
PreferConstantTimeSecretCompare.id: PreferConstantTimeSecretCompare,
|
|
@@ -165,6 +170,7 @@ REGISTRY: dict[str, type[Rule]] = {
|
|
|
165
170
|
TrailingValueNarration.id: TrailingValueNarration,
|
|
166
171
|
NoStdlibLogging.id: NoStdlibLogging,
|
|
167
172
|
NoGenRandomUuidInSql.id: NoGenRandomUuidInSql,
|
|
173
|
+
NoHiddenConstructorFallback.id: NoHiddenConstructorFallback,
|
|
168
174
|
NoFileLevelEscapeHatchNoqa.id: NoFileLevelEscapeHatchNoqa,
|
|
169
175
|
NoOptionalTenantPredicate.id: NoOptionalTenantPredicate,
|
|
170
176
|
NoTautologicalExpect.id: NoTautologicalExpect,
|