thailint 0.15.4__tar.gz → 0.15.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 (228) hide show
  1. {thailint-0.15.4 → thailint-0.15.6}/PKG-INFO +1 -1
  2. {thailint-0.15.4 → thailint-0.15.6}/pyproject.toml +1 -1
  3. {thailint-0.15.4 → thailint-0.15.6}/src/core/cli_utils.py +24 -4
  4. {thailint-0.15.4 → thailint-0.15.6}/src/linters/magic_numbers/config.py +38 -7
  5. thailint-0.15.6/src/linters/magic_numbers/definition_detector.py +226 -0
  6. {thailint-0.15.4 → thailint-0.15.6}/src/linters/magic_numbers/linter.py +7 -0
  7. thailint-0.15.6/src/linters/nesting/python_analyzer.py +156 -0
  8. {thailint-0.15.4 → thailint-0.15.6}/src/linters/stateless_class/config.py +4 -0
  9. {thailint-0.15.4 → thailint-0.15.6}/src/linters/stateless_class/linter.py +116 -4
  10. {thailint-0.15.4 → thailint-0.15.6}/src/linters/stateless_class/python_analyzer.py +86 -4
  11. thailint-0.15.4/src/linters/nesting/python_analyzer.py +0 -93
  12. {thailint-0.15.4 → thailint-0.15.6}/CHANGELOG.md +0 -0
  13. {thailint-0.15.4 → thailint-0.15.6}/LICENSE +0 -0
  14. {thailint-0.15.4 → thailint-0.15.6}/README.md +0 -0
  15. {thailint-0.15.4 → thailint-0.15.6}/src/__init__.py +0 -0
  16. {thailint-0.15.4 → thailint-0.15.6}/src/analyzers/__init__.py +0 -0
  17. {thailint-0.15.4 → thailint-0.15.6}/src/analyzers/ast_utils.py +0 -0
  18. {thailint-0.15.4 → thailint-0.15.6}/src/analyzers/rust_base.py +0 -0
  19. {thailint-0.15.4 → thailint-0.15.6}/src/analyzers/rust_context.py +0 -0
  20. {thailint-0.15.4 → thailint-0.15.6}/src/analyzers/typescript_base.py +0 -0
  21. {thailint-0.15.4 → thailint-0.15.6}/src/api.py +0 -0
  22. {thailint-0.15.4 → thailint-0.15.6}/src/cli/__init__.py +0 -0
  23. {thailint-0.15.4 → thailint-0.15.6}/src/cli/__main__.py +0 -0
  24. {thailint-0.15.4 → thailint-0.15.6}/src/cli/config.py +0 -0
  25. {thailint-0.15.4 → thailint-0.15.6}/src/cli/config_merge.py +0 -0
  26. {thailint-0.15.4 → thailint-0.15.6}/src/cli/linters/__init__.py +0 -0
  27. {thailint-0.15.4 → thailint-0.15.6}/src/cli/linters/code_patterns.py +0 -0
  28. {thailint-0.15.4 → thailint-0.15.6}/src/cli/linters/code_smells.py +0 -0
  29. {thailint-0.15.4 → thailint-0.15.6}/src/cli/linters/documentation.py +0 -0
  30. {thailint-0.15.4 → thailint-0.15.6}/src/cli/linters/performance.py +0 -0
  31. {thailint-0.15.4 → thailint-0.15.6}/src/cli/linters/shared.py +0 -0
  32. {thailint-0.15.4 → thailint-0.15.6}/src/cli/linters/structure.py +0 -0
  33. {thailint-0.15.4 → thailint-0.15.6}/src/cli/linters/structure_quality.py +0 -0
  34. {thailint-0.15.4 → thailint-0.15.6}/src/cli/main.py +0 -0
  35. {thailint-0.15.4 → thailint-0.15.6}/src/cli/utils.py +0 -0
  36. {thailint-0.15.4 → thailint-0.15.6}/src/cli_main.py +0 -0
  37. {thailint-0.15.4 → thailint-0.15.6}/src/config.py +0 -0
  38. {thailint-0.15.4 → thailint-0.15.6}/src/core/__init__.py +0 -0
  39. {thailint-0.15.4 → thailint-0.15.6}/src/core/base.py +0 -0
  40. {thailint-0.15.4 → thailint-0.15.6}/src/core/config_parser.py +0 -0
  41. {thailint-0.15.4 → thailint-0.15.6}/src/core/constants.py +0 -0
  42. {thailint-0.15.4 → thailint-0.15.6}/src/core/linter_utils.py +0 -0
  43. {thailint-0.15.4 → thailint-0.15.6}/src/core/python_lint_rule.py +0 -0
  44. {thailint-0.15.4 → thailint-0.15.6}/src/core/registry.py +0 -0
  45. {thailint-0.15.4 → thailint-0.15.6}/src/core/rule_discovery.py +0 -0
  46. {thailint-0.15.4 → thailint-0.15.6}/src/core/types.py +0 -0
  47. {thailint-0.15.4 → thailint-0.15.6}/src/core/violation_builder.py +0 -0
  48. {thailint-0.15.4 → thailint-0.15.6}/src/core/violation_utils.py +0 -0
  49. {thailint-0.15.4 → thailint-0.15.6}/src/formatters/__init__.py +0 -0
  50. {thailint-0.15.4 → thailint-0.15.6}/src/formatters/sarif.py +0 -0
  51. {thailint-0.15.4 → thailint-0.15.6}/src/linter_config/__init__.py +0 -0
  52. {thailint-0.15.4 → thailint-0.15.6}/src/linter_config/directive_markers.py +0 -0
  53. {thailint-0.15.4 → thailint-0.15.6}/src/linter_config/ignore.py +0 -0
  54. {thailint-0.15.4 → thailint-0.15.6}/src/linter_config/loader.py +0 -0
  55. {thailint-0.15.4 → thailint-0.15.6}/src/linter_config/pattern_utils.py +0 -0
  56. {thailint-0.15.4 → thailint-0.15.6}/src/linter_config/rule_matcher.py +0 -0
  57. {thailint-0.15.4 → thailint-0.15.6}/src/linters/__init__.py +0 -0
  58. {thailint-0.15.4 → thailint-0.15.6}/src/linters/collection_pipeline/__init__.py +0 -0
  59. {thailint-0.15.4 → thailint-0.15.6}/src/linters/collection_pipeline/any_all_analyzer.py +0 -0
  60. {thailint-0.15.4 → thailint-0.15.6}/src/linters/collection_pipeline/ast_utils.py +0 -0
  61. {thailint-0.15.4 → thailint-0.15.6}/src/linters/collection_pipeline/config.py +0 -0
  62. {thailint-0.15.4 → thailint-0.15.6}/src/linters/collection_pipeline/continue_analyzer.py +0 -0
  63. {thailint-0.15.4 → thailint-0.15.6}/src/linters/collection_pipeline/detector.py +0 -0
  64. {thailint-0.15.4 → thailint-0.15.6}/src/linters/collection_pipeline/filter_map_analyzer.py +0 -0
  65. {thailint-0.15.4 → thailint-0.15.6}/src/linters/collection_pipeline/linter.py +0 -0
  66. {thailint-0.15.4 → thailint-0.15.6}/src/linters/collection_pipeline/suggestion_builder.py +0 -0
  67. {thailint-0.15.4 → thailint-0.15.6}/src/linters/cqs/__init__.py +0 -0
  68. {thailint-0.15.4 → thailint-0.15.6}/src/linters/cqs/config.py +0 -0
  69. {thailint-0.15.4 → thailint-0.15.6}/src/linters/cqs/function_analyzer.py +0 -0
  70. {thailint-0.15.4 → thailint-0.15.6}/src/linters/cqs/input_detector.py +0 -0
  71. {thailint-0.15.4 → thailint-0.15.6}/src/linters/cqs/linter.py +0 -0
  72. {thailint-0.15.4 → thailint-0.15.6}/src/linters/cqs/output_detector.py +0 -0
  73. {thailint-0.15.4 → thailint-0.15.6}/src/linters/cqs/python_analyzer.py +0 -0
  74. {thailint-0.15.4 → thailint-0.15.6}/src/linters/cqs/types.py +0 -0
  75. {thailint-0.15.4 → thailint-0.15.6}/src/linters/cqs/typescript_cqs_analyzer.py +0 -0
  76. {thailint-0.15.4 → thailint-0.15.6}/src/linters/cqs/typescript_function_analyzer.py +0 -0
  77. {thailint-0.15.4 → thailint-0.15.6}/src/linters/cqs/typescript_input_detector.py +0 -0
  78. {thailint-0.15.4 → thailint-0.15.6}/src/linters/cqs/typescript_output_detector.py +0 -0
  79. {thailint-0.15.4 → thailint-0.15.6}/src/linters/cqs/violation_builder.py +0 -0
  80. {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/__init__.py +0 -0
  81. {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/base_token_analyzer.py +0 -0
  82. {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/block_filter.py +0 -0
  83. {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/block_grouper.py +0 -0
  84. {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/cache.py +0 -0
  85. {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/cache_query.py +0 -0
  86. {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/config.py +0 -0
  87. {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/config_loader.py +0 -0
  88. {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/constant.py +0 -0
  89. {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/constant_matcher.py +0 -0
  90. {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/constant_violation_builder.py +0 -0
  91. {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/deduplicator.py +0 -0
  92. {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/duplicate_storage.py +0 -0
  93. {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/file_analyzer.py +0 -0
  94. {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/inline_ignore.py +0 -0
  95. {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/linter.py +0 -0
  96. {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/python_analyzer.py +0 -0
  97. {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/python_constant_extractor.py +0 -0
  98. {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/single_statement_detector.py +0 -0
  99. {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/storage_initializer.py +0 -0
  100. {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/token_hasher.py +0 -0
  101. {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/typescript_analyzer.py +0 -0
  102. {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/typescript_constant_extractor.py +0 -0
  103. {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/typescript_statement_detector.py +0 -0
  104. {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/typescript_value_extractor.py +0 -0
  105. {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/violation_builder.py +0 -0
  106. {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/violation_filter.py +0 -0
  107. {thailint-0.15.4 → thailint-0.15.6}/src/linters/dry/violation_generator.py +0 -0
  108. {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_header/__init__.py +0 -0
  109. {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_header/atemporal_detector.py +0 -0
  110. {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_header/base_parser.py +0 -0
  111. {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_header/bash_parser.py +0 -0
  112. {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_header/config.py +0 -0
  113. {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_header/css_parser.py +0 -0
  114. {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_header/field_validator.py +0 -0
  115. {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_header/linter.py +0 -0
  116. {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_header/markdown_parser.py +0 -0
  117. {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_header/python_parser.py +0 -0
  118. {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_header/typescript_parser.py +0 -0
  119. {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_header/violation_builder.py +0 -0
  120. {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_placement/__init__.py +0 -0
  121. {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_placement/config_loader.py +0 -0
  122. {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_placement/directory_matcher.py +0 -0
  123. {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_placement/linter.py +0 -0
  124. {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_placement/path_resolver.py +0 -0
  125. {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_placement/pattern_matcher.py +0 -0
  126. {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_placement/pattern_validator.py +0 -0
  127. {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_placement/rule_checker.py +0 -0
  128. {thailint-0.15.4 → thailint-0.15.6}/src/linters/file_placement/violation_factory.py +0 -0
  129. {thailint-0.15.4 → thailint-0.15.6}/src/linters/lazy_ignores/__init__.py +0 -0
  130. {thailint-0.15.4 → thailint-0.15.6}/src/linters/lazy_ignores/config.py +0 -0
  131. {thailint-0.15.4 → thailint-0.15.6}/src/linters/lazy_ignores/directive_utils.py +0 -0
  132. {thailint-0.15.4 → thailint-0.15.6}/src/linters/lazy_ignores/header_parser.py +0 -0
  133. {thailint-0.15.4 → thailint-0.15.6}/src/linters/lazy_ignores/linter.py +0 -0
  134. {thailint-0.15.4 → thailint-0.15.6}/src/linters/lazy_ignores/matcher.py +0 -0
  135. {thailint-0.15.4 → thailint-0.15.6}/src/linters/lazy_ignores/python_analyzer.py +0 -0
  136. {thailint-0.15.4 → thailint-0.15.6}/src/linters/lazy_ignores/rule_id_utils.py +0 -0
  137. {thailint-0.15.4 → thailint-0.15.6}/src/linters/lazy_ignores/skip_detector.py +0 -0
  138. {thailint-0.15.4 → thailint-0.15.6}/src/linters/lazy_ignores/types.py +0 -0
  139. {thailint-0.15.4 → thailint-0.15.6}/src/linters/lazy_ignores/typescript_analyzer.py +0 -0
  140. {thailint-0.15.4 → thailint-0.15.6}/src/linters/lazy_ignores/violation_builder.py +0 -0
  141. {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/__init__.py +0 -0
  142. {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/config.py +0 -0
  143. {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/linter.py +0 -0
  144. {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/pattern_detectors/__init__.py +0 -0
  145. {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/pattern_detectors/base.py +0 -0
  146. {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/pattern_detectors/dict_key_detector.py +0 -0
  147. {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/pattern_detectors/division_check_detector.py +0 -0
  148. {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/pattern_detectors/file_exists_detector.py +0 -0
  149. {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/pattern_detectors/hasattr_detector.py +0 -0
  150. {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/pattern_detectors/isinstance_detector.py +0 -0
  151. {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/pattern_detectors/len_check_detector.py +0 -0
  152. {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/pattern_detectors/none_check_detector.py +0 -0
  153. {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/pattern_detectors/string_validator_detector.py +0 -0
  154. {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/python_analyzer.py +0 -0
  155. {thailint-0.15.4 → thailint-0.15.6}/src/linters/lbyl/violation_builder.py +0 -0
  156. {thailint-0.15.4 → thailint-0.15.6}/src/linters/magic_numbers/__init__.py +0 -0
  157. {thailint-0.15.4 → thailint-0.15.6}/src/linters/magic_numbers/context_analyzer.py +0 -0
  158. {thailint-0.15.4 → thailint-0.15.6}/src/linters/magic_numbers/python_analyzer.py +0 -0
  159. {thailint-0.15.4 → thailint-0.15.6}/src/linters/magic_numbers/typescript_analyzer.py +0 -0
  160. {thailint-0.15.4 → thailint-0.15.6}/src/linters/magic_numbers/typescript_ignore_checker.py +0 -0
  161. {thailint-0.15.4 → thailint-0.15.6}/src/linters/magic_numbers/violation_builder.py +0 -0
  162. {thailint-0.15.4 → thailint-0.15.6}/src/linters/method_property/__init__.py +0 -0
  163. {thailint-0.15.4 → thailint-0.15.6}/src/linters/method_property/config.py +0 -0
  164. {thailint-0.15.4 → thailint-0.15.6}/src/linters/method_property/linter.py +0 -0
  165. {thailint-0.15.4 → thailint-0.15.6}/src/linters/method_property/python_analyzer.py +0 -0
  166. {thailint-0.15.4 → thailint-0.15.6}/src/linters/method_property/violation_builder.py +0 -0
  167. {thailint-0.15.4 → thailint-0.15.6}/src/linters/nesting/__init__.py +0 -0
  168. {thailint-0.15.4 → thailint-0.15.6}/src/linters/nesting/config.py +0 -0
  169. {thailint-0.15.4 → thailint-0.15.6}/src/linters/nesting/linter.py +0 -0
  170. {thailint-0.15.4 → thailint-0.15.6}/src/linters/nesting/typescript_analyzer.py +0 -0
  171. {thailint-0.15.4 → thailint-0.15.6}/src/linters/nesting/typescript_function_extractor.py +0 -0
  172. {thailint-0.15.4 → thailint-0.15.6}/src/linters/nesting/violation_builder.py +0 -0
  173. {thailint-0.15.4 → thailint-0.15.6}/src/linters/performance/__init__.py +0 -0
  174. {thailint-0.15.4 → thailint-0.15.6}/src/linters/performance/config.py +0 -0
  175. {thailint-0.15.4 → thailint-0.15.6}/src/linters/performance/constants.py +0 -0
  176. {thailint-0.15.4 → thailint-0.15.6}/src/linters/performance/linter.py +0 -0
  177. {thailint-0.15.4 → thailint-0.15.6}/src/linters/performance/python_analyzer.py +0 -0
  178. {thailint-0.15.4 → thailint-0.15.6}/src/linters/performance/regex_analyzer.py +0 -0
  179. {thailint-0.15.4 → thailint-0.15.6}/src/linters/performance/regex_linter.py +0 -0
  180. {thailint-0.15.4 → thailint-0.15.6}/src/linters/performance/typescript_analyzer.py +0 -0
  181. {thailint-0.15.4 → thailint-0.15.6}/src/linters/performance/violation_builder.py +0 -0
  182. {thailint-0.15.4 → thailint-0.15.6}/src/linters/print_statements/__init__.py +0 -0
  183. {thailint-0.15.4 → thailint-0.15.6}/src/linters/print_statements/config.py +0 -0
  184. {thailint-0.15.4 → thailint-0.15.6}/src/linters/print_statements/linter.py +0 -0
  185. {thailint-0.15.4 → thailint-0.15.6}/src/linters/print_statements/python_analyzer.py +0 -0
  186. {thailint-0.15.4 → thailint-0.15.6}/src/linters/print_statements/typescript_analyzer.py +0 -0
  187. {thailint-0.15.4 → thailint-0.15.6}/src/linters/print_statements/violation_builder.py +0 -0
  188. {thailint-0.15.4 → thailint-0.15.6}/src/linters/srp/__init__.py +0 -0
  189. {thailint-0.15.4 → thailint-0.15.6}/src/linters/srp/class_analyzer.py +0 -0
  190. {thailint-0.15.4 → thailint-0.15.6}/src/linters/srp/config.py +0 -0
  191. {thailint-0.15.4 → thailint-0.15.6}/src/linters/srp/heuristics.py +0 -0
  192. {thailint-0.15.4 → thailint-0.15.6}/src/linters/srp/linter.py +0 -0
  193. {thailint-0.15.4 → thailint-0.15.6}/src/linters/srp/metrics_evaluator.py +0 -0
  194. {thailint-0.15.4 → thailint-0.15.6}/src/linters/srp/python_analyzer.py +0 -0
  195. {thailint-0.15.4 → thailint-0.15.6}/src/linters/srp/typescript_analyzer.py +0 -0
  196. {thailint-0.15.4 → thailint-0.15.6}/src/linters/srp/typescript_metrics_calculator.py +0 -0
  197. {thailint-0.15.4 → thailint-0.15.6}/src/linters/srp/violation_builder.py +0 -0
  198. {thailint-0.15.4 → thailint-0.15.6}/src/linters/stateless_class/__init__.py +0 -0
  199. {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/__init__.py +0 -0
  200. {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/config.py +0 -0
  201. {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/context_filter.py +0 -0
  202. {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/function_call_violation_builder.py +0 -0
  203. {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/ignore_checker.py +0 -0
  204. {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/ignore_utils.py +0 -0
  205. {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/linter.py +0 -0
  206. {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/python/__init__.py +0 -0
  207. {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/python/analyzer.py +0 -0
  208. {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/python/call_tracker.py +0 -0
  209. {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/python/comparison_tracker.py +0 -0
  210. {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/python/condition_extractor.py +0 -0
  211. {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/python/conditional_detector.py +0 -0
  212. {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/python/constants.py +0 -0
  213. {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/python/match_analyzer.py +0 -0
  214. {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/python/validation_detector.py +0 -0
  215. {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/python/variable_extractor.py +0 -0
  216. {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/storage.py +0 -0
  217. {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/storage_initializer.py +0 -0
  218. {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/typescript/__init__.py +0 -0
  219. {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/typescript/analyzer.py +0 -0
  220. {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/typescript/call_tracker.py +0 -0
  221. {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/typescript/comparison_tracker.py +0 -0
  222. {thailint-0.15.4 → thailint-0.15.6}/src/linters/stringly_typed/violation_generator.py +0 -0
  223. {thailint-0.15.4 → thailint-0.15.6}/src/orchestrator/__init__.py +0 -0
  224. {thailint-0.15.4 → thailint-0.15.6}/src/orchestrator/core.py +0 -0
  225. {thailint-0.15.4 → thailint-0.15.6}/src/orchestrator/language_detector.py +0 -0
  226. {thailint-0.15.4 → thailint-0.15.6}/src/templates/thailint_config_template.yaml +0 -0
  227. {thailint-0.15.4 → thailint-0.15.6}/src/utils/__init__.py +0 -0
  228. {thailint-0.15.4 → thailint-0.15.6}/src/utils/project_root.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: thailint
3
- Version: 0.15.4
3
+ Version: 0.15.6
4
4
  Summary: The AI Linter - Enterprise-grade linting and governance for AI-generated code across multiple languages
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -17,7 +17,7 @@ build-backend = "poetry.core.masonry.api"
17
17
 
18
18
  [tool.poetry]
19
19
  name = "thailint"
20
- version = "0.15.4"
20
+ version = "0.15.6"
21
21
  description = "The AI Linter - Enterprise-grade linting and governance for AI-generated code across multiple languages"
22
22
  authors = ["Steve Jackson"]
23
23
  license = "MIT"
@@ -143,6 +143,24 @@ def _load_json_config(config_file: Path) -> dict[str, Any]:
143
143
  return dict(result) if isinstance(result, dict) else {}
144
144
 
145
145
 
146
+ def _sanitize_string(text: str) -> str:
147
+ """Remove or replace surrogate characters that can't be encoded to UTF-8.
148
+
149
+ Surrogate characters (U+D800-U+DFFF) appear when Python reads filesystem paths
150
+ or file content with invalid UTF-8 bytes using surrogateescape error handling.
151
+ These characters cannot be encoded to UTF-8 and cause UnicodeEncodeError.
152
+
153
+ Args:
154
+ text: String that may contain surrogate characters
155
+
156
+ Returns:
157
+ String with surrogates replaced by the Unicode replacement character
158
+ """
159
+ # Encode with surrogateescape to handle surrogates, then decode back
160
+ # This effectively replaces surrogates with a replacement representation
161
+ return text.encode("utf-8", errors="surrogateescape").decode("utf-8", errors="replace")
162
+
163
+
146
164
  def format_violations(violations: list, output_format: str) -> None:
147
165
  """Format and print violations to console.
148
166
 
@@ -168,10 +186,10 @@ def _output_json(violations: list) -> None:
168
186
  "violations": [
169
187
  {
170
188
  "rule_id": v.rule_id,
171
- "file_path": str(v.file_path),
189
+ "file_path": _sanitize_string(str(v.file_path)),
172
190
  "line": v.line,
173
191
  "column": v.column,
174
- "message": v.message,
192
+ "message": _sanitize_string(v.message),
175
193
  "severity": v.severity.name,
176
194
  }
177
195
  for v in violations
@@ -215,9 +233,11 @@ def _print_violation(v: Any) -> None:
215
233
  Args:
216
234
  v: Violation object with file_path, line, column, severity, rule_id, message
217
235
  """
218
- location = f"{v.file_path}:{v.line}" if v.line else str(v.file_path)
236
+ file_path = _sanitize_string(str(v.file_path))
237
+ message = _sanitize_string(v.message)
238
+ location = f"{file_path}:{v.line}" if v.line else file_path
219
239
  if v.column:
220
240
  location += f":{v.column}"
221
241
  click.echo(f" {location}")
222
- click.echo(f" [{v.severity.name}] {v.rule_id}: {v.message}")
242
+ click.echo(f" [{v.severity.name}] {v.rule_id}: {message}")
223
243
  click.echo()
@@ -4,8 +4,9 @@ Purpose: Configuration schema for magic numbers linter
4
4
  Scope: MagicNumberConfig dataclass with allowed_numbers and max_small_integer settings
5
5
 
6
6
  Overview: Defines configuration schema for magic numbers linter. Provides MagicNumberConfig dataclass
7
- with allowed_numbers set (default includes common acceptable numbers like -1, 0, 1, 2, 3, 4, 5, 10, 100, 1000)
8
- and max_small_integer threshold (default 10) for range() contexts. Supports per-file and per-directory
7
+ with allowed_numbers set (default includes common acceptable numbers like -1, 0, 1, 2, 3, 4, 5, 10, 100, 1000
8
+ and standard ports like 80, 443, 22, 21, 8080, 8443, 3000, 5000) and max_small_integer threshold (default 10)
9
+ for range() contexts. Supports per-file and per-directory
9
10
  config overrides through from_dict class method. Validates that configuration values are appropriate
10
11
  types. Integrates with orchestrator's configuration system to allow users to customize allowed numbers
11
12
  via .thailint.yaml configuration files.
@@ -18,11 +19,41 @@ Interfaces: MagicNumberConfig(allowed_numbers: set, max_small_integer: int, enab
18
19
  from_dict class method for loading configuration from dictionary
19
20
 
20
21
  Implementation: Dataclass with validation and defaults, matches reference implementation patterns
22
+
23
+ Suppressions:
24
+ - unnecessary-lambda: The lambda is required here because dataclass field default_factory
25
+ needs a callable, and DEFAULT_ALLOWED_NUMBERS.copy() is a method call, not a callable.
26
+ Using `default_factory=DEFAULT_ALLOWED_NUMBERS.copy` would call the method at class
27
+ definition time, not at instance creation time.
21
28
  """
22
29
 
23
30
  from dataclasses import dataclass, field
24
31
  from typing import Any
25
32
 
33
+ # Default allowed numbers including common small integers and standard ports
34
+ DEFAULT_ALLOWED_NUMBERS: set[int | float] = {
35
+ # Common small integers
36
+ -1,
37
+ 0,
38
+ 1,
39
+ 2,
40
+ 3,
41
+ 4,
42
+ 5,
43
+ 10,
44
+ 100,
45
+ 1000,
46
+ # Standard ports
47
+ 21, # FTP
48
+ 22, # SSH
49
+ 80, # HTTP
50
+ 443, # HTTPS
51
+ 3000, # Common dev server (Node.js, Rails)
52
+ 5000, # Flask default
53
+ 8080, # Alternate HTTP
54
+ 8443, # Alternate HTTPS
55
+ }
56
+
26
57
 
27
58
  @dataclass
28
59
  class MagicNumberConfig:
@@ -30,10 +61,11 @@ class MagicNumberConfig:
30
61
 
31
62
  enabled: bool = True
32
63
  allowed_numbers: set[int | float] = field(
33
- default_factory=lambda: {-1, 0, 1, 2, 3, 4, 5, 10, 100, 1000}
64
+ default_factory=lambda: DEFAULT_ALLOWED_NUMBERS.copy() # pylint: disable=unnecessary-lambda
34
65
  )
35
66
  max_small_integer: int = 10
36
67
  ignore: list[str] = field(default_factory=list)
68
+ exempt_definition_files: bool = True
37
69
 
38
70
  def __post_init__(self) -> None:
39
71
  """Validate configuration values."""
@@ -58,16 +90,14 @@ class MagicNumberConfig:
58
90
  allowed_numbers = set(
59
91
  lang_config.get(
60
92
  "allowed_numbers",
61
- config.get("allowed_numbers", {-1, 0, 1, 2, 3, 4, 5, 10, 100, 1000}),
93
+ config.get("allowed_numbers", DEFAULT_ALLOWED_NUMBERS),
62
94
  )
63
95
  )
64
96
  max_small_integer = lang_config.get(
65
97
  "max_small_integer", config.get("max_small_integer", 10)
66
98
  )
67
99
  else:
68
- allowed_numbers = set(
69
- config.get("allowed_numbers", {-1, 0, 1, 2, 3, 4, 5, 10, 100, 1000})
70
- )
100
+ allowed_numbers = set(config.get("allowed_numbers", DEFAULT_ALLOWED_NUMBERS))
71
101
  max_small_integer = config.get("max_small_integer", 10)
72
102
 
73
103
  ignore_patterns = config.get("ignore", [])
@@ -79,4 +109,5 @@ class MagicNumberConfig:
79
109
  allowed_numbers=allowed_numbers,
80
110
  max_small_integer=max_small_integer,
81
111
  ignore=ignore_patterns,
112
+ exempt_definition_files=config.get("exempt_definition_files", True),
82
113
  )
@@ -0,0 +1,226 @@
1
+ """
2
+ Purpose: Detect constant definition files that should be exempt from magic number checking
3
+
4
+ Scope: File-level detection of definition patterns (status codes, constants files)
5
+
6
+ Overview: Provides functions to detect if a file is a constant definition file that should
7
+ be exempt from magic number violations. Definition files exist specifically to define
8
+ named constants and shouldn't be flagged. Detection is based on:
9
+ 1. Filename patterns (*_codes.py, *_constants.py, constants.py)
10
+ 2. Content patterns (dicts with 5+ int keys, 10+ UPPERCASE constant assignments)
11
+ Files matching these patterns contain legitimate constant definitions.
12
+
13
+ Dependencies: ast module for parsing, pathlib for Path handling, re for pattern matching
14
+
15
+ Exports: is_definition_file function
16
+
17
+ Interfaces: is_definition_file(file_path, content) -> bool
18
+
19
+ Implementation: Filename pattern matching and AST-based content analysis
20
+ """
21
+
22
+ import ast
23
+ import re
24
+ from pathlib import Path
25
+
26
+ # Threshold for number of UPPERCASE constants to consider a file as definition file
27
+ MIN_UPPERCASE_CONSTANTS = 10
28
+
29
+ # Threshold for number of int keys in a dict to consider it a definition pattern
30
+ MIN_DICT_INT_KEYS = 5
31
+
32
+
33
+ def is_definition_file(file_path: Path | str | None, content: str | None) -> bool:
34
+ """Check if file is a constant definition file that should be exempt.
35
+
36
+ Args:
37
+ file_path: Path to the file
38
+ content: File content
39
+
40
+ Returns:
41
+ True if file is a definition file that should be exempt
42
+ """
43
+ if _matches_definition_filename(file_path):
44
+ return True
45
+
46
+ if content and _has_definition_content_patterns(content):
47
+ return True
48
+
49
+ return False
50
+
51
+
52
+ def _matches_definition_filename(file_path: Path | str | None) -> bool:
53
+ """Check if filename matches definition file patterns.
54
+
55
+ Patterns:
56
+ - *_codes.py (status_codes.py, error_codes.py, etc.)
57
+ - *_constants.py (app_constants.py, etc.)
58
+ - constants.py
59
+
60
+ Args:
61
+ file_path: Path to the file
62
+
63
+ Returns:
64
+ True if filename matches definition patterns
65
+ """
66
+ if not file_path:
67
+ return False
68
+
69
+ file_name = Path(file_path).name.lower()
70
+
71
+ # Check for *_codes.py pattern
72
+ if file_name.endswith("_codes.py"):
73
+ return True
74
+
75
+ # Check for constants.py or *_constants.py
76
+ if file_name == "constants.py" or file_name.endswith("_constants.py"):
77
+ return True
78
+
79
+ return False
80
+
81
+
82
+ def _has_definition_content_patterns(content: str) -> bool:
83
+ """Check if content has definition file patterns.
84
+
85
+ Patterns:
86
+ - 10+ UPPERCASE constant assignments
87
+ - Dict with 5+ integer keys
88
+
89
+ Args:
90
+ content: File content
91
+
92
+ Returns:
93
+ True if content matches definition patterns
94
+ """
95
+ try:
96
+ tree = ast.parse(content)
97
+ except SyntaxError:
98
+ return False
99
+
100
+ # Check for many UPPERCASE constants
101
+ if _count_uppercase_constants(tree) >= MIN_UPPERCASE_CONSTANTS:
102
+ return True
103
+
104
+ # Check for dicts with many int keys
105
+ if _has_dict_with_int_keys(tree):
106
+ return True
107
+
108
+ return False
109
+
110
+
111
+ def _count_uppercase_constants(tree: ast.Module) -> int:
112
+ """Count UPPERCASE constant assignments at module level.
113
+
114
+ Args:
115
+ tree: Parsed AST module
116
+
117
+ Returns:
118
+ Number of UPPERCASE constant assignments
119
+ """
120
+ count = 0
121
+ for node in tree.body:
122
+ if isinstance(node, ast.Assign):
123
+ count += _count_numeric_constant_targets(node)
124
+ return count
125
+
126
+
127
+ def _count_numeric_constant_targets(assign_node: ast.Assign) -> int:
128
+ """Count UPPERCASE constant targets with numeric values in an assignment.
129
+
130
+ Args:
131
+ assign_node: AST Assign node
132
+
133
+ Returns:
134
+ Number of uppercase constant targets with numeric values
135
+ """
136
+ if not _is_numeric_constant(assign_node.value):
137
+ return 0
138
+ return sum(1 for t in assign_node.targets if _is_uppercase_name_target(t))
139
+
140
+
141
+ def _is_numeric_constant(value: ast.expr) -> bool:
142
+ """Check if value is a numeric constant.
143
+
144
+ Args:
145
+ value: AST expression node
146
+
147
+ Returns:
148
+ True if value is a numeric constant
149
+ """
150
+ return isinstance(value, ast.Constant) and isinstance(value.value, (int, float))
151
+
152
+
153
+ def _is_uppercase_name_target(target: ast.expr) -> bool:
154
+ """Check if target is an uppercase name.
155
+
156
+ Args:
157
+ target: AST expression node
158
+
159
+ Returns:
160
+ True if target is an uppercase Name node
161
+ """
162
+ return isinstance(target, ast.Name) and _is_constant_name(target.id)
163
+
164
+
165
+ def _is_constant_name(name: str) -> bool:
166
+ """Check if name follows UPPERCASE constant convention.
167
+
168
+ Args:
169
+ name: Variable name
170
+
171
+ Returns:
172
+ True if name is UPPERCASE (with underscores allowed)
173
+ """
174
+ # Must be uppercase and contain at least 2 characters
175
+ if len(name) < 2:
176
+ return False
177
+ # Allow underscores but must have uppercase letters
178
+ return re.match(r"^[A-Z][A-Z0-9_]*$", name) is not None
179
+
180
+
181
+ def _has_dict_with_int_keys(tree: ast.Module) -> bool:
182
+ """Check if module has a dict with many integer keys.
183
+
184
+ Args:
185
+ tree: Parsed AST module
186
+
187
+ Returns:
188
+ True if there's a dict with MIN_DICT_INT_KEYS+ int keys
189
+ """
190
+ return any(_has_enough_int_keys(node) for node in ast.walk(tree) if isinstance(node, ast.Dict))
191
+
192
+
193
+ def _has_enough_int_keys(dict_node: ast.Dict) -> bool:
194
+ """Check if dict has enough integer keys to be a definition pattern.
195
+
196
+ Args:
197
+ dict_node: AST Dict node
198
+
199
+ Returns:
200
+ True if dict has MIN_DICT_INT_KEYS or more integer keys
201
+ """
202
+ return _count_int_keys(dict_node) >= MIN_DICT_INT_KEYS
203
+
204
+
205
+ def _count_int_keys(dict_node: ast.Dict) -> int:
206
+ """Count integer keys in a dict.
207
+
208
+ Args:
209
+ dict_node: AST Dict node
210
+
211
+ Returns:
212
+ Number of integer constant keys
213
+ """
214
+ return sum(1 for key in dict_node.keys if _is_int_key(key))
215
+
216
+
217
+ def _is_int_key(key: ast.expr | None) -> bool:
218
+ """Check if key is an integer constant.
219
+
220
+ Args:
221
+ key: AST expression node (or None for **dict unpacking)
222
+
223
+ Returns:
224
+ True if key is an integer constant
225
+ """
226
+ return isinstance(key, ast.Constant) and isinstance(key.value, int)
@@ -40,6 +40,7 @@ from src.linter_config.ignore import get_ignore_parser
40
40
 
41
41
  from .config import MagicNumberConfig
42
42
  from .context_analyzer import is_acceptable_context
43
+ from .definition_detector import is_definition_file
43
44
  from .python_analyzer import PythonMagicNumberAnalyzer
44
45
  from .typescript_analyzer import TypeScriptMagicNumberAnalyzer
45
46
  from .typescript_ignore_checker import TypeScriptIgnoreChecker
@@ -174,6 +175,12 @@ class MagicNumberRule(MultiLanguageLintRule): # thailint: ignore[srp]
174
175
  if self._is_file_ignored(context, config):
175
176
  return []
176
177
 
178
+ # Check if file is a definition file (status_codes.py, constants.py, etc.)
179
+ if config.exempt_definition_files and is_definition_file(
180
+ context.file_path, context.file_content
181
+ ):
182
+ return []
183
+
177
184
  tree = self._parse_python_code(context.file_content)
178
185
  if tree is None:
179
186
  return []
@@ -0,0 +1,156 @@
1
+ """
2
+ Purpose: Python AST-based nesting depth calculator
3
+
4
+ Scope: Python code nesting depth analysis using ast module
5
+
6
+ Overview: Analyzes Python code to calculate maximum nesting depth using AST traversal. Implements
7
+ visitor pattern to walk AST, tracking current depth and maximum depth found. Increments depth
8
+ for If, For, While, With, AsyncWith, Try, ExceptHandler, Match, and match_case nodes. Correctly
9
+ handles elif chains by detecting when an If node is in elif position (sole child in parent's
10
+ orelse list) and not incrementing depth. Starts depth counting at 1 for function body, matching
11
+ reference implementation behavior. Returns maximum depth found and location information for
12
+ violation reporting. Provides helper method to find all function definitions in an AST tree
13
+ for batch processing.
14
+
15
+ Dependencies: ast module for Python parsing
16
+
17
+ Exports: PythonNestingAnalyzer class with calculate_max_depth method
18
+
19
+ Interfaces: calculate_max_depth(func_node: ast.FunctionDef) -> tuple[int, int], find_all_functions
20
+
21
+ Implementation: AST visitor pattern with depth tracking, elif detection via parent orelse inspection
22
+ """
23
+
24
+ import ast
25
+
26
+ # Control structure types that increase nesting depth
27
+ _CONTROL_STRUCTURES = (
28
+ ast.For,
29
+ ast.While,
30
+ ast.With,
31
+ ast.AsyncWith,
32
+ ast.Try,
33
+ ast.Match,
34
+ ast.match_case,
35
+ )
36
+
37
+
38
+ class _DepthTracker:
39
+ """Tracks maximum nesting depth during AST traversal."""
40
+
41
+ def __init__(self, default_line: int) -> None:
42
+ """Initialize tracker with default line number."""
43
+ self.max_depth = 0
44
+ self.max_depth_line = default_line
45
+
46
+ def record(self, node: ast.AST, depth: int, default_line: int) -> None:
47
+ """Record depth if it's the new maximum."""
48
+ if depth > self.max_depth:
49
+ self.max_depth = depth
50
+ self.max_depth_line = getattr(node, "lineno", default_line)
51
+
52
+
53
+ class PythonNestingAnalyzer:
54
+ """Calculates maximum nesting depth in Python functions."""
55
+
56
+ def __init__(self) -> None:
57
+ """Initialize the Python nesting analyzer."""
58
+ pass # Stateless analyzer for nesting depth calculation
59
+
60
+ def calculate_max_depth(
61
+ self, func_node: ast.FunctionDef | ast.AsyncFunctionDef
62
+ ) -> tuple[int, int]:
63
+ """Calculate maximum nesting depth in a function.
64
+
65
+ Args:
66
+ func_node: AST node for function definition
67
+
68
+ Returns:
69
+ Tuple of (max_depth, line_number_of_max_depth)
70
+ """
71
+ tracker = _DepthTracker(func_node.lineno)
72
+
73
+ for stmt in func_node.body:
74
+ _visit_node(stmt, 0, tracker, func_node.lineno)
75
+
76
+ return tracker.max_depth, tracker.max_depth_line
77
+
78
+ def find_all_functions(self, tree: ast.AST) -> list[ast.FunctionDef | ast.AsyncFunctionDef]:
79
+ """Find all function definitions in AST.
80
+
81
+ Args:
82
+ tree: Python AST to search
83
+
84
+ Returns:
85
+ List of all FunctionDef and AsyncFunctionDef nodes found
86
+ """
87
+ functions = []
88
+ for node in ast.walk(tree):
89
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
90
+ functions.append(node)
91
+ return functions
92
+
93
+
94
+ def _visit_node(
95
+ node: ast.AST,
96
+ current_depth: int,
97
+ tracker: _DepthTracker,
98
+ default_line: int,
99
+ is_elif: bool = False,
100
+ ) -> None:
101
+ """Visit AST node, tracking nesting depth for control structures only."""
102
+ if isinstance(node, ast.If):
103
+ _visit_if_node(node, current_depth, tracker, default_line, is_elif)
104
+ elif isinstance(node, _CONTROL_STRUCTURES):
105
+ _visit_control_structure(node, current_depth, tracker, default_line)
106
+ else:
107
+ _visit_children(node, current_depth, tracker, default_line)
108
+
109
+
110
+ def _visit_if_node(
111
+ node: ast.If, current_depth: int, tracker: _DepthTracker, default_line: int, is_elif: bool
112
+ ) -> None:
113
+ """Visit If node with special elif handling."""
114
+ if not is_elif:
115
+ current_depth += 1
116
+ tracker.record(node, current_depth, default_line)
117
+
118
+ # Visit body
119
+ for child in node.body:
120
+ _visit_node(child, current_depth, tracker, default_line)
121
+
122
+ # Handle orelse - check for elif chain
123
+ if _is_elif_chain(node.orelse):
124
+ _visit_node(node.orelse[0], current_depth, tracker, default_line, is_elif=True)
125
+ else:
126
+ for child in node.orelse:
127
+ _visit_node(child, current_depth, tracker, default_line)
128
+
129
+
130
+ def _visit_control_structure(
131
+ node: ast.AST, current_depth: int, tracker: _DepthTracker, default_line: int
132
+ ) -> None:
133
+ """Visit a control structure node that increases depth."""
134
+ current_depth += 1
135
+ tracker.record(node, current_depth, default_line)
136
+ _visit_children(node, current_depth, tracker, default_line)
137
+
138
+
139
+ def _visit_children(
140
+ node: ast.AST, current_depth: int, tracker: _DepthTracker, default_line: int
141
+ ) -> None:
142
+ """Visit all children of a node without incrementing depth."""
143
+ for child in ast.iter_child_nodes(node):
144
+ _visit_node(child, current_depth, tracker, default_line)
145
+
146
+
147
+ def _is_elif_chain(orelse: list[ast.stmt]) -> bool:
148
+ """Check if orelse list represents an elif (single If node).
149
+
150
+ Args:
151
+ orelse: The orelse list from an If node
152
+
153
+ Returns:
154
+ True if this is an elif (single If in orelse), False otherwise
155
+ """
156
+ return len(orelse) == 1 and isinstance(orelse[0], ast.If)
@@ -30,6 +30,8 @@ class StatelessClassConfig:
30
30
  enabled: bool = True
31
31
  min_methods: int = 2
32
32
  ignore: list[str] = field(default_factory=list)
33
+ exempt_test_classes: bool = True
34
+ exempt_mixins: bool = True
33
35
 
34
36
  @classmethod
35
37
  def from_dict(
@@ -55,4 +57,6 @@ class StatelessClassConfig:
55
57
  enabled=config.get("enabled", True),
56
58
  min_methods=config.get("min_methods", 2),
57
59
  ignore=ignore_patterns,
60
+ exempt_test_classes=config.get("exempt_test_classes", True),
61
+ exempt_mixins=config.get("exempt_mixins", True),
58
62
  )