check-empty 1.2.3__tar.gz → 2.0.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.3
3
+ Version: 2.0.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
@@ -15,9 +15,6 @@ Classifier: Natural Language :: English
15
15
  Classifier: Operating System :: OS Independent
16
16
  Classifier: Programming Language :: Python :: 3
17
17
  Classifier: Programming Language :: Python :: 3 :: Only
18
- Classifier: Programming Language :: Python :: 3.7
19
- Classifier: Programming Language :: Python :: 3.8
20
- Classifier: Programming Language :: Python :: 3.9
21
18
  Classifier: Programming Language :: Python :: 3.10
22
19
  Classifier: Programming Language :: Python :: 3.11
23
20
  Classifier: Programming Language :: Python :: 3.12
@@ -28,13 +25,27 @@ Classifier: Programming Language :: Python :: 3.16
28
25
  Classifier: Programming Language :: Python :: Free Threading :: 3 - Stable
29
26
  Classifier: Topic :: Utilities
30
27
  Classifier: Typing :: Typed
28
+ Requires-Dist: py7zr==1.1.3 ; extra == '7z'
29
+ Requires-Dist: acefile==0.6.14 ; extra == 'ace'
30
+ Requires-Dist: check-empty[7z,ace,ar,lzh,rar]==2.0.0 ; extra == 'all'
31
+ Requires-Dist: arpy==2.4.0 ; extra == 'ar'
32
+ Requires-Dist: lhafile==0.3.1 ; extra == 'lzh'
33
+ Requires-Dist: rarfile==4.5 ; extra == 'rar'
31
34
  Maintainer: Jonathan Dung
32
35
  Maintainer-email: Jonathan Dung <jonathandung@yahoo.com>
33
- Requires-Python: >=3.6
36
+ Requires-Python: >=3.10
37
+ Provides-Extra: 7z
38
+ Provides-Extra: ace
39
+ Provides-Extra: all
40
+ Provides-Extra: ar
41
+ Provides-Extra: lzh
42
+ Provides-Extra: rar
34
43
  Description-Content-Type: text/markdown
35
44
 
36
45
  # [check-empty](https://pypi.org/p/check-empty)
37
46
 
