sarj-python-lint 0.75.0__tar.gz → 0.77.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.75.0 → sarj_python_lint-0.77.0}/PKG-INFO +1 -1
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/pyproject.toml +1 -1
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_registry.py +12 -0
- sarj_python_lint-0.77.0/src/sarj_python_lint/rules/no_conftest_test_module_import.py +105 -0
- sarj_python_lint-0.77.0/src/sarj_python_lint/rules/no_fastapi_on_event.py +168 -0
- sarj_python_lint-0.77.0/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +171 -0
- sarj_python_lint-0.77.0/src/sarj_python_lint/rules/no_redundant_literal_description.py +214 -0
- sarj_python_lint-0.77.0/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +283 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/.gitignore +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/LICENSE +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/README.md +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/created_at_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/defect_xfail_requires_strict.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/duplicate_test_body.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/duplicated_override_docstring.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/fastapi_openapi_contract.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/get_delegates_to_get_many.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_long_comment.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_one_for_required_row.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/preserve_declared_nominal_id.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/preserve_enum_types.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/require_validated_row_factory.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/source_coupled_test.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/sql_requires_injected_pool_owner.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-0.75.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sarj-python-lint
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.77.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/
|
|
@@ -33,10 +33,12 @@ from sarj_python_lint.rules.no_aggregation_in_store_query import (
|
|
|
33
33
|
NoAggregationInStoreQuery,
|
|
34
34
|
)
|
|
35
35
|
from sarj_python_lint.rules.no_comment_cruft import NoCommentCruft
|
|
36
|
+
from sarj_python_lint.rules.no_conftest_test_module_import import NoConftestTestModuleImport
|
|
36
37
|
from sarj_python_lint.rules.no_cors_wildcard_with_credentials import (
|
|
37
38
|
NoCorsWildcardWithCredentials,
|
|
38
39
|
)
|
|
39
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
|
|
40
42
|
from sarj_python_lint.rules.no_file_level_escape_hatch_noqa import NoFileLevelEscapeHatchNoqa
|
|
41
43
|
from sarj_python_lint.rules.no_first_party_private_import import (
|
|
42
44
|
NoFirstPartyPrivateImport,
|
|
@@ -51,11 +53,13 @@ from sarj_python_lint.rules.no_hidden_constructor_fallback import (
|
|
|
51
53
|
)
|
|
52
54
|
from sarj_python_lint.rules.no_isinstance_union_chain import NoIsinstanceUnionChain
|
|
53
55
|
from sarj_python_lint.rules.no_long_comment import NoLongComment
|
|
56
|
+
from sarj_python_lint.rules.no_nested_pydantic_field_validator import NoNestedPydanticFieldValidator
|
|
54
57
|
from sarj_python_lint.rules.no_offset_pagination import NoOffsetPagination
|
|
55
58
|
from sarj_python_lint.rules.no_optional_tenant_predicate import (
|
|
56
59
|
NoOptionalTenantPredicate,
|
|
57
60
|
)
|
|
58
61
|
from sarj_python_lint.rules.no_query_with_many_joins import NoQueryWithManyJoins
|
|
62
|
+
from sarj_python_lint.rules.no_redundant_literal_description import NoRedundantLiteralDescription
|
|
59
63
|
from sarj_python_lint.rules.no_repeated_string_literal import NoRepeatedStringLiteral
|
|
60
64
|
from sarj_python_lint.rules.no_restated_comment import NoRestatedComment
|
|
61
65
|
from sarj_python_lint.rules.no_secret_in_log import NoSecretInLog
|
|
@@ -125,6 +129,9 @@ from sarj_python_lint.rules.repeated_static_call_cases import RepeatedStaticCall
|
|
|
125
129
|
from sarj_python_lint.rules.require_keyword_only_swap_prone_params import (
|
|
126
130
|
RequireKeywordOnlySwapProneParams,
|
|
127
131
|
)
|
|
132
|
+
from sarj_python_lint.rules.require_nodecode_for_splitting_settings_field import (
|
|
133
|
+
RequireNoDecodeForSplittingSettingsField,
|
|
134
|
+
)
|
|
128
135
|
from sarj_python_lint.rules.require_port_for_service import RequirePortForService
|
|
129
136
|
from sarj_python_lint.rules.require_pydantic_for_external_json import (
|
|
130
137
|
RequirePydanticForExternalJson,
|
|
@@ -169,6 +176,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
169
176
|
PreferTimedeltaForDurations.id: PreferTimedeltaForDurations,
|
|
170
177
|
PreferStructOverNamedtuple.id: PreferStructOverNamedtuple,
|
|
171
178
|
NoCommentCruft.id: NoCommentCruft,
|
|
179
|
+
NoConftestTestModuleImport.id: NoConftestTestModuleImport,
|
|
172
180
|
StoreInsertRequiresOnConflict.id: StoreInsertRequiresOnConflict,
|
|
173
181
|
SourceCoupledTest.id: SourceCoupledTest,
|
|
174
182
|
IacSourceCoupledTest.id: IacSourceCoupledTest,
|
|
@@ -198,6 +206,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
198
206
|
NoGenRandomUuidInSql.id: NoGenRandomUuidInSql,
|
|
199
207
|
NoHiddenConstructorFallback.id: NoHiddenConstructorFallback,
|
|
200
208
|
NoFileLevelEscapeHatchNoqa.id: NoFileLevelEscapeHatchNoqa,
|
|
209
|
+
NoFastapiOnEvent.id: NoFastapiOnEvent,
|
|
201
210
|
NoOptionalTenantPredicate.id: NoOptionalTenantPredicate,
|
|
202
211
|
NoTautologicalExpect.id: NoTautologicalExpect,
|
|
203
212
|
PreferLibraryFake.id: PreferLibraryFake,
|
|
@@ -226,6 +235,8 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
226
235
|
RestatedTestDocstring.id: RestatedTestDocstring,
|
|
227
236
|
TestPhaseLabelComment.id: TestPhaseLabelComment,
|
|
228
237
|
NoLongComment.id: NoLongComment,
|
|
238
|
+
NoNestedPydanticFieldValidator.id: NoNestedPydanticFieldValidator,
|
|
239
|
+
NoRedundantLiteralDescription.id: NoRedundantLiteralDescription,
|
|
229
240
|
NoTypedDocSections.id: NoTypedDocSections,
|
|
230
241
|
NoUnnecessaryDocstring.id: NoUnnecessaryDocstring,
|
|
231
242
|
PreferNominalIdTypes.id: PreferNominalIdTypes,
|
|
@@ -239,6 +250,7 @@ REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
|
|
|
239
250
|
PreserveDeclaredNominalId.id: PreserveDeclaredNominalId,
|
|
240
251
|
PreserveEnumTypes.id: PreserveEnumTypes,
|
|
241
252
|
RequirePydanticOrdinalLowerBound.id: RequirePydanticOrdinalLowerBound,
|
|
253
|
+
RequireNoDecodeForSplittingSettingsField.id: RequireNoDecodeForSplittingSettingsField,
|
|
242
254
|
NoVagueSuppressionDescription.id: NoVagueSuppressionDescription,
|
|
243
255
|
}
|
|
244
256
|
)
|
|
@@ -0,0 +1,105 @@
|
|
|
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
|
+
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@final
|
|
25
|
+
class NoConftestTestModuleImport(Rule):
|
|
26
|
+
id = "no-conftest-test-module-import"
|
|
27
|
+
code = "SARJ426"
|
|
28
|
+
documentation = RuleDocumentation(
|
|
29
|
+
summary="Do not import individual test modules from conftest.py.",
|
|
30
|
+
rationale=(
|
|
31
|
+
"pytest imports conftest for every test module in its scope; importing a specific test from conftest "
|
|
32
|
+
"turns that test into global collection infrastructure and creates cycles and order dependence."
|
|
33
|
+
),
|
|
34
|
+
remediation="Move shared fixtures or helpers into conftest.py or a dedicated support module.",
|
|
35
|
+
category=RuleCategory.TESTING,
|
|
36
|
+
autofix=AutofixPolicy.NONE,
|
|
37
|
+
limitations=(
|
|
38
|
+
"Only files named conftest.py are inspected.",
|
|
39
|
+
"A test module is recognized only when an explicit module-path component begins with `test_` or ends with `_test`.",
|
|
40
|
+
),
|
|
41
|
+
examples=(
|
|
42
|
+
RuleExample(
|
|
43
|
+
example_id="conftest-imports-test-module",
|
|
44
|
+
title="Conftest imports a specific test module",
|
|
45
|
+
outcome=ExampleOutcome.MATCH,
|
|
46
|
+
files=(ExampleFile.python("tests/conftest.py", "from tests.test_api import make_client\n"),),
|
|
47
|
+
focus_path=PurePosixPath("tests/conftest.py"),
|
|
48
|
+
expected_count=1,
|
|
49
|
+
public=True,
|
|
50
|
+
),
|
|
51
|
+
RuleExample(
|
|
52
|
+
example_id="conftest-imports-support-module",
|
|
53
|
+
title="Conftest imports dedicated support code",
|
|
54
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
55
|
+
files=(ExampleFile.python("tests/conftest.py", "from tests.helpers import make_client\n"),),
|
|
56
|
+
focus_path=PurePosixPath("tests/conftest.py"),
|
|
57
|
+
expected_count=0,
|
|
58
|
+
public=True,
|
|
59
|
+
),
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
description = documentation.summary
|
|
63
|
+
|
|
64
|
+
@override
|
|
65
|
+
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
66
|
+
if path.name != "conftest.py":
|
|
67
|
+
return []
|
|
68
|
+
tree = parse_or_none(path, source)
|
|
69
|
+
if tree is None:
|
|
70
|
+
return []
|
|
71
|
+
diagnostics: list[Diagnostic] = []
|
|
72
|
+
for statement in tree.body:
|
|
73
|
+
targets: list[str] = []
|
|
74
|
+
match statement:
|
|
75
|
+
case ast.Import(names=names):
|
|
76
|
+
targets.extend(alias.name for alias in names if _module_has_test_leaf(alias.name))
|
|
77
|
+
case ast.ImportFrom(module=module, names=names):
|
|
78
|
+
if module is not None and _module_has_test_leaf(module):
|
|
79
|
+
targets.append(module)
|
|
80
|
+
case _:
|
|
81
|
+
continue
|
|
82
|
+
if not targets:
|
|
83
|
+
continue
|
|
84
|
+
diagnostics.append(
|
|
85
|
+
Diagnostic(
|
|
86
|
+
path=path,
|
|
87
|
+
line=statement.lineno,
|
|
88
|
+
col=statement.col_offset + 1,
|
|
89
|
+
code=self.code,
|
|
90
|
+
message=(
|
|
91
|
+
f"conftest.py imports test module `{targets[0]}`; move shared fixtures or helpers to "
|
|
92
|
+
"conftest or a dedicated support module."
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
return diagnostics
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _module_has_test_leaf(module: str) -> bool:
|
|
100
|
+
parts = module.split(".")
|
|
101
|
+
return any(_is_test_module_name(part) for part in parts)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _is_test_module_name(name: str) -> bool:
|
|
105
|
+
return name.startswith("test_") or name.endswith("_test")
|
|
@@ -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
|
+
)
|
|
@@ -0,0 +1,171 @@
|
|
|
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
|
+
from sarj_python_lint.rules._paths import is_generated, is_test_path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_PYDANTIC_BASE_MODEL_SOURCES = frozenset({"pydantic", "pydantic.main"})
|
|
27
|
+
_PYDANTIC_SOURCES = frozenset({"pydantic", "pydantic.functional_validators"})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@final
|
|
31
|
+
class NoNestedPydanticFieldValidator(Rule):
|
|
32
|
+
id = "no-nested-pydantic-field-validator"
|
|
33
|
+
code = "SARJ425"
|
|
34
|
+
documentation = RuleDocumentation(
|
|
35
|
+
summary="Do not place an outer Pydantic model's field validator inside a nested helper class.",
|
|
36
|
+
rationale=(
|
|
37
|
+
"A decorator indented into a nested class registers on that class, not on the outer Pydantic model, "
|
|
38
|
+
"so the declared field silently loses validation."
|
|
39
|
+
),
|
|
40
|
+
remediation="Move the field-validator method into the outer model class.",
|
|
41
|
+
category=RuleCategory.CORRECTNESS,
|
|
42
|
+
autofix=AutofixPolicy.NONE,
|
|
43
|
+
limitations=(
|
|
44
|
+
"Only direct BaseModel subclasses and validators directly owned by one nested class are inspected.",
|
|
45
|
+
"Nested classes with bases are excluded because inherited Pydantic fields cannot be resolved locally.",
|
|
46
|
+
"Test and generated files are excluded.",
|
|
47
|
+
),
|
|
48
|
+
examples=(
|
|
49
|
+
RuleExample(
|
|
50
|
+
example_id="validator-nested-in-config",
|
|
51
|
+
title="Validator accidentally belongs to Config",
|
|
52
|
+
outcome=ExampleOutcome.MATCH,
|
|
53
|
+
files=(
|
|
54
|
+
ExampleFile.python(
|
|
55
|
+
"app/models.py",
|
|
56
|
+
"from pydantic import BaseModel, field_validator\n\n"
|
|
57
|
+
"class Settings(BaseModel):\n"
|
|
58
|
+
" language: str\n"
|
|
59
|
+
" class Config:\n"
|
|
60
|
+
" extra = 'forbid'\n"
|
|
61
|
+
" @field_validator('language')\n"
|
|
62
|
+
" @classmethod\n"
|
|
63
|
+
" def normalize(cls, value):\n"
|
|
64
|
+
" return value.lower()\n",
|
|
65
|
+
),
|
|
66
|
+
),
|
|
67
|
+
focus_path=PurePosixPath("app/models.py"),
|
|
68
|
+
expected_count=1,
|
|
69
|
+
public=True,
|
|
70
|
+
),
|
|
71
|
+
RuleExample(
|
|
72
|
+
example_id="validator-owned-by-model",
|
|
73
|
+
title="Validator belongs to the model",
|
|
74
|
+
outcome=ExampleOutcome.NO_MATCH,
|
|
75
|
+
files=(
|
|
76
|
+
ExampleFile.python(
|
|
77
|
+
"app/models.py",
|
|
78
|
+
"from pydantic import BaseModel, field_validator\n\n"
|
|
79
|
+
"class Settings(BaseModel):\n"
|
|
80
|
+
" language: str\n"
|
|
81
|
+
" @field_validator('language')\n"
|
|
82
|
+
" @classmethod\n"
|
|
83
|
+
" def normalize(cls, value):\n"
|
|
84
|
+
" return value.lower()\n",
|
|
85
|
+
),
|
|
86
|
+
),
|
|
87
|
+
focus_path=PurePosixPath("app/models.py"),
|
|
88
|
+
expected_count=0,
|
|
89
|
+
public=True,
|
|
90
|
+
),
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
description = documentation.summary
|
|
94
|
+
|
|
95
|
+
@override
|
|
96
|
+
def check(self, path: Path, source: str) -> list[Diagnostic]:
|
|
97
|
+
if is_test_path(path) or is_generated(path, source):
|
|
98
|
+
return []
|
|
99
|
+
tree = parse_or_none(path, source)
|
|
100
|
+
if tree is None:
|
|
101
|
+
return []
|
|
102
|
+
imports = ImportIndex.from_tree(tree)
|
|
103
|
+
diagnostics: list[Diagnostic] = []
|
|
104
|
+
for outer in (node for node in ast.walk(tree) if isinstance(node, ast.ClassDef)):
|
|
105
|
+
if not _is_direct_model(outer, imports):
|
|
106
|
+
continue
|
|
107
|
+
outer_fields = _direct_fields(outer)
|
|
108
|
+
for nested in (statement for statement in outer.body if isinstance(statement, ast.ClassDef)):
|
|
109
|
+
# A based nested class may be an indirect Pydantic model whose inherited
|
|
110
|
+
# fields are unavailable to this file-local analysis. Only base-less
|
|
111
|
+
# Config/helper classes provide a deterministic ownership mistake.
|
|
112
|
+
if nested.bases:
|
|
113
|
+
continue
|
|
114
|
+
nested_fields = _direct_fields(nested)
|
|
115
|
+
for function in (
|
|
116
|
+
statement
|
|
117
|
+
for statement in nested.body
|
|
118
|
+
if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
119
|
+
):
|
|
120
|
+
for decorator, fields in _field_validators(function, imports):
|
|
121
|
+
misplaced = sorted((fields & outer_fields) - nested_fields)
|
|
122
|
+
if not misplaced:
|
|
123
|
+
continue
|
|
124
|
+
diagnostics.append(
|
|
125
|
+
Diagnostic(
|
|
126
|
+
path=path,
|
|
127
|
+
line=decorator.lineno,
|
|
128
|
+
col=decorator.col_offset + 1,
|
|
129
|
+
code=self.code,
|
|
130
|
+
message=(
|
|
131
|
+
f"Validator for outer field(s) {', '.join(f'`{name}`' for name in misplaced)} "
|
|
132
|
+
f"is nested inside `{nested.name}`; move it to `{outer.name}`."
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
return diagnostics
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _is_direct_model(node: ast.ClassDef, imports: ImportIndex) -> bool:
|
|
140
|
+
return len(node.bases) == 1 and imports.resolves(
|
|
141
|
+
node.bases[0], sources=_PYDANTIC_BASE_MODEL_SOURCES, symbol="BaseModel"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _direct_fields(node: ast.ClassDef) -> frozenset[str]:
|
|
146
|
+
return frozenset(
|
|
147
|
+
statement.target.id
|
|
148
|
+
for statement in node.body
|
|
149
|
+
if isinstance(statement, ast.AnnAssign)
|
|
150
|
+
and isinstance(statement.target, ast.Name)
|
|
151
|
+
and not statement.target.id.startswith("_")
|
|
152
|
+
and statement.target.id != "model_config"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _field_validators(
|
|
157
|
+
node: ast.FunctionDef | ast.AsyncFunctionDef, imports: ImportIndex
|
|
158
|
+
) -> tuple[tuple[ast.Call, frozenset[str]], ...]:
|
|
159
|
+
validators: list[tuple[ast.Call, frozenset[str]]] = []
|
|
160
|
+
for decorator in node.decorator_list:
|
|
161
|
+
if not isinstance(decorator, ast.Call) or not imports.resolves(
|
|
162
|
+
decorator.func, sources=_PYDANTIC_SOURCES, symbol="field_validator"
|
|
163
|
+
):
|
|
164
|
+
continue
|
|
165
|
+
fields = frozenset(
|
|
166
|
+
argument.value
|
|
167
|
+
for argument in decorator.args
|
|
168
|
+
if isinstance(argument, ast.Constant) and isinstance(argument.value, str)
|
|
169
|
+
)
|
|
170
|
+
validators.append((decorator, fields))
|
|
171
|
+
return tuple(validators)
|