pathlint 0.2.0__tar.gz → 0.3.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.
@@ -0,0 +1,22 @@
1
+ name: Checks
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ branches: [ main ]
8
+
9
+ jobs:
10
+ static:
11
+ runs-on: ubuntu-latest
12
+ env:
13
+ PYTHONPATH: ${{ github.workspace }}/src
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: '3.12'
19
+ - run: python -m pip install pytest pytest-cov ruff
20
+ - run: ruff check .
21
+ - run: ruff format --check .
22
+ - run: python -m pytest
@@ -19,7 +19,7 @@ repos:
19
19
  hooks:
20
20
  - id: pathlint
21
21
  name: pathlint - Detect os.path usage
22
- entry: pathlint
22
+ entry: pathlint check
23
23
  language: system
24
24
  types: [ python ]
25
25
  pass_filenames: true
@@ -1,7 +1,7 @@
1
1
  - id: pathlint
2
2
  name: pathlint - Detect os.path usage
3
3
  description: 'Fast linter to detect os.path usage and encourage pathlib adoption'
4
- entry: pathlint
4
+ entry: pathlint check
5
5
  language: python
6
6
  types: [ python ]
7
7
  pass_filenames: true
@@ -11,7 +11,7 @@
11
11
  - id: pathlint-fix
12
12
  name: pathlint - Auto-fix os.path usage (experimental)
13
13
  description: 'Automatically convert os.path to pathlib'
14
- entry: pathlint --fix
14
+ entry: pathlint fix
15
15
  language: python
16
16
  types: [ python ]
17
17
  pass_filenames: true
