Configuration Reference¶
Complete reference for configuring thailint linters with presets, ignore patterns, and all options.
Table of Contents¶
- Quick Start with init-config
- Configuration File Basics
- Ignore Patterns (All Linters)
- Output Formats
- Configuration Schema
- File Placement
- Nesting Depth
- SRP
- DRY
- Collection Pipeline
- Magic Numbers
- Method Property
- Stateless Class
- Advanced Patterns
- Best Practices
- Troubleshooting
Linter-Specific Configuration
Each linter has its own dedicated documentation page with detailed configuration examples. See the individual linter docs for: improper-logging, performance, lazy-ignores, lbyl, stringly-typed, file-header, blocking-async, clone-abuse, unwrap-abuse, and cqs.
Quick Start with init-config¶
The fastest way to configure thailint is using the init-config command:
# Interactive mode (recommended for first-time setup)
thailint init-config
# Non-interactive with preset (for CI/CD or quick setup)
thailint init-config --preset lenient --non-interactive
# Overwrite existing config
thailint init-config --preset standard --force
# Custom output location
thailint init-config --output config/linting.yaml
Presets Explained¶
strict - New Projects¶
Best for: Greenfield projects, strict code quality
Characteristics: - Only universal values (-1, 0, 1) - Very low max_small_integer - Catches almost all numeric literals
standard - Balanced (Default)¶
Best for: Most projects
Characteristics: - Common small numbers (0-5) - Powers of 10 - Good balance of strictness
lenient - Existing Codebases¶
Best for: Legacy code, gradual adoption
magic-numbers:
allowed_numbers:
- -1
- 0
- 1
- 2
- 3
- 4
- 5
- 10
- 60 # Time: seconds in minute
- 100
- 1000
- 1024 # Binary: bytes in KB
- 3600 # Time: seconds in hour
max_small_integer: 10
Characteristics: - Includes time conversions (60s, 3600s) - Includes binary units (1024) - Reduces false positives
Configuration File Basics¶
File Locations¶
thailint searches for configuration in this order:
- Command-line flag:
thailint --config custom.yaml magic-numbers src/ - Current directory:
.thailint.yamlor.thailint.json - pyproject.toml:
[tool.thailint]section inpyproject.toml - Project root: Searches up directory tree
- Default config: Built-in defaults if no file found
Ignore Patterns (All Linters)¶
All linters support the ignore field to exclude files from linting. This section documents the complete glob pattern syntax and matching behavior.
Pattern Syntax¶
thailint uses Python's pathlib.Path.match() for glob pattern matching with substring fallback:
| Pattern Type | Syntax | Example | Matches |
|---|---|---|---|
| Exact file | path/to/file.py |
backend/app/famous_tracks.py |
Exact file path |
| Wildcard (*) | *.py |
test_*.py |
Files starting with test_ |
| Recursive ()** | **/pattern |
**/test_*.py |
Recursively matches in any directory |
| Directory | dir/** |
tests/** |
All files in directory tree |
| Substring | substring |
famous_tracks |
Any path containing substring |
How Pattern Matching Works¶
Important: Patterns are matched against the full file path relative to where you run the command.
# If you run from project root:
thailint magic-numbers backend/
# File path seen by thailint:
backend/app/famous_tracks.py
# Patterns that match:
- "backend/app/famous_tracks.py" # Exact match
- "**/famous_tracks.py" # Recursive match
- "backend/**" # Directory match
- "famous_tracks" # Substring match
Real-World Examples¶
Example 1: Ignore Specific File¶
Matches:
- backend/app/famous_tracks.py ✅
Does NOT match:
- backend/app/other_file.py ❌
- frontend/famous_tracks.py ❌
Example 2: Ignore All Test Files¶
magic-numbers:
ignore:
- "**/test_*.py" # test_foo.py in any directory
- "**/*_test.py" # foo_test.py in any directory
- "**/*.test.ts" # foo.test.ts in any directory
- "**/*.spec.js" # foo.spec.js in any directory
Matches:
- tests/test_app.py ✅
- backend/tests/test_models.py ✅
- src/utils_test.py ✅
- components/Button.test.ts ✅
- services/api.spec.js ✅
Example 3: Ignore Entire Directories¶
dry:
ignore:
- "tests/**" # All files in tests/ tree
- "**/migrations/**" # All migration dirs
- "node_modules/**" # Dependencies
Matches:
- tests/unit/test_foo.py ✅
- backend/tests/test_bar.py ✅
- backend/db/migrations/001_init.py ✅
- node_modules/package/index.js ✅
Example 4: Multiple Patterns (Real Project)¶
magic-numbers:
ignore:
# Specific files with legitimate magic numbers
- "backend/app/famous_tracks.py"
- "backend/config/constants.py"
# All test files
- "**/test_*.py"
- "**/*_test.py"
- "**/*.test.ts"
- "**/*.spec.js"
# Generated/vendor code
- "**/migrations/**"
- "node_modules/**"
- "**/dist/**"
- "**/build/**"
Common Patterns Reference¶
| Use Case | Pattern | Notes |
|---|---|---|
| Python tests | **/test_*.py, **/*_test.py |
Covers pytest conventions |
| JS/TS tests | **/*.test.{ts,js}, **/*.spec.{ts,js} |
Covers Jest/Vitest conventions |
| Test directories | tests/**, **/tests/** |
All files in test dirs |
| Generated code | **/migrations/**, **/generated/** |
Database migrations, codegen |
| Dependencies | node_modules/**, vendor/** |
Third-party code |
| Build outputs | dist/**, build/**, out/** |
Compiled artifacts |
| Config files | **/config/*.py, **/*_config.py |
Configuration files |
| Type stubs | **/*.pyi, **/stubs/** |
Python type hints |
Debugging Ignore Patterns¶
If your ignore patterns aren't working:
-
Check the file path thailint sees:
-
Verify pattern matching:
-
Common mistakes:
- ❌ Pattern:
famous_tracks.py(too specific, no directory context) -
✅ Pattern:
**/famous_tracks.py(matches in any directory) -
❌ Pattern:
test/*.py(only matches immediate children) -
✅ Pattern:
test/**/*.py(matches all descendants) -
Escape special characters in filenames:
Per-Linter Ignore Patterns¶
Each linter can have its own ignore list:
magic-numbers:
ignore:
- "backend/config/constants.py" # Lots of config numbers
- "**/test_*.py"
nesting:
ignore:
- "backend/legacy/**" # Deep nesting in legacy code
- "**/migrations/**"
dry:
ignore:
- "tests/**" # Duplicate test setup is OK
- "**/models.py" # Similar model definitions
srp:
ignore:
- "backend/monolith.py" # Legacy god object
- "**/admin.py" # Admin classes often complex
Version Notes¶
- 0.4.1+: Ignore patterns fully functional for magic-numbers linter
- 0.4.0: Ignore patterns not implemented (known bug, fixed in 0.4.1)
- Earlier: Check release notes for ignore support per linter
File Formats¶
YAML Format (Recommended)¶
.thailint.yaml:
# File placement linter configuration
file-placement:
# Global patterns apply to all directories
global_patterns:
allow:
- pattern: ".*\\.py$"
message: "Python files are allowed"
deny:
- pattern: "^(?!src/|tests/).*\\.py$"
message: "Python files must be in src/ or tests/"
# Directory-specific rules
directories:
src:
allow:
- ".*\\.py$"
deny:
- "test_.*\\.py$"
tests:
allow:
- "test_.*\\.py$"
- "conftest\\.py$"
- "__init__\\.py$"
# Files and directories to ignore
ignore:
- "__pycache__/"
- "*.pyc"
- ".venv/"
- ".git/"
- "node_modules/"
# Future linters can be added as separate top-level sections
# code-quality:
# max-complexity: 10
# ...
JSON Format¶
.thailint.json:
{
"file-placement": {
"global_patterns": {
"allow": [
{
"pattern": ".*\\.py$",
"message": "Python files are allowed"
}
],
"deny": [
{
"pattern": "^(?!src/|tests/).*\\.py$",
"message": "Python files must be in src/ or tests/"
}
]
},
"directories": {
"src": {
"allow": [".*\\.py$"],
"deny": ["test_.*\\.py$"]
},
"tests": {
"allow": ["test_.*\\.py$", "conftest\\.py$", "__init__\\.py$"]
}
},
"ignore": [
"__pycache__/",
"*.pyc",
".venv/",
".git/",
"node_modules/"
]
}
}
TOML Format (pyproject.toml)¶
Add a [tool.thailint] section to your existing pyproject.toml:
[tool.thailint]
[tool.thailint.file-placement]
ignore = ["__pycache__/", "*.pyc", ".venv/", ".git/", "node_modules/"]
[tool.thailint.file-placement.global_patterns]
# Configure via nested TOML tables
[tool.thailint.file-placement.directories.src]
allow = ['.*\.py$']
deny = ['test_.*\.py$']
[tool.thailint.file-placement.directories.tests]
allow = ['test_.*\.py$', 'conftest\.py$', '__init__\.py$']
Or using inline tables for simpler configs:
[tool.thailint]
dry = {enabled = true, min_duplicate_lines = 4}
nesting = {enabled = true, max_nesting_depth = 3}
magic-numbers = {enabled = true, allowed_numbers = [-1, 0, 1, 2, 10, 100]}
Output Formats¶
thailint supports three output formats for linting results:
Text Format (Default)¶
Human-readable format for terminal output:
Example output:
src/processor.py:42:9 - Nesting depth 5 exceeds maximum 4 in function 'process_data'
src/handler.py:88:13 - Nesting depth 6 exceeds maximum 4 in function 'handle_request'
Found 2 violations
JSON Format¶
Machine-readable JSON for custom tooling and CI/CD:
Example output:
[
{
"rule_id": "nesting.excessive-depth",
"message": "Nesting depth 5 exceeds maximum 4",
"file_path": "src/processor.py",
"line": 42,
"column": 8
}
]
SARIF Format¶
SARIF (Static Analysis Results Interchange Format) v2.1.0 for GitHub Code Scanning, Azure DevOps, and VS Code:
Example output:
{
"version": "2.1.0",
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
"runs": [{
"tool": {
"driver": {
"name": "thai-lint",
"version": "0.5.0",
"informationUri": "https://github.com/be-wise-be-kind/thai-lint"
}
},
"results": [...]
}]
}
Use cases: - GitHub Code Scanning integration - VS Code SARIF Viewer - Azure DevOps security dashboard - Enterprise security tooling
See SARIF Output Guide for comprehensive documentation on SARIF integration.
Configuration Schema¶
Root Level Options¶
| Option | Type | Required | Description |
|---|---|---|---|
file-placement |
object | No | File placement linter configuration |
nesting |
object | No | Nesting depth linter configuration |
srp |
object | No | Single Responsibility Principle linter configuration |
dry |
object | No | DRY (Don't Repeat Yourself) linter configuration |
magic-numbers |
object | No | Magic numbers linter configuration |
code-quality |
object | No | Code quality linter configuration (future) |
File Placement Linter Options¶
Under the file-placement key:
| Option | Type | Required | Description |
|---|---|---|---|
global_patterns |
object | No | Patterns that apply to all directories |
directories |
object | No | Directory-specific rule configurations |
ignore |
array | No | Files/directories to skip during linting |
Nesting Depth Linter Options¶
Under the nesting key:
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Enable/disable nesting depth linter |
max_nesting_depth |
integer | 4 |
Maximum allowed nesting depth within functions |
Example:
Single Responsibility Principle (SRP) Linter Options¶
Under the srp key:
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Enable/disable SRP linter |
max_responsibility_score |
integer | 5 |
Maximum allowed responsibility score |
Example:
DRY (Don't Repeat Yourself) Linter Options¶
Under the dry key:
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Enable/disable DRY linter |
min_duplicate_lines |
integer | 4 |
Minimum lines for duplicate detection |
min_duplicate_tokens |
integer | 30 |
Minimum tokens for duplicate detection |
min_occurrences |
integer | 2 |
Report duplicates appearing N+ times |
storage_mode |
string | "memory" |
SQLite storage mode: "memory" (RAM) or "tempfile" (disk) |
ignore |
array | [] |
Files/directories to exclude from DRY analysis |
filters |
object | See below | False positive filtering configuration |
python |
object | {} |
Python-specific threshold overrides |
typescript |
object | {} |
TypeScript-specific threshold overrides |
javascript |
object | {} |
JavaScript-specific threshold overrides |
Filter Options (under dry.filters):
| Option | Type | Default | Description |
|---|---|---|---|
keyword_argument_filter |
boolean | true |
Filter duplicate keyword argument patterns |
import_group_filter |
boolean | true |
Filter duplicate import statement groups |
Language-Specific Overrides:
Each language key (python, typescript, javascript) supports:
| Option | Type | Description |
|---|---|---|
min_duplicate_lines |
integer | Override global line threshold |
min_duplicate_tokens |
integer | Override global token threshold |
min_occurrences |
integer | Override global occurrence threshold |
Complete YAML Example:
dry:
enabled: true
# Detection thresholds
min_duplicate_lines: 4
min_duplicate_tokens: 30
min_occurrences: 2
# Storage configuration
storage_mode: "memory" # Options: "memory" (default, fast) or "tempfile" (for large projects)
# Language-specific thresholds
python:
min_occurrences: 3 # Python: require 3+ duplicates
min_duplicate_lines: 5 # Python: stricter line threshold
typescript:
min_duplicate_tokens: 35 # TypeScript: require more tokens
javascript:
min_occurrences: 2 # JavaScript: use global default
# Ignore patterns
ignore:
- "tests/"
- "__init__.py"
- "migrations/"
- "*.generated.py"
# False positive filtering
filters:
keyword_argument_filter: true
import_group_filter: true
Complete JSON Example:
{
"dry": {
"enabled": true,
"min_duplicate_lines": 4,
"min_duplicate_tokens": 30,
"min_occurrences": 2,
"storage_mode": "memory",
"python": {
"min_occurrences": 3,
"min_duplicate_lines": 5
},
"typescript": {
"min_duplicate_tokens": 35
},
"javascript": {
"min_occurrences": 2
},
"ignore": [
"tests/",
"__init__.py",
"migrations/",
"*.generated.py"
],
"filters": {
"keyword_argument_filter": true,
"import_group_filter": true
}
}
}
Configuration Hierarchy:
Language-specific settings override global settings:
- Language-specific (highest priority):
dry.python.min_occurrences - Global defaults (fallback):
dry.min_occurrences
Storage Behavior:
- SQLite storage used for fast duplicate detection during each run
storage_mode: "memory"(default): Stores in RAM for best performancestorage_mode: "tempfile": Stores in temporary disk file for large projects- Storage is automatically cleared after each run
- Every run analyzes files fresh (no persistence between runs)
Filter Behavior:
Filters reduce false positives:
- keyword_argument_filter: Ignores function calls with only keyword arguments (common pattern in configs)
- import_group_filter: Ignores import statement groups (naturally similar structure)
Collection Pipeline Linter Options¶
Under the collection-pipeline key:
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Enable/disable collection-pipeline linter |
min_continues |
integer | 1 |
Minimum if/continue patterns to flag |
suggest_filter |
boolean | false |
Suggest filter() instead of generator expression |
suggest_comprehension |
boolean | false |
Suggest list comprehension for .append patterns |
ignore |
array | [] |
Glob patterns for files to skip |
Example:
collection-pipeline:
enabled: true
min_continues: 1
suggest_filter: false
ignore:
- "tests/**"
- "**/legacy/**"
{
"collection-pipeline": {
"enabled": true,
"min_continues": 1,
"suggest_filter": false,
"ignore": ["tests/**", "**/legacy/**"]
}
}
Configuration Behavior:
- min_continues: Controls how many sequential if/continue patterns are needed to trigger a violation. Set to
1to catch all embedded filtering, or2to only flag more complex patterns. - suggest_filter: When
true, suggestions usefilter()instead of generator expressions. - suggest_comprehension: When
true, suggests list comprehensions for patterns involving.append(). - ignore: File patterns using glob syntax. Test files are often ignored since they may have intentional patterns for testing.
Acceptable Contexts (not flagged regardless of config):
- Generator expressions already: Loops iterating over
(x for x in items if cond) - filter() already: Loops iterating over
filter(pred, items) - Walrus operator conditions: Conditions with side effects like
if not (result := validate(item)) - If with else branch: If/else patterns (not simple filters)
Strictness Presets:
# Strict - flag all embedded filtering
collection-pipeline:
min_continues: 1
# Lenient - only flag complex patterns
collection-pipeline:
min_continues: 2
Magic Numbers Linter Options¶
Under the magic-numbers key:
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Enable/disable magic numbers linter |
allowed_numbers |
array | [-1, 0, 1, 2, 10, 100, 1000] |
Numbers that are acceptable without named constants |
max_small_integer |
integer | 10 |
Maximum value allowed in range() or enumerate() without being flagged |
Example:
{
"magic-numbers": {
"enabled": true,
"allowed_numbers": [-1, 0, 1, 2, 10, 100, 1000],
"max_small_integer": 10
}
}
Configuration Behavior:
- allowed_numbers: Numbers in this list will not be flagged as magic numbers. Common values like
-1,0,1,2are often self-explanatory and don't need constants. - max_small_integer: Small integers used in
range()orenumerate()below this threshold are considered acceptable (e.g.,range(5)is clear, butrange(100)should use a constant).
Acceptable Contexts (always allowed regardless of config):
- Constant definitions: Numbers assigned to UPPERCASE variable names (e.g.,
MAX_SIZE = 100) - Small integers in
range(): Integers ≤max_small_integerinrange()calls - Small integers in
enumerate(): Integers ≤max_small_integeras start value inenumerate() - Test files: Numbers in files matching
test_*.py,*_test.py,*.test.ts,*.spec.tspatterns - String repetition: Integers used for string multiplication (e.g.,
"-" * 40)
Complete YAML Example:
magic-numbers:
enabled: true
# Numbers that don't need constants (self-explanatory)
allowed_numbers:
- -1 # Common error/not-found indicator
- 0 # Zero/false/empty
- 1 # One/true/first
- 2 # Two/second
- 10 # Decimal base
- 100 # Percentage base
- 1000 # Thousand
# Maximum integer allowed in range() without constant
max_small_integer: 10
Complete JSON Example:
{
"magic-numbers": {
"enabled": true,
"allowed_numbers": [-1, 0, 1, 2, 10, 100, 1000],
"max_small_integer": 10
}
}
Customization Examples:
# Strict - only very common values allowed
magic-numbers:
allowed_numbers: [-1, 0, 1]
max_small_integer: 3
# Standard - recommended (default)
magic-numbers:
allowed_numbers: [-1, 0, 1, 2, 10, 100, 1000]
max_small_integer: 10
# Lenient - includes time units
magic-numbers:
allowed_numbers: [-1, 0, 1, 2, 10, 24, 60, 100, 1000, 3600]
max_small_integer: 20
Method Property Linter Options¶
Under the method-property key:
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Enable/disable method-property linter |
max_body_statements |
integer | 3 |
Maximum statements in method body to be considered a property candidate |
ignore |
array | [] |
File patterns to exclude from analysis |
ignore_methods |
array | [] |
Method names to exclude from analysis |
exclude_prefixes |
array | [] |
Additional action verb prefixes to exclude (extends defaults) |
exclude_names |
array | [] |
Additional action verb names to exclude (extends defaults) |
exclude_prefixes_override |
array | - | Replace default prefixes entirely |
exclude_names_override |
array | - | Replace default names entirely |
Example:
method-property:
enabled: true
max_body_statements: 3
ignore:
- "tests/"
- "*_test.py"
ignore_methods:
- "_get_css_styles"
- "_get_default_standards"
{
"method-property": {
"enabled": true,
"max_body_statements": 3,
"ignore": ["tests/", "*_test.py"],
"ignore_methods": ["_get_css_styles", "_get_default_standards"]
}
}
Configuration Behavior:
- max_body_statements: Methods with more statements than this threshold are not flagged. Simple property candidates typically have 1-3 statements (optional docstring, optional validation, return).
- ignore: File patterns using glob syntax. Test files (
test_*.py,*_test.py) are always ignored regardless of this setting. - ignore_methods: Method names to exclude. Useful for project-specific patterns that shouldn't be flagged.
Automatic Exclusions (always excluded regardless of config):
- Methods with parameters: Properties can't take parameters beyond
self - Decorated methods:
@staticmethod,@classmethod,@abstractmethod,@property - Dunder methods:
__str__,__repr__,__init__, etc. - Action verb methods:
to_*,finalize,serialize,validate - Methods with side effects: Assignments to
self.*attributes - Methods with control flow:
if,for,while,try,with - Methods calling external functions: Top-level function calls like
print(),fetch()
stateless-class¶
Detects Python classes without state that should be refactored to module-level functions.
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Enable/disable stateless-class linter |
min_methods |
integer | 2 |
Minimum number of methods for a class to be flagged |
ignore |
array | [] |
File patterns to exclude from analysis |
Example:
Configuration Behavior:
- min_methods: Classes with fewer methods than this threshold are not flagged. Set to 2 by default because single-method classes are often legitimate patterns (e.g., simple wrappers).
- ignore: File patterns using glob syntax. Useful for excluding test files or legacy code.
Automatic Exclusions (always excluded regardless of config):
- Classes with constructors: Classes with
__init__or__new__methods - Classes with instance state: Classes where methods assign to
self.attr - Classes with class attributes: Classes with class-level variable definitions
- ABC and Protocol classes: Abstract base classes and type protocols
- Classes with base classes: Classes that inherit from other classes (polymorphism)
- Decorated classes: Classes with any decorator (
@dataclass,@register, etc.) - Single-method classes: Classes with 0 or 1 methods
Global Patterns¶
Apply rules across the entire project regardless of directory.
file-placement:
global_patterns:
allow:
- pattern: ".*\\.py$"
message: "Python files are allowed globally"
deny:
- pattern: "secret.*"
message: "Files containing 'secret' are not allowed"
Fields:
- pattern (string, required): Regex pattern to match file paths
- message (string, optional): Custom error message when pattern matches
Directory-Specific Rules¶
Define rules that apply only within specific directories.
file-placement:
directories:
# Directory name (relative to project root)
src:
# Allowlist: Only these patterns are allowed
allow:
- ".*\\.py$" # Python source files
- ".*\\.pyi$" # Type stub files
- "__init__\\.py$" # Package init files
# Denylist: These patterns are forbidden
deny:
- "test_.*\\.py$" # No test files in src/
- ".*_test\\.py$" # No test files in src/
Pattern Precedence: 1. Deny takes precedence: If a file matches both allow and deny, it's denied 2. Most specific wins: More specific patterns override general ones 3. Directory rules override global: Directory-specific rules take precedence over global patterns
Ignore Patterns¶
Files and directories to skip during linting.
file-placement:
ignore:
- "__pycache__/" # Python cache directories
- "*.pyc" # Compiled Python files
- "*.pyo" # Optimized Python files
- ".venv/" # Virtual environments
- "venv/"
- ".git/" # Version control
- ".svn/"
- "node_modules/" # Node.js dependencies
- ".DS_Store" # macOS metadata
- "*.egg-info/" # Python package metadata
- "dist/" # Build artifacts
- "build/"
- ".pytest_cache/" # Test cache
- ".mypy_cache/" # Type checker cache
- ".ruff_cache/" # Linter cache
Ignore Pattern Formats:
- Exact match: .git/ matches .git/ directory
- Wildcard: *.pyc matches all .pyc files
- Directory wildcard: */temp/ matches any temp subdirectory
Pattern Syntax¶
Regex Patterns¶
thailint uses Python regex (re module) for pattern matching.
Common Patterns:
# File extensions
".*\\.py$" # Python files
".*\\.(ts|tsx)$" # TypeScript files
".*\\.(js|jsx)$" # JavaScript files
# Naming conventions
"test_.*\\.py$" # Test files (prefix)
".*_test\\.py$" # Test files (suffix)
"^[A-Z].*\\.py$" # Files starting with uppercase
# Path patterns
"^src/.*\\.py$" # Python files in src/
"^(?!src/).*\\.py$" # Python files NOT in src/
"^(src|lib)/.*$" # Files in src/ OR lib/
# Special files
"__init__\\.py$" # Package init files
"conftest\\.py$" # Pytest config files
Regex Special Characters:
| Character | Meaning | Example |
|---|---|---|
. |
Any character | a.c matches "abc", "a1c" |
* |
Zero or more of previous | a* matches "", "a", "aa" |
+ |
One or more of previous | a+ matches "a", "aa" |
? |
Zero or one of previous | a? matches "", "a" |
^ |
Start of string | ^src/ matches "src/file.py" |
$ |
End of string | \\.py$ matches "file.py" |
\| |
OR operator | (a\|b) matches "a" or "b" |
[] |
Character class | [abc] matches "a", "b", or "c" |
[^] |
Negated class | [^abc] matches anything except a, b, c |
() |
Grouping | (test\|spec) groups alternatives |
\\ |
Escape character | \\. matches literal "." |
Important: Escape special characters in YAML strings:
# Wrong - dot matches any character
allow:
- "file.py"
# Correct - dot is escaped to match literal "."
allow:
- "file\\.py"
Negative Lookahead¶
Exclude specific paths using negative lookahead:
# Match Python files NOT in src/ or tests/
"^(?!src/|tests/).*\\.py$"
# Match files NOT starting with "test_"
"^(?!test_).*\\.py$"
# Match TypeScript files NOT in node_modules/
"^(?!.*node_modules/).*\\.ts$"
Example Configurations¶
Python Project¶
file-placement:
global_patterns:
deny:
- pattern: "^(?!src/|tests/|scripts/).*\\.py$"
message: "Python files must be in src/, tests/, or scripts/"
directories:
src:
allow:
- ".*\\.py$"
- "__init__\\.py$"
deny:
- "test_.*\\.py$"
- ".*_test\\.py$"
tests:
allow:
- "test_.*\\.py$"
- ".*_test\\.py$"
- "conftest\\.py$"
- "__init__\\.py$"
scripts:
allow:
- ".*\\.py$"
ignore:
- "__pycache__/"
- "*.pyc"
- ".venv/"
- ".pytest_cache/"
- "*.egg-info/"
TypeScript/React Project¶
file-placement:
global_patterns:
deny:
- pattern: "^(?!src/|tests/|scripts/).*\\.(ts|tsx)$"
message: "TypeScript files must be in src/, tests/, or scripts/"
directories:
src:
allow:
- ".*\\.tsx?$" # .ts or .tsx files
- ".*\\.css$"
- ".*\\.scss$"
deny:
- ".*\\.test\\.tsx?$"
- ".*\\.spec\\.tsx?$"
tests:
allow:
- ".*\\.test\\.tsx?$"
- ".*\\.spec\\.tsx?$"
ignore:
- "node_modules/"
- "dist/"
- "build/"
- "*.d.ts"
- ".next/"
Monorepo Configuration¶
global_patterns:
deny:
- pattern: "^(?!packages/|apps/|libs/).*\\.(py|ts|tsx|js|jsx)$"
message: "Code files must be in packages/, apps/, or libs/"
directories:
# Backend package
packages/backend/src:
allow:
- ".*\\.py$"
deny:
- "test_.*\\.py$"
packages/backend/tests:
allow:
- "test_.*\\.py$"
- "conftest\\.py$"
# Frontend app
apps/web/src:
allow:
- ".*\\.(tsx?|jsx?|css|scss)$"
deny:
- ".*\\.(test|spec)\\.(tsx?|jsx?)$"
apps/web/tests:
allow:
- ".*\\.(test|spec)\\.(tsx?|jsx?)$"
ignore:
- "**/node_modules/"
- "**/__pycache__/"
- "**/dist/"
- "**/build/"
Microservices Architecture¶
directories:
# API Service
services/api/src:
allow:
- ".*\\.py$"
deny:
- "test_.*"
services/api/tests:
allow:
- "test_.*\\.py$"
# Auth Service
services/auth/src:
allow:
- ".*\\.py$"
deny:
- "test_.*"
services/auth/tests:
allow:
- "test_.*\\.py$"
# Shared libraries
shared/libs:
allow:
- ".*\\.py$"
ignore:
- "**/__pycache__/"
- "**/.venv/"
- "**/dist/"
Advanced Patterns¶
Complex Directory Structures¶
directories:
# Allow only models in models directory
src/models:
allow:
- ".*_model\\.py$"
- "__init__\\.py$"
deny:
- "^(?!.*_model\\.py$|__init__\\.py$).*"
# Controllers must end with _controller.py
src/controllers:
allow:
- ".*_controller\\.py$"
- "__init__\\.py$"
# Services must end with _service.py
src/services:
allow:
- ".*_service\\.py$"
- "__init__\\.py$"
File Naming Conventions¶
directories:
src/components:
# React components: PascalCase.tsx
allow:
- "^[A-Z][a-zA-Z0-9]*\\.tsx$"
- "index\\.tsx?$"
deny:
- "^[a-z].*\\.tsx$" # No lowercase component names
src/utils:
# Utilities: snake_case.py or kebab-case.ts
allow:
- "^[a-z][a-z0-9_]*\\.py$"
- "^[a-z][a-z0-9-]*\\.ts$"
deny:
- "^[A-Z].*" # No PascalCase utilities
Environment-Specific Files¶
directories:
config:
allow:
- "^(development|staging|production)\\.yaml$"
- "^config\\..*\\.(yaml|json)$"
deny:
- ".*\\.local\\..*" # No local override files in repo
environments:
allow:
- "^\\.env\\.(development|staging|production)$"
deny:
- "^\\.env$" # No unqualified .env files
Inline Configuration¶
Override config file with CLI flags:
# Inline JSON rules (single quotes for shell, double quotes for JSON)
thai-lint file-placement . --rules '{
"allow": [".*\\.py$"],
"deny": ["test_.*\\.py$"]
}'
# Compact format
thai-lint file-placement . --rules '{"allow": [".*\\.py$"], "deny": ["test_.*"]}'
Validation and Testing¶
Validate Configuration¶
# Test config with verbose output
thai-lint --verbose lint file-placement . --config .thailint.yaml
# Check specific directory
thai-lint file-placement src/ --config .thailint.yaml
Debug Pattern Matching¶
Use Python regex tester:
import re
pattern = r"^(?!src/|tests/).*\.py$"
test_paths = ["main.py", "src/api.py", "tests/test_api.py"]
for path in test_paths:
match = re.match(pattern, path)
print(f"{path}: {'MATCH' if match else 'NO MATCH'}")
Output:
Best Practices¶
1. Start Simple, Expand Gradually¶
# Start with basic rules
directories:
src:
allow:
- ".*\\.py$"
# Expand as needed
directories:
src:
allow:
- ".*\\.py$"
deny:
- "test_.*\\.py$"
2. Use Comments for Complex Patterns¶
directories:
src:
deny:
# No test files in src/ - they belong in tests/
- "test_.*\\.py$"
# No temporary or local files
- ".*\\.local\\.py$"
- ".*\\.tmp\\.py$"
# No backup files
- ".*\\.bak$"
- ".*~$"
3. Leverage Global Patterns for Project-Wide Rules¶
# Global deny patterns apply everywhere
global_patterns:
deny:
# No secrets anywhere
- pattern: ".*secret.*"
message: "Files containing 'secret' are forbidden"
# No credentials
- pattern: ".*credentials.*"
message: "Credential files should not be committed"
# Python files only in designated directories
- pattern: "^(?!src/|tests/|scripts/).*\\.py$"
message: "Python files must be in src/, tests/, or scripts/"
4. Test Patterns Before Deployment¶
# Test on small subset first
thai-lint file-placement src/ --config .thailint.yaml
# Then expand to full project
thai-lint file-placement . --config .thailint.yaml
5. Document Custom Patterns¶
# Document why patterns exist
directories:
src/api:
# API endpoints must use _api.py suffix for clarity
# This helps distinguish endpoints from business logic
allow:
- ".*_api\\.py$"
- "__init__\\.py$"
Troubleshooting¶
Pattern Not Matching¶
Problem: Pattern doesn't match expected files
Solution: Check regex escaping
# Wrong - matches any character followed by "py"
".*\.py"
# Correct - matches literal ".py" extension
".*\\.py$"
Deny Pattern Not Working¶
Problem: Files still pass despite deny pattern
Solution: Remember deny takes precedence, check pattern specificity
# This will deny test files even if they match allow
directories:
src:
allow:
- ".*\\.py$" # Allows all Python files
deny:
- "test_.*\\.py$" # But denies test files (deny wins)
Config File Not Found¶
Problem: "Config file not found" error
Solutions:
1. Ensure file is named .thailint.yaml, .thailint.json, or config is in pyproject.toml under [tool.thailint]
2. Check file is in current directory or project root
3. Use --config to specify explicitly:
Invalid YAML/JSON¶
Problem: Parse error when loading config
Solution: Validate syntax
# Validate YAML
python -c "import yaml; yaml.safe_load(open('.thailint.yaml'))"
# Validate JSON
python -c "import json; json.load(open('.thailint.json'))"
Configuration Templates¶
Minimal Configuration¶
Standard Python Project¶
global_patterns:
deny:
- pattern: "^(?!src/|tests/).*\\.py$"
message: "Python files must be in src/ or tests/"
directories:
src:
allow:
- ".*\\.py$"
deny:
- "test_.*"
tests:
allow:
- "test_.*\\.py$"
- "conftest\\.py$"
ignore:
- "__pycache__/"
- ".venv/"
- "*.pyc"
Multi-Language Project¶
global_patterns:
deny:
- pattern: "^(?!src/|tests/).*\\.(py|ts|tsx|js|jsx)$"
message: "Code files must be in src/ or tests/"
directories:
src:
allow:
- ".*\\.(py|ts|tsx|js|jsx)$"
deny:
- ".*(test|spec)\\.(py|ts|tsx|js|jsx)$"
tests:
allow:
- ".*(test|spec)\\.(py|ts|tsx|js|jsx)$"
ignore:
- "__pycache__/"
- "node_modules/"
- "*.pyc"
- "dist/"
- "build/"
Next Steps¶
- Getting Started - Basic configuration setup
- File Placement Linter - Detailed linter documentation
- CLI Reference - Command-line usage
- API Reference - Programmatic configuration