sarj-python-lint 0.82.5__tar.gz → 0.82.6__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 (132) hide show
  1. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/PKG-INFO +1 -1
  2. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/pyproject.toml +1 -1
  3. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/_ratchet_cli.py +6 -1
  4. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/ratchet.py +40 -8
  5. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/.gitignore +0 -0
  6. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/LICENSE +0 -0
  7. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/README.md +0 -0
  8. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/__init__.py +0 -0
  9. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/__main__.py +0 -0
  10. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/_analysis_session.py +0 -0
  11. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/_filesystem.py +0 -0
  12. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/_python_target.py +0 -0
  13. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/_secret_names.py +0 -0
  14. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/_version.py +0 -0
  15. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/py.typed +0 -0
  16. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rule_base.py +0 -0
  17. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/__init__.py +0 -0
  18. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  19. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_comments.py +0 -0
  20. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  21. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  22. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_first_party.py +0 -0
  23. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  24. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_imports.py +0 -0
  25. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_logging.py +0 -0
  26. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_paths.py +0 -0
  27. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_project_index.py +0 -0
  28. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  29. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_pytest.py +0 -0
  30. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_registry.py +0 -0
  31. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_sql.py +0 -0
  32. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  33. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  34. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +0 -0
  35. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  36. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  37. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  38. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  39. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  40. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  41. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  42. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  43. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  44. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  45. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  46. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  47. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  48. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  49. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  50. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_deleted_only_override_parameter.py +0 -0
  51. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  52. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  53. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  54. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  55. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  56. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  57. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  58. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  59. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  60. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  61. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  62. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  63. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +0 -0
  64. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  65. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  66. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  67. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  68. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  69. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  70. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  71. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  72. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  73. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  74. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  75. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  76. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  77. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  78. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  79. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  80. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  81. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  82. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  83. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  84. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  85. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  86. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  87. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  88. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  89. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  90. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  91. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  92. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  93. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +0 -0
  94. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  95. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  96. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  97. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  98. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  99. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  100. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  101. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  102. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  103. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  104. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  105. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  106. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  107. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  108. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  109. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  110. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  111. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/pydantic_at_boundaries.py +0 -0
  112. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  113. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  114. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  115. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  116. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  117. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  118. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  119. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  120. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  121. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  122. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  123. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  124. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/stepdown.py +0 -0
  125. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  126. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  127. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  128. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  129. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  130. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  131. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  132. {sarj_python_lint-0.82.5 → sarj_python_lint-0.82.6}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 0.82.5
3
+ Version: 0.82.6
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.82.5"
3
+ version = "0.82.6"
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" }]
@@ -63,6 +63,7 @@ def _run(args: _Options) -> int:
63
63
  packages=baseline.packages,
64
64
  per_file_ceiling=args.per_file_ceiling,
65
65
  file_exceptions=baseline.file_exceptions,
66
+ file_selector_ceilings=baseline.file_selector_ceilings,
66
67
  excluded_subtrees=baseline.excluded_subtrees,
67
68
  )
68
69
 
@@ -85,6 +86,7 @@ def _run(args: _Options) -> int:
85
86
  packages=baseline.packages,
86
87
  per_file_ceiling=baseline.per_file_ceiling,
87
88
  file_exceptions=baseline.file_exceptions,
89
+ file_selector_ceilings=baseline.file_selector_ceilings,
88
90
  excluded_subtrees=excluded_subtrees,
89
91
  )
90
92
  try:
@@ -182,7 +184,10 @@ class _Result:
182
184
 
183
185
 
184
186
  app = typer.Typer(
185
- help="Ratchet on lint/type suppressions: per-code, per-package and per-file ceilings that may only shrink.",
187
+ help=(
188
+ "Ratchet on lint/type suppressions: per-selector, per-package, per-file, and per-file-selector ceilings that "
189
+ "may only shrink."
190
+ ),
186
191
  add_completion=False,
187
192
  no_args_is_help=False,
188
193
  pretty_exceptions_enable=False,
@@ -69,6 +69,7 @@ class Measurement:
69
69
  codes: Counter[str]
70
70
  packages: Counter[str]
71
71
  files: dict[str, int]
72
+ file_selectors: dict[str, Counter[str]]
72
73
 
73
74
  @property
74
75
  def total(self) -> int:
@@ -82,6 +83,7 @@ class Baseline:
82
83
  packages: dict[str, int] = field(default_factory=dict[str, int])
83
84
  per_file_ceiling: int = DEFAULT_PER_FILE_CEILING
84
85
  file_exceptions: dict[str, int] = field(default_factory=dict[str, int])
86
+ file_selector_ceilings: dict[str, dict[str, int]] | None = None
85
87
  excluded_subtrees: tuple[str, ...] = ()
86
88
 
87
89
 
@@ -113,6 +115,10 @@ _REMEDIATION: Final[Mapping[str, str]] = MappingProxyType(
113
115
  "burn one down here, or grandfather the file explicitly in the baseline's "
114
116
  "`files.exceptions`."
115
117
  ),
118
+ "file-selector": (
119
+ "A suppression for this selector is new to this file. Fix the finding instead of moving suppression debt "
120
+ "between files or selectors, or get the exact ceiling raise reviewed explicitly (`--update --allow-increase`)."
121
+ ),
116
122
  }
117
123
  )
118
124
 