@@ -0,0 +1,345 @@
1
+ Metadata-Version: 2.5
2
+ Name: pathlint
3
+ Version: 0.3.0
4
+ Summary: Fast linter to detect os.path usage and encourage pathlib adoption
5
+ Project-URL: Repository, https://github.com/pszemraj/pathlint
6
+ Project-URL: Issues, https://github.com/pszemraj/pathlint/issues
7
+ Project-URL: Changelog, https://github.com/pszemraj/pathlint/releases
8
+ Project-URL: Documentation, https://github.com/pszemraj/pathlint#readme
9
+ Author-email: Peter Szemraj <peterszemraj+dev@gmail.com>
10
+ License: MIT
11
+ License-File: LICENSE.txt
12
+ Keywords: code-quality,linter,os.path,pathlib,pre-commit,python-linter
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Quality Assurance
23
+ Requires-Python: >=3.9
24
+ Provides-Extra: dev
25
+ Requires-Dist: mypy>=1.0; extra == 'dev'
26
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
27
+ Requires-Dist: pytest>=7.0; extra == 'dev'
28
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # pathlint
32
+
33
+ [![PyPI version](https://badge.fury.io/py/pathlint.svg)](https://pypi.org/project/pathlint/)
34
+ [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit)
35
+
36
+ Find `os.path` usage and migrate supported calls to `pathlib`. Pathlint has a
37
+ standard-library-only runtime and supports Python 3.9 and newer.
38
+
39
+ - Detect imports, aliases, function references, calls, and usage in annotations.
40
+ - Exclude files and directories with repeatable name or path patterns.
41
+ - Honor Git ignore rules automatically when Git is available.
42
+ - Get concise text diagnostics, quiet output, or JSON reports.
43
+ - Preview experimental fixes as unified diffs before applying them.
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ pip install pathlint
49
+
50
+ # Verify installation
51
+ pathlint --version
52
+
53
+ # Check Python files in the current directory
54
+ pathlint check .
55
+ ```
56
+
57
+ Git is optional. Without Git, directory scans use built-in and explicit
58
+ exclusions; `.gitignore` rules are not interpreted.
59
+
60
+ ## Usage
61
+
62
+ ```bash
63
+ # Check the current directory (also the behavior of bare pathlint)
64
+ pathlint check
65
+
66
+ # Check one or more files or directories
67
+ pathlint check myfile.py
68
+ pathlint check src/ tests/ scripts/
69
+
70
+ # See exactly which files will be selected, without parsing them
71
+ pathlint files .
72
+
73
+ # Preview fixes, then apply them
74
+ pathlint fix src/ --dry-run
75
+ pathlint fix src/
76
+
77
+ # Module invocation works too
78
+ python -m pathlint check .
79
+ ```
80
+
81
+ Use `pathlint --help` or `pathlint check --help` for command help.
82
+
83
+ ### Excluding files and directories
84
+
85
+ All three commands use the same selection rules:
86
+
87
+ ```bash
88
+ pathlint check . \
89
+ --exclude vendor \
90
+ --exclude 'training/wandb/*' \
91
+ --exclude backups
92
+
93
+ pathlint files . -e '*.generated.py' -e 'src/legacy/'
94
+ pathlint fix . --exclude vendor --dry-run
95
+
96
+ # Disable Git ignores or built-in exclusions independently
97
+ pathlint check . --no-gitignore
98
+ pathlint check . --no-default-excludes
99
+ ```
100
+
101
+ Each `-e` / `--exclude` accepts one pattern; repeat the argument for multiple
102
+ patterns. Explicit exclusions add to the built-in exclusions.
103
+
104
+ - Names without `/`, such as `backups` or `test_*.py`, match at any depth.
105
+ - Patterns containing `/` match paths relative to the working directory.
106
+ `./vendor` matches the top-level `vendor`, while `vendor` matches that name
107
+ anywhere. Absolute patterns can exclude paths outside the working directory.
108
+ - Directory matches exclude their descendants. A trailing `/` restricts a
109
+ pattern to directories.
110
+ - Patterns use case-sensitive `fnmatch` syntax: `*`, `?`, and bracket expressions.
111
+ `*` can span `/`; `**` has no additional meaning. Quote wildcards to prevent
112
+ shell expansion. These patterns do not support Git-style `!` negation.
113
+
114
+ Built-in exclusions are `__pycache__`, `.venv`, `venv`, `.git`, `node_modules`,
115
+ `.tox`, `build`, and `dist`.
116
+
117
+ In Git worktrees, directory scans include tracked files and untracked files
118
+ that Git does not ignore. Git handles nested `.gitignore` rules, negation,
119
+ repository-local excludes, and global ignores. Tracked files remain eligible
120
+ even when they match an ignore rule. Nonexcluded nested repositories use their
121
+ own Git rules.
122
+
123
+ Explicitly named files bypass Git and built-in exclusions. **Explicit
124
+ `--exclude` patterns still apply**, including to filenames supplied by
125
+ pre-commit. File symlinks select their resolved Python targets once; exclusions
126
+ match both the link and target paths.
127
+
128
+ ### Output
129
+
130
+ ```text
131
+ src/utils.py:12: PL001 os.path usage: os.path.exists(value)
132
+ Found 1 violation in 1 of 42 files checked.
133
+ ```
134
+
135
+ Paths are relative to the working directory when possible, and absolute for
136
+ external files. Text output retains the stream's encoding and escapes characters
137
+ it cannot represent, including when redirected through legacy Windows encodings.
138
+ Each offending source line is reported once, even if it contains several usages.
139
+ `PL001` means “Use pathlib instead of os.path.”
140
+
141
+ ```bash
142
+ # Findings only; no summary
143
+ pathlint check . --quiet
144
+
145
+ # Show the five files with the most violations
146
+ pathlint check . --stats
147
+
148
+ # Machine-readable report
149
+ pathlint check . --output-format json
150
+
151
+ # Include offender statistics in the JSON report
152
+ pathlint check . --output-format json --stats
153
+
154
+ # Optional aggressive message
155
+ pathlint check . --aggressive
156
+ ```
157
+
158
+ JSON reports contain `diagnostics`, `errors`, and `summary`. Each diagnostic has
159
+ `path`, `line`, `code`, `message`, and `source`. Summary fields are
160
+ `files_checked`, `files_with_issues`, `violations`, and `errors`; files that fail
161
+ reading or parsing are not counted as checked. `--stats` adds a `statistics`
162
+ list containing up to five `{path, violations}` entries.
163
+
164
+ JSON is available only for `check` and cannot be combined with `--quiet` or
165
+ `--aggressive`. Runtime errors are included in the JSON report without mixing
166
+ text into stdout. Text-mode errors go to stderr.
167
+
168
+ ### Exit codes and compatibility
169
+
170
+ | Command | `0` | `1` | `2` |
171
+ | --- | --- | --- | --- |
172
+ | `check` | Clean or no files selected | Violations found | Input, discovery, read, or parse error |
173
+ | `fix` | Completed | — | Input, discovery, read, parse, or write error |
174
+ | `fix --dry-run` | No edits proposed | Edits proposed | Error |
175
+ | `files` | Listing completed, including empty selection | — | Input or discovery error |
176
+
177
+ Errors take precedence over violations or proposed edits. Other valid files
178
+ are still processed. Ctrl-C exits with status `130`.
179
+
180
+ Existing invocations continue to work without warnings:
181
+
182
+ ```bash
183
+ pathlint .
184
+ pathlint --fix src/
185
+ pathlint --dry-run src/
186
+ ```
187
+
188
+ The original `python -m pathlint.linter` and
189
+ `python -m pathlint.autofix` entry points also remain available. A valid empty
190
+ selection now succeeds; dry runs now return `1` when they propose edits.
191
+
192
+ ## What it detects
193
+
194
+ ```python
195
+ import os
196
+ import os as system
197
+ import os.path as osp
198
+ from os import path as paths
199
+ from os.path import exists as present
200
+
201
+ os.path.exists("file")
202
+ system.path.join("directory", "file")
203
+ osp.isdir("directory")
204
+ paths.dirname(__file__)
205
+ present("file")
206
+ callback = os.path.join
207
+ ```
208
+
209
+ Pathlint resolves imported names in function, class, and comprehension scopes
210
+ so ordinary local shadowing is not mistaken for `os.path`. Private aliases in class
211
+ scopes are left unresolved because Python mangles their names. Deleted class
212
+ locals and cleared exception aliases fall back to module globals. Function and
213
+ generator bodies use the enclosing scope's final bindings for detection; a
214
+ generator's first iterable is inspected immediately. Dynamic rebinding can prevent
215
+ resolution. Wildcard imports invalidate tracked bindings; later explicit imports
216
+ can restore them. Detection inspects `if` branches independently and uses literal
217
+ conditions when merging bindings in the current scope. Loop bindings are merged
218
+ conservatively across iterations and `else` suites. Fixes in deferred bodies
219
+ require enclosing imports to exist when the function, lambda, or generator is
220
+ created, and conservatively skip aliases rebound anywhere in the file, including
221
+ imports with conflicting origins. Stable imports inside the body can still be
222
+ migrated. This is a static check, not full Python execution analysis.
223
+
224
+ Expressions in annotations are checked too. Valid `os.PathLike` annotations
225
+ are not `os.path` usage and are not flagged. The fixer leaves annotation
226
+ expressions unchanged: postponed annotations and Python 3.14's lazy annotations
227
+ can resolve names after their bindings change.
228
+
229
+ ## Auto-fix (experimental)
230
+
231
+ ```bash
232
+ # Review a unified diff without changing files
233
+ pathlint fix src/ --dry-run
234
+
235
+ # Apply supported migrations
236
+ pathlint fix src/
237
+
238
+ # Print only the diff
239
+ pathlint fix src/ --dry-run --quiet
240
+
241
+ # Disable terminal diff colors
242
+ pathlint fix src/ --dry-run --no-color
243
+ ```
244
+
245
+ Fixes use AST locations to change expressions while preserving surrounding
246
+ source text, including comments and strings. Imported aliases, named function
247
+ imports, calls inside nested expressions, and expressions inside f-strings are
248
+ supported. Pathlib imports follow module docstrings and future imports. Existing
249
+ Pathlib aliases are reused where possible; new constructor imports use a private
250
+ alias to preserve wildcard exports.
251
+
252
+ Supported calls include `isabs`, `join`, `basename`, `dirname`, and `expanduser`
253
+ with string-literal arguments. Calls with nonliteral arguments, including variables
254
+ and nested calls, remain unchanged; eligible literal calls inside them can still
255
+ be fixed. Values and types are not inferred.
256
+ Unsupported references retain their imports.
257
+ Keyword arguments, starred arguments, call targets containing comments, literal
258
+ file descriptors, byte paths, and known inexact literal forms are left unchanged.
259
+ Known inexact forms include empty paths, trailing separators, named-user
260
+ expansion, and rooted literals passed to `isabs` whose interpretation can differ
261
+ under Windows path rules.
262
+ All tilde-prefixed `expanduser` arguments remain unchanged because home values
263
+ can contain spellings that Pathlib normalizes or fail to resolve.
264
+ On Windows, string-returning fixes also skip literals whose separators or dot
265
+ components Pathlib would normalize.
266
+ Calls with uncertain branch, exception, context manager, match, or loop bindings
267
+ and methods whose `os.path` attributes or callable internals are assigned or
268
+ deleted are also left unchanged. Calls outside annotation expressions remain
269
+ eligible for fixes.
270
+ Mutation checks retain possible copies of imported modules and callables through
271
+ assignments, loop targets, and default parameters. They inspect all expression
272
+ children, conservatively including container selections and comprehensions. Calls
273
+ retain possible origins from their arguments; return types are not inferred.
274
+ Copies stored in object attributes, class fields, or container entries conservatively prevent
275
+ fixes for the copied module or callable. The same applies when these objects are
276
+ passed to calls, returned, yielded, or used by context managers or match patterns.
277
+ These copies do not establish bindings for detection or migration of copied calls.
278
+ Mutation origins are conservative across scopes and statement order; copies of
279
+ callable internals can also exclude fixes for their exported parent.
280
+ Assigning an imported module's `__class__` invalidates all of its lookups because
281
+ module subclasses can override attributes with descriptors.
282
+ Explicit `global` writes prevent affected aliases, constructors, and `str`
283
+ conversions from being used by fixes, including in module-level code.
284
+ Sources using dynamic code, namespace dictionaries, or dynamic import/attribute
285
+ builtins are left unchanged. This includes `exec`, `eval`, `globals`, `locals`,
286
+ `vars`, `__import__`, `getattr`, `setattr`, and `delattr`, their bound
287
+ `__setattr__`/`__delattr__` equivalents, exposed `builtins`,
288
+ function/frame namespaces, and module registries (`sys.modules` or
289
+ `importlib.import_module`); these capabilities can overwrite aliases or generated
290
+ constructors. Exposed function defaults, keyword defaults, and annotation dictionaries
291
+ also prevent fixes. Bare `dir()` use retains imports while supported calls remain eligible.
292
+ Fixes retain module-level imports for re-exports, including conditional imports
293
+ and bindings declared `global`. Imports referenced by annotations or `__all__`
294
+ also remain. Retained imports can still appear in check results. Unused local
295
+ imports can still be removed. Fixes avoid
296
+ constructors affected by Pathlib mutations. Sources containing wildcard imports
297
+ are left unchanged because
298
+ their exports can overwrite names used by a fix. Sources using type parameters
299
+ or type aliases are also left unchanged because their annotation scopes and lazy
300
+ values are not analyzed by the fixer. `abspath`, `splitext`, `sep`, and `pathsep`
301
+ are not rewritten because their former replacements changed behavior
302
+ or returned incorrect values. `exists`, `isfile`, and `isdir` are also left
303
+ unchanged: on supported Python versions, their Pathlib replacements can raise
304
+ filesystem errors where `os.path` returns `False`.
305
+
306
+ **Review every diff and test the result.** Pathlib normalizes path strings.
307
+ The fixer does not promise to remove every violation. Diff colors are enabled
308
+ only on interactive terminals.
309
+
310
+ ## Pre-commit hook integration
311
+
312
+ Add to `.pre-commit-config.yaml`:
313
+
314
+ ```yaml
315
+ repos:
316
+ - repo: https://github.com/pszemraj/pathlint
317
+ rev: v0.3.0 # Use the release you want to pin
318
+ hooks:
319
+ - id: pathlint
320
+ args: [--exclude, vendor, --exclude, backups]
321
+ ```
322
+
323
+ Install and verify the hook:
324
+
325
+ ```bash
326
+ pre-commit install
327
+ pre-commit run pathlint --all-files
328
+ ```
329
+
330
+ The `pathlint` hook runs `pathlint check`. To apply experimental fixes instead,
331
+ use the `pathlint-fix` hook, which runs `pathlint fix`. Review its changes before
332
+ committing.
333
+
334
+ ## Development
335
+
336
+ ```bash
337
+ pip install -e '.[dev]'
338
+ pytest
339
+ ruff check .
340
+ ruff format --check .
341
+ ```
342
+
343
+ ## License
344
+
345
+ MIT License — see [LICENSE.txt](LICENSE.txt).