Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/cookbook/attribute-matches-type.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ When an attribute holds a repository, service, or other typed object, keeping th
```yaml
rules:
- name: attribute-matches-type
description: Attribute names must match their type annotation in snake_case
type: variable
filter: { target: attribute }
naming: { source: type_annotation, transform: snake_case }
Expand Down Expand Up @@ -46,7 +47,8 @@ The `{prefix}_{expected}` form is also allowed. For example, `source_object_cont
```
$ pnl check
contexts/billing/domain/service.py:5
[attribute-matches-type] repo (expected: subscription_repository)
[attribute-matches-type] Attribute names must match their type annotation in snake_case
repo (expected: subscription_repository)

Found 1 violation(s).
```
4 changes: 3 additions & 1 deletion docs/cookbook/bool-method-prefix.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Functions that return `bool` are easier to read at call sites when their names r
```yaml
rules:
- name: bool-method-prefix
description: Bool-returning functions must start with is_, has_, or should_
type: function
filter: { return_type: bool }
naming: { prefix: [is_, has_, should_] }
Expand Down Expand Up @@ -44,7 +45,8 @@ class SubscriptionService:
```
$ pnl check
src/domain/service.py:4
[bool-method-prefix] validate (expected prefix: is_ | has_ | should_)
[bool-method-prefix] Bool-returning functions must start with is_, has_, or should_
validate (expected prefix: is_ | has_ | should_)

Found 1 violation(s).
```
7 changes: 5 additions & 2 deletions docs/cookbook/constant-upper-case.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Module-level constants are easier to distinguish from regular variables when the
```yaml
rules:
- name: constant-upper-case
description: Module-level constants must use UPPER_CASE
type: variable
filter: { target: constant }
naming: { case: UPPER_CASE }
Expand Down Expand Up @@ -42,10 +43,12 @@ DEFAULT_TIMEOUT_SECONDS = 30
```
$ pnl check
src/config.py:3
[constant-upper-case] max_retry_count (expected case: UPPER_CASE)
[constant-upper-case] Module-level constants must use UPPER_CASE
max_retry_count (expected case: UPPER_CASE)

src/config.py:4
[constant-upper-case] default_timeout_seconds (expected case: UPPER_CASE)
[constant-upper-case] Module-level constants must use UPPER_CASE
default_timeout_seconds (expected case: UPPER_CASE)

Found 2 violation(s).
```
8 changes: 6 additions & 2 deletions docs/cookbook/decorator-filtering.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Some naming conventions only apply to a specific kind of function or class. Deco
```yaml
rules:
- name: static-factory-prefix
description: Static factory methods must start with create_ or build_
type: function
filter: { decorator: staticmethod }
naming: { prefix: [create_, build_] }
Expand All @@ -26,6 +27,7 @@ apply:
```yaml
rules:
- name: dataclass-naming
description: Dataclass names must end with Data or Config
type: class
filter: { decorator: dataclass }
naming: { suffix: [Data, Config] }
Expand Down Expand Up @@ -75,10 +77,12 @@ class OrderData:
```
$ pnl check
src/domain/order.py:5
[static-factory-prefix] from_dict (expected prefix: create_ | build_)
[static-factory-prefix] Static factory methods must start with create_ or build_
from_dict (expected prefix: create_ | build_)

src/domain/order.py:9
[dataclass-naming] OrderPayload (expected suffix: Data | Config)
[dataclass-naming] Dataclass names must end with Data or Config
OrderPayload (expected suffix: Data | Config)

Found 2 violation(s).
```
4 changes: 3 additions & 1 deletion docs/cookbook/exception-naming.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Consistent exception names make error handling code easier to scan and understan
```yaml
rules:
- name: exception-naming
description: "Exception classes must follow the <Noun><Reason>Error pattern"
type: class
filter: { base_class: Exception }
naming: { regex: "^[A-Z][a-zA-Z]+(NotFound|Invalid|Denied|Conflict|Failed)Error$" }
Expand Down Expand Up @@ -42,7 +43,8 @@ class FilterNotFoundError(Exception):
```
$ pnl check
src/domain/exceptions.py:3
[exception-naming] FilterError (expected pattern: ^[A-Z][a-zA-Z]+(NotFound|Invalid|...)Error$)
[exception-naming] Exception classes must follow the <Noun><Reason>Error pattern
FilterError (expected pattern: ^[A-Z][a-zA-Z]+(NotFound|Invalid|...)Error$)

Found 1 violation(s).
```
14 changes: 11 additions & 3 deletions docs/cookbook/layer-based-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,30 @@ Real projects have distinct layers — domain, infrastructure, API — each with
```yaml
rules:
- name: attribute-matches-type
description: Attribute names must match their type annotation in snake_case
type: variable
filter: { target: attribute }
naming: { source: type_annotation, transform: snake_case }

- name: bool-method-prefix
description: Bool-returning functions must start with is_, has_, or should_
type: function
filter: { return_type: bool }
naming: { prefix: [is_, has_, should_] }

