check-empty 1.2.2__tar.gz → 1.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: check-empty
3
- Version: 1.2.2
3
+ Version: 1.3.0
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
@@ -28,13 +28,27 @@ Classifier: Programming Language :: Python :: 3.16
28
28
  Classifier: Programming Language :: Python :: Free Threading :: 3 - Stable
29
29
  Classifier: Topic :: Utilities
30
30
  Classifier: Typing :: Typed
31
+ Requires-Dist: py7zr==1.1.3 ; python_full_version >= '3.10' and extra == '7z'
32
+ Requires-Dist: py7zr==1.0.0 ; python_full_version == '3.9.*' and extra == '7z'
33
+ Requires-Dist: py7zr==0.22.0 ; python_full_version == '3.8.*' and extra == '7z'
34
+ Requires-Dist: py7zr==0.20.6 ; python_full_version == '3.7.*' and extra == '7z'
35
+ Requires-Dist: check-empty[7z,lzh,rar]==1.3.0 ; extra == 'all'
36
+ Requires-Dist: lhafile==0.3.1 ; extra == 'lzh'
37
+ Requires-Dist: rarfile==4.5 ; python_full_version >= '3.10' and extra == 'rar'
38
+ Requires-Dist: rarfile==4.2 ; python_full_version < '3.10' and extra == 'rar'
31
39
  Maintainer: Jonathan Dung
32
40
  Maintainer-email: Jonathan Dung <jonathandung@yahoo.com>
33
- Requires-Python: >=3.6
41
+ Requires-Python: >=3.7
42
+ Provides-Extra: 7z
43
+ Provides-Extra: all
44
+ Provides-Extra: lzh
45
+ Provides-Extra: rar
34
46
  Description-Content-Type: text/markdown
35
47
 
36
48
  # [check-empty](https://pypi.org/p/check-empty)
37
49
 
