check-empty 1.2.2__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.2 → check_empty-1.2.3}/PKG-INFO +5 -5
- {check_empty-1.2.2 → check_empty-1.2.3}/README.md +4 -4
- check_empty-1.2.3/check_empty/__init__.py +276 -0
- {check_empty-1.2.2 → check_empty-1.2.3}/check_empty/__main__.py +9 -17
- {check_empty-1.2.2 → check_empty-1.2.3}/pyproject.toml +13 -13
- {check_empty-1.2.2 → check_empty-1.2.3}/pyproject.toml.orig +6 -6
- check_empty-1.2.2/check_empty/__init__.py +0 -195
- {check_empty-1.2.2 → check_empty-1.2.3}/AUTHORS.md +0 -0
- {check_empty-1.2.2 → check_empty-1.2.3}/LICENSE +0 -0
- {check_empty-1.2.2 → 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
|
|
@@ -115,7 +115,7 @@ As a pre-commit hook:
|
|
|
115
115
|
# .pre-commit-config.yaml
|
|
116
116
|
repos:
|
|
117
117
|
- repo: https://github.com/jonathandung/check-empty
|
|
118
|
-
rev: v1.2.
|
|
118
|
+
rev: v1.2.3 # repository version
|
|
119
119
|
hooks:
|
|
120
120
|
- id: check-empty # the hook
|
|
121
121
|
args: # example list of arguments
|
|
@@ -131,7 +131,7 @@ equivalent in `prek.toml` format:
|
|
|
131
131
|
```toml
|
|
132
132
|
[[repos]]
|
|
133
133
|
repo = "https://github.com/jonathandung/check-empty"
|
|
134
|
-
rev = "v1.2.
|
|
134
|
+
rev = "v1.2.3"
|
|
135
135
|
|
|
136
136
|
[[repos.hooks]]
|
|
137
137
|
id = "check-empty"
|
|
@@ -155,7 +155,7 @@ or (TOML 1.1+):
|
|
|
155
155
|
# using multiline inline tables
|
|
156
156
|
[[repos]]
|
|
157
157
|
repo = "https://github.com/jonathandung/check-empty"
|
|
158
|
-
rev = "v1.2.
|
|
158
|
+
rev = "v1.2.3"
|
|
159
159
|
hooks = [{
|
|
160
160
|
id = "check-empty",
|
|
161
161
|
args = ["-Q"],
|
|
@@ -175,7 +175,7 @@ As a GitHub Actions workflow step:
|
|
|
175
175
|
|
|
176
176
|
```yaml
|
|
177
177
|
steps:
|
|
178
|
-
- uses: jonathandung/check-empty@v1.2.
|
|
178
|
+
- uses: jonathandung/check-empty@v1.2.3 # the latest version on the GitHub Actions
|
|
179
179
|
# marketplace; this step will fail and subsequent jobs will not run if any file is
|
|
180
180
|
# not empty
|
|
181
181
|
with:
|
|
@@ -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:
|
|
@@ -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. '
|
|
@@ -42,7 +39,7 @@ 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",
|
|
@@ -48,7 +58,7 @@ license-files = [
|
|
|
48
58
|
name = "check-empty"
|
|
49
59
|
readme = "README.md"
|
|
50
60
|
requires-python = ">=3.6"
|
|
51
|
-
version = "1.2.
|
|
61
|
+
version = "1.2.3"
|
|
52
62
|
|
|
53
63
|
[[project.authors]]
|
|
54
64
|
name = "Jonathan Dung"
|
|
@@ -61,16 +71,6 @@ email = "jonathandung@yahoo.com"
|
|
|
61
71
|
[project.scripts]
|
|
62
72
|
check-empty = "check_empty.__main__:main"
|
|
63
73
|
|
|
64
|
-
[dependency-groups]
|
|
65
|
-
docs = [
|
|
66
|
-
"furo==2025.12.19",
|
|
67
|
-
"sphinx==9.1.0",
|
|
68
|
-
"sphinx-argparse-cli==1.22.1",
|
|
69
|
-
"sphinx-copybutton==0.5.2",
|
|
70
|
-
"sphinx-lint==1.0.2",
|
|
71
|
-
"sphinx-notfound-page==1.1.0",
|
|
72
|
-
]
|
|
73
|
-
|
|
74
74
|
[tool.ruff]
|
|
75
75
|
required-version = ">=0.16.6"
|
|
76
76
|
target-version = "py37"
|
|
@@ -129,7 +129,7 @@ all = "error"
|
|
|
129
129
|
missing-override-decorator = "ignore"
|
|
130
130
|
|
|
131
131
|
[tool.uv]
|
|
132
|
-
required-version = ">=0.12.
|
|
132
|
+
required-version = ">=0.12.12"
|
|
133
133
|
|
|
134
134
|
[tool.uv.build-backend]
|
|
135
135
|
module-name = "check_empty"
|
|
@@ -1,7 +1,10 @@
|
|
|
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
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"]
|
|
@@ -13,10 +16,7 @@ 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
|
-
[dependency-groups]
|
|
19
|
-
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"]
|
|
19
|
+
version = "1.2.3"
|
|
20
20
|
|
|
21
21
|
[project.scripts]
|
|
22
22
|
check-empty = "check_empty.__main__:main"
|
|
@@ -58,7 +58,7 @@ all = "error"
|
|
|
58
58
|
missing-override-decorator = "ignore"
|
|
59
59
|
|
|
60
60
|
[tool.uv]
|
|
61
|
-
required-version = ">=0.12.
|
|
61
|
+
required-version = ">=0.12.12"
|
|
62
62
|
|
|
63
63
|
[tool.uv.build-backend]
|
|
64
64
|
module-name = "check_empty"
|
|
@@ -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.2'
|
|
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
|