- name: domain-module-naming
description: Module filename must match the primary class name in snake_case
type: module
naming: { source: class_name, transform: snake_case }

- name: constant-upper-case
description: Module-level constants must use UPPER_CASE
type: variable
filter: { target: constant }
naming: { case: UPPER_CASE }

- name: exception-naming
description: "Exception classes must follow the <Noun><Reason>Error pattern"
type: class
filter: { base_class: Exception }
naming: { regex: "^[A-Z][a-zA-Z]+(NotFound|Invalid|Denied|Conflict|Failed)Error$" }
Expand Down Expand Up @@ -91,13 +96,16 @@ class BillingNotFoundError(Exception):
```
$ pnl check
contexts/billing/domain/service.py:3
[constant-upper-case] max_retry (expected case: UPPER_CASE)
[constant-upper-case] Module-level constants must use UPPER_CASE
max_retry (expected case: UPPER_CASE)

contexts/billing/domain/service.py:6
[bool-method-prefix] validate (expected prefix: is_ | has_ | should_)
[bool-method-prefix] Bool-returning functions must start with is_, has_, or should_
validate (expected prefix: is_ | has_ | should_)

contexts/billing/domain/exceptions.py:3
[exception-naming] BillingError (expected pattern: ^[A-Z][a-zA-Z]+(NotFound|Invalid|...)Error$)
[exception-naming] Exception classes must follow the <Noun><Reason>Error pattern
BillingError (expected pattern: ^[A-Z][a-zA-Z]+(NotFound|Invalid|...)Error$)

Found 3 violation(s).
```
4 changes: 3 additions & 1 deletion docs/cookbook/module-matches-class.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ When each module contains one primary class, keeping the filename in sync with t
```yaml
rules:
- name: domain-module-naming
description: Module filename must match the primary class name in snake_case
type: module
naming: { source: class_name, transform: snake_case }

Expand Down Expand Up @@ -41,7 +42,8 @@ class CustomObject:
```
$ pnl check
contexts/catalog/domain/custom.py:1
[domain-module-naming] custom (expected: custom_object)
[domain-module-naming] Module filename must match the primary class name in snake_case
custom (expected: custom_object)

Found 1 violation(s).
```
10 changes: 7 additions & 3 deletions docs/getting-started/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ Create `.python-naming-linter.yaml` in your project root and define your naming
```yaml
rules:
- name: bool-method-prefix
description: Bool-returning functions must start with is_, has_, or should_
type: function
filter: { return_type: bool }
naming: { prefix: [is_, has_, should_] }

- name: exception-naming
description: "Exception classes must follow the <Noun><Reason>Error pattern"
type: class
filter: { base_class: Exception }
naming: { regex: "^[A-Z][a-zA-Z]+(NotFound|Invalid|Denied|Conflict|Failed)Error$" }
Expand Down Expand Up @@ -43,14 +45,16 @@ pnl check

## Step 3: Review the Output

Violations are reported with the file path, line number, rule name, and what was expected:
Violations are reported with the file path, line number, rule name, description, and what was expected:

```
src/domain/service.py:12
[bool-method-prefix] validate (expected prefix: is_ | has_ | should_)
[bool-method-prefix] Bool-returning functions must start with is_, has_, or should_
validate (expected prefix: is_ | has_ | should_)

src/domain/exceptions.py:8
[exception-naming] FilterError (expected pattern: ^[A-Z][a-zA-Z]+(NotFound|Invalid|...)Error$)
[exception-naming] Exception classes must follow the <Noun><Reason>Error pattern
FilterError (expected pattern: ^[A-Z][a-zA-Z]+(NotFound|Invalid|...)Error$)

Found 2 violation(s).
```
Expand Down
5 changes: 4 additions & 1 deletion docs/guide/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ Rules are the core building blocks of `pnl`. Each rule targets a specific kind o

## Structure

Every rule has three required fields and two optional ones:
Every rule has three required fields and three optional ones:

```yaml
rules:
- name: my-rule # Unique identifier for this rule
description: ... # (optional) Human-readable description shown in violation output
type: variable # What kind of name to lint
filter: { ... } # (optional) Narrow which names are checked
naming: { ... } # How the name must be formed
Expand All @@ -21,6 +22,7 @@ The `name` is used to reference the rule in `apply` blocks and in `# pnl: ignore
| Field | Required | Description |
|-------|----------|-------------|
| `name` | Yes | Unique identifier, referenced in `apply` and `# pnl: ignore` |
| `description` | No | Human-readable description shown in violation output |
| `type` | Yes | What kind of name to lint (`variable`, `function`, `class`, `module`, `package`) |
| `filter` | No | Narrow which names are checked (see [Filters](#filters) below) |
| `naming` | Yes | How the name must be formed (see [Naming Constraints](#naming-constraints) below) |
Expand Down Expand Up @@ -916,6 +918,7 @@ apply:
| Field | Required | Description |
|-------|----------|-------------|
| `name` | Yes | Unique identifier, referenced in `apply` and `# pnl: ignore` |
| `description` | No | Human-readable description shown in violation output |
| `type` | Yes | What kind of name to lint (`variable`, `function`, `class`, `module`, `package`) |
| `filter` | No | Narrow which names are checked |
| `naming` | Yes | How the name must be formed |
Expand Down
8 changes: 6 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@ pip install python-naming-linter
```yaml
rules:
- name: bool-method-prefix
description: Bool-returning functions must start with is_, has_, or should_
type: function
filter: { return_type: bool }
naming: { prefix: [is_, has_, should_] }

- name: exception-naming
description: "Exception classes must follow the <Noun><Reason>Error pattern"
type: class
filter: { base_class: Exception }
naming: { regex: "^[A-Z][a-zA-Z]+(NotFound|Invalid|Denied|Conflict|Failed)Error$" }
Expand All @@ -59,10 +61,12 @@ pnl check

```
src/domain/service.py:12
[bool-method-prefix] validate (expected prefix: is_ | has_ | should_)
[bool-method-prefix] Bool-returning functions must start with is_, has_, or should_
validate (expected prefix: is_ | has_ | should_)

src/domain/exceptions.py:8
[exception-naming] FilterError (expected pattern: ^[A-Z][a-zA-Z]+(NotFound|Invalid|...)Error$)
[exception-naming] Exception classes must follow the <Noun><Reason>Error pattern
FilterError (expected pattern: ^[A-Z][a-zA-Z]+(NotFound|Invalid|...)Error$)

Found 2 violation(s).
```
Expand Down
1 change: 1 addition & 0 deletions python_naming_linter/checkers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ class Violation:
lineno: int
name: str
message: str
rule_description: str | None = None
1 change: 1 addition & 0 deletions python_naming_linter/checkers/class_.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ def check_class(tree: ast.Module, rule: Rule, file_path: str) -> list[Violation]
lineno=node.lineno,
name=class_name,
message=msg,
rule_description=rule.description,
)
)

Expand Down
1 change: 1 addition & 0 deletions python_naming_linter/checkers/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ def check_function(tree: ast.Module, rule: Rule, file_path: str) -> list[Violati
lineno=node.lineno,
name=func_name,
message=msg,
rule_description=rule.description,
)
)

Expand Down
3 changes: 3 additions & 0 deletions python_naming_linter/checkers/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def check_module(tree: ast.Module, rule: Rule, file_path: str) -> list[Violation
lineno=0,
name=module_name,
message=msg,
rule_description=rule.description,
)
)

Expand All @@ -63,6 +64,7 @@ def check_module(tree: ast.Module, rule: Rule, file_path: str) -> list[Violation
lineno=0,
name=module_name,
message=msg,
rule_description=rule.description,
)
)

Expand All @@ -86,6 +88,7 @@ def check_module(tree: ast.Module, rule: Rule, file_path: str) -> list[Violation
lineno=0,
name=module_name,
message=msg,
rule_description=rule.description,
)
)

Expand Down
2 changes: 2 additions & 0 deletions python_naming_linter/checkers/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def check_package(rule: Rule, package_name: str) -> list[Violation]:
lineno=0,
name=package_name,
message="expected: snake_case",
rule_description=rule.description,
)
)

Expand All @@ -31,6 +32,7 @@ def check_package(rule: Rule, package_name: str) -> list[Violation]:
lineno=0,
name=package_name,
message=f"expected pattern: {naming['regex']}",
rule_description=rule.description,
)
)

Expand Down
1 change: 1 addition & 0 deletions python_naming_linter/checkers/variable.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ def check_variable(tree: ast.Module, rule: Rule, file_path: str) -> list[Violati
lineno=lineno,
name=var_name,
message=msg,
rule_description=rule.description,
)
)
return violations
2 changes: 2 additions & 0 deletions python_naming_linter/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class Rule:
type: str
naming: dict
filter: dict = field(default_factory=dict)
description: str | None = None


@dataclass
Expand Down Expand Up @@ -49,6 +50,7 @@ def _parse_rules(rules_data: list[dict]) -> list[Rule]:
type=r["type"],
naming=r.get("naming", {}),
filter=r.get("filter", {}),
description=r.get("description"),
)
)
return rules
Expand Down
6 changes: 5 additions & 1 deletion python_naming_linter/reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ def format_violations(file_path: str, violations: list[Violation]) -> str:
lines = []
for v in violations:
lines.append(f"{file_path}:{v.lineno}")
lines.append(f" [{v.rule_name}] {v.name} ({v.message})")
if v.rule_description:
lines.append(f" [{v.rule_name}] {v.rule_description}")
else:
lines.append(f" [{v.rule_name}]")
lines.append(f" {v.name} ({v.message})")
lines.append("")

return "\n".join(lines)
Loading
Loading