python-naming-linter 0.1.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.
Files changed (43) hide show
  1. python_naming_linter-0.1.0/.claude/skills/commit/SKILL.md +24 -0
  2. python_naming_linter-0.1.0/.claude/skills/release/SKILL.md +14 -0
  3. python_naming_linter-0.1.0/.github/workflows/ci.yaml +37 -0
  4. python_naming_linter-0.1.0/.github/workflows/publish.yaml +59 -0
  5. python_naming_linter-0.1.0/.gitignore +8 -0
  6. python_naming_linter-0.1.0/.pre-commit-config.yaml +19 -0
  7. python_naming_linter-0.1.0/.pre-commit-hooks.yaml +6 -0
  8. python_naming_linter-0.1.0/CLAUDE.md +9 -0
  9. python_naming_linter-0.1.0/CONTRIBUTING.md +84 -0
  10. python_naming_linter-0.1.0/LICENSE +21 -0
  11. python_naming_linter-0.1.0/PKG-INFO +317 -0
  12. python_naming_linter-0.1.0/README.md +291 -0
  13. python_naming_linter-0.1.0/pyproject.toml +102 -0
  14. python_naming_linter-0.1.0/python_naming_linter/__init__.py +0 -0
  15. python_naming_linter-0.1.0/python_naming_linter/checkers/__init__.py +12 -0
  16. python_naming_linter-0.1.0/python_naming_linter/checkers/class_.py +160 -0
  17. python_naming_linter-0.1.0/python_naming_linter/checkers/function.py +137 -0
  18. python_naming_linter-0.1.0/python_naming_linter/checkers/module.py +92 -0
  19. python_naming_linter-0.1.0/python_naming_linter/checkers/package.py +37 -0
  20. python_naming_linter-0.1.0/python_naming_linter/checkers/variable.py +175 -0
  21. python_naming_linter-0.1.0/python_naming_linter/cli.py +192 -0
  22. python_naming_linter-0.1.0/python_naming_linter/config.py +119 -0
  23. python_naming_linter-0.1.0/python_naming_linter/matcher.py +69 -0
  24. python_naming_linter-0.1.0/python_naming_linter/reporter.py +16 -0
  25. python_naming_linter-0.1.0/tests/__init__.py +0 -0
  26. python_naming_linter-0.1.0/tests/fixtures/sample_config.yaml +24 -0
  27. python_naming_linter-0.1.0/tests/fixtures/sample_project/contexts/__init__.py +0 -0
  28. python_naming_linter-0.1.0/tests/fixtures/sample_project/contexts/boards/__init__.py +0 -0
  29. python_naming_linter-0.1.0/tests/fixtures/sample_project/contexts/boards/domain/__init__.py +0 -0
  30. python_naming_linter-0.1.0/tests/fixtures/sample_project/contexts/boards/domain/models.py +7 -0
  31. python_naming_linter-0.1.0/tests/fixtures/sample_project/contexts/boards/domain/specs/__init__.py +0 -0
  32. python_naming_linter-0.1.0/tests/fixtures/sample_project/contexts/boards/domain/specs/filter_spec.py +10 -0
  33. python_naming_linter-0.1.0/tests/test_checkers/__init__.py +0 -0
  34. python_naming_linter-0.1.0/tests/test_checkers/test_class.py +104 -0
  35. python_naming_linter-0.1.0/tests/test_checkers/test_function.py +133 -0
  36. python_naming_linter-0.1.0/tests/test_checkers/test_module.py +90 -0
  37. python_naming_linter-0.1.0/tests/test_checkers/test_package.py +42 -0
  38. python_naming_linter-0.1.0/tests/test_checkers/test_variable.py +142 -0
  39. python_naming_linter-0.1.0/tests/test_cli.py +129 -0
  40. python_naming_linter-0.1.0/tests/test_config.py +103 -0
  41. python_naming_linter-0.1.0/tests/test_matcher.py +55 -0
  42. python_naming_linter-0.1.0/tests/test_reporter.py +34 -0
  43. python_naming_linter-0.1.0/uv.lock +394 -0
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: commit
3
+ description: Create a git commit following the project's commit convention
4
+ ---
5
+
6
+ Create a git commit following the project's commit convention defined in [CONTRIBUTING.md](../../../CONTRIBUTING.md).
7
+
8
+ ## Steps
9
+
10
+ 1. Read `CONTRIBUTING.md` to check the commit convention.
11
+ 2. Run `git status` and `git diff` to review all changes.
12
+ 3. Draft a commit message following the convention.
13
+ 4. Stage only relevant files (do NOT use `git add -A`). Exclude secrets and unrelated files.
14
+ 5. Commit using a HEREDOC for proper formatting:
15
+ ```bash
16
+ git commit -m "$(cat <<'EOF'
17
+ <commit message here>
18
+
19
+ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
20
+ EOF
21
+ )"
22
+ ```
23
+ 6. Run `git status` to verify success.
24
+ 7. If pre-commit hook fails, fix the issue and create a **new** commit (never amend).
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: release
3
+ description: Create a new release by calculating the next version from conventional commits and pushing a git tag
4
+ ---
5
+
6
+ Create a new release following the project's release process defined in [CONTRIBUTING.md](../../../CONTRIBUTING.md).
7
+
8
+ The release workflow is defined in [.github/workflows/publish.yaml](../../../.github/workflows/publish.yaml).
9
+
10
+ ## Steps
11
+
12
+ 1. Read `CONTRIBUTING.md` to check the release process.
13
+ 2. Follow the steps described in the Release section.
14
+ 3. Ask the user to confirm the version before creating and pushing the tag.
@@ -0,0 +1,37 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [main]
6
+ paths:
7
+ - "python_naming_linter/**"
8
+ - "tests/**"
9
+ - "pyproject.toml"
10
+
11
+ jobs:
12
+ lint:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v6
16
+ - uses: astral-sh/setup-uv@v4
17
+ - uses: actions/setup-python@v6
18
+ with:
19
+ python-version: "3.13"
20
+ - run: uv pip install --system -e ".[dev]"
21
+ - run: ruff check .
22
+ - run: ruff format --check .
23
+ - run: ty check
24
+
25
+ test:
26
+ runs-on: ubuntu-latest
27
+ strategy:
28
+ matrix:
29
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
30
+ steps:
31
+ - uses: actions/checkout@v6
32
+ - uses: astral-sh/setup-uv@v4
33
+ - uses: actions/setup-python@v6
34
+ with:
35
+ python-version: ${{ matrix.python-version }}
36
+ - run: uv pip install --system -e ".[dev]"
37
+ - run: pytest -v
@@ -0,0 +1,59 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ changelog:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: write
13
+ steps:
14
+ - uses: actions/checkout@v6
15
+ with:
16
+ fetch-depth: 0
17
+ - name: Generate full changelog
18
+ uses: orhun/git-cliff-action@v4
19
+ with:
20
+ config: pyproject.toml
21
+ args: --verbose
22
+ env:
23
+ OUTPUT: CHANGELOG.md
24
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
25
+ - name: Commit CHANGELOG.md
26
+ run: |
27
+ git config user.name "github-actions[bot]"
28
+ git config user.email "github-actions[bot]@users.noreply.github.com"
29
+ git add CHANGELOG.md
30
+ git commit -m "docs: Update CHANGELOG for ${{ github.ref_name }}" || true
31
+ git push origin HEAD:main
32
+ - name: Generate release notes
33
+ id: release_notes
34
+ uses: orhun/git-cliff-action@v4
35
+ with:
36
+ config: pyproject.toml
37
+ args: --verbose --latest --strip header
38
+ env:
39
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
40
+ - name: Create GitHub Release
41
+ uses: softprops/action-gh-release@v2
42
+ with:
43
+ body: ${{ steps.release_notes.outputs.content }}
44
+ env:
45
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
46
+
47
+ publish:
48
+ needs: changelog
49
+ runs-on: ubuntu-latest
50
+ permissions:
51
+ id-token: write
52
+ steps:
53
+ - uses: actions/checkout@v6
54
+ - uses: actions/setup-python@v5
55
+ with:
56
+ python-version: "3.13"
57
+ - run: pip install build
58
+ - run: python -m build
59
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ .pytest_cache/
8
+ .ruff_cache/
@@ -0,0 +1,19 @@
1
+ repos:
2
+ - repo: local
3
+ hooks:
4
+ - id: ruff check
5
+ name: ruff check (lint)
6
+ entry: uv run ruff check --fix
7
+ language: system
8
+ types: [python]
9
+ - id: ruff format
10
+ name: ruff format (format)
11
+ entry: uv run ruff format
12
+ language: system
13
+ types: [python]
14
+ - id: ty check
15
+ name: ty check (type check)
16
+ entry: uv run ty check
17
+ language: system
18
+ pass_filenames: false
19
+ types: [python]
@@ -0,0 +1,6 @@
1
+ - id: python-naming-linter
2
+ name: Python Naming Linter
3
+ entry: pnl check
4
+ language: python
5
+ types: [python]
6
+ pass_filenames: false
@@ -0,0 +1,9 @@
1
+ # CLAUDE.md
2
+
3
+ ## Project Overview
4
+
5
+ Python naming linter (`pnl`) - A CLI tool that lints naming conventions in Python projects.
6
+
7
+ ## Contributing
8
+
9
+ Follow the conventions in [CONTRIBUTING.md](./CONTRIBUTING.md).
@@ -0,0 +1,84 @@
1
+ # Contributing
2
+
3
+ ## Commit Convention
4
+
5
+ Commit messages must follow [Conventional Commits](https://www.conventionalcommits.org/) with [gitmoji](https://gitmoji.dev/) prefix.
6
+
7
+ ### Format
8
+
9
+ ```
10
+ <gitmoji> <type>: <description>
11
+ ```
12
+
13
+ - The first letter after the colon must be **capitalized**.
14
+ - The description must be in **English**.
15
+
16
+ ### Types
17
+
18
+ | Gitmoji | Type | Description |
19
+ |---------|------------|--------------------------|
20
+ | ✨ | `feat` | New feature |
21
+ | 🐛 | `fix` | Bug fix |
22
+ | ♻️ | `refactor` | Code refactoring |
23
+ | 📝 | `docs` | Documentation |
24
+ | ✅ | `test` | Adding or updating tests |
25
+ | 🔧 | `chore` | Maintenance tasks |
26
+ | 👷 | `ci` | CI/CD changes |
27
+ | ⚡ | `perf` | Performance improvement |
28
+
29
+ ### Examples
30
+
31
+ ```
32
+ ✨ feat: Add support for relative imports
33
+ 🐛 fix: Use exit code 2 for config file not found
34
+ ♻️ refactor: Simplify module resolver logic
35
+ ```
36
+
37
+ ## Pull Request Convention
38
+
39
+ - PRs are always **squash merged**, so the PR title becomes the final commit message.
40
+ - PR titles must follow the same format as commit messages (`<gitmoji> <type>: <description>`).
41
+ - PR descriptions must be written in **English**.
42
+
43
+ ## Pre-commit Hooks
44
+
45
+ This project uses [pre-commit](https://pre-commit.com/) for linting, formatting, and type checking.
46
+
47
+ ```bash
48
+ # Install pre-commit hooks
49
+ pre-commit install
50
+
51
+ # Run manually
52
+ pre-commit run --all-files
53
+ ```
54
+
55
+ All commits must pass the pre-commit hooks before being accepted.
56
+
57
+ ## Release
58
+
59
+ Releases are automated via GitHub Actions. You only need to create and push a version tag.
60
+
61
+ ### Steps
62
+
63
+ 1. Calculate the next version based on conventional commits:
64
+ ```bash
65
+ uvx git-cliff --bumped-version
66
+ ```
67
+ 2. Review the commits since the last tag:
68
+ ```bash
69
+ git log $(git describe --tags --abbrev=0)..HEAD --oneline
70
+ ```
71
+ 3. Push the latest commits to `main`:
72
+ ```bash
73
+ git push origin main
74
+ ```
75
+ 4. Create and push the tag:
76
+ ```bash
77
+ git tag <version>
78
+ git push origin <version>
79
+ ```
80
+
81
+ The GitHub Actions workflow will then automatically:
82
+ - Generate `CHANGELOG.md` and commit it to `main`
83
+ - Create a GitHub Release with release notes
84
+ - Publish the package to PyPI
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 heumsi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,317 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-naming-linter
3
+ Version: 0.1.0
4
+ Summary: A naming convention linter for Python projects
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: Software Development :: Quality Assurance
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: click>=8.0
18
+ Requires-Dist: pyyaml>=6.0
19
+ Requires-Dist: tomli>=2.0; python_version < '3.11'
20
+ Provides-Extra: dev
21
+ Requires-Dist: pre-commit>=3.0; extra == 'dev'
22
+ Requires-Dist: pytest>=7.0; extra == 'dev'
23
+ Requires-Dist: ruff>=0.4; extra == 'dev'
24
+ Requires-Dist: ty>=0.0.26; extra == 'dev'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # python-naming-linter
28
+
29
+ A naming convention linter for Python projects. Define custom naming rules and enforce them with a single CLI command.
30
+
31
+ ## What It Does
32
+
33
+ - Define naming rules for variables, functions, classes, modules, and packages
34
+ - Apply rules to specific modules using pattern matching
35
+ - Integrate into CI or pre-commit to keep your naming conventions consistent
36
+
37
+ For Python developers who want to enforce team-specific naming conventions beyond what PEP 8 and ruff cover.
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ pip install python-naming-linter
43
+ ```
44
+
45
+ Or with uv:
46
+
47
+ ```bash
48
+ uv add python-naming-linter
49
+ ```
50
+
51
+ ## Quick Start
52
+
53
+ Create `.python-naming-linter.yaml` in your project root:
54
+
55
+ ```yaml
56
+ rules:
57
+ - name: bool-method-prefix
58
+ type: function
59
+ filter: { return_type: bool }
60
+ naming: { prefix: [is_, has_, should_] }
61
+
62
+ - name: exception-naming
63
+ type: class
64
+ filter: { base_class: Exception }
65
+ naming: { regex: "^[A-Z][a-zA-Z]+(NotFound|Invalid|Denied|Conflict|Failed)Error$" }
66
+
67
+ apply:
68
+ - name: all
69
+ rules: [bool-method-prefix, exception-naming]
70
+ modules: "**"
71
+ ```
72
+
73
+ Run:
74
+
75
+ ```bash
76
+ pnl check
77
+ ```
78
+
79
+ Output:
80
+
81
+ ```
82
+ src/domain/service.py:12
83
+ [bool-method-prefix] validate (expected prefix: is_ | has_ | should_)
84
+
85
+ src/domain/exceptions.py:8
86
+ [exception-naming] FilterError (expected pattern: ^[A-Z][a-zA-Z]+(NotFound|Invalid|...)Error$)
87
+
88
+ Found 2 violation(s).
89
+ ```
90
+
91
+ ## Examples
92
+
93
+ ### Variable Naming — Match Type Annotation
94
+
95
+ Enforce that variable names match their type annotation in snake_case:
96
+
97
+ ```yaml
98
+ rules:
99
+ - name: attribute-matches-type
100
+ type: variable
101
+ filter: { target: attribute }
102
+ naming: { source: type_annotation, transform: snake_case }
103
+
104
+ apply:
105
+ - name: domain-layer
106
+ rules: [attribute-matches-type]
107
+ modules: contexts.*.domain
108
+ ```
109
+
110
+ This catches `repo: SubscriptionRepository` (should be `subscription_repository`).
111
+
112
+ The `{prefix}_{expected}` form is also allowed — `source_object_context: ObjectContext` passes because it ends with `_object_context`.
113
+
114
+ ### Module Naming — Match Class Name
115
+
116
+ Enforce that module filenames match the primary class they contain:
117
+
118
+ ```yaml
119
+ rules:
120
+ - name: domain-module-naming
121
+ type: module
122
+ naming: { source: class_name, transform: snake_case }
123
+
124
+ apply:
125
+ - name: domain-layer
126
+ rules: [domain-module-naming]
127
+ modules: contexts.*.domain
128
+ ```
129
+
130
+ A file `custom.py` containing `class CustomObject` is a violation — it should be `custom_object.py`.
131
+
132
+ ### Combining Rules Per Layer
133
+
134
+ Apply different rules to different parts of your codebase:
135
+
136
+ ```yaml
137
+ rules:
138
+ - name: attribute-matches-type
139
+ type: variable
140
+ filter: { target: attribute }
141
+ naming: { source: type_annotation, transform: snake_case }
142
+
143
+ - name: bool-method-prefix
144
+ type: function
145
+ filter: { return_type: bool }
146
+ naming: { prefix: [is_, has_, should_] }
147
+
148
+ - name: exception-naming
149
+ type: class
150
+ filter: { base_class: Exception }
151
+ naming: { regex: "^[A-Z][a-zA-Z]+(NotFound|Invalid|Denied|Conflict|Failed)Error$" }
152
+
153
+ - name: domain-module-naming
154
+ type: module
155
+ naming: { source: class_name, transform: snake_case }
156
+
157
+ - name: constant-upper-case
158
+ type: variable
159
+ filter: { target: constant }
160
+ naming: { case: UPPER_CASE }
161
+
162
+ apply:
163
+ - name: domain-layer
164
+ rules:
165
+ - attribute-matches-type
166
+ - bool-method-prefix
167
+ - domain-module-naming
168
+ - constant-upper-case
169
+ modules: contexts.*.domain
170
+
171
+ - name: global-exceptions
172
+ rules: [exception-naming]
173
+ modules: "**"
174
+ ```
175
+
176
+ ## Configuration
177
+
178
+ ### Rule Types
179
+
180
+ | Type | Target |
181
+ |------|--------|
182
+ | `variable` | Variable names (attribute, parameter, local_variable, constant) |
183
+ | `function` | Function/method names |
184
+ | `class` | Class names (including exceptions) |
185
+ | `module` | Module (file) names |
186
+ | `package` | Package (directory) names |
187
+
188
+ ### Filter
189
+
190
+ Each rule can narrow its scope with type-specific filters:
191
+
192
+ | Type | Filter | Example Values |
193
+ |------|--------|----------------|
194
+ | `variable` | `target` | `attribute`, `parameter`, `local_variable`, `constant` |
195
+ | `function` | `target` | `method`, `function` |
196
+ | `function` | `return_type` | `bool` |
197
+ | `function` | `decorator` | `staticmethod` |
198
+ | `class` | `base_class` | `Exception` |
199
+ | `class` | `decorator` | `dataclass` |
200
+
201
+ ### Naming Constraints
202
+
203
+ | Field | Description | Example |
204
+ |-------|-------------|---------|
205
+ | `prefix` | Name must start with one of the listed prefixes | `[is_, has_]` |
206
+ | `suffix` | Name must end with one of the listed suffixes | `[Repository, Service]` |
207
+ | `regex` | Name must match a regular expression | `"^[A-Z][a-zA-Z]+Error$"` |
208
+ | `source` + `transform` | Name must be derived from another element | `source: type_annotation`, `transform: snake_case` |
209
+ | `case` | Name must follow a casing convention | `snake_case`, `PascalCase`, `UPPER_CASE` |
210
+
211
+ ### Include / Exclude
212
+
213
+ Control which files are scanned:
214
+
215
+ ```yaml
216
+ include:
217
+ - src
218
+ exclude:
219
+ - src/generated/**
220
+
221
+ rules:
222
+ - name: ...
223
+ ```
224
+
225
+ - **No `include` or `exclude`** — All `.py` files under the project root are scanned
226
+ - **`include` only** — Only files matching the given paths are scanned
227
+ - **`exclude` only** — All files except those matching the given paths are scanned
228
+ - **Both** — `include` is applied first, then `exclude` filters within that result
229
+
230
+ ### Wildcard
231
+
232
+ `*` matches a single level in dotted module paths:
233
+
234
+ ```yaml
235
+ modules: contexts.*.domain # matches contexts.boards.domain, contexts.auth.domain, ...
236
+ ```
237
+
238
+ `**` matches one or more levels:
239
+
240
+ ```yaml
241
+ modules: contexts.**.domain # matches contexts.boards.domain, contexts.boards.sub.domain, ...
242
+ ```
243
+
244
+ ### Named Capture
245
+
246
+ `{name}` captures a single level (like `*`) and allows back-referencing:
247
+
248
+ ```yaml
249
+ apply:
250
+ - name: domain-isolation
251
+ rules: [attribute-matches-type]
252
+ modules: contexts.{context}.domain
253
+ ```
254
+
255
+ ### pyproject.toml
256
+
257
+ You can also configure in `pyproject.toml`:
258
+
259
+ ```toml
260
+ [[tool.python-naming-linter.rules]]
261
+ name = "bool-method-prefix"
262
+ type = "function"
263
+
264
+ [tool.python-naming-linter.rules.filter]
265
+ return_type = "bool"
266
+
267
+ [tool.python-naming-linter.rules.naming]
268
+ prefix = ["is_", "has_", "should_"]
269
+
270
+ [[tool.python-naming-linter.apply]]
271
+ name = "all"
272
+ rules = ["bool-method-prefix"]
273
+ modules = "**"
274
+ ```
275
+
276
+ ## CLI
277
+
278
+ ```bash
279
+ # Check with auto-discovered config (searches upward from cwd)
280
+ pnl check
281
+
282
+ # Specify config file (project root = config file's parent directory)
283
+ pnl check --config path/to/config.yaml
284
+ ```
285
+
286
+ Exit codes:
287
+
288
+ - `0` — No violations
289
+ - `1` — Violations found
290
+ - `2` — Config file not found
291
+
292
+ If no `--config` is given, the tool searches upward from the current directory for `.python-naming-linter.yaml` or `pyproject.toml` (with `[tool.python-naming-linter]`). The config file's parent directory is used as the project root.
293
+
294
+ ## Pre-commit
295
+
296
+ Add to `.pre-commit-config.yaml`:
297
+
298
+ ```yaml
299
+ - repo: https://github.com/heumsi/python-naming-linter
300
+ rev: '' # Use the tag you want to point at (e.g., v0.1.0)
301
+ hooks:
302
+ - id: python-naming-linter
303
+ ```
304
+
305
+ To pass custom options:
306
+
307
+ ```yaml
308
+ - repo: https://github.com/heumsi/python-naming-linter
309
+ rev: ''
310
+ hooks:
311
+ - id: python-naming-linter
312
+ args: [--config, custom-config.yaml]
313
+ ```
314
+
315
+ ## License
316
+
317
+ MIT