@@ -128,6 +134,7 @@ def measure(
128
134
  codes: Counter[str] = Counter()
129
135
  package_counts: Counter[str] = Counter()
130
136
  files: dict[str, int] = {}
137
+ file_selectors: dict[str, Counter[str]] = {}
131
138
  excluded = tuple(_normalized_subtree(value) for value in excluded_subtrees)
132
139
  for package in packages:
133
140
  package_counts[package] = 0
@@ -142,7 +149,8 @@ def measure(
142
149
  n = sum(found.values())
143
150
  package_counts[package] += n
144
151
  files[relative] = n
145
- return Measurement(codes=codes, packages=package_counts, files=files)
152
+ file_selectors[relative] = found
153
+ return Measurement(codes=codes, packages=package_counts, files=files, file_selectors=file_selectors)
146
154
 
147
155
 
148
156
  def count_source(source: str, *, ruff_aliases: Mapping[str, str] | None = None) -> Counter[str]:
@@ -173,6 +181,13 @@ def gate(measurement: Measurement, baseline: Baseline) -> list[Failure]:
173
181
  for key, n in sorted(measurement.files.items())
174
182
  if n > (ceiling := baseline.file_exceptions.get(key, baseline.per_file_ceiling))
175
183
  ]
184
+ if baseline.file_selector_ceilings is not None:
185
+ failures += [
186
+ Failure(dimension="file-selector", key=f"{path} [{code}]", ceiling=ceiling, actual=actual)
187
+ for path, codes in sorted(measurement.file_selectors.items())
188
+ for code, actual in sorted(codes.items())
189
+ if actual > (ceiling := baseline.file_selector_ceilings.get(path, {}).get(code, 0))
190
+ ]
176
191
  return failures
177
192
 
178
193
 
@@ -193,6 +208,9 @@ def seed(measurement: Measurement, baseline: Baseline) -> Baseline:
193
208
  packages=dict(sorted(measurement.packages.items())),
194
209
  per_file_ceiling=baseline.per_file_ceiling,
195
210
  file_exceptions=dict(sorted(exceptions.items())),
211
+ file_selector_ceilings={
212
+ path: dict(sorted(codes.items())) for path, codes in sorted(measurement.file_selectors.items())
213
+ },
196
214
  excluded_subtrees=baseline.excluded_subtrees,
197
215
  )
198
216
 
@@ -214,6 +232,7 @@ def load_baseline(path: Path) -> Baseline:
214
232
  if isinstance(ceiling, int) and not isinstance(ceiling, bool)
215
233
  else DEFAULT_PER_FILE_CEILING,
216
234
  file_exceptions=_int_map(_get(files, "exceptions")),
235
+ file_selector_ceilings=_optional_nested_int_map(_get(files, "selectors")),
217
236
  excluded_subtrees=_string_tuple(_get(raw, "excluded_subtrees")),
218
237
  )
219
238
 
@@ -231,7 +250,8 @@ def dump_baseline(baseline: Baseline, packages: Iterable[str]) -> str:
231
250
  "Suppression ceilings, written by `sarj-ratchet --update`. Counts may "
232
251
  "only go DOWN: `codes` is per dialect+code, `packages` is per "
233
252
  "top-level package, `files.per_file_ceiling` caps any single file "
234
- "with `files.exceptions` grandfathering the pre-existing hot spots. "
253
+ "with `files.exceptions` grandfathering the pre-existing hot spots, and "
254
+ "`files.selectors` prevents debt from moving between files or lint rules. "
235
255
  "Raising a ceiling requires `--update --allow-increase` and review."
236
256
  ),
237
257
  "packages_scanned": sorted(packages),
@@ -241,6 +261,7 @@ def dump_baseline(baseline: Baseline, packages: Iterable[str]) -> str:
241
261
  "files": {
242
262
  "per_file_ceiling": baseline.per_file_ceiling,
243
263
  "exceptions": baseline.file_exceptions,
264
+ "selectors": baseline.file_selector_ceilings or {},
244
265
  },
245
266
  }
246
267
  return json.dumps(payload, indent=2) + "\n"
@@ -258,13 +279,17 @@ def discover_packages(root: Path, excluded_dir_names: frozenset[str] = DEFAULT_E
258
279
 
259
280
 
260
281
  def _int_map(value: object) -> dict[str, int]:
261
- if not isinstance(value, dict):
282
+ if not _is_string_object_mapping(value):
262
283
  return {}
263
- return {
264
- key: count
265
- for key, count in value.items() # pyright: ignore[reportUnknownVariableType] — json leaves are Any; narrowed in the guard
266
- if isinstance(key, str) and isinstance(count, int) and not isinstance(count, bool)
267
- }
284
+ return {key: count for key, count in value.items() if isinstance(count, int) and not isinstance(count, bool)}
285
+
286
+
287
+ def _optional_nested_int_map(value: object) -> dict[str, dict[str, int]] | None:
288
+ if value is None:
289
+ return None
290
+ if not _is_string_object_mapping(value):
291
+ return {}
292
+ return {path: counts for path, raw_counts in value.items() if (counts := _int_map(raw_counts))}
268
293
 
269
294
 
270
295
  def _string_tuple(value: object) -> tuple[str, ...]:
@@ -282,6 +307,13 @@ def _is_object_list(value: object) -> TypeGuard[list[object]]:
282
307
  return isinstance(value, list)
283
308
 
284
309
 
310
+ def _is_string_object_mapping(value: object) -> TypeGuard[dict[str, object]]:
311
+ return isinstance(value, dict) and all(
312
+ isinstance(key, str)
313
+ for key in value # pyright: ignore[reportUnknownVariableType] — runtime validation narrows untyped JSON keys.
314
+ )
315
+
316
+
285
317
  def _normalized_subtree(value: str) -> str:
286
318
  return value.strip("/")
287
319