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