sarj-python-lint 0.76.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.
Files changed (122) hide show
  1. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_registry.py +2 -0
  4. sarj_python_lint-0.77.0/src/sarj_python_lint/rules/no_fastapi_on_event.py +168 -0
  5. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/.gitignore +0 -0
  6. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/LICENSE +0 -0
  7. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/README.md +0 -0
  8. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/__init__.py +0 -0
  9. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/__main__.py +0 -0
  10. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/_filesystem.py +0 -0
  11. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  12. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/_secret_names.py +0 -0
  13. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/_version.py +0 -0
  14. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/py.typed +0 -0
  15. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/ratchet.py +0 -0
  16. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rule_base.py +0 -0
  17. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  18. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  19. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  20. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  21. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  22. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  23. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  24. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  25. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  26. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  27. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  28. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  29. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  30. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  31. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  32. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/created_at_order_requires_tiebreaker.py +0 -0
  33. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/defect_xfail_requires_strict.py +0 -0
  34. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  35. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  36. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/duplicate_test_body.py +0 -0
  37. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/duplicated_override_docstring.py +0 -0
  38. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/fastapi_openapi_contract.py +0 -0
  39. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py +0 -0
  40. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/get_delegates_to_get_many.py +0 -0
  41. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  42. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  43. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py +0 -0
  44. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  45. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  46. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_aggregation_in_store_query.py +0 -0
  47. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  48. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  49. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  50. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  51. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py +0 -0
  52. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  53. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  54. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py +0 -0
  55. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  56. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  57. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_isinstance_union_chain.py +0 -0
  58. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_long_comment.py +0 -0
  59. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  60. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  61. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_optional_tenant_predicate.py +0 -0
  62. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_query_with_many_joins.py +0 -0
  63. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  64. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  65. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  66. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  67. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  68. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_sentinel_return_on_except.py +0 -0
  69. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_stdlib_logging.py +0 -0
  70. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  71. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  72. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  73. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  74. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  75. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  76. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  77. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  78. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  79. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  80. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  81. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  82. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  83. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  84. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  85. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  86. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  87. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  88. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  89. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  90. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_one_for_required_row.py +0 -0
  91. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  92. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  93. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  94. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  95. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  96. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py +0 -0
  97. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  98. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  99. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  100. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/preserve_declared_nominal_id.py +0 -0
  101. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/preserve_enum_types.py +0 -0
  102. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  103. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
  104. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  105. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  106. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  107. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  108. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  109. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  110. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  111. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  112. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  113. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/require_validated_row_factory.py +0 -0
  114. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  115. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/source_coupled_test.py +0 -0
  116. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/sql_requires_injected_pool_owner.py +0 -0
  117. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  118. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  119. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  120. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/trivially_true_assertion.py +0 -0
  121. {sarj_python_lint-0.76.0 → sarj_python_lint-0.77.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  122. {sarj_python_lint-0.76.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.76.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/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "0.76.0"
3
+ version = "0.77.0"
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" }]
@@ -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
+ )