fireaid 0.1.0__py3-none-any.whl

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.
fireaid/__init__.py ADDED
@@ -0,0 +1,400 @@
1
+ """
2
+ fireaid
3
+ =======
4
+
5
+ A transparent wrapper around Google Python Fire that rounds off its
6
+ command-line help syntax.
7
+
8
+ Instead of:
9
+
10
+ import fire
11
+
12
+ use:
13
+
14
+ import fireaid as fire
15
+
16
+ Existing code can continue to use:
17
+
18
+ fire.Fire(...)
19
+
20
+ and attributes exposed by the original ``fire`` package remain available.
21
+
22
+ Fire itself accepts:
23
+
24
+ tool foo -- --help
25
+ tool foo --help
26
+ tool foo -h
27
+
28
+ fireaid additionally accepts Git-style help:
29
+
30
+ tool help
31
+ tool help foo
32
+ tool help foo bar
33
+
34
+ and makes ``--help`` and ``-h`` print the help text to stdout, without
35
+ Fire's INFO banner or pager, including for functions that take ``**kwargs``.
36
+
37
+ An incomplete command, such as plain ``tool``, prints Fire's usage text
38
+ and exits with an error, where Fire shows the full help as a success.
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import inspect
44
+ import shlex
45
+ import sys
46
+ from collections.abc import Sequence
47
+ from importlib import metadata as _metadata
48
+ from typing import Any
49
+
50
+ import fire as _fire
51
+
52
+ __all__ = ["Fire"]
53
+
54
+ _HELP = ["--", "--help"]
55
+ _FLAGS = ("-h", "--help")
56
+ _NOT_CALLED = ("Initial component", "Instantiated class", "Accessed property")
57
+ _NAMED = (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY)
58
+
59
+
60
+ def _member(component: Any, name: str) -> tuple[bool, Any]:
61
+ """
62
+ Look up a member the way Fire does: by name, then with ``-`` as ``_``.
63
+ """
64
+ for key in (name, name.replace("-", "_")):
65
+ if isinstance(component, dict):
66
+ if key in component:
67
+ return True, component[key]
68
+ else:
69
+ try:
70
+ return True, getattr(component, key)
71
+ except Exception:
72
+ pass
73
+ return False, None
74
+
75
+
76
+ def _resolve(component: Any, path: Sequence[str]) -> tuple[Any, bool]:
77
+ """
78
+ Follow ``path`` from ``component`` as far as it names members.
79
+
80
+ Returns the component reached and whether it is known to be the
81
+ target of the command. It is when the whole path was followed, or
82
+ when a function was reached, since the rest are then its arguments.
83
+ Members that only exist on an instance cannot be followed; the class
84
+ is returned as not known to be the target.
85
+ """
86
+ for name in path:
87
+ if inspect.isroutine(component):
88
+ return component, True
89
+ found, member = _member(component, name)
90
+ if not found:
91
+ return component, False
92
+ component = member
93
+ return component, True
94
+
95
+
96
+ def _accepts(target: Any, flag: str) -> bool:
97
+ """
98
+ Tell whether Fire would pass ``flag`` to ``target`` as an argument.
99
+
100
+ ``--help`` is an argument of a callable with a ``help`` parameter.
101
+ ``-h`` is Fire's shortcut for any parameter starting with ``h``.
102
+ """
103
+ if not (inspect.isclass(target) or inspect.isroutine(target)):
104
+ return False
105
+ try:
106
+ parameters = inspect.signature(target).parameters.values()
107
+ except (TypeError, ValueError):
108
+ return False
109
+ names = [p.name for p in parameters if p.kind in _NAMED]
110
+ if flag == "--help":
111
+ return "help" in names
112
+ return any(name.startswith("h") for name in names)
113
+
114
+
115
+ def _normalize_help(argv: Sequence[str], component: Any, ours: bool) -> list[str]:
116
+ """
117
+ Translate conventional help syntax into Python Fire's native syntax.
118
+
119
+ Examples:
120
+
121
+ --help
122
+ -> -- --help
123
+
124
+ foo --help
125
+ -> foo -- --help
126
+
127
+ foo -h
128
+ -> foo -- --help
129
+
130
+ help
131
+ -> -- --help
132
+
133
+ help foo bar
134
+ -> foo bar -- --help
135
+
136
+ If an explicit ``--`` separator is already present, the command is
137
+ assumed to use native Fire syntax and is left untouched.
138
+
139
+ ``help`` is only interpreted as the first word, and only if it is
140
+ ``ours``: the component is not a function and has no ``help`` member
141
+ of its own. ``help --help`` is help for the help command itself.
142
+
143
+ ``-h`` and ``--help`` are only interpreted as the last word, and
144
+ only if the command they follow does not take them as an argument.
145
+ Where that command cannot be determined, ``-h`` is left to Fire.
146
+ """
147
+ argv = list(argv)
148
+
149
+ if not argv or "--" in argv:
150
+ return argv
151
+
152
+ flag = argv[-1]
153
+
154
+ if argv[0] == "help" and ours:
155
+ if flag not in _FLAGS:
156
+ return argv[1:] + _HELP
157
+ if len(argv) > 2 or not _member(component, "help")[0]:
158
+ return argv[1:-1] + _HELP
159
+
160
+ if flag not in _FLAGS:
161
+ return argv
162
+
163
+ target, known = _resolve(component, argv[:-1])
164
+ if _accepts(target, flag) or (flag == "-h" and not known):
165
+ return argv
166
+ return argv[:-1] + _HELP
167
+
168
+
169
+ def _help_command(component: Any, name: Any) -> tuple[Any, Any]:
170
+ """
171
+ Make the help command of a component, as a function and as a method.
172
+
173
+ Command lines starting with ``help`` are translated before Fire sees
174
+ them, so this is mostly there for Fire to list with the component's
175
+ other commands. It is what runs if Fire is given the command in its
176
+ native syntax.
177
+ """
178
+
179
+ def help(*command):
180
+ """Show help for the program, or for a command.
181
+
182
+ Args:
183
+ command: The command to show help for, as it would be run.
184
+ """
185
+ words = [str(word) for word in command]
186
+ return _fire_with(_display_stdout, component, command=words + _HELP, name=name)
187
+
188
+ def method(self, *command):
189
+ return help(*command)
190
+
191
+ method.__name__ = help.__name__
192
+ method.__doc__ = help.__doc__
193
+ return help, method
194
+
195
+
196
+ def _add_help(component: Any, name: Any = None) -> tuple[Any, Any]:
197
+ """
198
+ Give a component a help command.
199
+
200
+ Returns the component to pass to Fire, and a function to undo it.
201
+ A dict gets a copy with the command added. A module gets it as an
202
+ attribute, and anything else as a method of its class, for as long
203
+ as Fire runs. A component that cannot take one is returned as it is.
204
+ """
205
+ help, method = _help_command(component, name)
206
+ if isinstance(component, dict):
207
+ return dict(component, help=help), None
208
+ if inspect.ismodule(component):
209
+ owner, member = component, help
210
+ elif inspect.isclass(component):
211
+ owner, member = component, method
212
+ else:
213
+ owner, member = type(component), method
214
+ try:
215
+ setattr(owner, "help", member)
216
+ except (AttributeError, TypeError):
217
+ return component, None
218
+ return component, lambda: delattr(owner, "help")
219
+
220
+
221
+ def _display_stdout(lines: Sequence[str], out: Any = None) -> None:
222
+ """
223
+ Stand in for fire.core.Display, printing to stdout instead of stderr,
224
+ and not through a pager.
225
+ """
226
+ sys.stdout.write("\n".join(lines) + "\n")
227
+
228
+
229
+ def _display_unpaged(lines: Sequence[str], out: Any) -> None:
230
+ """
231
+ Stand in for fire.core.Display, paging only what Fire sends to stderr.
232
+
233
+ What Fire sends to stdout is the text for a command line that names
234
+ a group rather than a command. Its native help goes to stderr.
235
+ """
236
+ if out is sys.stdout:
237
+ _display_stdout(lines)
238
+ else:
239
+ _display(lines, out=out)
240
+
241
+
242
+ def _called(trace: Any) -> bool:
243
+ """
244
+ Tell whether the last step of a Fire trace was a function call.
245
+
246
+ If the trace does not say, assume so, which leaves things to Fire.
247
+ """
248
+ try:
249
+ action = trace.elements[-1]._action
250
+ except (AttributeError, IndexError):
251
+ return True
252
+ return action not in _NOT_CALLED
253
+
254
+
255
+ class _HelpText:
256
+ """
257
+ Stand in for the fire.helptext module, as fire.core sees it.
258
+
259
+ Fire prints the full help for a command line that names a group
260
+ rather than a command, and calls it a success. This makes it the
261
+ usage text, and keeps the trace, for the caller to call it an error.
262
+
263
+ Fire prints the same for an object returned by a function. That is
264
+ left alone: the command was complete.
265
+ """
266
+
267
+ def __init__(self) -> None:
268
+ self.incomplete = None
269
+
270
+ def __getattr__(self, name: str) -> Any:
271
+ return getattr(_helptext, name)
272
+
273
+ def HelpText(self, component: Any, trace: Any = None, verbose: bool = False) -> str:
274
+ if trace is None or trace.show_help or trace.HasError() or _called(trace):
275
+ return _helptext.HelpText(component, trace=trace, verbose=verbose)
276
+ self.incomplete = trace
277
+ return _helptext.UsageText(component, trace=trace, verbose=verbose)
278
+
279
+
280
+ _display = getattr(_fire.core, "Display", None)
281
+ _helptext = getattr(_fire.core, "helptext", None)
282
+
283
+ # Obtain the signature from the installed Fire version instead of
284
+ # duplicating it here. This makes fireaid less dependent on Fire's
285
+ # particular API version.
286
+ _FIRE_SIGNATURE = inspect.signature(_fire.Fire)
287
+
288
+
289
+ def Fire(*args: Any, **kwargs: Any) -> Any:
290
+ """
291
+ Call fire.Fire(), adding conventional command-line help handling.
292
+
293
+ All arguments other than ``component`` and ``command`` are passed
294
+ through unchanged. The command, by default taken from sys.argv, is
295
+ normalized first, making this work:
296
+
297
+ fire.Fire(MyCLI, command=["foo", "--help"])
298
+ fire.Fire(MyCLI, command="help foo")
299
+ """
300
+ bound = _FIRE_SIGNATURE.bind_partial(*args, **kwargs)
301
+ arguments = bound.arguments
302
+
303
+ if arguments.get("component") is None:
304
+ # Fire would look at its caller, which is this function. Supply
305
+ # the context of our own caller instead.
306
+ frame = sys._getframe(1)
307
+ arguments["component"] = {**frame.f_globals, **frame.f_locals}
308
+
309
+ command = arguments.get("command")
310
+ if command is None:
311
+ command = sys.argv[1:]
312
+ elif isinstance(command, str):
313
+ command = shlex.split(command)
314
+
315
+ if not isinstance(command, (list, tuple)):
316
+ # Not a valid command. Let Fire report it.
317
+ return _fire.Fire(*bound.args, **bound.kwargs)
318
+
319
+ component = arguments["component"]
320
+ ours = not (inspect.isroutine(component) or _member(component, "help")[0])
321
+ undo = None
322
+ if ours:
323
+ arguments["component"], undo = _add_help(component, arguments.get("name"))
324
+
325
+ try:
326
+ arguments["command"] = _normalize_help(command, arguments["component"], ours)
327
+ if arguments["command"] == list(command):
328
+ display = _display_unpaged
329
+ else:
330
+ # Help was asked for in conventional syntax: it belongs on stdout.
331
+ display = _display_stdout
332
+ return _fire_with(display, *bound.args, **bound.kwargs)
333
+ finally:
334
+ if undo is not None:
335
+ undo()
336
+
337
+
338
+ def _fire_with(display: Any, *args: Any, **kwargs: Any) -> Any:
339
+ """
340
+ Call fire.Fire() with a stand-in for its display of help.
341
+ """
342
+ if _display is None or _helptext is None:
343
+ return _fire.Fire(*args, **kwargs)
344
+
345
+ helptext = _HelpText()
346
+ _fire.core.helptext = helptext
347
+ _fire.core.Display = display
348
+ try:
349
+ result = _fire.Fire(*args, **kwargs)
350
+ finally:
351
+ _fire.core.Display = _display
352
+ _fire.core.helptext = _helptext
353
+
354
+ if helptext.incomplete is not None:
355
+ # Fire has printed the usage in place of a result.
356
+ raise _fire.core.FireExit(2, helptext.incomplete)
357
+ return result
358
+
359
+
360
+ # Make introspection of fireaid.Fire look like fire.Fire rather than
361
+ # showing the wrapper's (*args, **kwargs) implementation.
362
+ Fire.__signature__ = _FIRE_SIGNATURE
363
+ Fire.__wrapped__ = _fire.Fire
364
+ Fire.__doc__ = _fire.Fire.__doc__
365
+
366
+
367
+ def __getattr__(name: str) -> Any:
368
+ """
369
+ Proxy attributes not defined by fireaid to the real fire package.
370
+
371
+ Examples:
372
+
373
+ fireaid.core
374
+ fireaid.decorators
375
+ fireaid.inspectutils
376
+ fireaid.trace
377
+ """
378
+ return getattr(_fire, name)
379
+
380
+
381
+ def __dir__() -> list[str]:
382
+ """
383
+ Return the combined namespace of fireaid and the wrapped fire module.
384
+ """
385
+ return sorted(set(globals()) | set(dir(_fire)))
386
+
387
+
388
+ # Make the submodules Fire has loaded importable under our name too:
389
+ #
390
+ # import fireaid.core
391
+ # from fireaid.core import FireExit
392
+ #
393
+ for _name, _module in list(sys.modules.items()):
394
+ if _name.startswith("fire.") and not _name.endswith("__main__"):
395
+ sys.modules.setdefault(__name__ + _name[len("fire"):], _module)
396
+
397
+ try:
398
+ __version__ = _metadata.version("fireaid")
399
+ except _metadata.PackageNotFoundError:
400
+ __version__ = "unknown"
fireaid/__main__.py ADDED
@@ -0,0 +1,28 @@
1
+ """
2
+ Enables ``python -m fireaid [module] [arg] ...``, like ``python -m fire``.
3
+ """
4
+
5
+ import sys
6
+
7
+ import fireaid
8
+
9
+ try:
10
+ from fire.__main__ import import_module
11
+ except ImportError:
12
+ import_module = None
13
+
14
+
15
+ def main(args):
16
+ if import_module is None:
17
+ sys.exit("python -m fireaid requires a Fire version with python -m fire")
18
+
19
+ if len(args) < 2:
20
+ print("usage: python -m fireaid [module] [arg] ...")
21
+ sys.exit(1)
22
+
23
+ module, module_name = import_module(args[1])
24
+ fireaid.Fire(module, name=module_name, command=args[2:])
25
+
26
+
27
+ if __name__ == "__main__":
28
+ main(sys.argv)
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: fireaid
3
+ Version: 0.1.0
4
+ Summary: Familiar help for command-line tools made with Python Fire
5
+ Author-email: Christiane Ruetten <cr@23bit.net>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/cr/fireaid
8
+ Project-URL: Repository, https://github.com/cr/fireaid
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: fire>=0.2.0
13
+ Provides-Extra: test
14
+ Requires-Dist: pytest; extra == "test"
15
+ Dynamic: license-file
16
+
17
+ # fireaid
18
+
19
+ *A little help for Python Fire.*
20
+
21
+ [Python Fire](https://github.com/google/python-fire) turns any Python
22
+ function, class or module into a command-line tool with a single line
23
+ of code. `fireaid` is a thin wrapper around it that adds the help
24
+ conventions people know from tools like `git`: a `help` command,
25
+ `--help` printed plainly to the terminal, and a short usage message
26
+ when a command is incomplete. Everything else is Fire, unchanged.
27
+
28
+ All you need is:
29
+
30
+ pip install git+https://github.com/cr/fireaid
31
+
32
+ import fireaid as fire
33
+
34
+ fire.Fire(MyTool)
35
+
36
+ ## Installation
37
+
38
+ Directly from the repository:
39
+
40
+ pip install git+https://github.com/cr/fireaid
41
+
42
+ As a dependency in another project's `pyproject.toml`:
43
+
44
+ dependencies = ["fireaid @ git+https://github.com/cr/fireaid"]
45
+
46
+ From the project directory:
47
+
48
+ pip install .
49
+
50
+ For development:
51
+
52
+ pip install -e .
53
+
54
+ fireaid requires Fire 0.2.0 or later, the first version to accept
55
+ `--help` without a separator. On Python 3.13 and later, Fire itself
56
+ requires version 0.7.0.
57
+
58
+ ## Usage
59
+
60
+ Change:
61
+
62
+ import fire
63
+
64
+ to:
65
+
66
+ import fireaid as fire
67
+
68
+ Existing code such as:
69
+
70
+ fire.Fire(MyCLI)
71
+
72
+ continues to work, as do `import fireaid.core` and `python -m fireaid`.
73
+
74
+ ## Help
75
+
76
+ Python Fire accepts:
77
+
78
+ tool foo -- --help
79
+ tool foo --help
80
+ tool foo -h
81
+
82
+ fireaid additionally supports Git-style help:
83
+
84
+ tool help
85
+ tool help foo
86
+ tool help foo bar
87
+
88
+ `help` is a command like any other: Fire lists it with the program's
89
+ commands in the usage text and in the help, and `tool help help` shows
90
+ help for it.
91
+ To that end, fireaid adds the command to the program for as long as
92
+ Fire runs: to its class, or to a copy if the program is a dict.
93
+
94
+ With `--help` and `-h`, fireaid also
95
+
96
+ - prints the help text to stdout, so that `tool --help | less` works,
97
+ - prints it as it is, where Fire starts a pager on a terminal,
98
+ - omits Fire's `INFO: Showing help with the command ...` banner,
99
+ - shows help for functions taking `**kwargs`, where Fire passes
100
+ `help=True` to the function.
101
+
102
+ Native Fire syntax continues to work unchanged, printing to stderr,
103
+ or to a pager on a terminal.
104
+
105
+ ## Usage text
106
+
107
+ A command line that names a group rather than a command, such as
108
+ plain `tool`, is a success to Fire, which shows the full help for the
109
+ group, in a pager. To fireaid it is an incomplete command, as it is to
110
+ Git: it prints Fire's short usage text to stdout, and exits with 2,
111
+ Fire's exit code for errors:
112
+
113
+ Usage: tool <group|command>
114
+ available groups: shelf
115
+ available commands: add | help | list | remove | search
116
+
117
+ For detailed information on this command, run:
118
+ tool --help
119
+
120
+ A function that returns an object is a complete command. There, Fire
121
+ shows the help for the object, which fireaid only keeps from the pager.
122
+
123
+ `examples/pantry.py` is a small program to try all of this on, and
124
+ `examples/tour.sh` runs through it.
125
+
126
+ ## Limits
127
+
128
+ fireaid does not take anything away from the wrapped program:
129
+
130
+ - `help` is only help as the first word, and not if the program has
131
+ a `help` command of its own, or is a single function.
132
+ - `--help` and `-h` are only help as the last word. `--help` is left
133
+ to a command with a `help` parameter, `-h` to a command with a
134
+ parameter starting with `h`, for which it is Fire's shortcut.
135
+ - A command that fireaid cannot find without running the program, such
136
+ as a member created in `__init__`, may have such a parameter. There,
137
+ `-h` is left to Fire, which shows help with its banner on stderr.
138
+
139
+ Help for a command that does not exist is an error, as it is in
140
+ Fire's native syntax.
141
+
142
+ As with Fire, help is shown for what the command line evaluates to.
143
+ `tool foo x --help` calls `foo` and shows help for its result. Ask
144
+ for `tool foo --help` to get help for `foo`.
145
+
146
+ ## Tests
147
+
148
+ make test
149
+
150
+ creates a virtual environment in `.venv`, upgrades it to the latest
151
+ stable Fire, and runs the test suite. The tests hold fireaid to what
152
+ this README says, mostly by comparing it to Fire on the same command
153
+ lines. `make test PYTHON=python3.14` selects the Python for a new
154
+ environment, `make clean` removes it.
155
+
156
+ ## Releasing
157
+
158
+ Set the new `version` in `pyproject.toml`, commit, then tag and push:
159
+
160
+ git tag v0.1.0 && git push origin main v0.1.0
161
+
162
+ The tag starts `.github/workflows/publish.yml`, which runs the tests,
163
+ checks that the tag is the package version, builds, and publishes to
164
+ PyPI as a trusted publisher. PyPI never takes the same version twice.
@@ -0,0 +1,7 @@
1
+ fireaid/__init__.py,sha256=GwBnKkdldgFtZb3RVn0tFbrngkbp7MAkG6EYsI_NkA4,12150
2
+ fireaid/__main__.py,sha256=2UpNfCNbPU0vQqTMGsafsOcyPLqajcAd3bW1CvDwDdE,599
3
+ fireaid-0.1.0.dist-info/licenses/LICENSE,sha256=J_uMfTQ0iAQ7TPxSa2X8EshsnriTEFCGZR9V5qUA-qA,1075
4
+ fireaid-0.1.0.dist-info/METADATA,sha256=bp61sQVuPxZNgdK2Hn9lc3FGSRp7uJxmKzeqf3Yx6-Q,4956
5
+ fireaid-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
6
+ fireaid-0.1.0.dist-info/top_level.txt,sha256=65JX_jgamkJKwojn8qXQDY0vqq-ch4gUVbOgnCjiOzk,8
7
+ fireaid-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Christiane Ruetten
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ fireaid