50
+ [![#StandWithPalestine](https://raw.githubusercontent.com/TheBSD/StandWithPalestine/main/badges/StandWithPalestine.svg)](https://github.com/TheBSD/StandWithPalestine/blob/main/docs/README.md)
51
+
38
52
  A simple, dependency-free [pre-commit](https://pre-commit.com) /
39
53
  [prek](https://prek.j178.dev) hook, CLI, library and
40
54
  [GitHub Action](https://github.com/marketplace/actions/check-empty-files) conglomerate
@@ -46,8 +60,11 @@ specified.
46
60
 
47
61
  ## Prerequisites
48
62
 
49
- Supports CPython 3.6+, PyPy 7.0+, GraalPy 19.0+ out-of-the-box, and most likely every
50
- Python 3.6 runtime you can think of. This is the only requirement to use this tool.
63
+ Supports CPython 3.7+, PyPy 7.3.4+, GraalPy 19.0+ out-of-the-box, and most likely every
64
+ Python 3.7 runtime you can think of. This is the only requirement to use this tool.
65
+
66
+ We cannot support Python 3.6 because of a vulnerability in `py7zr` 0.19.2, which is the
67
+ latest version of `py7zr` that supports Python 3.6.
51
68
 
52
69
  ## Quickstart
53
70
 
@@ -115,7 +132,7 @@ As a pre-commit hook:
115
132
  # .pre-commit-config.yaml
116
133
  repos:
117
134
  - repo: https://github.com/jonathandung/check-empty
118
- rev: v1.2.2 # repository version
135
+ rev: v1.3.0 # repository version
119
136
  hooks:
120
137
  - id: check-empty # the hook
121
138
  args: # example list of arguments
@@ -131,7 +148,7 @@ equivalent in `prek.toml` format:
131
148
  ```toml
132
149
  [[repos]]
133
150
  repo = "https://github.com/jonathandung/check-empty"
134
- rev = "v1.2.2"
151
+ rev = "v1.3.0"
135
152
 
136
153
  [[repos.hooks]]
137
154
  id = "check-empty"
@@ -155,7 +172,7 @@ or (TOML 1.1+):
155
172
  # using multiline inline tables
156
173
  [[repos]]
157
174
  repo = "https://github.com/jonathandung/check-empty"
158
- rev = "v1.2.2"
175
+ rev = "v1.3.0"
159
176
  hooks = [{
160
177
  id = "check-empty",
161
178
  args = ["-Q"],
@@ -175,12 +192,12 @@ As a GitHub Actions workflow step:
175
192
 
176
193
  ```yaml
177
194
  steps:
178
- - uses: jonathandung/check-empty@v1.2.2 # the latest version on the GitHub Actions
195
+ - uses: jonathandung/check-empty@v1.3.0 # the latest version on the GitHub Actions
179
196
  # marketplace; this step will fail and subsequent jobs will not run if any file is
180
197
  # not empty
181
198
  with:
182
199
  python-version: '3.14' # run the script on the latest stable Python version
183
- # Python down to 3.6 is supported but not recommended due to end-of-life
200
+ # Python down to 3.7 is supported but not recommended due to end-of-life
184
201
  verbosity: 1
185
202
  filenames: |
186
203
  src/mylib/py.typed
@@ -227,9 +244,10 @@ to read the
227
244
  and the
228
245
  [contributing guide](https://github.com/jonathandung/.github/blob/main/CONTRIBUTING.md).
229
246
 
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.
247
+ To build the docs locally (needs Python 3.12+ because of Sphinx), install with the
248
+ `docs` [group](https://packaging.python.org/en/latest/specifications/dependency-groups)
249
+ using a package manager that supports it (e.g. pip 25.1+ or uv 0.4.27+), preferably
250
+ into a virtual environment.
233
251
 
234
252
  Tests are run with:
235
253
 
@@ -1,5 +1,7 @@
1
1
  # [check-empty](https://pypi.org/p/check-empty)
2
2
 
3
+ [![#StandWithPalestine](https://raw.githubusercontent.com/TheBSD/StandWithPalestine/main/badges/StandWithPalestine.svg)](https://github.com/TheBSD/StandWithPalestine/blob/main/docs/README.md)
4
+
3
5
  A simple, dependency-free [pre-commit](https://pre-commit.com) /
4
6
  [prek](https://prek.j178.dev) hook, CLI, library and
5
7
  [GitHub Action](https://github.com/marketplace/actions/check-empty-files) conglomerate
@@ -11,8 +13,11 @@ specified.
11
13
 
12
14
  ## Prerequisites
13
15
 
14
- Supports CPython 3.6+, PyPy 7.0+, GraalPy 19.0+ out-of-the-box, and most likely every
15
- Python 3.6 runtime you can think of. This is the only requirement to use this tool.
16
+ Supports CPython 3.7+, PyPy 7.3.4+, GraalPy 19.0+ out-of-the-box, and most likely every
17
+ Python 3.7 runtime you can think of. This is the only requirement to use this tool.
18
+
19
+ We cannot support Python 3.6 because of a vulnerability in `py7zr` 0.19.2, which is the
20
+ latest version of `py7zr` that supports Python 3.6.
16
21
 
17
22
  ## Quickstart
18
23
 
@@ -80,7 +85,7 @@ As a pre-commit hook:
80
85
  # .pre-commit-config.yaml
81
86
  repos:
82
87
  - repo: https://github.com/jonathandung/check-empty
83
- rev: v1.2.2 # repository version
88
+ rev: v1.3.0 # repository version
84
89
  hooks:
85
90
  - id: check-empty # the hook
86
91
  args: # example list of arguments
@@ -96,7 +101,7 @@ equivalent in `prek.toml` format:
96
101
  ```toml
97
102
  [[repos]]
98
103
  repo = "https://github.com/jonathandung/check-empty"
99
- rev = "v1.2.2"
104
+ rev = "v1.3.0"
100
105
 
101
106
  [[repos.hooks]]
102
107
  id = "check-empty"
@@ -120,7 +125,7 @@ or (TOML 1.1+):
120
125
  # using multiline inline tables
121
126
  [[repos]]
122
127
  repo = "https://github.com/jonathandung/check-empty"
123
- rev = "v1.2.2"
128
+ rev = "v1.3.0"
124
129
  hooks = [{
125
130
  id = "check-empty",
126
131
  args = ["-Q"],
@@ -140,12 +145,12 @@ As a GitHub Actions workflow step:
140
145
 
141
146
  ```yaml
142
147
  steps:
143
- - uses: jonathandung/check-empty@v1.2.2 # the latest version on the GitHub Actions
148
+ - uses: jonathandung/check-empty@v1.3.0 # the latest version on the GitHub Actions
144
149
  # marketplace; this step will fail and subsequent jobs will not run if any file is
145
150
  # not empty
146
151
  with:
147
152
  python-version: '3.14' # run the script on the latest stable Python version
148
- # Python down to 3.6 is supported but not recommended due to end-of-life
153
+ # Python down to 3.7 is supported but not recommended due to end-of-life
149
154
  verbosity: 1
150
155
  filenames: |
151
156
  src/mylib/py.typed
@@ -192,9 +197,10 @@ to read the
192
197
  and the
193
198
  [contributing guide](https://github.com/jonathandung/.github/blob/main/CONTRIBUTING.md).
194
199
 
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.
200
+ To build the docs locally (needs Python 3.12+ because of Sphinx), install with the
201
+ `docs` [group](https://packaging.python.org/en/latest/specifications/dependency-groups)
202
+ using a package manager that supports it (e.g. pip 25.1+ or uv 0.4.27+), preferably
203
+ into a virtual environment.
198
204
 
199
205
  Tests are run with:
200
206
 
@@ -0,0 +1,234 @@
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
+ from . import constants, reporter, util
13
+
14
+ if constants.TYPE_CHECKING:
15
+ import typing
16
+ from collections.abc import Iterable
17
+
18
+ from _typeshed import FileDescriptorOrPath
19
+
20
+ __all__ = ('check', 'default_reporter')
21
+ __version__ = '1.3.0'
22
+ default_reporter: reporter.Reporter = reporter.Reporter()
23
+ """The default reporter used by :func:`check`."""
24
+
25
+
26
+ def check(
27
+ files: Iterable[FileDescriptorOrPath],
28
+ *,
29
+ clear: bool = False,
30
+ may_not_exist: bool = False,
31
+ verbosity: typing.SupportsIndex = 2,
32
+ recurse_into: typing.SupportsIndex = constants.RecurseInto.NONE,
33
+ reporter: reporter.Reporter = default_reporter,
34
+ ) -> int:
35
+ # ruff: ignore[docstring-missing-exception]
36
+ r"""Check the emptiness of files, recursing into directories if passed.
37
+
38
+ If using a custom reporter, remember to pass a ``verbosity`` greater than or equal
39
+ to 5, since some info will be missing from the parameters passed to
40
+ :meth:`Reporter.report` otherwise.
41
+
42
+ Args:
43
+ files: an iterable of file descriptors or paths representing the files and
44
+ directories to check; directory descriptors (\*nix) are not supported.
45
+ clear: if ``True``, clear the contents of non-empty files; directories, notably,
46
+ are not purged, but all files within should become empty.
47
+ may_not_exist: if ``True``, do not treat absent files or directories as errors.
48
+ verbosity: how much detail the program should print to stdout.
49
+ recurse_into: a bitwise or of flags indicating which types of archives to
50
+ recurse into; by default, all archives are treated as regular files.
51
+ reporter: The reporter to use for reporting the results. All output of this
52
+ function is produced by the reporter.
53
+
54
+ Returns:
55
+ The integer exit code calculated by the reporter.
56
+
57
+ """
58
+ files = list(files)
59
+ if not files:
60
+ return 0
61
+ k, j, t, n, o = [0] * 5, [], 0, len(files), files.pop
62
+ u, e, _ = j.extend, j.pop, lambda v: lambda _, k=k: k.__setitem__(v, k[v] + 1)
63
+ verbosity, recurse_into = (type(x).__index__(x) for x in (verbosity, recurse_into))
64
+ # ruff: disable[magic-value-comparison]
65
+ if verbosity > 4:
66
+ b = ['']
67
+ f = b.append
68
+ else:
69
+ b, f = None, _(0)
70
+ if verbosity > 3:
71
+ v, ra = [''], ['']
72
+ x, aa = v.append, ra.append
73
+ else:
74
+ v = ra = None
75
+ x = aa = lambda _: None
76
+ if verbosity > 2:
77
+ z, w = [''], ['']
78
+ i = z.append, w.append
79
+ else:
80
+ z = w = None
81
+ i = _(1), _(2)
82
+ if verbosity > 1:
83
+ r = []
84
+ g = r.append
85
+ else:
86
+ r, g = None, _(3)
87
+ # ruff: enable[magic-value-comparison]
88
+ zp, tr, rr, z7, lh = map(
89
+ recurse_into.__and__,
90
+ (x for x in constants.RecurseInto if x > 0 == x & (x - 1))
91
+ if __import__('sys').version_info < (3, 11)
92
+ else constants.RecurseInto,
93
+ )
94
+ an = bool(recurse_into)
95
+ if an:
96
+ from . import archives as ar
97
+ md = 'a' if clear else 'r'
98
+ if constants.TYPE_CHECKING:
99
+ import tarfile as tr
100
+ import zipfile as zp
101
+
102
+ import lhafile as lh
103
+ import py7zr as z7
104
+ import rarfile as rr
105
+ else:
106
+ if tr:
107
+ import tarfile as tr
108
+ if zp:
109
+ import zipfile as zp
110
+ if lh:
111
+ import lhafile as lh
112
+ if z7:
113
+ import py7zr as z7
114
+ if rr:
115
+ import rarfile as rr
116
+
117
+ def ha(a: str) -> bool:
118
+ if not an:
119
+ return False
120
+
121
+ def c() -> None:
122
+ nonlocal t
123
+ g(q)
124
+ t += s
125
+
126
+ def e() -> None:
127
+ c() if s else f(a)
128
+
129
+ def w() -> None:
130
+ if s:
131
+ if clear:
132
+ raise NotImplementedError(m)
133
+ c()
134
+ else:
135
+ f(a)
136
+
137
+ y = []
138
+ if tr and util.try_tar(a, y.append):
139
+ d = y.pop()
140
+ aa(a)
141
+ for r in d.getmembers():
142
+ a, s = q = r.name, r.size
143
+ e()
144
+ if clear:
145
+ ar.clear_tar(d)
146
+ elif zp and zp.is_zipfile(a):
147
+ with zp.ZipFile(a, md) as d:
148
+ aa(a)
149
+ for r in d.infolist():
150
+ a, s = q = r.filename, r.file_size
151
+ if s:
152
+ c()
153
+ if clear:
154
+ ar.clear_file_in_zip(d, r)
155
+ else:
156
+ f(a)
157
+ elif z7 and z7.is_7zfile(a):
158
+ with z7.SevenZipFile(a) as d:
159
+ aa(a)
160
+ for r in d.files:
161
+ a, s = q = r.filename, r.origin.stat().st_size
162
+ e()
163
+ if clear:
164
+ ar.clear_7z(d)
165
+ elif rr and rr.is_rarfile(a):
166
+ m = 'Cannot clear files in RAR archives'
167
+ with rr.RarFile(a) as d:
168
+ aa(a)
169
+ for r in d.infolist():
170
+ a, s = q = r.filename, r.file_size
171
+ w()
172
+ else:
173
+ try:
174
+ d = lh.LhaFile(a)
175
+ except (RuntimeError, lh.BadLhafile):
176
+ return False
177
+ m = 'Cannot clear files in LHA archives'
178
+ aa(a)
179
+ for r in d.filelist:
180
+ a, s = q = r.filename, r.file_size
181
+ w()
182
+ return True
183
+
184
+ while files:
185
+ a, q = o(), None
186
+ m = isinstance(a, int)
187
+ if not m:
188
+ a = os.fsdecode(a)
189
+ if ha(a):
190
+ continue
191
+ with util.Handler(*i, a) as h:
192
+ q = os.stat(a) # ruff: ignore[os-stat]
193
+ if q is None:
194
+ continue
195
+ c = h.e
196
+ if q.st_mode & constants.TYPE_MASK == constants.S_IFDIR:
197
+ if m:
198
+ raise constants.DIRFD_UNSUPPORTED
199
+ x(c)
200
+ u(os.scandir(c))
201
+ continue
202
+ s = q.st_size
203
+ if not s:
204
+ f(c)
205
+ continue
206
+ g((c, s))
207
+ t += s
208
+ if clear:
209
+ h.o()
210
+ del o, files
211
+ while j:
212
+ m = e()
213
+ a = m.path
214
+ if m.is_dir():
215
+ x(a)
216
+ u(os.scandir(a))
217
+ continue
218
+ if ha(a):
219
+ continue
220
+ n += 1
221
+ s = m.stat().st_size
222
+ if not s:
223
+ f(a)
224
+ continue
225
+ g((a, s))
226
+ t += s
227
+ if clear:
228
+ util.Handler(*i, a).o()
229
+ del j
230
+ x, d = k[1] if z is None else len(z) - 1, k[2] if w is None else len(w) - 1
231
+ y, p = k[3] if r is None else len(r), k[0] if b is None else len(b) - 1
232
+ reporter.report(z, w, v, b, r, x, y, d, p, t, n, verbosity, clear)
233
+ reporter.reset_stream()
234
+ return reporter.calculate_result(*map(bool, (y, x and not may_not_exist, d)))
@@ -1,4 +1,4 @@
1
- """Implementation of the main routine. Also exports the argument parser."""
1
+ """Implementation of the main routine."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -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) -> ExitCode | typing.Literal[2]:
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
- o = open(o, 'w', encoding='utf-8')
63
- try:
64
- return c.check(
65
- n.filenames,
66
- clear=n.clear,
67
- may_not_exist=n.may_not_exist,
68
- verbosity=2 + n.verbose - n.quiet,
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
@@ -0,0 +1,45 @@
1
+ """Functions used to clear files in archives."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .constants import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ import zipfile
9
+ from tarfile import TarFile
10
+
11
+ from py7zr import SevenZipFile
12
+ __all__ = ('clear_7z', 'clear_file_in_zip', 'clear_tar')
13
+ _ = __import__('io').BytesIO()
14
+
15
+
16
+ def clear_file_in_zip(z: zipfile.ZipFile, i: zipfile.ZipInfo) -> None:
17
+ """Clear the contents of a file in a .zip archive."""
18
+ z.writestr(i, b'')
19
+
20
+
21
+ def clear_7z(z: SevenZipFile) -> None:
22
+ """Clear the contents of each file in a .7z archive."""
23
+ x = z.filename
24
+ if x is None:
25
+ raise NotImplementedError
26
+ y = z.namelist()
27
+ with __import__('py7zr').SevenZipFile(x, 'w') as a:
28
+ for n in y:
29
+ a.writef(_, n)
30
+
31
+
32
+ def clear_tar(t: TarFile) -> None:
33
+ """Clear the contents of each file in a .tar archive."""
34
+ x = t.name
35
+ if x is None:
36
+ raise NotImplementedError
37
+ import tarfile as z
38
+
39
+ with __import__('tempfile').NamedTemporaryFile(
40
+ suffix='.tar.gz', delete=False
41
+ ) as p, z.open(fileobj=p, mode='w:gz') as f:
42
+ a = f.addfile
43
+ for m in t.getmembers():
44
+ a(z.TarInfo(m.name))
45
+ __import__('os').replace(p.name, x)
@@ -0,0 +1,44 @@
1
+ """Defines constants used by :mod:`check_empty`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ TYPE_CHECKING: bool = False
8
+ if TYPE_CHECKING:
9
+ import typing
10
+ __all__ = ('DIRFD_UNSUPPORTED', 'ENOENT', 'S_IFDIR', 'TYPE_MASK', 'RecurseInto')
11
+ TYPE_MASK: typing.Final = 0xF000
12
+ """This mask extracts the type of a file from its mode bits from :func:`os.stat`."""
13
+ S_IFDIR: typing.Final = 0x4000
14
+ """Equal to :const:`stat.S_IFDIR`."""
15
+ ENOENT: typing.Final = 2
16
+ """Equal to :const:`errno.ENOENT`."""
17
+ DIRFD_UNSUPPORTED: typing.Final[BaseException] = (
18
+ SystemError('got directory descriptor on Windows')
19
+ if os.name == 'nt'
20
+ else NotImplementedError('directory descriptors are not supported')
21
+ )
22
+ """The exception raised when a directory descriptor is passed to :func:`check`."""
23
+
24
+
25
+ class RecurseInto(__import__('enum').IntFlag):
26
+ """Flags for the ``recurse_into`` argument of :func:`check_empty.check`."""
27
+
28
+ NONE = 0
29
+ """Don't recurse into any archives; the default behaviour."""
30
+ ZIP = 1
31
+ """Recurse into .zip archives."""
32
+ TAR = 2
33
+ """Recurse into .tar archives."""
34
+ RAR = 4
35
+ """Recurse into .rar archives; requires :mod:`!rarfile`."""
36
+ _7Z = 8
37
+ """Recurse into .7z archives; requires :mod:`!py7zr`."""
38
+ LZH = 16
39
+ """Recurse into .lzh and .lha archives; requires :mod:`!lhafile`."""
40
+ ALL = 31
41
+ """Recurse into all archive types."""
42
+
43
+
44
+ del os
@@ -0,0 +1,144 @@
1
+ """The reporter API for :func:`~check_empty.check`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys as s
6
+
7
+ from .constants import TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ import typing
11
+
12
+ __all__ = ('DelayedReporter', 'Reporter')
13
+
14
+
15
+ class Reporter:
16
+ """Type of all reporters accepted by :func:`check` for the ``reporter`` argument."""
17
+
18
+ __slots__ = ('o',)
19
+ o: typing.IO[str] | None
20
+
21
+ def __init__(self) -> None:
22
+ """Initialize the reporter."""
23
+ self.o = None
24
+
25
+ def to(self, out: typing.IO[str] | None = None) -> typing.IO[str] | None:
26
+ """Temporarily redirect the output of the reporter for one check.
27
+
28
+ Args:
29
+ out: The output stream to redirect to. If ``None``, the reporter will write
30
+ to :data:`sys.stdout`.
31
+
32
+ Returns:
33
+ The previous output stream, or ``None`` if no redirection is in effect.
34
+
35
+ """
36
+ self.o, o = out, self.o
37
+ return o
38
+
39
+ @staticmethod
40
+ def calculate_result(y: bool, x: bool, d: bool) -> int:
41
+ """Set the result of the check as :attr:`r`.
42
+
43
+ Returns:
44
+ The exit code of the check. For the default implementation, this is a
45
+ bitwise or of 1 (some files were not empty), 4 (some files or directories
46
+ were absent and :ref:`-m <check-empty--m>` /
47
+ :ref:`--may-not-exist <check-empty---may-not-exist>` was omitted) and 8
48
+ (caught :exc:`OSError` while processing some files), such that 0 is
49
+ correctly the only return value that represents success. The 2 bit is
50
+ skipped since 2 is the exit code of :class:`argparse.ArgumentParser` when
51
+ it encounters invalid arguments.
52
+ """
53
+ return y | x << 2 | d << 3
54
+
55
+ def report( # ruff: ignore[too-many-arguments, too-many-positional-arguments]
56
+ self,
57
+ z: list[str] | None,
58
+ w: list[str] | None,
59
+ a: list[str] | None,
60
+ b: list[str] | None,
61
+ r: list[tuple[str, int]] | None,
62
+ x: int,
63
+ y: int,
64
+ d: int,
65
+ p: int,
66
+ t: int,
67
+ n: int,
68
+ v: int,
69
+ c: bool,
70
+ ) -> None:
71
+ """Report the results of the check.
72
+
73
+ Args:
74
+ z: List of paths to files that were not found.
75
+ w: List of error messages for files that could not be processed.
76
+ a: List of directories that were recursed into.
77
+ b: List of empty file paths.
78
+ r: List of 2-tuples representing non-empty files and their sizes.
79
+ x: Number of files that were not found.
80
+ y: Number of non-empty files.
81
+ d: Number of I/O errors encountered.
82
+ p: Number of empty files.
83
+ t: Total size of non-empty files in bytes.
84
+ n: Total number of files checked.
85
+ v: The integer value of ``verbosity`` as passed to :func:`check`. For the
86
+ default implementation, verbosity > 5 is equivalent to verbosity = 5, and
87
+ a non-positive verbosity gives no output.
88
+ c: The value of ``clear`` as passed to :func:`check`.
89
+ """
90
+ if v <= 0:
91
+ return
92
+ q = self.o
93
+ q = (s.stdout if q is None else q).write
94
+ if a is not None:
95
+ q('\nRecursing into directory: '.join(a))
96
+ q('\n')
97
+ if z is not None:
98
+ q('\nNot found: '.join(z))
99
+ q(
100
+ f'{x} file{"s" if x > 1 else ""} not found\n'
101
+ if x
102
+ else 'All files were found\n'
103
+ )
104
+ if w is not None:
105
+ q('\nError: '.join(w))
106
+ if d:
107
+ q(f'{d} I/O error{"s" if d > 1 else ""} encountered\n')
108
+ if b is not None:
109
+ q('\nEmpty: '.join(b))
110
+ if v > 2: # ruff: ignore[magic-value-comparison]
111
+ q(f'\n{p or "No"} empty file{"" if p == 1 else "s"}\n')
112
+ if y:
113
+ if r is not None:
114
+ q(
115
+ ('\nCleared: ' if c else '\nNot empty: ').join(
116
+ __import__('itertools').chain(
117
+ ('',), map('%s (%d bytes)'.__mod__, r)
118
+ )
119
+ )
120
+ )
121
+ q('\n')
122
+ q(f'{y} offending file{"s" if y > 1 else ""}\nTotal size: {t} bytes\n')
123
+ elif n > x:
124
+ q('All found files were empty\n')
125
+
126
+ def reset_stream(self) -> None:
127
+ """Reset the reporter, closing the output stream if appropriate."""
128
+ q, self.o = self.o, None
129
+ if q not in {None, s.stdout, s.stderr, s.__stdout__, s.__stderr__}:
130
+ q.close()
131
+
132
+
133
+ class DelayedReporter(Reporter):
134
+ """Only report the results only when :meth:`do_report` is called."""
135
+
136
+ __slots__ = ('a',)
137
+
138
+ def report(self, *a: typing.Any) -> None: # ruff: ignore[any-type] # ty: ignore[invalid-method-override]
139
+ """Store the report arguments for later use."""
140
+ self.a = a
141
+
142
+ def do_report(self) -> None:
143
+ """Conduct the report for the most recent check using this reporter."""
144
+ super().report(*self.a)
@@ -0,0 +1,86 @@
1
+ """Utility functions for :mod:`check_empty`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from contextlib import suppress
6
+
7
+ from .constants import ENOENT, TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ from collections.abc import Callable
11
+ from tarfile import TarFile
12
+ from types import TracebackType
13
+ from typing import Any
14
+
15
+ from typing_extensions import Self
16
+ __all__ = ('try_tar',)
17
+
18
+
19
+ def try_tar(a: str, f: Callable[[TarFile], Any]) -> bool:
20
+ """Open a file as a .tar archive and return success.
21
+
22
+ Since :func:`tarfile.is_tarfile` calls :func:`tarfile.open` internally and
23
+ discards the result, using the LBYL approach ends up opening the file twice,
24
+ incurring twice the I/O cost.
25
+ Thus, the resultant tarfile is recorded by the callback ``f`` via which the user
26
+ may retrieve it later. The tarfile is not returned directly because the boolean
27
+ return value allows this function to be used as a condition in an if-elif-else
28
+ block, which is integral to the handling of multiple archive formats.
29
+
30
+ Args:
31
+ a: the string path to the file
32
+ f: the callback
33
+
34
+ Returns:
35
+ Whether the archive was opened and appended to ``e`` with no errors.
36
+ """
37
+ import tarfile as m
38
+
39
+ with suppress(m.TarError):
40
+ f(m.open(a)) # ruff: ignore[open-file-with-context-handler]
41
+ return True
42
+ return False
43
+
44
+
45
+ class Handler:
46
+ __slots__ = 'a', 'c', 'f', 'j', 'v'
47
+ a: int | str
48
+ c: bool
49
+ f: Callable[[str], None]
50
+ j: Callable[[str], None]
51
+ v: str | None
52
+
53
+ def __init__(self, *a: Any) -> None: # ruff: ignore[any-type]
54
+ self.j, self.f, self.a = a
55
+ self.v = None
56
+
57
+ def __enter__(self) -> Self:
58
+ self.c = False
59
+ return self
60
+
61
+ def o(self) -> None:
62
+ with self, open(self.a, 'wb'):
63
+ ...
64
+
65
+ @property
66
+ def e(self, s: str = 'invalid fd (negative): %d', t: str = 'fd: %d') -> str: # ruff: ignore[property-with-parameters]
67
+ r = self.v
68
+ if r is None:
69
+ a = self.a
70
+ self.v = r = (t, s)[a < 0] % a if isinstance(a, int) else a
71
+ return r
72
+
73
+ def __exit__(
74
+ self,
75
+ t: type[BaseException] | None,
76
+ v: BaseException | None,
77
+ _: TracebackType | None,
78
+ ) -> bool:
79
+ if t is None or not isinstance(v, OSError):
80
+ return False
81
+ self.j(self.e) if v.errno == ENOENT else self.f(str(v))
82
+ self.c = True
83
+ return True
84
+
85
+
86
+ del TYPE_CHECKING
@@ -1,7 +1,17 @@
1
1
  [build-system]
2
- requires = ["uv_build>=0.12.10"]
2
+ requires = ["uv_build>=0.12.13"]
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",
@@ -47,8 +57,8 @@ license-files = [
47
57
  ]
48
58
  name = "check-empty"
49
59
  readme = "README.md"
50
- requires-python = ">=3.6"
51
- version = "1.2.2"
60
+ requires-python = ">=3.7"
61
+ version = "1.3.0"
52
62
 
53
63
  [[project.authors]]
54
64
  name = "Jonathan Dung"
@@ -58,21 +68,25 @@ email = "jonathandung@yahoo.com"
58
68
  name = "Jonathan Dung"
59
69
  email = "jonathandung@yahoo.com"
60
70
 
71
+ [project.optional-dependencies]
72
+ 7z = [
73
+ "py7zr==1.1.3; python_version >= '3.10'",
74
+ "py7zr==1.0.0; python_version == '3.9'",
75
+ "py7zr==0.22.0; python_version == '3.8'",
76
+ "py7zr==0.20.6; python_version == '3.7'",
77
+ ]
78
+ all = ["check-empty[7z,lzh,rar]==1.3.0"]
79
+ lzh = ["lhafile==0.3.1"]
80
+ rar = [
81
+ "rarfile==4.5; python_version >= '3.10'",
82
+ "rarfile==4.2; python_version < '3.10'",
83
+ ]
84
+
61
85
  [project.scripts]
62
86
  check-empty = "check_empty.__main__:main"
63
87
 
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
88
  [tool.ruff]
75
- required-version = ">=0.16.6"
89
+ required-version = ">=0.16.7"
76
90
  target-version = "py37"
77
91
 
78
92
  [tool.ruff.analyze]
@@ -83,19 +97,17 @@ quote-style = "single"
83
97
 
84
98
  [tool.ruff.lint]
85
99
  ignore = [
86
- "ANN",
87
100
  "COM",
88
101
  "FBT",
89
102
  "PT",
90
- "PTH",
91
103
  "avoidable-escaped-quote",
92
104
  "bad-quotes-inline-string",
105
+ "builtin-open",
93
106
  "complex-structure",
94
107
  "docstring-extraneous-exception",
95
108
  "implicit-namespace-package",
96
109
  "import-outside-top-level",
97
110
  "incorrect-blank-line-before-class",
98
- "magic-value-comparison",
99
111
  "multi-line-summary-second-line",
100
112
  "non-empty-init-module",
101
113
  "print",
@@ -111,12 +123,20 @@ select = ["ALL"]
111
123
  combine-as-imports = true
112
124
 
113
125
  [tool.ruff.lint.per-file-ignores]
114
- "test_check_empty.py" = ["D"]
126
+ "test_check_empty.py" = [
127
+ "ANN",
128
+ "D",
129
+ ]
115
130
  "!__init__.py" = ["missing-copyright-notice"]
131
+ "__main__.py" = ["missing-return-type-private-function"]
116
132
 
117
133
  [tool.ruff.lint.pydocstyle]
118
134
  convention = "google"
119
135
 
136
+ [tool.ruff.lint.pylint]
137
+ max-args = 6
138
+ max-nested-blocks = 6
139
+
120
140
  [tool.ty.analysis]
121
141
  respect-type-ignore-comments = false
122
142
 
@@ -128,8 +148,29 @@ python-platform = "all"
128
148
  all = "error"
129
149
  missing-override-decorator = "ignore"
130
150
 
151
+ [[tool.ty.overrides]]
152
+ include = ["check_empty/__init__.py"]
153
+
154
+ [tool.ty.overrides.rules]
155
+ redundant-condition = "ignore"
156
+ unsound-assignment = "ignore"
157
+
131
158
  [tool.uv]
132
- required-version = ">=0.12.10"
159
+ required-version = ">=0.12.13"
160
+
161
+ [tool.uv.audit]
162
+ ignore = [
163
+ "GHSA-gjrg-mpp7-g774",
164
+ "GHSA-h4gh-22qq-72r7",
165
+ "GHSA-q6rc-2cgv-63h7",
166
+ "GHSA-gjrg-mpp7-g774",
167
+ "GHSA-h4gh-22qq-72r7",
168
+ "GHSA-q6rc-2cgv-63h7",
169
+ "GHSA-gjrg-mpp7-g774",
170
+ "GHSA-h4gh-22qq-72r7",
171
+ "GHSA-q6rc-2cgv-63h7",
172
+ "GHSA-jfmj-5v4g-7637",
173
+ ]
133
174
 
134
175
  [tool.uv.build-backend]
135
176
  module-name = "check_empty"
@@ -1,7 +1,10 @@
1
1
  [build-system]
2
- requires = ["uv_build>=0.12.10"]
2
+ requires = ["uv_build>=0.12.13"]
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"]
@@ -12,17 +15,20 @@ license-files = ["LICENSE", "AUTHORS.md"]
12
15
  maintainers = [{name = "Jonathan Dung", email = "jonathandung@yahoo.com"}]
13
16
  name = "check-empty"
14
17
  readme = "README.md"
15
- requires-python = ">=3.6"
16
- version = "1.2.2"
18
+ requires-python = ">=3.7"
19
+ version = "1.3.0"
17
20
 
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"]
21
+ [project.optional-dependencies]
22
+ "7z" = ["py7zr==1.1.3; python_version >= '3.10'", "py7zr==1.0.0; python_version == '3.9'", "py7zr==0.22.0; python_version == '3.8'", "py7zr==0.20.6; python_version == '3.7'"]
23
+ all = ["check-empty[7z,lzh,rar]==1.3.0"]
24
+ lzh = ["lhafile==0.3.1"]
25
+ rar = ["rarfile==4.5; python_version >= '3.10'", "rarfile==4.2; python_version < '3.10'"]
20
26
 
21
27
  [project.scripts]
22
28
  check-empty = "check_empty.__main__:main"
23
29
 
24
30
  [tool.ruff]
25
- required-version = ">=0.16.6"
31
+ required-version = ">=0.16.7"
26
32
  target-version = "py37"
27
33
 
28
34
  [tool.ruff.analyze]
@@ -32,7 +38,7 @@ type-checking-imports = false
32
38
  quote-style = "single"
33
39
 
34
40
  [tool.ruff.lint]
35
- ignore = ["ANN", "COM", "FBT", "PT", "PTH", "avoidable-escaped-quote", "bad-quotes-inline-string", "complex-structure", "docstring-extraneous-exception", "implicit-namespace-package", "import-outside-top-level", "incorrect-blank-line-before-class", "magic-value-comparison", "multi-line-summary-second-line", "non-empty-init-module", "print", "too-many-branches", "too-many-locals", "too-many-statements", "unnecessary-dunder-call"]
41
+ ignore = ["COM", "FBT", "PT", "avoidable-escaped-quote", "bad-quotes-inline-string", "builtin-open", "complex-structure", "docstring-extraneous-exception", "implicit-namespace-package", "import-outside-top-level", "incorrect-blank-line-before-class", "multi-line-summary-second-line", "non-empty-init-module", "print", "too-many-branches", "too-many-locals", "too-many-statements", "unnecessary-dunder-call"]
36
42
  preview = true
37
43
  select = ["ALL"]
38
44
 
@@ -40,12 +46,17 @@ select = ["ALL"]
40
46
  combine-as-imports = true
41
47
 
42
48
  [tool.ruff.lint.per-file-ignores]
43
- "test_check_empty.py" = ["D"]
49
+ "test_check_empty.py" = ["ANN", "D"]
44
50
  "!__init__.py" = ["missing-copyright-notice"]
51
+ "__main__.py" = ["missing-return-type-private-function"]
45
52
 
46
53
  [tool.ruff.lint.pydocstyle]
47
54
  convention = "google"
48
55
 
56
+ [tool.ruff.lint.pylint]
57
+ max-args = 6
58
+ max-nested-blocks = 6
59
+
49
60
  [tool.ty.analysis]
50
61
  respect-type-ignore-comments = false
51
62
 
@@ -57,8 +68,26 @@ python-platform = "all"
57
68
  all = "error"
58
69
  missing-override-decorator = "ignore"
59
70
 
71
+ [[tool.ty.overrides]]
72
+ include = ["check_empty/__init__.py"]
73
+ rules = {redundant-condition = "ignore", unsound-assignment = "ignore"}
74
+
60
75
  [tool.uv]
61
- required-version = ">=0.12.10"
76
+ required-version = ">=0.12.13"
77
+
78
+ [tool.uv.audit]
79
+ ignore = [
80
+ "GHSA-gjrg-mpp7-g774",
81
+ "GHSA-h4gh-22qq-72r7",
82
+ "GHSA-q6rc-2cgv-63h7",
83
+ "GHSA-gjrg-mpp7-g774",
84
+ "GHSA-h4gh-22qq-72r7",
85
+ "GHSA-q6rc-2cgv-63h7",
86
+ "GHSA-gjrg-mpp7-g774",
87
+ "GHSA-h4gh-22qq-72r7",
88
+ "GHSA-q6rc-2cgv-63h7",
89
+ "GHSA-jfmj-5v4g-7637",
90
+ ]
62
91
 
63
92
  [tool.uv.build-backend]
64
93
  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