47
+ [![#StandWithPalestine](https://raw.githubusercontent.com/TheBSD/StandWithPalestine/main/badges/StandWithPalestine.svg)](https://github.com/TheBSD/StandWithPalestine/blob/main/docs/README.md)
48
+
38
49
  A simple, dependency-free [pre-commit](https://pre-commit.com) /
39
50
  [prek](https://prek.j178.dev) hook, CLI, library and
40
51
  [GitHub Action](https://github.com/marketplace/actions/check-empty-files) conglomerate
@@ -46,8 +57,7 @@ specified.
46
57
 
47
58
  ## Prerequisites
48
59
 
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.
60
+ Supports every Python 3.10 runtime. There are no other requirements.
51
61
 
52
62
  ## Quickstart
53
63
 
@@ -115,7 +125,7 @@ As a pre-commit hook:
115
125
  # .pre-commit-config.yaml
116
126
  repos:
117
127
  - repo: https://github.com/jonathandung/check-empty
118
- rev: v1.2.3 # repository version
128
+ rev: v2.0.0 # repository version
119
129
  hooks:
120
130
  - id: check-empty # the hook
121
131
  args: # example list of arguments
@@ -131,7 +141,7 @@ equivalent in `prek.toml` format:
131
141
  ```toml
132
142
  [[repos]]
133
143
  repo = "https://github.com/jonathandung/check-empty"
134
- rev = "v1.2.3"
144
+ rev = "v2.0.0"
135
145
 
136
146
  [[repos.hooks]]
137
147
  id = "check-empty"
@@ -155,7 +165,7 @@ or (TOML 1.1+):
155
165
  # using multiline inline tables
156
166
  [[repos]]
157
167
  repo = "https://github.com/jonathandung/check-empty"
158
- rev = "v1.2.3"
168
+ rev = "v2.0.0"
159
169
  hooks = [{
160
170
  id = "check-empty",
161
171
  args = ["-Q"],
@@ -175,13 +185,11 @@ As a GitHub Actions workflow step:
175
185
 
176
186
  ```yaml
177
187
  steps:
178
- - uses: jonathandung/check-empty@v1.2.3 # the latest version on the GitHub Actions
188
+ - uses: jonathandung/check-empty@v2.0.0 # the latest version on the GitHub Actions
179
189
  # marketplace; this step will fail and subsequent jobs will not run if any file is
180
190
  # not empty
181
191
  with:
182
192
  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
184
- verbosity: 1
185
193
  filenames: |
186
194
  src/mylib/py.typed
187
195
  docs/.nojekyll
@@ -194,26 +202,34 @@ steps:
194
202
 
195
203
  Also see the GitHub Action
196
204
  [manifest](https://github.com/jonathandung/check-empty/blob/main/action.yaml), which
197
- contains the accepted action inputs and descriptions thereof.
205
+ contains the accepted action inputs, action outputs produced and their respective
206
+ descriptions.
198
207
 
199
208
  ## Notes
200
209
 
201
- 1. If your file name starts with a hyphen, to avoid having it misinterpreted as a flag,
202
- use a command of the form `check-empty -- -this_is_actually_a_file.txt`.
210
+ 1. If your file name starts with a hyphen, use a command of the form
211
+ `check-empty -- -this_is_actually_a_file.txt` to avoid having the filename
212
+ misinterpreted as a flag.
203
213
  2. Forward slashes can be used even on Windows, so there is no need to escape anything.
204
214
  3. Glob patterns are supported on \*nix only. If on Windows, use a shell like Git Bash.
205
215
  4. To pass an
206
216
  [argfile](https://docs.python.org/3/library/argparse.html#fromfile-prefix-chars), use
207
217
  the `@` prefix, and escape files whose names actually start with `@` using the
208
218
  double-hyphen syntax.
209
- 5. It may be unintuitive that a directory being "empty" means all its files are empty,
210
- but this project explicitly targets files, since version control systems track files
211
- rather than directories.
212
- 6. The program does not recurse into archives, since identification of compressed
213
- archives would require reading the first few bytes of each file seen, which is
214
- error-prone and inefficient.
215
-
216
- ## Additional Links
219
+ 5. It may be unintuitive that a directory or archive being "empty" means all its files
220
+ are empty, but this project explicitly targets files, since version control systems
221
+ track files rather than directories.
222
+ 6. The program can recurse into some archives if specified, but it requires certain
223
+ libraries to be installed to do so for certain formats. .7z (corresponding to the `7z`
224
+ extra) needs `py7zr`, .rar (the `rar` extra) needs `rarfile`, .lha / .lzh (the `lzh`
225
+ extra) needs `lhafile`, .a / .ar / .lib (the `ar` extra) needs `arpy`, .ace (the `ace`
226
+ extra) needs `acefile`. These may also slow down the checking significantly for large
227
+ directories, since magic numbers must be read for every file and the I/O overhead
228
+ accumulates. All the above extras are included in the `all` extra.
229
+ 7. Keep weird characters in your filenames to a minimum. They may become a problem in
230
+ GitHub Actions usage.
231
+
232
+ ## Additional links
217
233
 
218
234
  - [API and CLI Reference](https://check-empty.readthedocs.io)
219
235
  - [Releases](https://github.com/jonathandung/check-empty/releases)
@@ -227,15 +243,15 @@ to read the
227
243
  and the
228
244
  [contributing guide](https://github.com/jonathandung/.github/blob/main/CONTRIBUTING.md).
229
245
 
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.
246
+ To build the docs locally (needs Python 3.12+ because of Sphinx), install with the
247
+ `docs` [group](https://packaging.python.org/en/latest/specifications/dependency-groups)
248
+ using a package manager that supports it (e.g. pip 25.1+ or uv 0.4.27+), preferably
249
+ into a virtual environment.
233
250
 
234
251
  Tests are run with:
235
252
 
236
253
  ```bash
237
- python -m test_check_empty # explicit; or
238
- python -m unittest discover # alternative
254
+ python -m test_check_empty
239
255
  ```
240
256
 
241
257
  at the project root. `pytest` is not needed.
@@ -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,7 @@ 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 every Python 3.10 runtime. There are no other requirements.
16
17
 
17
18
  ## Quickstart
18
19
 
@@ -80,7 +81,7 @@ As a pre-commit hook:
80
81
  # .pre-commit-config.yaml
81
82
  repos:
82
83
  - repo: https://github.com/jonathandung/check-empty
83
- rev: v1.2.3 # repository version
84
+ rev: v2.0.0 # repository version
84
85
  hooks:
85
86
  - id: check-empty # the hook
86
87
  args: # example list of arguments
@@ -96,7 +97,7 @@ equivalent in `prek.toml` format:
96
97
  ```toml
97
98
  [[repos]]
98
99
  repo = "https://github.com/jonathandung/check-empty"
99
- rev = "v1.2.3"
100
+ rev = "v2.0.0"
100
101
 
101
102
  [[repos.hooks]]
102
103
  id = "check-empty"
@@ -120,7 +121,7 @@ or (TOML 1.1+):
120
121
  # using multiline inline tables
121
122
  [[repos]]
122
123
  repo = "https://github.com/jonathandung/check-empty"
123
- rev = "v1.2.3"
124
+ rev = "v2.0.0"
124
125
  hooks = [{
125
126
  id = "check-empty",
126
127
  args = ["-Q"],
@@ -140,13 +141,11 @@ As a GitHub Actions workflow step:
140
141
 
141
142
  ```yaml
142
143
  steps:
143
- - uses: jonathandung/check-empty@v1.2.3 # the latest version on the GitHub Actions
144
+ - uses: jonathandung/check-empty@v2.0.0 # the latest version on the GitHub Actions
144
145
  # marketplace; this step will fail and subsequent jobs will not run if any file is
145
146
  # not empty
146
147
  with:
147
148
  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
149
- verbosity: 1
150
149
  filenames: |
151
150
  src/mylib/py.typed
152
151
  docs/.nojekyll
@@ -159,26 +158,34 @@ steps:
159
158
 
160
159
  Also see the GitHub Action
161
160
  [manifest](https://github.com/jonathandung/check-empty/blob/main/action.yaml), which
162
- contains the accepted action inputs and descriptions thereof.
161
+ contains the accepted action inputs, action outputs produced and their respective
162
+ descriptions.
163
163
 
164
164
  ## Notes
165
165
 
166
- 1. If your file name starts with a hyphen, to avoid having it misinterpreted as a flag,
167
- use a command of the form `check-empty -- -this_is_actually_a_file.txt`.
166
+ 1. If your file name starts with a hyphen, use a command of the form
167
+ `check-empty -- -this_is_actually_a_file.txt` to avoid having the filename
168
+ misinterpreted as a flag.
168
169
  2. Forward slashes can be used even on Windows, so there is no need to escape anything.
169
170
  3. Glob patterns are supported on \*nix only. If on Windows, use a shell like Git Bash.
170
171
  4. To pass an
171
172
  [argfile](https://docs.python.org/3/library/argparse.html#fromfile-prefix-chars), use
172
173
  the `@` prefix, and escape files whose names actually start with `@` using the
173
174
  double-hyphen syntax.
174
- 5. It may be unintuitive that a directory being "empty" means all its files are empty,
175
- but this project explicitly targets files, since version control systems track files
176
- rather than directories.
177
- 6. The program does not recurse into archives, since identification of compressed
178
- archives would require reading the first few bytes of each file seen, which is
179
- error-prone and inefficient.
180
-
181
- ## Additional Links
175
+ 5. It may be unintuitive that a directory or archive being "empty" means all its files
176
+ are empty, but this project explicitly targets files, since version control systems
177
+ track files rather than directories.
178
+ 6. The program can recurse into some archives if specified, but it requires certain
179
+ libraries to be installed to do so for certain formats. .7z (corresponding to the `7z`
180
+ extra) needs `py7zr`, .rar (the `rar` extra) needs `rarfile`, .lha / .lzh (the `lzh`
181
+ extra) needs `lhafile`, .a / .ar / .lib (the `ar` extra) needs `arpy`, .ace (the `ace`
182
+ extra) needs `acefile`. These may also slow down the checking significantly for large
183
+ directories, since magic numbers must be read for every file and the I/O overhead
184
+ accumulates. All the above extras are included in the `all` extra.
185
+ 7. Keep weird characters in your filenames to a minimum. They may become a problem in
186
+ GitHub Actions usage.
187
+
188
+ ## Additional links
182
189
 
183
190
  - [API and CLI Reference](https://check-empty.readthedocs.io)
184
191
  - [Releases](https://github.com/jonathandung/check-empty/releases)
@@ -192,15 +199,15 @@ to read the
192
199
  and the
193
200
  [contributing guide](https://github.com/jonathandung/.github/blob/main/CONTRIBUTING.md).
194
201
 
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.
202
+ To build the docs locally (needs Python 3.12+ because of Sphinx), install with the
203
+ `docs` [group](https://packaging.python.org/en/latest/specifications/dependency-groups)
204
+ using a package manager that supports it (e.g. pip 25.1+ or uv 0.4.27+), preferably
205
+ into a virtual environment.
198
206
 
199
207
  Tests are run with:
200
208
 
201
209
  ```bash
202
- python -m test_check_empty # explicit; or
203
- python -m unittest discover # alternative
210
+ python -m test_check_empty
204
211
  ```
205
212
 
206
213
  at the project root. `pytest` is not needed.
@@ -0,0 +1,334 @@
1
+ # Copyright © 2026 Jonathan Dung. All rights reserved.
2
+ # SPDX-License-Identifier: MIT
3
+ """Utility to check the emptiness of files.
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
+ from contextlib import suppress
12
+
13
+ from . import constants, reporter
14
+
15
+ TYPE_CHECKING = False
16
+ if TYPE_CHECKING:
17
+ from collections.abc import Callable, Iterable
18
+ from types import ModuleType, TracebackType
19
+ from typing import Any, SupportsIndex
20
+
21
+ from _typeshed import FileDescriptorOrPath
22
+ from typing_extensions import Self
23
+
24
+ __all__ = ('Handler', 'check', 'default_reporter')
25
+ __version__ = '2.0.0'
26
+ """The version of the package."""
27
+ default_reporter: reporter.Reporter = reporter.Reporter()
28
+ """The default reporter used by :func:`check`."""
29
+
30
+
31
+ class Handler:
32
+ """Used to handle errors when opening a file."""
33
+
34
+ __slots__ = 'a', 'c', 'f', 'j', 'v'
35
+ a: int | str
36
+ """The file descriptor or string path to the file being handled."""
37
+ c: bool
38
+ """Whether an I/O error was handled."""
39
+ f: Callable[[str], None]
40
+ """The function to call when handling an I/O error.
41
+
42
+ Should take a string representing the error message as the only argument and
43
+ preferably return ``None``.
44
+ """
45
+ j: Callable[[str], None]
46
+ """The function to call when handling the case where the file is missing.
47
+
48
+ Should take the path to the file or a string encapsulating the validity of the file
49
+ descriptor as the only argument and preferably return ``None``.
50
+ """
51
+ v: str | None
52
+ """The cached value of :attr:`e`."""
53
+
54
+ def __init__(self, *a: Any) -> None: # ruff: ignore[any-type]
55
+ """Initialize the handler."""
56
+ self.j, self.f, self.a = a
57
+ self.v = None
58
+
59
+ def __enter__(self) -> Self:
60
+ """Enter the handler context.
61
+
62
+ Returns:
63
+ The handler itself.
64
+ """
65
+ self.c = False
66
+ return self
67
+
68
+ def o(self) -> None:
69
+ """Clear the file immediately."""
70
+ with self, open(self.a, 'wb'):
71
+ ...
72
+
73
+ @property
74
+ def e(self, s: str = 'invalid fd (negative): %d', t: str = 'fd: %d') -> str: # ruff: ignore[property-with-parameters]
75
+ """The file path itself, or the file descriptor coerced to a string."""
76
+ r = self.v
77
+ if r is None:
78
+ a = self.a
79
+ self.v = r = (t, s)[a < 0] % a if isinstance(a, int) else a
80
+ return r
81
+
82
+ def __exit__(
83
+ self,
84
+ t: type[BaseException] | None,
85
+ v: BaseException | None,
86
+ _: TracebackType | None,
87
+ ) -> bool:
88
+ """Exit the handler context.
89
+
90
+ Returns:
91
+ Whether there was an exception that is to be suppressed.
92
+ """
93
+ if t is None or not isinstance(v, OSError):
94
+ return False
95
+ self.j(self.e) if v.errno == constants.ENOENT else self.f(str(v))
96
+ self.c = True
97
+ return True
98
+
99
+
100
+ def check(
101
+ files: Iterable[FileDescriptorOrPath],
102
+ *,
103
+ clear: bool = False,
104
+ may_not_exist: bool = False,
105
+ verbosity: SupportsIndex = 2,
106
+ recurse_into: SupportsIndex = constants.RecurseInto.NONE,
107
+ reporter: reporter.abc.ReporterABC = default_reporter,
108
+ ) -> int:
109
+ # ruff: ignore[docstring-missing-exception]
110
+ r"""Check the emptiness of files, recursing into directories if passed.
111
+
112
+ If using a custom reporter and you want to use all the information that can be
113
+ gathered during the check, remember to pass a ``verbosity`` greater than or equal
114
+ to 5, since some parameters to :meth:`~check_empty.reporter.Reporter.report` will
115
+ take values of ``None`` otherwise.
116
+
117
+ Args:
118
+ files: an iterable of file descriptors or paths representing the files and
119
+ directories to check; directory descriptors (\*nix) are not supported.
120
+ clear: if ``True``, clear the contents of non-empty files; directories, notably,
121
+ are not purged, but all files within should become empty.
122
+ may_not_exist: if ``True``, do not treat absent files or directories as errors.
123
+ verbosity: how much detail the program should print to stdout.
124
+ recurse_into: a bitwise or of flags indicating which types of archives to
125
+ recurse into; by default, all archives are treated as regular files.
126
+ reporter: The reporter to use for reporting the results. All output of this
127
+ function is produced by the reporter.
128
+
129
+ Returns:
130
+ The integer exit code calculated by the reporter.
131
+
132
+ """
133
+ files = list(files)
134
+ if not files:
135
+ return 0
136
+ k, j, t, n, o = [0] * 5, [], 0, len(files), files.pop
137
+ u, e, _ = j.extend, j.pop, lambda v: lambda _, k=k: k.__setitem__(v, k[v] + 1)
138
+ verbosity, recurse_into = (type(x).__index__(x) for x in (verbosity, recurse_into))
139
+ # ruff: disable[magic-value-comparison]
140
+ if verbosity > 4:
141
+ b = ['']
142
+ f = b.append
143
+ else:
144
+ b, f = None, _(0)
145
+ if verbosity > 3:
146
+ v, ra = [''], ['']
147
+ x, aa = v.append, ra.append
148
+ else:
149
+ v = ra = None
150
+ x = aa = lambda _: None
151
+ if verbosity > 2:
152
+ z, w = [''], ['']
153
+ i = z.append, w.append
154
+ else:
155
+ z = w = None
156
+ i = _(1), _(2)
157
+ if verbosity > 1:
158
+ r = []
159
+ g = r.append
160
+ else:
161
+ r, g = None, _(3)
162
+ # ruff: enable[magic-value-comparison]
163
+ zp, tr, rr, z7, ar, lh, ac = map(recurse_into.__and__, constants.ARCHIVE_FORMATS)
164
+ an = bool(recurse_into)
165
+ if an:
166
+ from . import archives as av
167
+ md = 'a' if clear else 'r'
168
+ if TYPE_CHECKING:
169
+ import tarfile as tr
170
+ import zipfile as zp
171
+
172
+ import acefile as ac
173
+ import arpy as ar
174
+ import lhafile as lh
175
+ import py7zr as z7
176
+ import rarfile as rr
177
+ else:
178
+ if tr:
179
+ import tarfile as tr
180
+ if zp:
181
+ import zipfile as zp
182
+ if ac:
183
+ import acefile as ac
184
+ if ar:
185
+ import arpy as ar
186
+ if lh:
187
+ import lhafile as lh
188
+ if z7:
189
+ import py7zr as z7
190
+ if rr:
191
+ import rarfile as rr
192
+
193
+ def ha(a: str) -> bool:
194
+
195
+ def c() -> None:
196
+ nonlocal t
197
+ g(q)
198
+ t += s
199
+
200
+ def e() -> None:
201
+ c() if s else f(a)
202
+
203
+ def w() -> None:
204
+ if s:
205
+ if clear:
206
+ raise NotImplementedError(m)
207
+ c()
208
+ else:
209
+ f(a)
210
+
211
+ def v(m: ModuleType, e: str) -> bool:
212
+ nonlocal d
213
+ with suppress(getattr(m, e)):
214
+ d = m.open(a)
215
+ return True
216
+ return False
217
+
218
+ if tr and v(tr, 'TarError'):
219
+ aa(a)
220
+ for r in d.getmembers():
221
+ a, s = q = r.name, r.size
222
+ e()
223
+ if clear:
224
+ av.purge_tar(d)
225
+ elif zp and zp.is_zipfile(a):
226
+ with zp.ZipFile(a, md) as d:
227
+ aa(a)
228
+ for r in d.infolist():
229
+ a, s = q = r.filename, r.file_size
230
+ if s:
231
+ c()
232
+ if clear:
233
+ av.clear_file_in_zip(d, r)
234
+ else:
235
+ f(a)
236
+ elif z7 and z7.is_7zfile(a):
237
+ with z7.SevenZipFile(a) as d:
238
+ aa(a)
239
+ for r in d.files:
240
+ a, s = q = r.filename, r.origin.stat().st_size
241
+ e()
242
+ if clear:
243
+ av.purge_7z(d)
244
+ elif rr and rr.is_rarfile(a):
245
+ m = 'Cannot clear files in RAR archives'
246
+ with rr.RarFile(a) as d:
247
+ aa(a)
248
+ for r in d.infolist():
249
+ a, s = q = r.filename, r.file_size
250
+ w()
251
+ elif ar and v(ar, 'ArchiveFormatError'):
252
+ aa(a)
253
+ for r in d.infolist():
254
+ a, s = q = r.name, r.size
255
+ e()
256
+ if clear:
257
+ av.purge_ar(d, ar.HEADER_GNU)
258
+ elif ac and v(ac, 'AceError'):
259
+ aa(a)
260
+ for r in filter(ac.AceMember.is_reg, d):
261
+ a, s = q = r.filename, r.size
262
+ w()
263
+ elif lh:
264
+ try:
265
+ d = lh.LhaFile(a)
266
+ except (RuntimeError, lh.BadLhafile):
267
+ return False
268
+ m = 'Cannot clear files in LHA archives'
269
+ aa(a)
270
+ for r in d.filelist:
271
+ a, s = q = r.filename, r.file_size
272
+ w()
273
+ else:
274
+ return False
275
+ return True
276
+
277
+ def hb(a: str) -> bool:
278
+ try:
279
+ return an and ha(a)
280
+ except Exception as e: # ruff: ignore[blind-except]
281
+ i[1](str(e))
282
+ return True
283
+
284
+ while files:
285
+ a, q = o(), None
286
+ m = isinstance(a, int)
287
+ if not m:
288
+ a = os.fsdecode(a)
289
+ with Handler(*i, a) as h:
290
+ q = os.stat(a) # ruff: ignore[os-stat]
291
+ if q is None:
292
+ continue
293
+ c = h.e
294
+ if q.st_mode & constants.TYPE_MASK == constants.S_IFDIR:
295
+ if m:
296
+ raise constants.DIRFD_UNSUPPORTED
297
+ x(c)
298
+ u(os.scandir(c))
299
+ continue
300
+ if isinstance(a, str) and hb(a):
301
+ continue
302
+ s = q.st_size
303
+ if not s:
304
+ f(c)
305
+ continue
306
+ g((c, s))
307
+ t += s
308
+ if clear:
309
+ h.o()
310
+ del o, files
311
+ while j:
312
+ m = e()
313
+ a = m.path
314
+ if m.is_dir():
315
+ x(a)
316
+ u(os.scandir(a))
317
+ continue
318
+ if hb(a):
319
+ continue
320
+ n += 1
321
+ s = m.stat().st_size
322
+ if not s:
323
+ f(a)
324
+ continue
325
+ g((a, s))
326
+ t += s
327
+ if clear:
328
+ Handler(*i, a).o()
329
+ del j
330
+ x, d = k[1] if z is None else len(z) - 1, k[2] if w is None else len(w) - 1
331
+ y, p = k[3] if r is None else len(r), k[0] if b is None else len(b) - 1
332
+ reporter.report(z, w, v, b, r, x, y, d, p, n, t, verbosity, clear)
333
+ reporter.reset_out()
334
+ 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
 
@@ -33,6 +33,16 @@ f(
33
33
  f('-Q', '--quiet', action='count', default=0, help='decrease output verbosity')
34
34
  f('-V', '--verbose', action='count', default=0, help='increase output verbosity')
35
35
  f('-o', '--out', help='write output to this file instead of stdout')
36
+ f = _p.add_argument_group(
37
+ 'archive recursion', 'options for recursing into archives'
38
+ ).add_argument
39
+ f('-z', '--zip', action='store_true', help='recurse into .zip archives')
40
+ f('-t', '--tar', action='store_true', help='recurse into .tar archives')
41
+ f('-r', '--rar', action='store_true', help='recurse into .rar archives')
42
+ f('-a', '--ar', action='store_true', help='recurse into .a, .ar and .lib archives')
43
+ f('-7', '--7z', action='store_true', dest='z7', help='recurse into .7z archives')
44
+ f('-l', '--lzh', action='store_true', help='recurse into .lzh and .lha archives')
45
+ f('-A', '--ace', action='store_true', help='recurse into .ace archives')
36
46
 
37
47
 
38
48
  def _r():
@@ -57,10 +67,16 @@ def main(argv: Iterable[str] | None = None) -> int:
57
67
  if o is not None:
58
68
  # ruff: ignore[open-file-with-context-handler]
59
69
  c.default_reporter.to(open(o, 'w', encoding='utf-8'))
70
+ m = c.constants
71
+ r = m.RecurseInto.NONE
72
+ for a in m.ARCHIVE_FORMATS:
73
+ if getattr(n, a._name_.lower()):
74
+ r |= a
60
75
  return c.check(
61
76
  n.filenames,
62
77
  clear=n.clear,
63
78
  may_not_exist=n.may_not_exist,
79
+ recurse_into=r,
64
80
  verbosity=2 + n.verbose - n.quiet,
65
81
  )
66
82