check-empty 1.2.1__tar.gz → 1.2.3__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.
- {check_empty-1.2.1 → check_empty-1.2.3}/PKG-INFO +9 -14
- {check_empty-1.2.1 → check_empty-1.2.3}/README.md +7 -6
- check_empty-1.2.3/check_empty/__init__.py +276 -0
- {check_empty-1.2.1 → check_empty-1.2.3}/check_empty/__main__.py +10 -18
- {check_empty-1.2.1 → check_empty-1.2.3}/pyproject.toml +18 -32
- {check_empty-1.2.1 → check_empty-1.2.3}/pyproject.toml.orig +11 -11
- check_empty-1.2.1/check_empty/__init__.py +0 -195
- {check_empty-1.2.1 → check_empty-1.2.3}/AUTHORS.md +0 -0
- {check_empty-1.2.1 → check_empty-1.2.3}/LICENSE +0 -0
- {check_empty-1.2.1 → check_empty-1.2.3}/check_empty/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: check-empty
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
4
4
|
Summary: Command-line utility, as well as a hook usable by the Python pre-commit framework, and a GitHub Action, to ensure that selected files are empty.
|
|
5
5
|
Keywords: check,command-line,empty,files,github-action,helper,hook,lightweight,pre-commit,prek,tool,utility
|
|
6
6
|
Author: Jonathan Dung
|
|
@@ -24,19 +24,13 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
24
24
|
Classifier: Programming Language :: Python :: 3.13
|
|
25
25
|
Classifier: Programming Language :: Python :: 3.14
|
|
26
26
|
Classifier: Programming Language :: Python :: 3.15
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.16
|
|
27
28
|
Classifier: Programming Language :: Python :: Free Threading :: 3 - Stable
|
|
28
29
|
Classifier: Topic :: Utilities
|
|
29
30
|
Classifier: Typing :: Typed
|
|
30
|
-
Requires-Dist: furo==2025.12.19 ; python_full_version >= '3.9' and extra == 'docs'
|
|
31
|
-
Requires-Dist: sphinx==9.1.0 ; python_full_version >= '3.12' and extra == 'docs'
|
|
32
|
-
Requires-Dist: sphinx-argparse-cli==1.22.1 ; python_full_version >= '3.11' and extra == 'docs'
|
|
33
|
-
Requires-Dist: sphinx-copybutton==0.5.2 ; python_full_version >= '3.7' and extra == 'docs'
|
|
34
|
-
Requires-Dist: sphinx-lint==1.0.2 ; python_full_version >= '3.10' and extra == 'docs'
|
|
35
|
-
Requires-Dist: sphinx-notfound-page==1.1.0 ; python_full_version >= '3.8' and extra == 'docs'
|
|
36
31
|
Maintainer: Jonathan Dung
|
|
37
32
|
Maintainer-email: Jonathan Dung <jonathandung@yahoo.com>
|
|
38
33
|
Requires-Python: >=3.6
|
|
39
|
-
Provides-Extra: docs
|
|
40
34
|
Description-Content-Type: text/markdown
|
|
41
35
|
|
|
42
36
|
# [check-empty](https://pypi.org/p/check-empty)
|
|
@@ -121,7 +115,7 @@ As a pre-commit hook:
|
|
|
121
115
|
# .pre-commit-config.yaml
|
|
122
116
|
repos:
|
|
123
117
|
- repo: https://github.com/jonathandung/check-empty
|
|
124
|
-
rev: v1.2.
|
|
118
|
+
rev: v1.2.3 # repository version
|
|
125
119
|
hooks:
|
|
126
120
|
- id: check-empty # the hook
|
|
127
121
|
args: # example list of arguments
|
|
@@ -137,7 +131,7 @@ equivalent in `prek.toml` format:
|
|
|
137
131
|
```toml
|
|
138
132
|
[[repos]]
|
|
139
133
|
repo = "https://github.com/jonathandung/check-empty"
|
|
140
|
-
rev = "v1.2.
|
|
134
|
+
rev = "v1.2.3"
|
|
141
135
|
|
|
142
136
|
[[repos.hooks]]
|
|
143
137
|
id = "check-empty"
|
|
@@ -161,7 +155,7 @@ or (TOML 1.1+):
|
|
|
161
155
|
# using multiline inline tables
|
|
162
156
|
[[repos]]
|
|
163
157
|
repo = "https://github.com/jonathandung/check-empty"
|
|
164
|
-
rev = "v1.2.
|
|
158
|
+
rev = "v1.2.3"
|
|
165
159
|
hooks = [{
|
|
166
160
|
id = "check-empty",
|
|
167
161
|
args = ["-Q"],
|
|
@@ -181,7 +175,7 @@ As a GitHub Actions workflow step:
|
|
|
181
175
|
|
|
182
176
|
```yaml
|
|
183
177
|
steps:
|
|
184
|
-
- uses: jonathandung/check-empty@v1.2.
|
|
178
|
+
- uses: jonathandung/check-empty@v1.2.3 # the latest version on the GitHub Actions
|
|
185
179
|
# marketplace; this step will fail and subsequent jobs will not run if any file is
|
|
186
180
|
# not empty
|
|
187
181
|
with:
|
|
@@ -233,8 +227,9 @@ to read the
|
|
|
233
227
|
and the
|
|
234
228
|
[contributing guide](https://github.com/jonathandung/.github/blob/main/CONTRIBUTING.md).
|
|
235
229
|
|
|
236
|
-
To build the docs locally (needs Python 3.12+), install the `docs`
|
|
237
|
-
|
|
230
|
+
To build the docs locally (needs Python 3.12+), install with the `docs`
|
|
231
|
+
[group](https://packaging.python.org/en/latest/specifications/dependency-groups),
|
|
232
|
+
preferably into a virtual environment.
|
|
238
233
|
|
|
239
234
|
Tests are run with:
|
|
240
235
|
|
|
@@ -80,7 +80,7 @@ As a pre-commit hook:
|
|
|
80
80
|
# .pre-commit-config.yaml
|
|
81
81
|
repos:
|
|
82
82
|
- repo: https://github.com/jonathandung/check-empty
|
|
83
|
-
rev: v1.2.
|
|
83
|
+
rev: v1.2.3 # repository version
|
|
84
84
|
hooks:
|
|
85
85
|
- id: check-empty # the hook
|
|
86
86
|
args: # example list of arguments
|
|
@@ -96,7 +96,7 @@ equivalent in `prek.toml` format:
|
|
|
96
96
|
```toml
|
|
97
97
|
[[repos]]
|
|
98
98
|
repo = "https://github.com/jonathandung/check-empty"
|
|
99
|
-
rev = "v1.2.
|
|
99
|
+
rev = "v1.2.3"
|
|
100
100
|
|
|
101
101
|
[[repos.hooks]]
|
|
102
102
|
id = "check-empty"
|
|
@@ -120,7 +120,7 @@ or (TOML 1.1+):
|
|
|
120
120
|
# using multiline inline tables
|
|
121
121
|
[[repos]]
|
|
122
122
|
repo = "https://github.com/jonathandung/check-empty"
|
|
123
|
-
rev = "v1.2.
|
|
123
|
+
rev = "v1.2.3"
|
|
124
124
|
hooks = [{
|
|
125
125
|
id = "check-empty",
|
|
126
126
|
args = ["-Q"],
|
|
@@ -140,7 +140,7 @@ As a GitHub Actions workflow step:
|
|
|
140
140
|
|
|
141
141
|
```yaml
|
|
142
142
|
steps:
|
|
143
|
-
- uses: jonathandung/check-empty@v1.2.
|
|
143
|
+
- uses: jonathandung/check-empty@v1.2.3 # the latest version on the GitHub Actions
|
|
144
144
|
# marketplace; this step will fail and subsequent jobs will not run if any file is
|
|
145
145
|
# not empty
|
|
146
146
|
with:
|
|
@@ -192,8 +192,9 @@ to read the
|
|
|
192
192
|
and the
|
|
193
193
|
[contributing guide](https://github.com/jonathandung/.github/blob/main/CONTRIBUTING.md).
|
|
194
194
|
|
|
195
|
-
To build the docs locally (needs Python 3.12+), install the `docs`
|
|
196
|
-
|
|
195
|
+
To build the docs locally (needs Python 3.12+), install with the `docs`
|
|
196
|
+
[group](https://packaging.python.org/en/latest/specifications/dependency-groups),
|
|
197
|
+
preferably into a virtual environment.
|
|
197
198
|
|
|
198
199
|
Tests are run with:
|
|
199
200
|
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# Copyright © 2026 Jonathan Dung. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Utility to check the emptiness of files and directories.
|
|
4
|
+
|
|
5
|
+
Pre-commit hook, command-line tool and GitHub Action all-in-one.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
from itertools import chain
|
|
13
|
+
|
|
14
|
+
__all__ = ('DelayedReporter', 'Reporter', 'check', 'default_reporter')
|
|
15
|
+
__version__ = '1.2.3'
|
|
16
|
+
|
|
17
|
+
TYPE_CHECKING = False
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
import typing
|
|
20
|
+
from collections.abc import Iterable
|
|
21
|
+
|
|
22
|
+
from _typeshed import FileDescriptorOrPath
|
|
23
|
+
_DIRECTORY_DESCRIPTOR_UNSUPPORTED: BaseException = (
|
|
24
|
+
SystemError('got directory descriptor on Windows')
|
|
25
|
+
if os.name == 'nt'
|
|
26
|
+
else NotImplementedError('directory descriptors are not supported')
|
|
27
|
+
)
|
|
28
|
+
_TYPE_MASK: int = 0xF000
|
|
29
|
+
_S_IFDIR: int = 0x4000
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _Handler:
|
|
33
|
+
__slots__ = 'a', 'c', 'f', 'j', 'v'
|
|
34
|
+
|
|
35
|
+
def __init__(self, *a):
|
|
36
|
+
self.j, self.f, self.a = a
|
|
37
|
+
self.v = None
|
|
38
|
+
|
|
39
|
+
def __enter__(self):
|
|
40
|
+
self.c = False
|
|
41
|
+
return self
|
|
42
|
+
|
|
43
|
+
def o(self):
|
|
44
|
+
with self, open(self.a, 'wb'):
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def e(self, s='invalid fd (negative): %d', t='fd: %d'): # ruff: ignore[property-with-parameters]
|
|
49
|
+
r = self.v
|
|
50
|
+
if r is None:
|
|
51
|
+
a = self.a
|
|
52
|
+
self.v = r = (t, s)[a < 0] % a if isinstance(a, int) else os.fsdecode(a)
|
|
53
|
+
return r
|
|
54
|
+
|
|
55
|
+
def __exit__(self, t, v, _):
|
|
56
|
+
if t is None or not issubclass(t, OSError):
|
|
57
|
+
return False
|
|
58
|
+
self.j(self.e) if v.errno == 2 else self.f(str(v))
|
|
59
|
+
self.c = True
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Reporter:
|
|
64
|
+
"""Type of all reporters that :func:`check` accepts as the ``reporter`` argument."""
|
|
65
|
+
__slots__ = 'o',
|
|
66
|
+
o: typing.IO[str] | None
|
|
67
|
+
|
|
68
|
+
def __init__(self):
|
|
69
|
+
"""Initialize the reporter."""
|
|
70
|
+
self.o = None
|
|
71
|
+
|
|
72
|
+
def to(self, out: typing.IO[str] | None = None) -> typing.IO[str] | None:
|
|
73
|
+
"""Temporarily redirect the output of the reporter for one check.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
out: The output stream to redirect to. If ``None``, the reporter will write
|
|
77
|
+
to :data:`sys.stdout`.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
The previous output stream.
|
|
81
|
+
|
|
82
|
+
"""
|
|
83
|
+
self.o, o = out, self.o
|
|
84
|
+
return o
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def calculate_result(y: bool, x: bool, d: bool) -> int:
|
|
88
|
+
"""Set the result of the check as :attr:`r`.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
The exit code of the check. For the default implementation, this is a
|
|
92
|
+
bitwise or of 1 (some files were not empty), 4 (some files or directories
|
|
93
|
+
were absent and :ref:`-m <check-empty--m>` / :ref:`--may-not-exist
|
|
94
|
+
<check-empty---may-not-exist>` was omitted) and 8 (caught :exc:`OSError`
|
|
95
|
+
while processing some files), such that 0 is correctly the only return
|
|
96
|
+
value that represents success. The 2 bit is skipped since 2 is the exit
|
|
97
|
+
code of :class:`argparse.ArgumentParser` when it encounters invalid
|
|
98
|
+
arguments.
|
|
99
|
+
"""
|
|
100
|
+
return y | x << 2 | d << 3
|
|
101
|
+
|
|
102
|
+
def report(self, z, w, r, x, y, d, p, a, b, t, n, v, c): # ruff: ignore[too-many-arguments, too-many-positional-arguments]
|
|
103
|
+
"""Report the results of the check.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
z: List of files that were not found.
|
|
107
|
+
w: List of error messages for files that could not be processed.
|
|
108
|
+
r: List of 2-tuples representing non-empty files and their sizes.
|
|
109
|
+
x: Number of files that were not found.
|
|
110
|
+
y: Number of non-empty files.
|
|
111
|
+
d: Number of I/O errors encountered.
|
|
112
|
+
p: Number of empty files.
|
|
113
|
+
a: List of directories that were recursed into.
|
|
114
|
+
b: List of empty file paths.
|
|
115
|
+
t: Total size of non-empty files in bytes.
|
|
116
|
+
n: Total number of files checked.
|
|
117
|
+
v: The value of ``verbosity`` as passed to :func:`check`. For the default
|
|
118
|
+
implementation, verbosity > 5 is equivalent to verbosity = 5, and a
|
|
119
|
+
non-positive verbosity gives no output.
|
|
120
|
+
c: The value of ``clear`` as passed to :func:`check`.
|
|
121
|
+
"""
|
|
122
|
+
q = self.o
|
|
123
|
+
q = (sys.stdout if q is None else q).write
|
|
124
|
+
if a is not None:
|
|
125
|
+
q('\nRecursing into directory: '.join(a))
|
|
126
|
+
q('\n')
|
|
127
|
+
if z is not None:
|
|
128
|
+
q('\nNot found: '.join(z))
|
|
129
|
+
q(
|
|
130
|
+
f'{x} file{"s" if x > 1 else ""} not found\n'
|
|
131
|
+
if x
|
|
132
|
+
else 'All files were found\n'
|
|
133
|
+
)
|
|
134
|
+
if w is not None:
|
|
135
|
+
q('\nError: '.join(w))
|
|
136
|
+
if d:
|
|
137
|
+
q(f'{d} I/O error{"s" if d > 1 else ""} encountered\n')
|
|
138
|
+
if b is not None:
|
|
139
|
+
q('\nEmpty: '.join(b))
|
|
140
|
+
if v > 2:
|
|
141
|
+
q(f'\n{p or "No"} empty file{"" if p == 1 else "s"}\n')
|
|
142
|
+
if y:
|
|
143
|
+
if r is not None:
|
|
144
|
+
q(
|
|
145
|
+
('\nCleared: ' if c else '\nNot empty: ').join(
|
|
146
|
+
chain(('',), map('%s (%d bytes)'.__mod__, r))
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
q('\n')
|
|
150
|
+
q(f'{y} offending file{"s" if y > 1 else ""}\nTotal size: {t} bytes\n')
|
|
151
|
+
elif n > x:
|
|
152
|
+
q('All found files were empty\n')
|
|
153
|
+
|
|
154
|
+
def reset_stream(self) -> None:
|
|
155
|
+
"""Reset the reporter, closing the output stream if appropriate."""
|
|
156
|
+
q, self.o = self.o, None
|
|
157
|
+
if q not in {None, sys.stdout, sys.stderr, sys.__stdout__, sys.__stderr__}:
|
|
158
|
+
q.close()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class DelayedReporter(Reporter):
|
|
162
|
+
"""Only report the results only when :meth:`do_report` is called."""
|
|
163
|
+
__slots__ = ('a',)
|
|
164
|
+
|
|
165
|
+
def report(self, *a): # ty: ignore[invalid-method-override]
|
|
166
|
+
"""Store the report arguments for later use."""
|
|
167
|
+
self.a = a
|
|
168
|
+
|
|
169
|
+
def do_report(self) -> None:
|
|
170
|
+
"""Conduct the report for the most recent check using this reporter."""
|
|
171
|
+
super().report(*self.a)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
default_reporter: Reporter = Reporter()
|
|
175
|
+
"""The default reporter used by :func:`check`."""
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def check(
|
|
179
|
+
files: Iterable[FileDescriptorOrPath],
|
|
180
|
+
*,
|
|
181
|
+
clear: bool = False,
|
|
182
|
+
may_not_exist: bool = False,
|
|
183
|
+
verbosity: typing.SupportsIndex = 2,
|
|
184
|
+
reporter: Reporter = default_reporter,
|
|
185
|
+
) -> int:
|
|
186
|
+
"""Check the emptiness of files, recursing into directories if passed.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
files: an iterable of file descriptors or paths representing the files and
|
|
190
|
+
directories to check; directory descriptors (*nix) are not supported.
|
|
191
|
+
clear: if ``True``, clear the contents of non-empty files; directories, notably,
|
|
192
|
+
are not purged, but all files within should become empty.
|
|
193
|
+
may_not_exist: if ``True``, do not treat absent files or directories as errors.
|
|
194
|
+
verbosity: how much detail the program should print to stdout.
|
|
195
|
+
reporter: The reporter to use for reporting the results. All output of this
|
|
196
|
+
function is produced by the reporter.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
The integer exit code calculated by the reporter.
|
|
200
|
+
"""
|
|
201
|
+
files = list(files)
|
|
202
|
+
if not files:
|
|
203
|
+
return 0
|
|
204
|
+
k, j, t, n, o = [0] * 4, [], 0, len(files), files.pop
|
|
205
|
+
u, e, _ = j.extend, j.pop, lambda v: lambda _, k=k: k.__setitem__(v, k[v] + 1)
|
|
206
|
+
verbosity = max(0, type(verbosity).__index__(verbosity))
|
|
207
|
+
if verbosity > 4:
|
|
208
|
+
b = ['']
|
|
209
|
+
f = b.append
|
|
210
|
+
else:
|
|
211
|
+
b, f = None, _(0)
|
|
212
|
+
if verbosity > 3:
|
|
213
|
+
v = ['']
|
|
214
|
+
x = v.append
|
|
215
|
+
else:
|
|
216
|
+
v, x = None, lambda _: None
|
|
217
|
+
if verbosity > 2:
|
|
218
|
+
z, w = [''], ['']
|
|
219
|
+
i = z.append, w.append
|
|
220
|
+
else:
|
|
221
|
+
z = w = None
|
|
222
|
+
i = _(1), _(2)
|
|
223
|
+
if verbosity > 1:
|
|
224
|
+
r = []
|
|
225
|
+
g = r.append
|
|
226
|
+
else:
|
|
227
|
+
r, g = None, _(3)
|
|
228
|
+
while files:
|
|
229
|
+
a = o()
|
|
230
|
+
with _Handler(*i, a) as h:
|
|
231
|
+
q = os.stat(a)
|
|
232
|
+
if h.c:
|
|
233
|
+
continue
|
|
234
|
+
c = h.e
|
|
235
|
+
if q.st_mode & _TYPE_MASK == _S_IFDIR:
|
|
236
|
+
if isinstance(a, int):
|
|
237
|
+
raise _DIRECTORY_DESCRIPTOR_UNSUPPORTED
|
|
238
|
+
x(c)
|
|
239
|
+
u(os.scandir(c))
|
|
240
|
+
continue
|
|
241
|
+
s = q.st_size
|
|
242
|
+
if not s:
|
|
243
|
+
f(c)
|
|
244
|
+
continue
|
|
245
|
+
g((c, s))
|
|
246
|
+
t += s
|
|
247
|
+
if clear:
|
|
248
|
+
h.o()
|
|
249
|
+
del o, files
|
|
250
|
+
while j:
|
|
251
|
+
m = e()
|
|
252
|
+
a = m.path
|
|
253
|
+
if m.is_dir():
|
|
254
|
+
x(a)
|
|
255
|
+
u(os.scandir(a))
|
|
256
|
+
continue
|
|
257
|
+
n += 1
|
|
258
|
+
s = m.stat().st_size
|
|
259
|
+
if not s:
|
|
260
|
+
f(a)
|
|
261
|
+
continue
|
|
262
|
+
g((a, s))
|
|
263
|
+
t += s
|
|
264
|
+
if clear:
|
|
265
|
+
_Handler(*i, a).o()
|
|
266
|
+
x = k[1] if z is None else len(z) - 1
|
|
267
|
+
d = k[2] if w is None else len(w) - 1
|
|
268
|
+
y = k[3] if r is None else len(r)
|
|
269
|
+
if verbosity:
|
|
270
|
+
p = k[0] if b is None else len(b) - 1
|
|
271
|
+
reporter.report(z, w, r, x, y, d, p, v, b, t, n, verbosity, clear)
|
|
272
|
+
reporter.reset_stream()
|
|
273
|
+
return reporter.calculate_result(*map(bool, (y, x and not may_not_exist, d)))
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
del TYPE_CHECKING
|
|
@@ -6,11 +6,8 @@ import check_empty as c
|
|
|
6
6
|
|
|
7
7
|
TYPE_CHECKING = False
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
|
-
import typing
|
|
10
9
|
from collections.abc import Iterable
|
|
11
|
-
|
|
12
|
-
from check_empty import ExitCode
|
|
13
|
-
__all__ = 'main',
|
|
10
|
+
__all__ = ('main',)
|
|
14
11
|
_p = __import__('argparse').ArgumentParser(
|
|
15
12
|
'check-empty',
|
|
16
13
|
description='Assert or enforce that some files, or even directories, are empty. '
|
|
@@ -38,11 +35,11 @@ f('-V', '--verbose', action='count', default=0, help='increase output verbosity'
|
|
|
38
35
|
f('-o', '--out', help='write output to this file instead of stdout')
|
|
39
36
|
|
|
40
37
|
|
|
41
|
-
def
|
|
38
|
+
def _r():
|
|
42
39
|
return _p
|
|
43
40
|
|
|
44
41
|
|
|
45
|
-
def main(argv: Iterable[str] | None = None) ->
|
|
42
|
+
def main(argv: Iterable[str] | None = None) -> int:
|
|
46
43
|
"""Run the hook/CLI on the files in the command-line arguments passed.
|
|
47
44
|
|
|
48
45
|
Args:
|
|
@@ -59,18 +56,13 @@ def main(argv: Iterable[str] | None = None) -> ExitCode | typing.Literal[2]:
|
|
|
59
56
|
o = n.out
|
|
60
57
|
if o is not None:
|
|
61
58
|
# ruff: ignore[open-file-with-context-handler]
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
out=o,
|
|
70
|
-
)
|
|
71
|
-
finally:
|
|
72
|
-
if o is not None:
|
|
73
|
-
o.close()
|
|
59
|
+
c.default_reporter.to(open(o, 'w', encoding='utf-8'))
|
|
60
|
+
return c.check(
|
|
61
|
+
n.filenames,
|
|
62
|
+
clear=n.clear,
|
|
63
|
+
may_not_exist=n.may_not_exist,
|
|
64
|
+
verbosity=2 + n.verbose - n.quiet,
|
|
65
|
+
)
|
|
74
66
|
|
|
75
67
|
|
|
76
68
|
del f, TYPE_CHECKING
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["uv_build>=0.12.
|
|
2
|
+
requires = ["uv_build>=0.12.12"]
|
|
3
3
|
build-backend = "uv_build"
|
|
4
4
|
|
|
5
|
+
[dependency-groups]
|
|
6
|
+
docs = [
|
|
7
|
+
"furo==2025.12.19",
|
|
8
|
+
"sphinx==9.1.0",
|
|
9
|
+
"sphinx-argparse-cli==1.22.1",
|
|
10
|
+
"sphinx-copybutton==0.5.2",
|
|
11
|
+
"sphinx-lint==1.0.2",
|
|
12
|
+
"sphinx-notfound-page==1.1.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
5
15
|
[project]
|
|
6
16
|
classifiers = [
|
|
7
17
|
"Development Status :: 5 - Production/Stable",
|
|
@@ -20,6 +30,7 @@ classifiers = [
|
|
|
20
30
|
"Programming Language :: Python :: 3.13",
|
|
21
31
|
"Programming Language :: Python :: 3.14",
|
|
22
32
|
"Programming Language :: Python :: 3.15",
|
|
33
|
+
"Programming Language :: Python :: 3.16",
|
|
23
34
|
"Programming Language :: Python :: Free Threading :: 3 - Stable",
|
|
24
35
|
"Topic :: Utilities",
|
|
25
36
|
"Typing :: Typed",
|
|
@@ -47,7 +58,7 @@ license-files = [
|
|
|
47
58
|
name = "check-empty"
|
|
48
59
|
readme = "README.md"
|
|
49
60
|
requires-python = ">=3.6"
|
|
50
|
-
version = "1.2.
|
|
61
|
+
version = "1.2.3"
|
|
51
62
|
|
|
52
63
|
[[project.authors]]
|
|
53
64
|
name = "Jonathan Dung"
|
|
@@ -57,21 +68,11 @@ email = "jonathandung@yahoo.com"
|
|
|
57
68
|
name = "Jonathan Dung"
|
|
58
69
|
email = "jonathandung@yahoo.com"
|
|
59
70
|
|
|
60
|
-
[project.optional-dependencies]
|
|
61
|
-
docs = [
|
|
62
|
-
"furo==2025.12.19; python_version>='3.9'",
|
|
63
|
-
"sphinx==9.1.0; python_version>='3.12'",
|
|
64
|
-
"sphinx-argparse-cli==1.22.1; python_version>='3.11'",
|
|
65
|
-
"sphinx-copybutton==0.5.2; python_version>='3.7'",
|
|
66
|
-
"sphinx-lint==1.0.2; python_version>='3.10'",
|
|
67
|
-
"sphinx-notfound-page==1.1.0; python_version>='3.8'",
|
|
68
|
-
]
|
|
69
|
-
|
|
70
71
|
[project.scripts]
|
|
71
72
|
check-empty = "check_empty.__main__:main"
|
|
72
73
|
|
|
73
74
|
[tool.ruff]
|
|
74
|
-
required-version = ">=0.16.
|
|
75
|
+
required-version = ">=0.16.6"
|
|
75
76
|
target-version = "py37"
|
|
76
77
|
|
|
77
78
|
[tool.ruff.analyze]
|
|
@@ -128,26 +129,11 @@ all = "error"
|
|
|
128
129
|
missing-override-decorator = "ignore"
|
|
129
130
|
|
|
130
131
|
[tool.uv]
|
|
131
|
-
required-version = ">=0.12.
|
|
132
|
-
|
|
133
|
-
[tool.uv.audit]
|
|
134
|
-
ignore = [
|
|
135
|
-
"GHSA-2xpw-w6gg-jr37",
|
|
136
|
-
"GHSA-34jh-p97f-mpxf",
|
|
137
|
-
"GHSA-38jv-5279-wg99",
|
|
138
|
-
"GHSA-48p4-8xcf-vxj5",
|
|
139
|
-
"GHSA-5239-wwwm-4pmq",
|
|
140
|
-
"GHSA-65pc-fj4g-8rjx",
|
|
141
|
-
"GHSA-9hjg-9r4m-mvj7",
|
|
142
|
-
"GHSA-9wx4-h78v-vm56",
|
|
143
|
-
"GHSA-gc5v-m9x4-r6x2",
|
|
144
|
-
"GHSA-gm62-xv2j-4w53",
|
|
145
|
-
"GHSA-jfmj-5v4g-7637",
|
|
146
|
-
"GHSA-mf9v-mfxr-j63j",
|
|
147
|
-
"GHSA-pq67-6m6q-mj2v",
|
|
148
|
-
"GHSA-qccp-gfcp-xxvc",
|
|
149
|
-
]
|
|
132
|
+
required-version = ">=0.12.12"
|
|
150
133
|
|
|
151
134
|
[tool.uv.build-backend]
|
|
152
135
|
module-name = "check_empty"
|
|
153
136
|
module-root = ""
|
|
137
|
+
|
|
138
|
+
[tool.uv.dependency-groups.docs]
|
|
139
|
+
requires-python = ">=3.12"
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["uv_build>=0.12.
|
|
2
|
+
requires = ["uv_build>=0.12.12"]
|
|
3
3
|
build-backend = "uv_build"
|
|
4
4
|
|
|
5
|
+
[dependency-groups]
|
|
6
|
+
docs = ["furo==2025.12.19", "sphinx==9.1.0", "sphinx-argparse-cli==1.22.1", "sphinx-copybutton==0.5.2", "sphinx-lint==1.0.2", "sphinx-notfound-page==1.1.0"]
|
|
7
|
+
|
|
5
8
|
[project]
|
|
6
9
|
authors = [{name = "Jonathan Dung", email = "jonathandung@yahoo.com"}]
|
|
7
|
-
classifiers = ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.15", "Programming Language :: Python :: Free Threading :: 3 - Stable", "Topic :: Utilities", "Typing :: Typed"]
|
|
10
|
+
classifiers = ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3.15", "Programming Language :: Python :: 3.16", "Programming Language :: Python :: Free Threading :: 3 - Stable", "Topic :: Utilities", "Typing :: Typed"]
|
|
8
11
|
description = "Command-line utility, as well as a hook usable by the Python pre-commit framework, and a GitHub Action, to ensure that selected files are empty."
|
|
9
12
|
keywords = ["check", "command-line", "empty", "files", "github-action", "helper", "hook", "lightweight", "pre-commit", "prek", "tool", "utility"]
|
|
10
13
|
license = "MIT"
|
|
@@ -13,16 +16,13 @@ maintainers = [{name = "Jonathan Dung", email = "jonathandung@yahoo.com"}]
|
|
|
13
16
|
name = "check-empty"
|
|
14
17
|
readme = "README.md"
|
|
15
18
|
requires-python = ">=3.6"
|
|
16
|
-
version = "1.2.
|
|
17
|
-
|
|
18
|
-
[project.optional-dependencies]
|
|
19
|
-
docs = ["furo==2025.12.19; python_version>='3.9'", "sphinx==9.1.0; python_version>='3.12'", "sphinx-argparse-cli==1.22.1; python_version>='3.11'", "sphinx-copybutton==0.5.2; python_version>='3.7'", "sphinx-lint==1.0.2; python_version>='3.10'", "sphinx-notfound-page==1.1.0; python_version>='3.8'"]
|
|
19
|
+
version = "1.2.3"
|
|
20
20
|
|
|
21
21
|
[project.scripts]
|
|
22
22
|
check-empty = "check_empty.__main__:main"
|
|
23
23
|
|
|
24
24
|
[tool.ruff]
|
|
25
|
-
required-version = ">=0.16.
|
|
25
|
+
required-version = ">=0.16.6"
|
|
26
26
|
target-version = "py37"
|
|
27
27
|
|
|
28
28
|
[tool.ruff.analyze]
|
|
@@ -58,11 +58,11 @@ all = "error"
|
|
|
58
58
|
missing-override-decorator = "ignore"
|
|
59
59
|
|
|
60
60
|
[tool.uv]
|
|
61
|
-
required-version = ">=0.12.
|
|
62
|
-
|
|
63
|
-
[tool.uv.audit]
|
|
64
|
-
ignore = ["GHSA-2xpw-w6gg-jr37", "GHSA-34jh-p97f-mpxf", "GHSA-38jv-5279-wg99", "GHSA-48p4-8xcf-vxj5", "GHSA-5239-wwwm-4pmq", "GHSA-65pc-fj4g-8rjx", "GHSA-9hjg-9r4m-mvj7", "GHSA-9wx4-h78v-vm56", "GHSA-gc5v-m9x4-r6x2", "GHSA-gm62-xv2j-4w53", "GHSA-jfmj-5v4g-7637", "GHSA-mf9v-mfxr-j63j", "GHSA-pq67-6m6q-mj2v", "GHSA-qccp-gfcp-xxvc"]
|
|
61
|
+
required-version = ">=0.12.12"
|
|
65
62
|
|
|
66
63
|
[tool.uv.build-backend]
|
|
67
64
|
module-name = "check_empty"
|
|
68
65
|
module-root = ""
|
|
66
|
+
|
|
67
|
+
[tool.uv.dependency-groups]
|
|
68
|
+
docs = {requires-python = ">=3.12"}
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
# Copyright © 2026 Jonathan Dung. All rights reserved.
|
|
2
|
-
# SPDX-License-Identifier: MIT
|
|
3
|
-
"""Utility to check the emptiness of files and directories.
|
|
4
|
-
|
|
5
|
-
Pre-commit hook, command-line tool and GitHub Action all-in-one.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import os
|
|
11
|
-
|
|
12
|
-
__all__ = ('ExitCode', 'check')
|
|
13
|
-
__version__ = '1.2.1'
|
|
14
|
-
|
|
15
|
-
TYPE_CHECKING = False
|
|
16
|
-
if TYPE_CHECKING:
|
|
17
|
-
import typing
|
|
18
|
-
from collections.abc import Iterable
|
|
19
|
-
|
|
20
|
-
from _typeshed import FileDescriptorOrPath
|
|
21
|
-
|
|
22
|
-
ExitCode: typing.TypeAlias = typing.Literal[0, 1, 4, 5, 8, 9, 12, 13]
|
|
23
|
-
else:
|
|
24
|
-
ExitCode = int
|
|
25
|
-
_DIRECTORY_DESCRIPTOR_UNSUPPORTED: BaseException = (
|
|
26
|
-
SystemError('got directory descriptor on Windows')
|
|
27
|
-
if os.name == 'nt'
|
|
28
|
-
else NotImplementedError('directory descriptors are not supported')
|
|
29
|
-
)
|
|
30
|
-
_TYPE_MASK: int = 0xF000
|
|
31
|
-
_S_IFDIR: int = 0x4000
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class _Handler:
|
|
35
|
-
__slots__ = 'a', 'c', 'f', 'j', 'v'
|
|
36
|
-
|
|
37
|
-
def __init__(self, *a):
|
|
38
|
-
self.j, self.f, self.a = a
|
|
39
|
-
self.v = None
|
|
40
|
-
|
|
41
|
-
def __enter__(self):
|
|
42
|
-
self.c = False
|
|
43
|
-
return self
|
|
44
|
-
|
|
45
|
-
def o(self):
|
|
46
|
-
with self, open(self.a, 'wb'):
|
|
47
|
-
...
|
|
48
|
-
|
|
49
|
-
@property
|
|
50
|
-
def e(self, s='invalid fd (negative): %d', t='fd: %d'): # ruff: ignore[property-with-parameters]
|
|
51
|
-
r = self.v
|
|
52
|
-
if r is None:
|
|
53
|
-
a = self.a
|
|
54
|
-
self.v = r = (t, s)[a < 0] % a if isinstance(a, int) else os.fsdecode(a)
|
|
55
|
-
return r
|
|
56
|
-
|
|
57
|
-
def __exit__(self, t, v, _):
|
|
58
|
-
if t is None or not issubclass(t, OSError):
|
|
59
|
-
return False
|
|
60
|
-
self.j(self.e) if v.errno == 2 else self.f(str(v))
|
|
61
|
-
self.c = True
|
|
62
|
-
return True
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def check(
|
|
66
|
-
files: Iterable[FileDescriptorOrPath],
|
|
67
|
-
*,
|
|
68
|
-
clear: bool = False,
|
|
69
|
-
may_not_exist: bool = False,
|
|
70
|
-
verbosity: typing.SupportsIndex = 2,
|
|
71
|
-
out: typing.IO[str] | None = None,
|
|
72
|
-
) -> ExitCode:
|
|
73
|
-
"""Check the emptiness of files, recursing into directories if passed.
|
|
74
|
-
|
|
75
|
-
Args:
|
|
76
|
-
files: an iterable of file descriptors or paths representing the files and
|
|
77
|
-
directories to check; directory descriptors (Unix) are not supported.
|
|
78
|
-
clear: if ``True``, clear the contents of non-empty files; directories, notably,
|
|
79
|
-
are not purged, but all files within should become empty.
|
|
80
|
-
may_not_exist: if ``True``, do not treat absent files or directories as errors.
|
|
81
|
-
verbosity: how much detail the program should print to stdout; verbosity > 5 is
|
|
82
|
-
equivalent to verbosity = 5, and a non-positive verbosity gives no output.
|
|
83
|
-
out: The file to which output is printed; default :data:`sys.stdout`.
|
|
84
|
-
|
|
85
|
-
Returns:
|
|
86
|
-
The integer exit code. A bitwise or of 1 (some files were not empty), 4 (some
|
|
87
|
-
files or directories were absent and :ref:`-m <check-empty--m>` /
|
|
88
|
-
:ref:`--may-not-exist <check-empty---may-not-exist>` was omitted) and 8 (caught
|
|
89
|
-
:exc:`OSError` while processing some files), such that 0 is correctly the only
|
|
90
|
-
return value that represents success. The 2 bit is skipped since 2 is the exit
|
|
91
|
-
code of :class:`argparse.ArgumentParser` when it encounters invalid arguments.
|
|
92
|
-
|
|
93
|
-
"""
|
|
94
|
-
files = list(files)
|
|
95
|
-
if not files:
|
|
96
|
-
return 0
|
|
97
|
-
k, j, t, n, o = [0] * 4, [], 0, len(files), files.pop
|
|
98
|
-
u, e, _ = j.extend, j.pop, lambda v: lambda _, k=k: k.__setitem__(v, k[v] + 1)
|
|
99
|
-
|
|
100
|
-
verbosity = type(verbosity).__index__(verbosity)
|
|
101
|
-
if verbosity > 4:
|
|
102
|
-
b = ['']
|
|
103
|
-
f = b.append
|
|
104
|
-
else:
|
|
105
|
-
b, f = None, _(0)
|
|
106
|
-
if verbosity > 3:
|
|
107
|
-
rc = ['']
|
|
108
|
-
x = rc.append
|
|
109
|
-
else:
|
|
110
|
-
rc, x = None, lambda _: None
|
|
111
|
-
if verbosity > 2:
|
|
112
|
-
z, w = [''], ['']
|
|
113
|
-
i = z.append, w.append
|
|
114
|
-
else:
|
|
115
|
-
z = w = None
|
|
116
|
-
i = _(1), _(2)
|
|
117
|
-
if verbosity > 1:
|
|
118
|
-
r = ['']
|
|
119
|
-
g = r.append
|
|
120
|
-
else:
|
|
121
|
-
r, g = None, _(3)
|
|
122
|
-
while files:
|
|
123
|
-
a = o()
|
|
124
|
-
with _Handler(*i, a) as h:
|
|
125
|
-
q = os.stat(a)
|
|
126
|
-
if h.c:
|
|
127
|
-
continue
|
|
128
|
-
c = h.e
|
|
129
|
-
if q.st_mode & _TYPE_MASK == _S_IFDIR:
|
|
130
|
-
if isinstance(a, int):
|
|
131
|
-
raise _DIRECTORY_DESCRIPTOR_UNSUPPORTED
|
|
132
|
-
x(c)
|
|
133
|
-
u(os.scandir(c))
|
|
134
|
-
continue
|
|
135
|
-
s = q.st_size
|
|
136
|
-
if not s:
|
|
137
|
-
f(c)
|
|
138
|
-
continue
|
|
139
|
-
g(f'{c} ({s} bytes)')
|
|
140
|
-
t += s
|
|
141
|
-
if clear:
|
|
142
|
-
h.o()
|
|
143
|
-
del o, files
|
|
144
|
-
while j:
|
|
145
|
-
m = e()
|
|
146
|
-
a = m.path
|
|
147
|
-
if m.is_dir():
|
|
148
|
-
x(a)
|
|
149
|
-
u(os.scandir(a))
|
|
150
|
-
continue
|
|
151
|
-
n += 1
|
|
152
|
-
s = m.stat().st_size
|
|
153
|
-
if not s:
|
|
154
|
-
f(a)
|
|
155
|
-
continue
|
|
156
|
-
g(f'{a} ({s} bytes)')
|
|
157
|
-
t += s
|
|
158
|
-
if clear:
|
|
159
|
-
_Handler(*i, a).o()
|
|
160
|
-
x = k[1] if z is None else len(z) - 1
|
|
161
|
-
y = k[3] if r is None else len(r) - 1
|
|
162
|
-
d = k[2] if w is None else len(w) - 1
|
|
163
|
-
v = bool(y) | (bool(x) and not may_not_exist) << 2 | bool(d) << 3
|
|
164
|
-
if verbosity <= 0:
|
|
165
|
-
return v # ty: ignore[invalid-return-type]
|
|
166
|
-
q = (__import__('sys').stdout if out is None else out).write
|
|
167
|
-
if rc is not None:
|
|
168
|
-
q('\nRecursing into directory: '.join(rc))
|
|
169
|
-
q('\n\n')
|
|
170
|
-
if z is not None:
|
|
171
|
-
q('\nNot found: '.join(z))
|
|
172
|
-
q('\n')
|
|
173
|
-
q(f'{x} file{"s" if x > 1 else ""} not found\n' if x else 'All files were found\n')
|
|
174
|
-
if w is not None:
|
|
175
|
-
q('\nError: '.join(w))
|
|
176
|
-
q('\n')
|
|
177
|
-
if d:
|
|
178
|
-
q(f'{d} I/O error{"s" if d > 1 else ""} encountered\n')
|
|
179
|
-
if b is not None:
|
|
180
|
-
q('\nEmpty: '.join(b))
|
|
181
|
-
q('\n')
|
|
182
|
-
if verbosity > 2:
|
|
183
|
-
p = k[0] if b is None else len(b) - 1
|
|
184
|
-
q(f'{p or "No"} empty file{"" if p == 1 else "s"}\n')
|
|
185
|
-
if y:
|
|
186
|
-
if r is not None:
|
|
187
|
-
q(('\nCleared: ' if clear else '\nNot empty: ').join(r))
|
|
188
|
-
q('\n\n')
|
|
189
|
-
q(f'{y} offending file{"s" if y > 1 else ""}\nTotal size: {t} bytes\n')
|
|
190
|
-
elif n > x:
|
|
191
|
-
q('All found files were empty\n')
|
|
192
|
-
return v # ty: ignore[invalid-return-type]
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
del TYPE_CHECKING
|
|
File without changes
|
|
File without changes
|
|
File without changes
|