thailint 0.9.0__tar.gz → 0.10.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.
- {thailint-0.9.0 → thailint-0.10.0}/PKG-INFO +116 -3
- {thailint-0.9.0 → thailint-0.10.0}/README.md +115 -2
- {thailint-0.9.0 → thailint-0.10.0}/pyproject.toml +1 -1
- {thailint-0.9.0 → thailint-0.10.0}/src/cli.py +127 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/config.py +2 -3
- {thailint-0.9.0 → thailint-0.10.0}/src/core/rule_discovery.py +43 -10
- thailint-0.10.0/src/linters/collection_pipeline/__init__.py +90 -0
- thailint-0.10.0/src/linters/collection_pipeline/config.py +63 -0
- thailint-0.10.0/src/linters/collection_pipeline/continue_analyzer.py +100 -0
- thailint-0.10.0/src/linters/collection_pipeline/detector.py +130 -0
- thailint-0.10.0/src/linters/collection_pipeline/linter.py +437 -0
- thailint-0.10.0/src/linters/collection_pipeline/suggestion_builder.py +63 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/block_filter.py +2 -8
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/python_analyzer.py +34 -18
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/typescript_analyzer.py +61 -31
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_header/linter.py +7 -11
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_placement/linter.py +28 -8
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/srp/heuristics.py +4 -3
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/srp/linter.py +2 -3
- {thailint-0.9.0 → thailint-0.10.0}/CHANGELOG.md +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/LICENSE +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/analyzers/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/analyzers/typescript_base.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/api.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/core/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/core/base.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/core/cli_utils.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/core/config_parser.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/core/linter_utils.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/core/registry.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/core/types.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/core/violation_builder.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/formatters/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/formatters/sarif.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linter_config/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linter_config/ignore.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linter_config/loader.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/base_token_analyzer.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/block_grouper.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/cache.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/cache_query.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/config.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/config_loader.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/deduplicator.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/duplicate_storage.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/file_analyzer.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/inline_ignore.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/linter.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/storage_initializer.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/token_hasher.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/violation_builder.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/violation_filter.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/dry/violation_generator.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_header/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_header/atemporal_detector.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_header/base_parser.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_header/bash_parser.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_header/config.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_header/css_parser.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_header/field_validator.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_header/markdown_parser.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_header/python_parser.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_header/typescript_parser.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_header/violation_builder.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_placement/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_placement/config_loader.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_placement/directory_matcher.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_placement/path_resolver.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_placement/pattern_matcher.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_placement/pattern_validator.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_placement/rule_checker.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/file_placement/violation_factory.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/magic_numbers/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/magic_numbers/config.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/magic_numbers/context_analyzer.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/magic_numbers/linter.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/magic_numbers/python_analyzer.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/magic_numbers/typescript_analyzer.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/magic_numbers/violation_builder.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/method_property/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/method_property/config.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/method_property/linter.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/method_property/python_analyzer.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/method_property/violation_builder.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/nesting/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/nesting/config.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/nesting/linter.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/nesting/python_analyzer.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/nesting/typescript_analyzer.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/nesting/typescript_function_extractor.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/nesting/violation_builder.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/print_statements/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/print_statements/config.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/print_statements/linter.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/print_statements/python_analyzer.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/print_statements/typescript_analyzer.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/print_statements/violation_builder.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/srp/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/srp/class_analyzer.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/srp/config.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/srp/metrics_evaluator.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/srp/python_analyzer.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/srp/typescript_analyzer.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/srp/typescript_metrics_calculator.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/srp/violation_builder.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/stateless_class/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/stateless_class/config.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/stateless_class/linter.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/linters/stateless_class/python_analyzer.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/orchestrator/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/orchestrator/core.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/orchestrator/language_detector.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/templates/thailint_config_template.yaml +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/utils/__init__.py +0 -0
- {thailint-0.9.0 → thailint-0.10.0}/src/utils/project_root.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: thailint
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
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
|
|
@@ -37,8 +37,8 @@ Description-Content-Type: text/markdown
|
|
|
37
37
|
|
|
38
38
|
[](https://opensource.org/licenses/MIT)
|
|
39
39
|
[](https://www.python.org/downloads/)
|
|
40
|
-
[](tests/)
|
|
41
|
+
[](htmlcov/)
|
|
42
42
|
[](https://thai-lint.readthedocs.io/en/latest/?badge=latest)
|
|
43
43
|
[](docs/sarif-output.md)
|
|
44
44
|
|
|
@@ -98,6 +98,12 @@ thailint complements your existing linting stack by catching the patterns AI too
|
|
|
98
98
|
- Configurable thresholds (lines, tokens, occurrences)
|
|
99
99
|
- Language-specific detection (Python, TypeScript, JavaScript)
|
|
100
100
|
- False positive filtering (keyword args, imports)
|
|
101
|
+
- **Collection Pipeline Linting** - Detect for loops with embedded filtering
|
|
102
|
+
- Based on Martin Fowler's "Replace Loop with Pipeline" refactoring
|
|
103
|
+
- Detects if/continue patterns that should use generator expressions
|
|
104
|
+
- Generates refactoring suggestions with generator syntax
|
|
105
|
+
- Configurable threshold (min_continues)
|
|
106
|
+
- Python support with AST analysis
|
|
101
107
|
- **Method Property Linting** - Detect methods that should be @property decorators
|
|
102
108
|
- Python AST-based detection
|
|
103
109
|
- get_* prefix detection (Java-style getters)
|
|
@@ -741,6 +747,109 @@ Built-in filters automatically exclude common non-duplication patterns:
|
|
|
741
747
|
|
|
742
748
|
See [DRY Linter Guide](https://thai-lint.readthedocs.io/en/latest/dry-linter/) for comprehensive documentation, storage modes, and refactoring patterns.
|
|
743
749
|
|
|
750
|
+
## Collection Pipeline Linter
|
|
751
|
+
|
|
752
|
+
### Overview
|
|
753
|
+
|
|
754
|
+
The collection-pipeline linter detects for loops with embedded filtering (if/continue patterns) that should be refactored to use generator expressions or other collection pipelines. Based on Martin Fowler's "Replace Loop with Pipeline" refactoring pattern.
|
|
755
|
+
|
|
756
|
+
### The Anti-Pattern
|
|
757
|
+
|
|
758
|
+
```python
|
|
759
|
+
# Anti-pattern: Embedded filtering in loop body
|
|
760
|
+
for file_path in dir_path.glob(pattern):
|
|
761
|
+
if not file_path.is_file():
|
|
762
|
+
continue
|
|
763
|
+
if ignore_parser.is_ignored(file_path):
|
|
764
|
+
continue
|
|
765
|
+
violations.extend(lint_file(file_path))
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
### The Solution
|
|
769
|
+
|
|
770
|
+
```python
|
|
771
|
+
# Collection pipeline: Filtering separated from processing
|
|
772
|
+
valid_files = (
|
|
773
|
+
f for f in dir_path.glob(pattern)
|
|
774
|
+
if f.is_file() and not ignore_parser.is_ignored(f)
|
|
775
|
+
)
|
|
776
|
+
for file_path in valid_files:
|
|
777
|
+
violations.extend(lint_file(file_path))
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
### Quick Start
|
|
781
|
+
|
|
782
|
+
```bash
|
|
783
|
+
# Check current directory
|
|
784
|
+
thailint pipeline .
|
|
785
|
+
|
|
786
|
+
# Check specific directory
|
|
787
|
+
thailint pipeline src/
|
|
788
|
+
|
|
789
|
+
# Only flag patterns with 2+ filter conditions
|
|
790
|
+
thailint pipeline --min-continues 2 src/
|
|
791
|
+
|
|
792
|
+
# JSON output
|
|
793
|
+
thailint pipeline --format json src/
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
### Configuration
|
|
797
|
+
|
|
798
|
+
```yaml
|
|
799
|
+
# .thailint.yaml
|
|
800
|
+
collection-pipeline:
|
|
801
|
+
enabled: true
|
|
802
|
+
min_continues: 1 # Minimum if/continue patterns to flag
|
|
803
|
+
ignore:
|
|
804
|
+
- "tests/**"
|
|
805
|
+
- "**/legacy/**"
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
### Example Violation
|
|
809
|
+
|
|
810
|
+
**Detected Pattern:**
|
|
811
|
+
```python
|
|
812
|
+
def process_files(paths):
|
|
813
|
+
for path in paths:
|
|
814
|
+
if not path.is_file():
|
|
815
|
+
continue
|
|
816
|
+
analyze(path)
|
|
817
|
+
```
|
|
818
|
+
|
|
819
|
+
**Violation Message:**
|
|
820
|
+
```
|
|
821
|
+
src/processor.py:3 - For loop over 'paths' has embedded filtering.
|
|
822
|
+
Consider using a generator expression:
|
|
823
|
+
for path in (path for path in paths if path.is_file()):
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
**Refactored Code:**
|
|
827
|
+
```python
|
|
828
|
+
def process_files(paths):
|
|
829
|
+
valid_paths = (p for p in paths if p.is_file())
|
|
830
|
+
for path in valid_paths:
|
|
831
|
+
analyze(path)
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
### Why This Matters
|
|
835
|
+
|
|
836
|
+
- **Separation of concerns**: Filtering logic is separate from processing logic
|
|
837
|
+
- **Readability**: Intent is clear at a glance
|
|
838
|
+
- **Testability**: Filtering can be tested independently
|
|
839
|
+
- **Based on**: Martin Fowler's "Replace Loop with Pipeline" refactoring
|
|
840
|
+
|
|
841
|
+
### Ignoring Violations
|
|
842
|
+
|
|
843
|
+
```python
|
|
844
|
+
# Line-level ignore
|
|
845
|
+
for item in items: # thailint: ignore[collection-pipeline]
|
|
846
|
+
if not item.valid:
|
|
847
|
+
continue
|
|
848
|
+
process(item)
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
See [Collection Pipeline Linter Guide](docs/collection-pipeline-linter.md) for comprehensive documentation and refactoring patterns.
|
|
852
|
+
|
|
744
853
|
## Magic Numbers Linter
|
|
745
854
|
|
|
746
855
|
### Overview
|
|
@@ -1351,6 +1460,9 @@ docker run --rm -v $(pwd):/data washad/thailint:latest nesting /data/src/file1.p
|
|
|
1351
1460
|
# Lint specific subdirectory
|
|
1352
1461
|
docker run --rm -v $(pwd):/data washad/thailint:latest nesting /data/src
|
|
1353
1462
|
|
|
1463
|
+
# Collection pipeline linter
|
|
1464
|
+
docker run --rm -v $(pwd):/data washad/thailint:latest pipeline /data/src
|
|
1465
|
+
|
|
1354
1466
|
# With custom config
|
|
1355
1467
|
docker run --rm -v $(pwd):/data \
|
|
1356
1468
|
washad/thailint:latest nesting --config /data/.thailint.yaml /data
|
|
@@ -1414,6 +1526,7 @@ docker run --rm -v /path/to/workspace:/workspace \
|
|
|
1414
1526
|
- **[Nesting Depth Linter](https://thai-lint.readthedocs.io/en/latest/nesting-linter/)** - Nesting depth analysis guide
|
|
1415
1527
|
- **[SRP Linter](https://thai-lint.readthedocs.io/en/latest/srp-linter/)** - Single Responsibility Principle guide
|
|
1416
1528
|
- **[DRY Linter](https://thai-lint.readthedocs.io/en/latest/dry-linter/)** - Duplicate code detection guide
|
|
1529
|
+
- **[Collection Pipeline Linter](https://thai-lint.readthedocs.io/en/latest/collection-pipeline-linter/)** - Loop filtering refactoring guide
|
|
1417
1530
|
- **[Method Property Linter](https://thai-lint.readthedocs.io/en/latest/method-property-linter/)** - Method-to-property conversion guide
|
|
1418
1531
|
- **[Stateless Class Linter](https://thai-lint.readthedocs.io/en/latest/stateless-class-linter/)** - Stateless class detection guide
|
|
1419
1532
|
- **[Pre-commit Hooks](https://thai-lint.readthedocs.io/en/latest/pre-commit-hooks/)** - Automated quality checks
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://opensource.org/licenses/MIT)
|
|
4
4
|
[](https://www.python.org/downloads/)
|
|
5
|
-
[](tests/)
|
|
6
|
+
[](htmlcov/)
|
|
7
7
|
[](https://thai-lint.readthedocs.io/en/latest/?badge=latest)
|
|
8
8
|
[](docs/sarif-output.md)
|
|
9
9
|
|
|
@@ -63,6 +63,12 @@ thailint complements your existing linting stack by catching the patterns AI too
|
|
|
63
63
|
- Configurable thresholds (lines, tokens, occurrences)
|
|
64
64
|
- Language-specific detection (Python, TypeScript, JavaScript)
|
|
65
65
|
- False positive filtering (keyword args, imports)
|
|
66
|
+
- **Collection Pipeline Linting** - Detect for loops with embedded filtering
|
|
67
|
+
- Based on Martin Fowler's "Replace Loop with Pipeline" refactoring
|
|
68
|
+
- Detects if/continue patterns that should use generator expressions
|
|
69
|
+
- Generates refactoring suggestions with generator syntax
|
|
70
|
+
- Configurable threshold (min_continues)
|
|
71
|
+
- Python support with AST analysis
|
|
66
72
|
- **Method Property Linting** - Detect methods that should be @property decorators
|
|
67
73
|
- Python AST-based detection
|
|
68
74
|
- get_* prefix detection (Java-style getters)
|
|
@@ -706,6 +712,109 @@ Built-in filters automatically exclude common non-duplication patterns:
|
|
|
706
712
|
|
|
707
713
|
See [DRY Linter Guide](https://thai-lint.readthedocs.io/en/latest/dry-linter/) for comprehensive documentation, storage modes, and refactoring patterns.
|
|
708
714
|
|
|
715
|
+
## Collection Pipeline Linter
|
|
716
|
+
|
|
717
|
+
### Overview
|
|
718
|
+
|
|
719
|
+
The collection-pipeline linter detects for loops with embedded filtering (if/continue patterns) that should be refactored to use generator expressions or other collection pipelines. Based on Martin Fowler's "Replace Loop with Pipeline" refactoring pattern.
|
|
720
|
+
|
|
721
|
+
### The Anti-Pattern
|
|
722
|
+
|
|
723
|
+
```python
|
|
724
|
+
# Anti-pattern: Embedded filtering in loop body
|
|
725
|
+
for file_path in dir_path.glob(pattern):
|
|
726
|
+
if not file_path.is_file():
|
|
727
|
+
continue
|
|
728
|
+
if ignore_parser.is_ignored(file_path):
|
|
729
|
+
continue
|
|
730
|
+
violations.extend(lint_file(file_path))
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
### The Solution
|
|
734
|
+
|
|
735
|
+
```python
|
|
736
|
+
# Collection pipeline: Filtering separated from processing
|
|
737
|
+
valid_files = (
|
|
738
|
+
f for f in dir_path.glob(pattern)
|
|
739
|
+
if f.is_file() and not ignore_parser.is_ignored(f)
|
|
740
|
+
)
|
|
741
|
+
for file_path in valid_files:
|
|
742
|
+
violations.extend(lint_file(file_path))
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
### Quick Start
|
|
746
|
+
|
|
747
|
+
```bash
|
|
748
|
+
# Check current directory
|
|
749
|
+
thailint pipeline .
|
|
750
|
+
|
|
751
|
+
# Check specific directory
|
|
752
|
+
thailint pipeline src/
|
|
753
|
+
|
|
754
|
+
# Only flag patterns with 2+ filter conditions
|
|
755
|
+
thailint pipeline --min-continues 2 src/
|
|
756
|
+
|
|
757
|
+
# JSON output
|
|
758
|
+
thailint pipeline --format json src/
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
### Configuration
|
|
762
|
+
|
|
763
|
+
```yaml
|
|
764
|
+
# .thailint.yaml
|
|
765
|
+
collection-pipeline:
|
|
766
|
+
enabled: true
|
|
767
|
+
min_continues: 1 # Minimum if/continue patterns to flag
|
|
768
|
+
ignore:
|
|
769
|
+
- "tests/**"
|
|
770
|
+
- "**/legacy/**"
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
### Example Violation
|
|
774
|
+
|
|
775
|
+
**Detected Pattern:**
|
|
776
|
+
```python
|
|
777
|
+
def process_files(paths):
|
|
778
|
+
for path in paths:
|
|
779
|
+
if not path.is_file():
|
|
780
|
+
continue
|
|
781
|
+
analyze(path)
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
**Violation Message:**
|
|
785
|
+
```
|
|
786
|
+
src/processor.py:3 - For loop over 'paths' has embedded filtering.
|
|
787
|
+
Consider using a generator expression:
|
|
788
|
+
for path in (path for path in paths if path.is_file()):
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
**Refactored Code:**
|
|
792
|
+
```python
|
|
793
|
+
def process_files(paths):
|
|
794
|
+
valid_paths = (p for p in paths if p.is_file())
|
|
795
|
+
for path in valid_paths:
|
|
796
|
+
analyze(path)
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
### Why This Matters
|
|
800
|
+
|
|
801
|
+
- **Separation of concerns**: Filtering logic is separate from processing logic
|
|
802
|
+
- **Readability**: Intent is clear at a glance
|
|
803
|
+
- **Testability**: Filtering can be tested independently
|
|
804
|
+
- **Based on**: Martin Fowler's "Replace Loop with Pipeline" refactoring
|
|
805
|
+
|
|
806
|
+
### Ignoring Violations
|
|
807
|
+
|
|
808
|
+
```python
|
|
809
|
+
# Line-level ignore
|
|
810
|
+
for item in items: # thailint: ignore[collection-pipeline]
|
|
811
|
+
if not item.valid:
|
|
812
|
+
continue
|
|
813
|
+
process(item)
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
See [Collection Pipeline Linter Guide](docs/collection-pipeline-linter.md) for comprehensive documentation and refactoring patterns.
|
|
817
|
+
|
|
709
818
|
## Magic Numbers Linter
|
|
710
819
|
|
|
711
820
|
### Overview
|
|
@@ -1316,6 +1425,9 @@ docker run --rm -v $(pwd):/data washad/thailint:latest nesting /data/src/file1.p
|
|
|
1316
1425
|
# Lint specific subdirectory
|
|
1317
1426
|
docker run --rm -v $(pwd):/data washad/thailint:latest nesting /data/src
|
|
1318
1427
|
|
|
1428
|
+
# Collection pipeline linter
|
|
1429
|
+
docker run --rm -v $(pwd):/data washad/thailint:latest pipeline /data/src
|
|
1430
|
+
|
|
1319
1431
|
# With custom config
|
|
1320
1432
|
docker run --rm -v $(pwd):/data \
|
|
1321
1433
|
washad/thailint:latest nesting --config /data/.thailint.yaml /data
|
|
@@ -1379,6 +1491,7 @@ docker run --rm -v /path/to/workspace:/workspace \
|
|
|
1379
1491
|
- **[Nesting Depth Linter](https://thai-lint.readthedocs.io/en/latest/nesting-linter/)** - Nesting depth analysis guide
|
|
1380
1492
|
- **[SRP Linter](https://thai-lint.readthedocs.io/en/latest/srp-linter/)** - Single Responsibility Principle guide
|
|
1381
1493
|
- **[DRY Linter](https://thai-lint.readthedocs.io/en/latest/dry-linter/)** - Duplicate code detection guide
|
|
1494
|
+
- **[Collection Pipeline Linter](https://thai-lint.readthedocs.io/en/latest/collection-pipeline-linter/)** - Loop filtering refactoring guide
|
|
1382
1495
|
- **[Method Property Linter](https://thai-lint.readthedocs.io/en/latest/method-property-linter/)** - Method-to-property conversion guide
|
|
1383
1496
|
- **[Stateless Class Linter](https://thai-lint.readthedocs.io/en/latest/stateless-class-linter/)** - Stateless class detection guide
|
|
1384
1497
|
- **[Pre-commit Hooks](https://thai-lint.readthedocs.io/en/latest/pre-commit-hooks/)** - Automated quality checks
|
|
@@ -17,7 +17,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
17
17
|
|
|
18
18
|
[tool.poetry]
|
|
19
19
|
name = "thailint"
|
|
20
|
-
version = "0.
|
|
20
|
+
version = "0.10.0"
|
|
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"
|
|
@@ -2010,5 +2010,132 @@ def _execute_stateless_class_lint( # pylint: disable=too-many-arguments,too-man
|
|
|
2010
2010
|
sys.exit(1 if stateless_class_violations else 0)
|
|
2011
2011
|
|
|
2012
2012
|
|
|
2013
|
+
# Collection Pipeline command helper functions
|
|
2014
|
+
|
|
2015
|
+
|
|
2016
|
+
def _setup_pipeline_orchestrator(
|
|
2017
|
+
path_objs: list[Path], config_file: str | None, verbose: bool, project_root: Path | None = None
|
|
2018
|
+
):
|
|
2019
|
+
"""Set up orchestrator for pipeline command."""
|
|
2020
|
+
from src.orchestrator.core import Orchestrator
|
|
2021
|
+
from src.utils.project_root import get_project_root
|
|
2022
|
+
|
|
2023
|
+
# Use provided project_root or fall back to auto-detection
|
|
2024
|
+
if project_root is None:
|
|
2025
|
+
first_path = path_objs[0] if path_objs else Path.cwd()
|
|
2026
|
+
search_start = first_path if first_path.is_dir() else first_path.parent
|
|
2027
|
+
project_root = get_project_root(search_start)
|
|
2028
|
+
|
|
2029
|
+
orchestrator = Orchestrator(project_root=project_root)
|
|
2030
|
+
|
|
2031
|
+
if config_file:
|
|
2032
|
+
_load_config_file(orchestrator, config_file, verbose)
|
|
2033
|
+
|
|
2034
|
+
return orchestrator
|
|
2035
|
+
|
|
2036
|
+
|
|
2037
|
+
def _apply_pipeline_config_override(orchestrator, min_continues: int | None, verbose: bool):
|
|
2038
|
+
"""Apply min_continues override to orchestrator config."""
|
|
2039
|
+
if min_continues is None:
|
|
2040
|
+
return
|
|
2041
|
+
|
|
2042
|
+
if "collection_pipeline" not in orchestrator.config:
|
|
2043
|
+
orchestrator.config["collection_pipeline"] = {}
|
|
2044
|
+
|
|
2045
|
+
orchestrator.config["collection_pipeline"]["min_continues"] = min_continues
|
|
2046
|
+
if verbose:
|
|
2047
|
+
logger.debug(f"Overriding min_continues to {min_continues}")
|
|
2048
|
+
|
|
2049
|
+
|
|
2050
|
+
def _run_pipeline_lint(orchestrator, path_objs: list[Path], recursive: bool):
|
|
2051
|
+
"""Execute collection-pipeline lint on files or directories."""
|
|
2052
|
+
all_violations = _execute_linting_on_paths(orchestrator, path_objs, recursive)
|
|
2053
|
+
return [v for v in all_violations if "collection-pipeline" in v.rule_id]
|
|
2054
|
+
|
|
2055
|
+
|
|
2056
|
+
@cli.command("pipeline")
|
|
2057
|
+
@click.argument("paths", nargs=-1, type=click.Path())
|
|
2058
|
+
@click.option("--config", "-c", "config_file", type=click.Path(), help="Path to config file")
|
|
2059
|
+
@format_option
|
|
2060
|
+
@click.option("--min-continues", type=int, help="Override min continue guards to flag (default: 1)")
|
|
2061
|
+
@click.option("--recursive/--no-recursive", default=True, help="Scan directories recursively")
|
|
2062
|
+
@click.pass_context
|
|
2063
|
+
def pipeline( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
2064
|
+
ctx,
|
|
2065
|
+
paths: tuple[str, ...],
|
|
2066
|
+
config_file: str | None,
|
|
2067
|
+
format: str,
|
|
2068
|
+
min_continues: int | None,
|
|
2069
|
+
recursive: bool,
|
|
2070
|
+
):
|
|
2071
|
+
"""Check for collection pipeline anti-patterns in code.
|
|
2072
|
+
|
|
2073
|
+
Detects for loops with embedded if/continue filtering patterns that could
|
|
2074
|
+
be refactored to use collection pipelines (generator expressions, filter()).
|
|
2075
|
+
|
|
2076
|
+
PATHS: Files or directories to lint (defaults to current directory if none provided)
|
|
2077
|
+
|
|
2078
|
+
Examples:
|
|
2079
|
+
|
|
2080
|
+
\b
|
|
2081
|
+
# Check current directory (all Python files recursively)
|
|
2082
|
+
thai-lint pipeline
|
|
2083
|
+
|
|
2084
|
+
\b
|
|
2085
|
+
# Check specific directory
|
|
2086
|
+
thai-lint pipeline src/
|
|
2087
|
+
|
|
2088
|
+
\b
|
|
2089
|
+
# Check single file
|
|
2090
|
+
thai-lint pipeline src/app.py
|
|
2091
|
+
|
|
2092
|
+
\b
|
|
2093
|
+
# Only flag loops with 2+ continue guards
|
|
2094
|
+
thai-lint pipeline --min-continues 2 src/
|
|
2095
|
+
|
|
2096
|
+
\b
|
|
2097
|
+
# Get JSON output
|
|
2098
|
+
thai-lint pipeline --format json .
|
|
2099
|
+
|
|
2100
|
+
\b
|
|
2101
|
+
# Get SARIF output for CI/CD integration
|
|
2102
|
+
thai-lint pipeline --format sarif src/
|
|
2103
|
+
|
|
2104
|
+
\b
|
|
2105
|
+
# Use custom config file
|
|
2106
|
+
thai-lint pipeline --config .thailint.yaml src/
|
|
2107
|
+
"""
|
|
2108
|
+
verbose = ctx.obj.get("verbose", False)
|
|
2109
|
+
project_root = _get_project_root_from_context(ctx)
|
|
2110
|
+
|
|
2111
|
+
if not paths:
|
|
2112
|
+
paths = (".",)
|
|
2113
|
+
|
|
2114
|
+
path_objs = [Path(p) for p in paths]
|
|
2115
|
+
|
|
2116
|
+
try:
|
|
2117
|
+
_execute_pipeline_lint(
|
|
2118
|
+
path_objs, config_file, format, min_continues, recursive, verbose, project_root
|
|
2119
|
+
)
|
|
2120
|
+
except Exception as e:
|
|
2121
|
+
_handle_linting_error(e, verbose)
|
|
2122
|
+
|
|
2123
|
+
|
|
2124
|
+
def _execute_pipeline_lint( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
2125
|
+
path_objs, config_file, format, min_continues, recursive, verbose, project_root=None
|
|
2126
|
+
):
|
|
2127
|
+
"""Execute collection-pipeline lint."""
|
|
2128
|
+
_validate_paths_exist(path_objs)
|
|
2129
|
+
orchestrator = _setup_pipeline_orchestrator(path_objs, config_file, verbose, project_root)
|
|
2130
|
+
_apply_pipeline_config_override(orchestrator, min_continues, verbose)
|
|
2131
|
+
pipeline_violations = _run_pipeline_lint(orchestrator, path_objs, recursive)
|
|
2132
|
+
|
|
2133
|
+
if verbose:
|
|
2134
|
+
logger.info(f"Found {len(pipeline_violations)} collection-pipeline violation(s)")
|
|
2135
|
+
|
|
2136
|
+
format_violations(pipeline_violations, format)
|
|
2137
|
+
sys.exit(1 if pipeline_violations else 0)
|
|
2138
|
+
|
|
2139
|
+
|
|
2013
2140
|
if __name__ == "__main__":
|
|
2014
2141
|
cli()
|
|
@@ -103,9 +103,8 @@ def _load_from_explicit_path(config_path: Path) -> dict[str, Any]:
|
|
|
103
103
|
|
|
104
104
|
def _load_from_default_locations() -> dict[str, Any]:
|
|
105
105
|
"""Load config from default locations."""
|
|
106
|
-
for
|
|
107
|
-
|
|
108
|
-
continue
|
|
106
|
+
existing_locations = (loc for loc in CONFIG_LOCATIONS if loc.exists())
|
|
107
|
+
for location in existing_locations:
|
|
109
108
|
loaded_config = _try_load_from_location(location)
|
|
110
109
|
if loaded_config:
|
|
111
110
|
return loaded_config
|
|
@@ -20,6 +20,7 @@ Implementation: Package traversal with pkgutil, class introspection with inspect
|
|
|
20
20
|
import importlib
|
|
21
21
|
import inspect
|
|
22
22
|
import pkgutil
|
|
23
|
+
from types import ModuleType
|
|
23
24
|
from typing import Any
|
|
24
25
|
|
|
25
26
|
from .base import BaseLintRule
|
|
@@ -87,19 +88,51 @@ def _discover_from_module(module_path: str) -> list[BaseLintRule]:
|
|
|
87
88
|
Returns:
|
|
88
89
|
List of discovered rule instances
|
|
89
90
|
"""
|
|
91
|
+
module = _try_import_module(module_path)
|
|
92
|
+
if module is None:
|
|
93
|
+
return []
|
|
94
|
+
return _extract_rules_from_module(module)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _try_import_module(module_path: str) -> ModuleType | None:
|
|
98
|
+
"""Try to import a module, returning None on failure.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
module_path: Full module path to import
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Module object or None if import fails
|
|
105
|
+
"""
|
|
90
106
|
try:
|
|
91
|
-
|
|
107
|
+
return importlib.import_module(module_path)
|
|
92
108
|
except (ImportError, AttributeError):
|
|
93
|
-
return
|
|
109
|
+
return None
|
|
94
110
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
111
|
+
|
|
112
|
+
def _extract_rules_from_module(module: ModuleType) -> list[BaseLintRule]:
|
|
113
|
+
"""Extract rule instances from a module.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
module: Imported module to scan
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
List of discovered rule instances
|
|
120
|
+
"""
|
|
121
|
+
rule_classes = [obj for _name, obj in inspect.getmembers(module) if _is_rule_class(obj)]
|
|
122
|
+
return _instantiate_rules(rule_classes)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _instantiate_rules(rule_classes: list[type[BaseLintRule]]) -> list[BaseLintRule]:
|
|
126
|
+
"""Instantiate a list of rule classes.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
rule_classes: List of rule classes to instantiate
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
List of successfully instantiated rules
|
|
133
|
+
"""
|
|
134
|
+
instances = (_try_instantiate_rule(cls) for cls in rule_classes)
|
|
135
|
+
return [inst for inst in instances if inst is not None]
|
|
103
136
|
|
|
104
137
|
|
|
105
138
|
def _try_instantiate_rule(rule_class: type[BaseLintRule]) -> BaseLintRule | None:
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Collection pipeline linter package initialization
|
|
3
|
+
|
|
4
|
+
Scope: Exports for collection-pipeline linter module
|
|
5
|
+
|
|
6
|
+
Overview: Initializes the collection-pipeline linter package and exposes the main rule class
|
|
7
|
+
for external use. Exports CollectionPipelineRule as the primary interface for the linter,
|
|
8
|
+
allowing the orchestrator to discover and instantiate the rule. Also exports configuration
|
|
9
|
+
and detector classes for advanced use cases. Provides a convenience lint() function for
|
|
10
|
+
direct usage without orchestrator setup. This module serves as the entry point for
|
|
11
|
+
the collection-pipeline linter functionality within the thai-lint framework.
|
|
12
|
+
|
|
13
|
+
Dependencies: CollectionPipelineRule, CollectionPipelineConfig, PipelinePatternDetector
|
|
14
|
+
|
|
15
|
+
Exports: CollectionPipelineRule (primary), CollectionPipelineConfig, PipelinePatternDetector, lint
|
|
16
|
+
|
|
17
|
+
Interfaces: Standard Python package initialization with __all__ for explicit exports
|
|
18
|
+
|
|
19
|
+
Implementation: Simple re-export pattern for package interface, convenience lint function
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from .config import DEFAULT_MIN_CONTINUES, CollectionPipelineConfig
|
|
26
|
+
from .detector import PatternMatch, PipelinePatternDetector
|
|
27
|
+
from .linter import CollectionPipelineRule
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"CollectionPipelineRule",
|
|
31
|
+
"CollectionPipelineConfig",
|
|
32
|
+
"PipelinePatternDetector",
|
|
33
|
+
"PatternMatch",
|
|
34
|
+
"lint",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def lint(
|
|
39
|
+
path: Path | str,
|
|
40
|
+
config: dict[str, Any] | None = None,
|
|
41
|
+
min_continues: int = DEFAULT_MIN_CONTINUES,
|
|
42
|
+
) -> list:
|
|
43
|
+
"""Lint a file or directory for collection pipeline violations.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
path: Path to file or directory to lint
|
|
47
|
+
config: Configuration dict (optional, uses defaults if not provided)
|
|
48
|
+
min_continues: Minimum if/continue patterns to flag (default: 1)
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
List of violations found
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
>>> from src.linters.collection_pipeline import lint
|
|
55
|
+
>>> violations = lint('src/my_module.py', min_continues=2)
|
|
56
|
+
>>> for v in violations:
|
|
57
|
+
... print(f"{v.file_path}:{v.line} - {v.message}")
|
|
58
|
+
"""
|
|
59
|
+
path_obj = Path(path) if isinstance(path, str) else path
|
|
60
|
+
project_root = path_obj if path_obj.is_dir() else path_obj.parent
|
|
61
|
+
|
|
62
|
+
orchestrator = _setup_pipeline_orchestrator(project_root, config, min_continues)
|
|
63
|
+
violations = _execute_pipeline_lint(orchestrator, path_obj)
|
|
64
|
+
|
|
65
|
+
return [v for v in violations if "collection-pipeline" in v.rule_id]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _setup_pipeline_orchestrator(
|
|
69
|
+
project_root: Path, config: dict[str, Any] | None, min_continues: int
|
|
70
|
+
) -> Any:
|
|
71
|
+
"""Set up orchestrator with collection-pipeline config."""
|
|
72
|
+
from src.orchestrator.core import Orchestrator
|
|
73
|
+
|
|
74
|
+
orchestrator = Orchestrator(project_root=project_root)
|
|
75
|
+
|
|
76
|
+
if config:
|
|
77
|
+
orchestrator.config["collection-pipeline"] = config
|
|
78
|
+
else:
|
|
79
|
+
orchestrator.config["collection-pipeline"] = {"min_continues": min_continues}
|
|
80
|
+
|
|
81
|
+
return orchestrator
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _execute_pipeline_lint(orchestrator: Any, path_obj: Path) -> list:
|
|
85
|
+
"""Execute linting on file or directory."""
|
|
86
|
+
if path_obj.is_file():
|
|
87
|
+
return orchestrator.lint_file(path_obj)
|
|
88
|
+
if path_obj.is_dir():
|
|
89
|
+
return orchestrator.lint_directory(path_obj)
|
|
90
|
+
return []
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Purpose: Configuration dataclass for collection-pipeline linter
|
|
3
|
+
|
|
4
|
+
Scope: Define configurable options for embedded filtering pattern detection
|
|
5
|
+
|
|
6
|
+
Overview: Provides CollectionPipelineConfig for customizing linter behavior including
|
|
7
|
+
minimum number of continue patterns to flag, enable/disable toggle, and ignore
|
|
8
|
+
patterns. Integrates with the orchestrator's configuration system to allow users
|
|
9
|
+
to customize collection-pipeline detection via .thailint.yaml configuration files.
|
|
10
|
+
Follows the same configuration pattern as other thai-lint linters.
|
|
11
|
+
|
|
12
|
+
Dependencies: dataclasses, typing
|
|
13
|
+
|
|
14
|
+
Exports: CollectionPipelineConfig dataclass, DEFAULT_MIN_CONTINUES constant
|
|
15
|
+
|
|
16
|
+
Interfaces: CollectionPipelineConfig.from_dict() class method for configuration loading
|
|
17
|
+
|
|
18
|
+
Implementation: Dataclass with sensible defaults and config loading from dictionary
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
# Default threshold for minimum continue guards to flag
|
|
25
|
+
DEFAULT_MIN_CONTINUES = 1
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class CollectionPipelineConfig:
|
|
30
|
+
"""Configuration for collection-pipeline linter."""
|
|
31
|
+
|
|
32
|
+
enabled: bool = True
|
|
33
|
+
"""Whether the linter is enabled."""
|
|
34
|
+
|
|
35
|
+
min_continues: int = DEFAULT_MIN_CONTINUES
|
|
36
|
+
"""Minimum number of if/continue patterns required to flag a violation."""
|
|
37
|
+
|
|
38
|
+
ignore: list[str] = field(default_factory=list)
|
|
39
|
+
"""File patterns to ignore."""
|
|
40
|
+
|
|
41
|
+
def __post_init__(self) -> None:
|
|
42
|
+
"""Validate configuration values."""
|
|
43
|
+
if self.min_continues < 1:
|
|
44
|
+
raise ValueError(f"min_continues must be at least 1, got {self.min_continues}")
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def from_dict(
|
|
48
|
+
cls, config: dict[str, Any], language: str | None = None
|
|
49
|
+
) -> "CollectionPipelineConfig":
|
|
50
|
+
"""Load configuration from dictionary.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
config: Dictionary containing configuration values
|
|
54
|
+
language: Programming language (unused, for interface compatibility)
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
CollectionPipelineConfig instance with values from dictionary
|
|
58
|
+
"""
|
|
59
|
+
return cls(
|
|
60
|
+
enabled=config.get("enabled", True),
|
|
61
|
+
min_continues=config.get("min_continues", DEFAULT_MIN_CONTINUES),
|
|
62
|
+
ignore=config.get("ignore", []),
|
|
63
|
+
)
|