check-empty 0.6.0__tar.gz → 0.7.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: 0.6.0
3
+ Version: 0.7.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
@@ -32,18 +32,20 @@ Maintainer-email: Jonathan Dung <jonathandung@yahoo.com>
32
32
  Requires-Python: >=3.6
33
33
  Description-Content-Type: text/markdown
34
34
 
35
- # check-empty
35
+ # [check-empty](https://pypi.org/p/check-empty)
36
36
 
37
37
  A simple, dependency-free and intuitive [pre-commit](https://pre-commit.com) /
38
- [prek](https://prek.j178.dev) hook, CLI, library and GitHub Action conglomerate that
39
- makes sure selected files are empty according to a filesystem stat call and clears them
40
- effectively with minimal I/O if required, written in Python. Also available as a tool on
41
- uv. Supports CPython 3.6+, PyPy 7.0+, GraalPy 19.0+ and most likely every Python 3.6
42
- runtime you can think of.
38
+ [prek](https://prek.j178.dev) hook, CLI, library,
39
+ [`uv` tool](https://docs.astral.sh/uv/guides/tools/) and
40
+ [GitHub Action](https://github.com/marketplace/actions/check-empty-files) conglomerate
41
+ written in Python that makes sure selected files, even within directories, are empty
42
+ according to as little filesystem stat calls as possible and clears them effectively
43
+ with minimal I/O if specified. Supports CPython 3.6+, PyPy 7.0+, GraalPy 19.0+
44
+ out-of-the-box, and most likely every Python 3.6 runtime you can think of.
43
45
 
44
46
  ## Quickstart
45
47
 
46
- Without installation (testing out the capabilities):
48
+ Without installation (just trying out the capabilities):
47
49
 
48
50
  ```bash
49
51
  uvx check-empty -Q src/mylib/py.typed docs/.nojekyll static/.gitkeep some_directory
@@ -52,12 +54,12 @@ uvx check-empty -Q src/mylib/py.typed docs/.nojekyll static/.gitkeep some_direct
52
54
  ```bash
53
55
  # uv
54
56
  uv tool install check-empty # bare executable on PATH
55
- uv pip install check-empty # if you want the library as well
57
+ uv pip install check-empty # if you want to import check_empty for programmatic usage
56
58
 
57
59
  pip install check-empty # pip
58
60
  ```
59
61
 
60
- Check the version with:
62
+ Show the version with:
61
63
 
62
64
  ```bash
63
65
  check-empty --version # or check-empty -v
@@ -75,11 +77,11 @@ As a pre-commit hook:
75
77
  # .pre-commit-config.yaml
76
78
  repos:
77
79
  - repo: https://github.com/jonathandung/check-empty
78
- rev: v0.6.0 # repository version
80
+ rev: v0.7.0 # repository version
79
81
  hooks:
80
82
  - id: check-empty # the hook
81
83
  args: # example list of arguments
82
- - -Q # flag to silence output (shorthand for --quiet)
84
+ - -Q # flag to decrease output, applicable twice (shorthand for --quiet)
83
85
  # below: paths to files/directories to clear or keep empty, relative to project
84
86
  # root (absolute paths are possible but not recommended)
85
87
  - src/mylib/py.typed
@@ -93,7 +95,7 @@ equivalent in `prek.toml` format:
93
95
  ```toml
94
96
  [[repos]]
95
97
  repo = "https://github.com/jonathandung/check-empty"
96
- rev = "v0.6.0"
98
+ rev = "v0.7.0"
97
99
 
98
100
  [[repos.hooks]]
99
101
  id = "check-empty"
@@ -111,7 +113,7 @@ or a more terse format (TOML 1.1+):
111
113
  ```toml
112
114
  [[repos]]
113
115
  repo = "https://github.com/jonathandung/check-empty"
114
- rev = "v0.6.0"
116
+ rev = "v0.7.0"
115
117
  hooks = [{
116
118
  id = "check-empty",
117
119
  args = [
@@ -124,7 +126,7 @@ As a GitHub action step:
124
126
 
125
127
  ```yaml
126
128
  steps:
127
- - uses: jonathandung/check-empty@v0.6.0 # the latest version on the GitHub Actions
129
+ - uses: jonathandung/check-empty@v0.7.0 # the latest version on the GitHub Actions
128
130
  # marketplace; this step will fail and subsequent jobs will not run if any file is
129
131
  # not empty
130
132
  with:
@@ -138,7 +140,26 @@ steps:
138
140
  some_directory
139
141
  ```
140
142
 
143
+ ## Notes
144
+
145
+ 1. If your file name starts with a hyphen, to avoid having it misinterpreted as a flag,
146
+ use a command of the form `check-empty -- -this_is_actually_a_file.txt`. Thus, for a
147
+ file literally named "--", you have little choice but to call the underlying library
148
+ function (`check`) directly.
149
+ 2. Forward slashes can be used even on Windows, so there is no need to escape anything.
150
+ 3. Glob patterns are supported on \*nix only. If on Windows, use a shell like Git Bash.
151
+ 4. The program does not recurse into archives, since identification of compressed
152
+ archives would require reading the first few bytes of each file seen, which is
153
+ error-prone and inefficient.
154
+
141
155
  ## Development
142
156
 
143
157
  If you wish to contribute to this project, you are more than welcome, but please
144
- remember to read the [contributing guide](CONTRIBUTING.md).
158
+ remember to read the [contributing guide](CONTRIBUTING.md). Tests are run with:
159
+
160
+ ```bash
161
+ python -m test_check_empty # explicit
162
+ python -m unittest discover # alternative
163
+ ```
164
+
165
+ at the project root.
@@ -1,15 +1,17 @@
1
- # check-empty
1
+ # [check-empty](https://pypi.org/p/check-empty)
2
2
 
3
3
  A simple, dependency-free and intuitive [pre-commit](https://pre-commit.com) /
4
- [prek](https://prek.j178.dev) hook, CLI, library and GitHub Action conglomerate that
5
- makes sure selected files are empty according to a filesystem stat call and clears them
6
- effectively with minimal I/O if required, written in Python. Also available as a tool on
7
- uv. Supports CPython 3.6+, PyPy 7.0+, GraalPy 19.0+ and most likely every Python 3.6
8
- runtime you can think of.
4
+ [prek](https://prek.j178.dev) hook, CLI, library,
5
+ [`uv` tool](https://docs.astral.sh/uv/guides/tools/) and
6
+ [GitHub Action](https://github.com/marketplace/actions/check-empty-files) conglomerate
7
+ written in Python that makes sure selected files, even within directories, are empty
8
+ according to as little filesystem stat calls as possible and clears them effectively
9
+ with minimal I/O if specified. Supports CPython 3.6+, PyPy 7.0+, GraalPy 19.0+
10
+ out-of-the-box, and most likely every Python 3.6 runtime you can think of.
9
11
 
10
12
  ## Quickstart
11
13
 
12
- Without installation (testing out the capabilities):
14
+ Without installation (just trying out the capabilities):
13
15
 
14
16
  ```bash
15
17
  uvx check-empty -Q src/mylib/py.typed docs/.nojekyll static/.gitkeep some_directory
@@ -18,12 +20,12 @@ uvx check-empty -Q src/mylib/py.typed docs/.nojekyll static/.gitkeep some_direct
18
20
  ```bash
19
21
  # uv
20
22
  uv tool install check-empty # bare executable on PATH
21
- uv pip install check-empty # if you want the library as well
23
+ uv pip install check-empty # if you want to import check_empty for programmatic usage
22
24
 
23
25
  pip install check-empty # pip
24
26
  ```
25
27
 
26
- Check the version with:
28
+ Show the version with:
27
29
 
28
30
  ```bash
29
31
  check-empty --version # or check-empty -v
@@ -41,11 +43,11 @@ As a pre-commit hook:
41
43
  # .pre-commit-config.yaml
42
44
  repos:
43
45
  - repo: https://github.com/jonathandung/check-empty
44
- rev: v0.6.0 # repository version
46
+ rev: v0.7.0 # repository version
45
47
  hooks:
46
48
  - id: check-empty # the hook
47
49
  args: # example list of arguments
48
- - -Q # flag to silence output (shorthand for --quiet)
50
+ - -Q # flag to decrease output, applicable twice (shorthand for --quiet)
49
51
  # below: paths to files/directories to clear or keep empty, relative to project
50
52
  # root (absolute paths are possible but not recommended)
51
53
  - src/mylib/py.typed
@@ -59,7 +61,7 @@ equivalent in `prek.toml` format:
59
61
  ```toml
60
62
  [[repos]]
61
63
  repo = "https://github.com/jonathandung/check-empty"
62
- rev = "v0.6.0"
64
+ rev = "v0.7.0"
63
65
 
64
66
  [[repos.hooks]]
65
67
  id = "check-empty"
@@ -77,7 +79,7 @@ or a more terse format (TOML 1.1+):
77
79
  ```toml
78
80
  [[repos]]
79
81
  repo = "https://github.com/jonathandung/check-empty"
80
- rev = "v0.6.0"
82
+ rev = "v0.7.0"
81
83
  hooks = [{
82
84
  id = "check-empty",
83
85
  args = [
@@ -90,7 +92,7 @@ As a GitHub action step:
90
92
 
91
93
  ```yaml
92
94
  steps:
93
- - uses: jonathandung/check-empty@v0.6.0 # the latest version on the GitHub Actions
95
+ - uses: jonathandung/check-empty@v0.7.0 # the latest version on the GitHub Actions
94
96
  # marketplace; this step will fail and subsequent jobs will not run if any file is
95
97
  # not empty
96
98
  with:
@@ -104,7 +106,26 @@ steps:
104
106
  some_directory
105
107
  ```
106
108
 
109
+ ## Notes
110
+
111
+ 1. If your file name starts with a hyphen, to avoid having it misinterpreted as a flag,
112
+ use a command of the form `check-empty -- -this_is_actually_a_file.txt`. Thus, for a
113
+ file literally named "--", you have little choice but to call the underlying library
114
+ function (`check`) directly.
115
+ 2. Forward slashes can be used even on Windows, so there is no need to escape anything.
116
+ 3. Glob patterns are supported on \*nix only. If on Windows, use a shell like Git Bash.
117
+ 4. The program does not recurse into archives, since identification of compressed
118
+ archives would require reading the first few bytes of each file seen, which is
119
+ error-prone and inefficient.
120
+
107
121
  ## Development
108
122
 
109
123
  If you wish to contribute to this project, you are more than welcome, but please
110
- remember to read the [contributing guide](CONTRIBUTING.md).
124
+ remember to read the [contributing guide](CONTRIBUTING.md). Tests are run with:
125
+
126
+ ```bash
127
+ python -m test_check_empty # explicit
128
+ python -m unittest discover # alternative
129
+ ```
130
+
131
+ at the project root.
@@ -10,21 +10,26 @@ from __future__ import annotations
10
10
  import os
11
11
 
12
12
  __all__ = ('check',)
13
- __version__ = '0.6.0'
13
+ __version__ = '0.7.0'
14
14
 
15
15
  TYPE_CHECKING = False
16
16
  if TYPE_CHECKING:
17
17
  from collections.abc import Iterable
18
+ from typing import IO
18
19
 
19
20
  from _typeshed import FileDescriptorOrPath
20
- from typing_extensions import Literal, SupportsIndex
21
+ from typing_extensions import Literal, SupportsIndex, TypeAlias
22
+
23
+ ExitCode: TypeAlias = Literal[0, 1, 4, 5, 8, 9, 12, 13]
21
24
 
22
25
  DIRECTORY_DESCRIPTOR_UNSUPPORTED: BaseException = (
23
- SystemError('got directory descriptor on Windows??')
26
+ SystemError('somehow got directory descriptor on Windows')
24
27
  if os.name == 'nt'
25
28
  else NotImplementedError('directory descriptors are not supported')
26
29
  )
27
30
 
31
+ TYPE_MASK, S_IFDIR = 0xF000, 0x4000
32
+
28
33
 
29
34
  class _Handler:
30
35
  __slots__ = 'a', 'c', 'f', 'j', 'v'
@@ -46,13 +51,14 @@ class _Handler:
46
51
  self.v = r = (s if a < 0 else t) % a if isinstance(a, int) else os.fsdecode(a)
47
52
  return r
48
53
 
49
- def _x(self, v):
50
- self.j(self.e) if v.errno == 2 else self.f(str(v))
54
+ def __call__(self):
55
+ with self, open(self.a, 'wb'):
56
+ ...
51
57
 
52
58
  def __exit__(self, t, v, _):
53
59
  if t is None or not issubclass(t, OSError):
54
60
  return False
55
- self._x(v)
61
+ self.j(self.e) if v.errno == 2 else self.f(str(v))
56
62
  self.c = True
57
63
  return True
58
64
 
@@ -63,8 +69,9 @@ def check( # ruff: ignore[too-many-branches, too-many-locals, too-many-statemen
63
69
  clear: bool = False,
64
70
  may_not_exist: bool = False,
65
71
  verbosity: SupportsIndex = 2,
66
- ) -> Literal[0, 1, 4, 5, 8, 9, 12, 13]:
67
- """Check the emptiness of files and directories.
72
+ out: IO[str] | None = None,
73
+ ) -> ExitCode:
74
+ """Check the emptiness of files, recursing into directories if passed.
68
75
 
69
76
  Args:
70
77
  files: an iterable of file descriptors or paths representing the files and
@@ -74,14 +81,14 @@ def check( # ruff: ignore[too-many-branches, too-many-locals, too-many-statemen
74
81
  may_not_exist: if True, do not treat absent files or directories as errors.
75
82
  verbosity: how much detail the program should print to stdout; if 0, print
76
83
  nothing; verbosity > 5 is equivalent to verbosity = 5.
84
+ out: The file to which output is printed; default `sys.stdout`.
77
85
 
78
86
  Returns:
79
87
  The integer exit code. A bitwise or of 1 (some files were not empty), 4 (some
80
- files or directories were absent and `-m`/`--may-not-exist` was not specified)
81
- and 8 (caught OSError while processing some files), such that 0 is the only
82
- return value that represents success as expected. The 2 bit is skipped since it
83
- conflicts with the exit code of `argparse.ArgumentParser` when it encounters
84
- invalid arguments.
88
+ files or directories were absent and `-m`/`--may-not-exist` was omitted) and 8
89
+ (caught 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 `argparse.ArgumentParser` when it encounters invalid arguments.
85
92
 
86
93
  """
87
94
  files = list(files)
@@ -90,11 +97,8 @@ def check( # ruff: ignore[too-many-branches, too-many-locals, too-many-statemen
90
97
  k, j, t, n, o = [0] * 4, [], 0, len(files), files.pop
91
98
  u, e = j.extend, j.pop
92
99
 
93
- def _(v, k=k):
94
- def f(_):
95
- k[v] += 1
96
-
97
- return f
100
+ def _(v):
101
+ return lambda _, k=k: k.__setitem__(v, k[v] + 1)
98
102
 
99
103
  verbosity = type(verbosity).__index__(verbosity)
100
104
  if verbosity > 4:
@@ -102,6 +106,14 @@ def check( # ruff: ignore[too-many-branches, too-many-locals, too-many-statemen
102
106
  f = b.append
103
107
  else:
104
108
  b, f = None, _(0)
109
+ if verbosity > 3:
110
+ rc = ['']
111
+ ra = rc.append
112
+ else:
113
+ rc = None
114
+
115
+ def ra(_): ...
116
+
105
117
  if verbosity > 2:
106
118
  z, w = [''], ['']
107
119
  i = z.append, w.append
@@ -120,11 +132,10 @@ def check( # ruff: ignore[too-many-branches, too-many-locals, too-many-statemen
120
132
  if h.c:
121
133
  continue
122
134
  c = h.e
123
- if (q.st_mode >> 12) & 15 == 4:
135
+ if q.st_mode & TYPE_MASK == S_IFDIR:
124
136
  if isinstance(a, int):
125
137
  raise DIRECTORY_DESCRIPTOR_UNSUPPORTED
126
- if verbosity > 3:
127
- print('Recursing into directory:', c)
138
+ ra(c)
128
139
  u(os.scandir(c))
129
140
  continue
130
141
  s = q.st_size
@@ -134,15 +145,13 @@ def check( # ruff: ignore[too-many-branches, too-many-locals, too-many-statemen
134
145
  g(f'{c} ({s} bytes)')
135
146
  t += s
136
147
  if clear:
137
- with h, open(a, 'wb'):
138
- ...
148
+ h()
139
149
  del o, files
140
150
  while j:
141
151
  m = e()
142
152
  a = m.path
143
153
  if m.is_dir():
144
- if verbosity > 3:
145
- print('Recursing into directory:', a)
154
+ ra(a)
146
155
  u(os.scandir(a))
147
156
  continue
148
157
  n += 1
@@ -153,32 +162,37 @@ def check( # ruff: ignore[too-many-branches, too-many-locals, too-many-statemen
153
162
  g(f'{a} ({s} bytes)')
154
163
  t += s
155
164
  if clear:
156
- with _Handler(*i, a), open(a, 'wb'):
157
- ...
165
+ _Handler(*i, a)()
158
166
  x = k[1] if z is None else len(z) - 1
159
167
  y = k[3] if r is None else len(r) - 1
160
168
  d = k[2] if w is None else len(w) - 1
161
169
  v = bool(y) | (bool(x) and not may_not_exist) << 2 | bool(d) << 3
162
170
  if verbosity <= 0:
163
171
  return v # ty: ignore[invalid-return-type]
164
- if verbosity > 2:
165
- print(*z, sep='\nNot found: ')
166
- print(f'{x} file{"" if x == 1 else "s"} not found' if x else 'All files were found')
167
- if verbosity > 2:
168
- print(*w, sep='\nError: ')
172
+ fw = (__import__('sys').stdout if out is None else out).write
173
+ if rc is not None:
174
+ fw('\nRecursing into directory: '.join(rc))
175
+ fw('\n\n')
176
+ if z is not None:
177
+ fw('\nNot found: '.join(z))
178
+ fw('\n')
179
+ fw(f'{x} file{"s" if x > 1 else ""} not found\n' if x else 'All files were found\n')
180
+ if w is not None:
181
+ fw('\nError: '.join(w))
182
+ fw('\n')
169
183
  if d:
170
- print(f'{d} I/O error{"s" if d > 1 else ""} encountered')
171
- if verbosity > 4:
172
- print(*b, sep='\nEmpty: ')
184
+ fw(f'{d} I/O error{"s" if d > 1 else ""} encountered\n')
185
+ if b is not None:
186
+ fw('\nEmpty: '.join(b))
187
+ fw('\n')
173
188
  if verbosity > 2:
174
189
  p = k[0] if b is None else len(b) - 1
175
- print(f'{p} empty file{"" if p == 1 else "s"}' if p else 'No empty files')
190
+ fw(f'{p} empty file{"" if p == 1 else "s"}\n' if p else 'No empty files\n')
176
191
  if y:
177
- if verbosity > 1:
178
- print(*r, sep='\nCleared: ' if clear else '\nNot empty: ', end='')
179
- print(end='\n\n')
180
- print(y, 'offending files' if y > 1 else 'offending file')
181
- print(f'Total size: {t} bytes')
192
+ if r is not None:
193
+ fw(('\nCleared: ' if clear else '\nNot empty: ').join(r))
194
+ fw('\n\n')
195
+ fw(f'{y} offending file{"s" if y > 1 else ""}\nTotal size: {t} bytes\n')
182
196
  elif n > x:
183
- print('All found files were empty')
197
+ fw('All found files were empty\n')
184
198
  return v # ty: ignore[invalid-return-type]
@@ -7,7 +7,8 @@ import check_empty as c
7
7
  TYPE_CHECKING = False
8
8
  if TYPE_CHECKING:
9
9
  from collections.abc import Iterable
10
- from typing import Literal
10
+
11
+ from typing_extensions import Literal
11
12
 
12
13
  parser = __import__('argparse').ArgumentParser(
13
14
  'check-empty',
@@ -31,8 +32,8 @@ f('-Q', '--quiet', action='count', default=0, help='decrease output verbosity')
31
32
  f('-V', '--verbose', action='count', default=0, help='increase output verbosity')
32
33
 
33
34
 
34
- def main(argv: Iterable[str] | None = None) -> Literal[0, 1, 2, 4, 5, 8, 9, 12, 13]:
35
- """Run the hook on the files in the command-line arguments passed.
35
+ def main(argv: Iterable[str] | None = None) -> c.ExitCode | Literal[2]:
36
+ """Run the hook/CLI on the files in the command-line arguments passed.
36
37
 
37
38
  Args:
38
39
  argv: a list of arguments; default `sys.argv[1:]`.
@@ -53,6 +54,6 @@ def main(argv: Iterable[str] | None = None) -> Literal[0, 1, 2, 4, 5, 8, 9, 12,
53
54
  )
54
55
 
55
56
 
57
+ del f, TYPE_CHECKING
56
58
  if __name__ == '__main__':
57
59
  parser.exit(main())
58
- del f, TYPE_CHECKING
@@ -13,7 +13,7 @@ maintainers = [{name = "Jonathan Dung", email = "jonathandung@yahoo.com"}]
13
13
  name = "check-empty"
14
14
  readme = "README.md"
15
15
  requires-python = ">=3.6"
16
- version = "0.6.0"
16
+ version = "0.7.0"
17
17
 
18
18
  [project.scripts]
19
19
  check-empty = "check_empty.__main__:main"
@@ -36,7 +36,7 @@ select = ["ALL"]
36
36
  combine-as-imports = true
37
37
 
38
38
  [tool.ruff.lint.per-file-ignores]
39
- "test.py" = ["D"]
39
+ "test_check_empty.py" = ["D"]
40
40
  "!__init__.py" = ["missing-copyright-notice"]
41
41
 
42
42
  [tool.ruff.lint.pydocstyle]
@@ -53,10 +53,6 @@ python-platform = "all"
53
53
  all = "error"
54
54
  missing-override-decorator = "ignore"
55
55
 
56
- [[tool.ty.overrides]]
57
- include = ["check_empty/__init__.py"]
58
- rules = {not-iterable = "ignore"}
59
-
60
56
  [tool.uv]
61
57
  required-version = ">=0.11.31,<0.12"
62
58
 
File without changes
File without changes