textualize / trogon Goto Github PK
View Code? Open in Web Editor NEWEasily turn your Click CLI into a powerful terminal application
License: MIT License
Easily turn your Click CLI into a powerful terminal application
License: MIT License
These are currently hard-coded here:
Lines 288 to 294 in b9ddd90
I'd like the option to customize them, for example like this:
@tui(name="ui", help="Interactive UI for this tool")
@click.group()
def cli():
...
Possibly a misunderstanding on my behalf, but anyway:
I tried the nogroup_demo.py
verbatim, running it from a higher directory:
❯ cd /Users/alexhunsley/Documents/dev/
❯ mkdir test_dir
❯ cat > test_dir/nogroup_demo.py <then I pasted in the nogroup_demo src>
❯ python test_dir/nogroup_demo.py tui
<I did config in TUI interace, then hit CTRL-R>
Running python nogroup_demo.py add --category work -v --labels important sada
python: can't open file '/Users/alexhunsley/Documents/dev/test_dir/nogroup_demo.py':
[Errno 2] No such file or directory
So it's missing out the test_dir/
bit before the nogroup_demo.py
.
Trogon: 0.4.0
M1 MacBook Pro (16-inch, 2021)
Mac OS: 13.3.1 (a)
Python: 3.10.1
Click: 8.1.3
Thanks for making Trogon btw!
Sub-commands that don't take arguments don't show up in trogon. For example, with the following code:
import click
from trogon import tui
@tui()
@click.group()
def cli():
return
@cli.command()
@click.argument("test")
@click.pass_context
def test1():
return
@cli.command()
@click.pass_context
def test2():
return
if __name__ == "__main__":
cli(obj={})
Running it with python test.py tui
will result in the test1
command showing up in the UI but not the test2
command. Adding a click.argument
decorator to the function makes test2
show up.
(BTW as a side note awesome and neat looking project. I just love rich and textual already and you keep adding awesome stuff, so thanks.)
Hello!
When trying to add a TUI to my program, see jeertmans/manim-slides#249, the TUI crashes on two subcommands: present
and wizard
. The crash occurs when loading the subcommand's documentation, not actually running it.
FYI, both commands launch some GUI with PySide6.
I have attached the full error traceback below. As it is quite long and not very easy to understand, I was hoping to get some help here :-)
Thanks!
───────────────────── Traceback (most recent call last) ──────────────────────╮
│ /home/eertmans/.cache/pypoetry/virtualenvs/manim-slides-M-PeeMP--py3.10/lib/ │
│ python3.10/site-packages/trogon/trogon.py:171 in update_command_data │
│ │
│ 168 │ @on(CommandForm.Changed) │
│ 169 │ def update_command_data(self, event: CommandForm.Changed) -> None: │
│ 170 │ │ self.command_data = event.command_data │
│ ❱ 171 │ │ self._update_execution_string_preview( │
│ 172 │ │ │ self.selected_command_schema, self.command_data │
│ 173 │ │ ) │
│ 174 │
│ │
│ ╭───────── locals ─────────╮ │
│ │ event = Changed() │ │
│ │ self = CommandBuilder() │ │
│ ╰──────────────────────────╯ │
│ │
│ /home/eertmans/.cache/pypoetry/virtualenvs/manim-slides-M-PeeMP--py3.10/lib/ │
│ python3.10/site-packages/trogon/trogon.py:193 in │
│ _update_execution_string_preview │
│ │
│ 190 │ │ │ │ "command-name-syntax" │
│ 191 │ │ │ ) │
│ 192 │ │ │ prefix = Text(f"{self.click_app_name} ", command_name_synt │
│ ❱ 193 │ │ │ new_value = command_data.to_cli_string(include_root_comman │
│ 194 │ │ │ highlighted_new_value = Text.assemble(prefix, self.highlig │
│ 195 │ │ │ prompt_style = self.get_component_rich_style("prompt") │
│ 196 │ │ │ preview_string = Text.assemble(("$ ", prompt_style), highl │
│ │
│ ╭───────────────────────────────── locals ─────────────────────────────────╮ │
│ │ command_data = UserCommandData( │ │
│ │ │ name='root', │ │
│ │ │ options=[ │ │
│ │ │ │ UserOptionData( │ │
│ │ │ │ │ name=[ │ │
│ │ │ │ │ │ '--notify-outdated-version' │ │
│ │ │ │ │ ], │ │
│ │ │ │ │ value=(True,), │ │
│ │ │ │ │ option_schema=OptionSchema( │ │
│ │ │ │ │ │ name=[ │ │
│ │ │ │ │ │ │ │ │
│ │ Discontinued because GitHub limits the length. See attached file. │ │
[error.txt](https://github.com/Textualize/trogon/files/12407498/error.txt)
│ │ │ │ │ }, │ │
│ │ │ │ │ parent=None, │ │
│ │ │ │ │ is_group=True │ │
│ │ │ │ ), │ │
│ │ │ │ is_group=False │ │
│ │ │ ) │ │
│ │ ) │ │
│ │ value_data = [('None',)] │ │
│ │ values_are_defaults = True │ │
│ │ values_supplied = True │ │
│ ╰──────────────────────────────────────────────────────────────────────────╯ │
╰──────────────────────────────────────────────────────────────────────────────╯
TypeError: '<' not supported between instances of 'NoneType' and 'NoneType'
I'm adding Trogon support to a script that uses both arguments and parameters. The click
help displays help text for the arguments, e.g.:
python src/airflow_log_search.py mlops-pod-logs --help
╭─ Arguments ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ * dag TEXT Airflow DAG ID, e.g. recs_sku_coview [default: None] [required] │
│ * task_id TEXT Airflow task ID, e.g. predict_batch [default: None] [required] │
│ execution_date [EXECUTION_DATE] DAG execution date, e.g. 2023-07-06T19:52:45.488702+00:00 [default: None] │
│ try_number [TRY_NUMBER] Task try number, e.g. 1 [default: 1] │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
However, the TUI for the same script does not display the help text for arguments, only for parameters:
As far as I can tell Trogon supports this:
@click.option(
"-c",
"--column",
type=str,
multiple=True,
help="Column definitions for the table",
)
This creates an input field with a "+ value" button for adding multiple rows.
But it doesn't support this:
@click.argument(
"file_or_dir",
nargs=-1,
required=True,
type=click.Path(file_okay=True, dir_okay=True, allow_dash=True),
)
Where nargs=-1
indicates a positional argument can be provided multiple times.
These examples are for sqlite-utils insert-files
- which looks like this in Trogon:
file_or_dir
there should have a + value
button in the same way that -c / --column
does.
This is the --help
for that command:
Usage: sqlite-utils insert-files [OPTIONS] PATH TABLE FILE_OR_DIR...
Insert one or more files using BLOB columns in the specified table
Example:
sqlite-utils insert-files pics.db images *.gif \
-c name:name \
-c content:content \
-c content_hash:sha256 \
-c created:ctime_iso \
-c modified:mtime_iso \
-c size:size \
--pk name
Note how FILE_OR_DIR...
in the summary has that ...
indicating this can be passed multiple times.
When using Textual 0.54.0, the right-side panel with the inputs for building up a command with arguments and options no longer loads.
Explicitly pinning to textual<0.54.0
returns things to working order.
Reproduction here
It seems a bad idea to have the dist/
folder versioned in the repo. In general it's an ignored folder in python projects.
I recommend to purge them with the filter-repo tool
On the other hand, it's worth mentioning that a good way to manage package publishing in an automatic way is via github action as a "trusted-publisher".
Somehow I run into "Permission denied" when trying to run any command from tui (running from cli is ok). I get the following trace:
$ python3 demo.py tui
Running demo.py add
Traceback (most recent call last):
File "/home/user/util/demo.py", line 89, in <module>
cli(obj={})
File "/usr/lib/python3/dist-packages/click/core.py", line 1128, in __call__
return self.main(*args, **kwargs)
File "/usr/lib/python3/dist-packages/click/core.py", line 1053, in main
rv = self.invoke(ctx)
File "/usr/lib/python3/dist-packages/click/core.py", line 1659, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
File "/usr/lib/python3/dist-packages/click/core.py", line 1395, in invoke
return ctx.invoke(self.callback, **ctx.params)
File "/usr/lib/python3/dist-packages/click/core.py", line 754, in invoke
return __callback(*args, **kwargs)
File "/usr/lib/python3/dist-packages/click/decorators.py", line 26, in new_func
return f(get_current_context(), *args, **kwargs)
File "/home/lexxpluss/.local/lib/python3.10/site-packages/trogon/trogon.py", line 292, in wrapped_tui
Trogon(app, app_name=name, click_context=ctx).run()
File "/home/lexxpluss/.local/lib/python3.10/site-packages/trogon/trogon.py", line 260, in run
os.execvp(program_name, arguments)
File "/usr/lib/python3.10/os.py", line 574, in execvp
_execvpe(file, args)
File "/usr/lib/python3.10/os.py", line 615, in _execvpe
raise saved_exc
File "/usr/lib/python3.10/os.py", line 607, in _execvpe
exec_func(fullname, *argrest)
PermissionError: [Errno 13] Permission denied
Any ideas would be helpful?
Hi, I noticed Trogon works with the popular [Click](https://click.palletsprojects.com/) library for Python, but will support other libraries and languages in the future.
in the readme.
Since I used both click and python-fire, maybe I use python-fire more personally.
Are there any plans to support python-fire?
If I have a CLI with a script defined in a Poetry pyproject.toml
like this
[tool.poetry.scripts]
my-cli= "my_cli.main:app"
and I start the TUI with poetry run my-cli tui
, then poetry run
won't be prepended to the command my-cli ...
and it will fail to run. Would it be possible to support this use case?
Thanks!
Today, Trogon only supports click
, and click-specific code is exists throughout Trogon. Frankly, Trogon is too amazing to be specific to click.
In addition to the reverse-engineering ("introspection") method of building a Trogon TUI from an existing click CLI, Trogon should offer an interface for any library to integrate Trogon by manually building and providing the command-schemas to Trogon. This way, Trogon could function without click
, and any CLI app would have the ability to build and display a Trogon TUI. This would also lessen the burden on Trogon development, since it would not be necessary for Trogon devs to build "introspector" logic for each desired library, but would instead give devs of those libraries a path to D.I.Y. Trogon integration.
Instead of Trogon devs working to reverse-engineer and support this library and others, the developers of each library should have a path to integrate Trogon themselves.
Related issue: #5
I'd like to use trogon when it's available, and fail gracefully when it is not. It seems this should be sufficient
try:
from trogon import tui
except ImportError:
tui = PassThroughDecorator()
@tui()
@click.group(...)
def cli():
...
but I've been unable to come up with a suitable PassThroughDecorator
?
When running a command via Trogon, a FileNotFoundError exception is raised. The UI is accessible, arguments and options are configurable, and the CLI application (click) works when running directly.
My application is running in a venv virtual environment. Trogon and dependencies are installed in the venv. Click was already in use in my project prior to adding Trogon, but it appears all the relevant dependencies meet versioning requirements.
Requirement already satisfied: trogon in c:\users\<username>\desktop\vs codium\app\venv\lib\site-packages (0.2.1)
Requirement already satisfied: click>=8.0.0 in c:\users\<username>\desktop\vs codium\app\venv\lib\site-packages (from trogon) (8.1.2)
Requirement already satisfied: textual>=0.26.0 in c:\users\<username>\desktop\vs codium\app\venv\lib\site-packages (from trogon) (0.26.0)
Requirement already satisfied: colorama in c:\users\<username>\desktop\vs codium\app\venv\lib\site-packages (from click>=8.0.0->trogon) (0.4.4)
Requirement already satisfied: importlib-metadata>=4.11.3 in c:\users\<username>\desktop\vs codium\app\venv\lib\site-packages (from textual>=0.26.0->trogon) (6.6.0)
Requirement already satisfied: markdown-it-py[linkify,plugins]<3.0.0,>=2.1.0 in c:\users\<username>\desktop\vs codium\app\venv\lib\site-packages (from textual>=0.26.0->trogon) (2.2.0)
Requirement already satisfied: rich>=13.3.3 in c:\users\<username>\desktop\vs codium\app\venv\lib\site-packages (from textual>=0.26.0->trogon) (13.3.5)
Requirement already satisfied: typing-extensions<5.0.0,>=4.4.0 in c:\users\<username>\desktop\vs codium\app\venv\lib\site-packages (from textual>=0.26.0->trogon) (4.5.0)
Requirement already satisfied: zipp>=0.5 in c:\users\<username>\desktop\vs codium\app\venv\lib\site-packages (from importlib-metadata>=4.11.3->textual>=0.26.0->trogon) (3.15.0)
Requirement already satisfied: mdurl~=0.1 in c:\users\<username>\desktop\vs codium\app\venv\lib\site-packages (from markdown-it-py[linkify,plugins]<3.0.0,>=2.1.0->textual>=0.26.0->trogon) (0.1.2)
Requirement already satisfied: linkify-it-py<3,>=1 in c:\users\<username>\desktop\vs codium\app\venv\lib\site-packages (from markdown-it-py[linkify,plugins]<3.0.0,>=2.1.0->textual>=0.26.0->trogon) (2.0.2)
Requirement already satisfied: mdit-py-plugins in c:\users\<username>\desktop\vs codium\app\venv\lib\site-packages (from markdown-it-py[linkify,plugins]<3.0.0,>=2.1.0->textual>=0.26.0->trogon) (0.3.5)
Requirement already satisfied: pygments<3.0.0,>=2.13.0 in c:\users\<username>\desktop\vs codium\app\venv\lib\site-packages (from rich>=13.3.3->textual>=0.26.0->trogon) (2.15.1)
Requirement already satisfied: uc-micro-py in c:\users\<username>\desktop\vs codium\app\venv\lib\site-packages (from linkify-it-py<3,>=1->markdown-it-py[linkify,plugins]<3.0.0,>=2.1.0->textual>=0.26.0->trogon) (1.0.2)
Not certain if this is an issue with my system configuration, or a bug. Installed via pip and imported as shown in the README.
import click
from trogon import tui
from app.cli import run, create_template, version
@tui()
@click.group()
def main() -> None:
pass
main.add_command(run)
main.add_command(create_template)
main.add_command(version)
if __name__ == "__main__":
main()
(venv) PS C:\Users\<username>\Desktop\VS Codium\app> python -m app tui
Running python -m app run
Traceback (most recent call last):
File "C:\Users\<username>\AppData\Local\Programs\Python\Python39\lib\runpy.py", line 197, in _run_module_as_main
return _run_code(code, main_globals, None,
File "C:\Users\<username>\AppData\Local\Programs\Python\Python39\lib\runpy.py", line 87, in _run_code
exec(code, run_globals)
File "C:\Users\<username>\Desktop\VS Codium\app\app\__main__.py", line 18, in <module>
main()
File "C:\Users\<username>\Desktop\VS Codium\app\venv\lib\site-packages\click\core.py", line 1130, in __call__
return self.main(*args, **kwargs)
File "C:\Users\<username>\Desktop\VS Codium\app\venv\lib\site-packages\click\core.py", line 1055, in main
rv = self.invoke(ctx)
File "C:\Users\<username>\Desktop\VS Codium\app\venv\lib\site-packages\click\core.py", line 1657, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
File "C:\Users\<username>\Desktop\VS Codium\app\venv\lib\site-packages\click\core.py", line 1404, in invoke
return ctx.invoke(self.callback, **ctx.params)
File "C:\Users\<username>\Desktop\VS Codium\app\venv\lib\site-packages\click\core.py", line 760, in invoke
return __callback(*args, **kwargs)
File "C:\Users\<username>\Desktop\VS Codium\app\venv\lib\site-packages\click\decorators.py", line 26, in new_func
return f(get_current_context(), *args, **kwargs)
File "C:\Users\<username>\Desktop\VS Codium\app\venv\lib\site-packages\trogon\trogon.py", line 286, in wrapped_tui
Trogon(app, app_name=name, click_context=ctx).run()
File "C:\Users\<username>\Desktop\VS Codium\app\venv\lib\site-packages\trogon\trogon.py", line 254, in run
os.execvp(self.app_name, [self.app_name, *self.post_run_command])
File "C:\Users\<username>\AppData\Local\Programs\Python\Python39\lib\os.py", line 574, in execvp
_execvpe(file, args)
File "C:\Users\<username>\AppData\Local\Programs\Python\Python39\lib\os.py", line 616, in _execvpe
raise last_exc
File "C:\Users\<username>\AppData\Local\Programs\Python\Python39\lib\os.py", line 607, in _execvpe
exec_func(fullname, *argrest)
FileNotFoundError: [Errno 2] No such file or directory
First of all, I'd like to say how much I like this tool short of some funky output I get in windows (addressed with another issue). The paradigm of keyboard friendly gui for someone like me who struggles with RSI combined with mouse functionality is great, and the ease to add it to my app is amazing.
I am using this tool with typer, and I have limited knowledge of click and what capabilities it has. Type has the ability to using type annotations to define an input as a folder or a file:
somefile: Annotated[typer.FileText, typer.Argument(mode="r")]
See the docs here.
It would be great if trogon could read these types and provide an icon for brining up a file/folder selector and if some autocomplete could be included (I love how Powershell in Windows Terminal will either complete a path or give me a list of options that fit).
ctrl+t can be a toggle between command tree and command form?
When executing my small test script scripts/trogon_test.py
@tui()
@click.command()
@click.option(
"--plot_opt_metrics/--no-plot_opt_metrics",
type=click.BOOL,
default=False,
help="Whether to plot and display the loss, defects, and Lagrangian multipliers of the constrained optimization.",
)
def main(
plot_opt_metrics: bool,
):
"""Create and run a dynamic experimental design."""
print("printing the only option: ", plot_opt_metrics)
if __name__ == "__main__":
main()
in the command line using
poetry run python scripts/trogon_test.py tui
it displays the desired TUI, however, instantly crashes when hitten Crtl + R
for running it:
poetry run python scripts/trogon_test.py tui
Running trogon_test.py main
Traceback (most recent call last):
File "scripts/trogon_test.py", line 52, in <module>
main()
File "/home/redacted/.venv/lib/python3.8/site-packages/click/core.py", line 1157, in __call__
return self.main(*args, **kwargs)
File "/home/redacted/.venv/lib/python3.8/site-packages/click/core.py", line 1078, in main
rv = self.invoke(ctx)
File "/home/redacted/.venv/lib/python3.8/site-packages/click/core.py", line 1688, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
File "/home/redacted/.venv/lib/python3.8/site-packages/click/core.py", line 1434, in invoke
return ctx.invoke(self.callback, **ctx.params)
File "/home/redacted/.venv/lib/python3.8/site-packages/click/core.py", line 783, in invoke
return __callback(*args, **kwargs)
File "/home/redacted/.venv/lib/python3.8/site-packages/click/decorators.py", line 33, in new_func
return f(get_current_context(), *args, **kwargs)
File "/home/redacted/.venv/lib/python3.8/site-packages/trogon/trogon.py", line 296, in wrapped_tui
Trogon(app, app_name=name, command_name=command, click_context=ctx).run()
File "/home/redacted/.venv/lib/python3.8/site-packages/trogon/trogon.py", line 264, in run
os.execvp(program_name, arguments)
File "/home/muf2rng/.pyenv/versions/3.8.17/lib/python3.8/os.py", line 568, in execvp
_execvpe(file, args)
File "/home/muf2rng/.pyenv/versions/3.8.17/lib/python3.8/os.py", line 610, in _execvpe
raise last_exc
File "/home/muf2rng/.pyenv/versions/3.8.17/lib/python3.8/os.py", line 601, in _execvpe
exec_func(fullname, *argrest)
FileNotFoundError: [Errno 2] No such file or directory
If I got it correctly, typer is also based up on click. Might it be possible to use trogon with typer in mind?
https://github.com/alecthomas/kong
Curious if this is python exclusive or if multi-language is possible.
Trogon is a really awesome project, thank you very much!
AFAICS, it is currently only possible to run individual subcommands, right?
Many of my CLIs depend on chain
ing subcommands together.
Would be awesome if trogon supported that, similar to how arguments with multiple allowed invocations already work.
I'm looking for a tool and this is almost what I wanted except that its not a stand-alone utility, but rather an ad-on for people already using click
.
Can I please suggest providing an example of how to use this tool independently? Concretely, the user provides a dictionary of keys with potential values for each and defaults and description etc, and boom, we have a TUI.
I think this is already what's happening internally in trogon where it is scraping the output of click
to design the TUI, so in this case, we are providing that directly.
Thank you.
Click Options have a shell_complete
kwarg that takes a function that returns a list of completions. It would be helpful if trogon supported running this function by passing in the context and param based similar to how click does so that a dropdown of completions could be shown to pick from.
See Also: Click documentation on how Shell Completions are implemented
If I try to use click.Tuple with 2 different data types i.e. type=click.Tuple([float, str]), default=(0.0, 'test') The TUI breaks with this error: TypeError: '<' not supported between instances of 'float' and 'str'
It works fine if no default is provided or the default is set to None. It also works if both tuple values are str.
Not sure if that is a known issue or if it is on my side.
Similar to #12 it would be interesting to have the ability to paste a command into the tui and switch to the subcommand page and have everything filled in so that it can be tweaked or so that the specific documentation for a flag could be quickly understood
Running the nogroup_demo example fails with FileNotFoundError: [Errno 2] No such file or directory
The cwd and the program_name arguments to the os.execvp call seem to be correct, however the run still fails. The cli version works fine though.
I added a debug log just before the call to execvp listing the following
prog_name = nogroup_demo.py, args = ['nogroup_demo.py', 'add', '--category', 'work', 'foo'] app_name = nogroup_demo.py cwd = /Users/arunabhaghosh/dev/import_migrate_data
---- Details--------------------------
Running nogroup_demo.py add --category work foo
prog_name = nogroup_demo.py, args = ['nogroup_demo.py', 'add', '--category', 'work', 'foo'] app_name = nogroup_demo.py cwd = /Users/arunabhaghosh/dev/import_migrate_data
Traceback (most recent call last):
File "/Users/arunabhaghosh/dev/import_migrate_data/nogroup_demo.py", line 52, in
add()
File "/Users/arunabhaghosh/dev/import_migrate_data/venv/lib/python3.11/site-packages/click/core.py", line 1157, in call
return self.main(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/arunabhaghosh/dev/import_migrate_data/venv/lib/python3.11/site-packages/click/core.py", line 1078, in main
rv = self.invoke(ctx)
^^^^^^^^^^^^^^^^
File "/Users/arunabhaghosh/dev/import_migrate_data/venv/lib/python3.11/site-packages/click/core.py", line 1688, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/arunabhaghosh/dev/import_migrate_data/venv/lib/python3.11/site-packages/click/core.py", line 1434, in invoke
return ctx.invoke(self.callback, **ctx.params)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/arunabhaghosh/dev/import_migrate_data/venv/lib/python3.11/site-packages/click/core.py", line 783, in invoke
return __callback(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/arunabhaghosh/dev/import_migrate_data/venv/lib/python3.11/site-packages/click/decorators.py", line 33, in new_func
return f(get_current_context(), *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/arunabhaghosh/dev/import_migrate_data/venv/lib/python3.11/site-packages/trogon/trogon.py", line 297, in wrapped_tui
Trogon(app, app_name=name, command_name=command, click_context=ctx).run()
File "/Users/arunabhaghosh/dev/import_migrate_data/venv/lib/python3.11/site-packages/trogon/trogon.py", line 265, in run
os.execvp(program_name, arguments)
File "", line 574, in execvp
File "", line 616, in _execvpe
File "", line 607, in _execvpe
FileNotFoundError: [Errno 2] No such file or directory
I get some strange behaviour when using arguments with defaults on command groups. Here is a minimal example:
import click
from trogon import tui
@tui()
@click.group()
def cli():
pass
@cli.command()
@click.argument("name", default="Bob")
def hello(name):
"""Pring hello."""
print(f"Hello world and {name}")
if __name__ == "__main__":
cli()
When running python cli.py tui
and selecting the hello command, the command displayed at the bottom is:
cli.py '('"'"'Bob'"'"',)' hello Bob
When trying to run the command using ctrl+r, I get the following stacktrace:
Traceback (most recent call last):
File "/some-dir/test-trogon/cli.py", line 16, in <module>
cli()
File "/some-dir/test-trogon/.venv/lib/python3.11/site-packages/click/core.py", line 1130, in __call__
return self.main(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/some-dir/test-trogon/.venv/lib/python3.11/site-packages/click/core.py", line 1055, in main
rv = self.invoke(ctx)
^^^^^^^^^^^^^^^^
File "/some-dir/test-trogon/.venv/lib/python3.11/site-packages/click/core.py", line 1657, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/some-dir/test-trogon/.venv/lib/python3.11/site-packages/click/core.py", line 1404, in invoke
return ctx.invoke(self.callback, **ctx.params)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/some-dir/test-trogon/.venv/lib/python3.11/site-packages/click/core.py", line 760, in invoke
return __callback(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/some-dir/test-trogon/.venv/lib/python3.11/site-packages/click/decorators.py", line 26, in new_func
return f(get_current_context(), *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/some-dir/test-trogon/.venv/lib/python3.11/site-packages/trogon/trogon.py", line 286, in wrapped_tui
Trogon(app, app_name=name, click_context=ctx).run()
File "/some-dir/test-trogon/.venv/lib/python3.11/site-packages/trogon/trogon.py", line 252, in run
f"Running [b cyan]{self.app_name} {' '.join(shlex.quote(s) for s in self.post_run_command)}[/]"
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/some-dir/test-trogon/.venv/lib/python3.11/site-packages/trogon/trogon.py", line 252, in <genexpr>
f"Running [b cyan]{self.app_name} {' '.join(shlex.quote(s) for s in self.post_run_command)}[/]"
^^^^^^^^^^^^^^
File "/somewhere/[email protected]/3.11.3/Frameworks/Python.framework/Versions/3.11/lib/python3.11/shlex.py", line 329, in quote
if _find_unsafe(s) is None:
^^^^^^^^^^^^^^^
TypeError: expected string or bytes-like object, got 'tuple'
Note that it works if I remove the default
on the argument or if I get rid of the group:
@tui()
@click.command(name="world")
@click.argument("name", default="Bob")
def cli(name):
"""Pring hello."""
print(f"Hello world and {name}")
if __name__ == "__main__":
cli()
Without decorator arichtman/gitlab-lint@cdaa8ff
With decorator arichtman/gitlab-lint@84d7016
As stated in the readme, trogon is made for discoverability, but after trying out the command, we want to actually add it to our history and use it directly. This feature request is thus about adding a shortcut to copy the command to the clipboard.
This is quite possibly a stupid question, but is there a way to wrap this in a Trogon TUI? I instantiate my typer.Typer
app like this, as shown in that project's README
app = typer.Typer()
Is there a way to wrap that in a Trogon TUI?
Hi team! I love your work, really. I discovered this new tool and would like to integrate it in our internal stack. We are using Click for an internal CLI, and we are using it commands and sub-commands with that implementation:
https://click.palletsprojects.com/en/8.1.x/commands/#custom-multi-commands
Basically we have a folder structure:
mycli/
|_ commands/
|_ cmd1.py
|_ cmd2.py
|_ cmd3.py
|_ ...
Running mycli
would list all the commands, and running mycli cmd1
would list all the cmd
subcommands
If I implement Trogon
from the docs, I would need to do mycli cmd1 tui
to have a TUI for the mycli cmd1
subcommands, but I would love having a mycli tui
command that would create a TUI for all commands and associated subcommands. Is there a way to do it with the current Trogon code, or this would need extra development?
Let me know if it's unclear
When I specify the default
parameter in an option, trogon considers this value in the TUI. But when I pass a default_map as part of the context_settings (like shown in the click docs here and here) the option doesn't get prefilled.
This behaviour might be misleading because an existing default_map overwrites an options default value. Thus, the application actually runs with a different option value than trogon suggested.
Consider the following code:
import click
from trogon import tui
@tui(command="ui", help="Open terminal UI")
@click.group()
def cli():
pass
@cli.command()
@click.option("--port", default=8000, show_default=True)
def runserver(port):
click.echo(f"Serving on http://127.0.0.1:{port}/")
if __name__ == "__main__":
cli(default_map={"runserver": {"port": 5000}})
The runserver
sub-command actually runs with port 5000 because of the default_map
. But trogon prefills the textbox in the TUI with port 8000, which is wrong.
As pointed out over on Discord, there is no licence file in the repository, it's not made clear in the README what the licence is, and by extension the presentation of the repo here on GitHub doesn't make clear the licence.
It is indicated in the project file but nowhere else.
I have a very simple CLI for which I am using trogon
. Upon running command tui
, I get the following error. The same code works as expected on WSL.
Version:
python
: 3.8.10
click
: 8.1.3
trogon
: 0.4.0
(fastai) C:\git\public\nested-cli-tui-demo>hello tui
Traceback (most recent call last):
File "C:\Users\deven\Envs\fastai\Scripts\hello-script.py", line 33, in <module>
sys.exit(load_entry_point('foo', 'console_scripts', 'hello')())
File "C:\Users\deven\Envs\fastai\lib\site-packages\click\core.py", line 1130, in __call__
return self.main(*args, **kwargs)
File "C:\Users\deven\Envs\fastai\lib\site-packages\click\core.py", line 1055, in main
rv = self.invoke(ctx)
File "C:\Users\deven\Envs\fastai\lib\site-packages\click\core.py", line 1657, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
File "C:\Users\deven\Envs\fastai\lib\site-packages\click\core.py", line 1404, in invoke
return ctx.invoke(self.callback, **ctx.params)
File "C:\Users\deven\Envs\fastai\lib\site-packages\click\core.py", line 760, in invoke
return __callback(*args, **kwargs)
File "C:\Users\deven\Envs\fastai\lib\site-packages\click\decorators.py", line 26, in new_func
return f(get_current_context(), *args, **kwargs)
File "C:\Users\deven\Envs\fastai\lib\site-packages\trogon\trogon.py", line 292, in wrapped_tui
Trogon(app, app_name=name, click_context=ctx).run()
File "C:\Users\deven\Envs\fastai\lib\site-packages\trogon\trogon.py", line 228, in __init__
self.app_name = detect_run_string()
File "C:\Users\deven\Envs\fastai\lib\site-packages\trogon\detect_run_string.py", line 39, in detect_run_string
argv = get_orig_argv()
File "C:\Users\deven\Envs\fastai\lib\site-packages\trogon\detect_run_string.py", line 17, in get_orig_argv
ctypes.pythonapi.Py_GetArgcArgv(ctypes.byref(_argc), ctypes.byref(_argv))
File "c:\python38\lib\ctypes\__init__.py", line 386, in __getattr__
func = self.__getitem__(name)
File "c:\python38\lib\ctypes\__init__.py", line 391, in __getitem__
func = self._FuncPtr((name_or_ordinal, self))
AttributeError: function 'Py_GetArgcArgv' not found
Hello!
When trying to add a TUI to my program, see jeertmans/manim-slides#249, the TUI crashes when pressing F1. The issue seems to also occur during tests, see logs.
I have attached the full error traceback below.
Thanks!
╭─────────────── Traceback (most recent call last) ────────────────╮
│ /home/eertmans/.cache/pypoetry/virtualenvs/manim-slides-M-PeeMP- │
│ -py3.10/lib/python3.10/site-packages/trogon/trogon.py:138 in │
│ action_about │
│ │
│ 135 │ │ self.app.exit() │
│ 136 │ │
│ 137 │ def action_about(self) -> None: │
│ ❱ 138 │ │ from .widgets.about import AboutDialog │
│ 139 │ │ │
│ 140 │ │ self.app.push_screen(AboutDialog()) │
│ 141 │
│ │
│ ╭──────── locals ─────────╮ │
│ │ self = CommandBuilder() │ │
│ ╰─────────────────────────╯ │
│ │
│ /home/eertmans/.cache/pypoetry/virtualenvs/manim-slides-M-PeeMP- │
│ -py3.10/lib/python3.10/site-packages/trogon/widgets/about.py:8 │
│ in <module> │
│ │
│ 5 from textual.binding import Binding │
│ 6 from textual.containers import Center, Vertical │
│ 7 from textual.screen import ModalScreen │
│ ❱ 8 from textual.widgets import Button, Static │
│ 9 from textual.widgets._button import ButtonVariant │
│ 10 │
│ 11 │
│ │
│ ╭──────────────────────── locals ────────────────────────╮ │
│ │ Binding = <class 'textual.binding.Binding'> │ │
│ │ Center = <class 'textual.containers.Center'> │ │
│ │ ComposeResult = typing.Iterable[textual.widget.Widget] │ │
│ │ ModalScreen = <class 'textual.screen.ModalScreen'> │ │
│ │ Text = <class 'rich.text.Text'> │ │
│ │ TextType = typing.Union[str, ForwardRef('Text')] │ │
│ │ Vertical = <class 'textual.containers.Vertical'> │ │
│ ╰────────────────────────────────────────────────────────╯ │
│ │
│ in feature_imported:61 │
│ │
│ in feature_imported:137 │
│ │
│ in _mod_uses_pyside:148 │
│ │
│ /usr/lib/python3.10/inspect.py:1139 in getsource │
│ │
│ 1136 │ The argument may be a module, class, method, function │
│ 1137 │ or code object. The source code is returned as a sin │
│ 1138 │ OSError is raised if the source code cannot be retrie │
│ ❱ 1139 │ lines, lnum = getsourcelines(object) │
│ 1140 │ return ''.join(lines) │
│ 1141 │
│ 1142 # --------------------------------------------------- cla │
│ │
│ ╭─────────────────────────── locals ───────────────────────────╮ │
│ │ object = <module 'textual.widgets' from │ │
│ │ '/home/eertmans/.cache/pypoetry/virtualenvs/manim-… │ │
│ ╰──────────────────────────────────────────────────────────────╯ │
│ │
│ /usr/lib/python3.10/inspect.py:1120 in getsourcelines │
│ │
│ 1117 │ corresponding to the object and the line number indic │
│ 1118 │ original source file the first line of code was found │
│ 1119 │ raised if the source code cannot be retrieved.""" │
│ ❱ 1120 │ object = unwrap(object) │
│ 1121 │ lines, lnum = findsource(object) │
│ 1122 │ │
│ 1123 │ if istraceback(object): │
│ │
│ ╭─────────────────────────── locals ───────────────────────────╮ │
│ │ object = <module 'textual.widgets' from │ │
│ │ '/home/eertmans/.cache/pypoetry/virtualenvs/manim-… │ │
│ ╰──────────────────────────────────────────────────────────────╯ │
│ │
│ /usr/lib/python3.10/inspect.py:639 in unwrap │
│ │
│ 636 │ # ensure they aren't destroyed, which would allow the │
│ 637 │ memo = {id(f): f} │
│ 638 │ recursion_limit = sys.getrecursionlimit() │
│ ❱ 639 │ while _is_wrapper(func): │
│ 640 │ │ func = func.__wrapped__ │
│ 641 │ │ id_func = id(func) │
│ 642 │ │ if (id_func in memo) or (len(memo) >= recursion_l │
│ │
│ ╭─────────────────────────── locals ───────────────────────────╮ │
│ │ _is_wrapper = <function unwrap.<locals>._is_wrapper at │ │
│ │ 0x7f0d12e38ee0> │ │
│ │ f = <module 'textual.widgets' from │ │
│ │ '/home/eertmans/.cache/pypoetry/virtualen… │ │
│ │ func = <module 'textual.widgets' from │ │
│ │ '/home/eertmans/.cache/pypoetry/virtualen… │ │
│ │ memo = { │ │
│ │ │ 139695144842192: <module │ │
│ │ 'textual.widgets' from │ │
│ │ '/home/eertmans/.cache/pypoetry/virtualen… │ │
│ │ } │ │
│ │ recursion_limit = 1000 │ │
│ │ stop = None │ │
│ ╰──────────────────────────────────────────────────────────────╯ │
│ │
│ /usr/lib/python3.10/inspect.py:630 in _is_wrapper │
│ │
│ 627 │ """ │
│ 628 │ if stop is None: │
│ 629 │ │ def _is_wrapper(f): │
│ ❱ 630 │ │ │ return hasattr(f, '__wrapped__') │
│ 631 │ else: │
│ 632 │ │ def _is_wrapper(f): │
│ 633 │ │ │ return hasattr(f, '__wrapped__') and not stop │
│ │
│ ╭─────────────────────────── locals ───────────────────────────╮ │
│ │ f = <module 'textual.widgets' from │ │
│ │ '/home/eertmans/.cache/pypoetry/virtualenvs/manim-slide… │ │
│ ╰──────────────────────────────────────────────────────────────╯ │
│ │
│ /home/eertmans/.cache/pypoetry/virtualenvs/manim-slides-M-PeeMP- │
│ -py3.10/lib/python3.10/site-packages/textual/widgets/__init__.py │
│ :96 in __getattr__ │
│ │
│ 93 │ │ pass │
│ 94 │ │
│ 95 │ if widget_class not in __all__: │
│ ❱ 96 │ │ raise ImportError(f"Package 'textual.widgets' has │
│ 97 │ │
│ 98 │ widget_module_path = f"._{camel_to_snake(widget_class) │
│ 99 │ module = import_module(widget_module_path, package="te │
│ │
│ ╭─────────── locals ───────────╮ │
│ │ widget_class = '__wrapped__' │ │
│ ╰──────────────────────────────╯ │
╰──────────────────────────────────────────────────────────────────╯
ImportError: Package 'textual.widgets' has no class '__wrapped__'
As the title notes, this isn't directly related to the project, but I love the typeface used I your screenshots. What is it? Tried auto-detecting it, but couldn't figure out what it was.
Awesome project by the way!
System: Windows 10 (have not tried on Linux yet).
Terminal: Windows Terminal set to Command Prompt.
When using @click.option with a prompt, the command prompt appears to interweave with the running program.
Also occurs when using click.prompt() or rich's Prompt.ask() inside of the function definition.
Will cause the command prompt (such as "(venv) C:\py>") to be displayed throughout click prompts while program is running.
Sometimes will treat response to prompt as if trying to run a system command, sometimes will accept prompt input from the command prompt when the click prompt is not the active display.
As an example, the attached screenshot shows one run of a program through trogon, asking for 2 prompt inputs (a staff name, and an email address). The greenish-yellow squares show the command prompt appearing inside of the running trogon program.
No such problems occur when directly running the click program without trogon. No issues occur if provide the option values in trogon and/or bypass prompts inside program. Have tried on several different functions.
EDIT:
Minimal viable product:
mvp.py:
import click
from trogon import tui
@tui()
@click.group()
def cli():
pass
@click.command()
@click.option('--name', prompt="Name")
def minimal(name):
"""Prompt for a name."""
pass
cli.add_command(minimal)
if __name__ == '__main__':
cli()
setup.py:
from setuptools import setup, find_packages
setup(
name='mvp',
version='0.1',
py_modules=['mvp'],
install_requires=[
'Click'
],
packages=find_packages(),
entry_points='''
[console_scripts]
mvp=mvp:cli
''',
)
Note
I am aware that Trogon is currently geared toward Click, but having seen a bit of past discussion on Typer I wanted to raise this as a rough edge stopping me from using Trogon full bore. Thanks for making a great tool, and if I can provide more clarity please let me know!
Whereas Click recommends using click.Choice(["one", "two", ...])
, Typer recommends using something like the following:
from typing_extensions import Annotated
class SomeChoiceType(str, Enum):
ONE = "one"
TWO = "two"
some_app = typer.Typer()
@some_app.command
def some_command(some_option: Annotated[SomeChoiceType] = SomeChoiceType.ONE):
...
This all works great in Typer land, and Trogon will start up okay. But once one navigates to the some_command
command in the Trogon TUI, it currently spits out an exception like the following:
╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
│ /path/to/python3.11/site-packages/textual/widget.py:3325 │
│ in _on_compose │
│ │
│ 3322 │ │
│ 3323 │ async def _on_compose(self) -> None: │
│ 3324 │ │ try: │
│ ❱ 3325 │ │ │ widgets = [*self._nodes, *compose(self)] │
│ 3326 │ │ except TypeError as error: │
│ 3327 │ │ │ raise TypeError( │
│ 3328 │ │ │ │ f"{self!r} compose() method returned an invalid result; {error}" │
│ │
│ /path/to/python3.11/site-packages/trogon/widgets/parameter_controls.py:163 in compose │
│ │
│ 160 │ │ │ │ │ │ for default_value, control_widget in zip( │
│ 161 │ │ │ │ │ │ │ default_value_tuple, widget_group │
│ 162 │ │ │ │ │ │ ): │
│ ❱ 163 │ │ │ │ │ │ │ self._apply_default_value(control_widget, default_value) │
│ 164 │ │ │ │ │ │ │ yield control_widget │
│ 165 │ │ │ │ │ │ │ # Keep track of the first control we render, for easy focus │
│ 166 │ │ │ │ │ │ │ if first_focus_control is None: │
│ │
│ /path/to/python3.11/site-packages/trogon/widgets/parameter_controls.py:247 |
| in _apply_default_value │
│ │
│ 244 │ │ │ control_widget.value = str(default_value) │
│ 245 │ │ │ control_widget.placeholder = f"{default_value} (default)" │
│ 246 │ │ elif isinstance(control_widget, Select): │
│ ❱ 247 │ │ │ control_widget.value = str(default_value) │
│ 248 │ │ │ control_widget.prompt = f"{default_value} (default)" │
│ 249 │ │
│ 250 │ @staticmethod │
│ │
│ /path/to/python3.11/site-packages/textual/widgets/_select.py:387 in _validate_value |
│ │
│ 384 │ │ │ # so we provide a helpful message to catch this mistake in case people didn' │
│ 385 │ │ │ # realise we use a special value to flag "no selection". │
│ 386 │ │ │ help_text = " Did you mean to use Select.clear()?" if value is None else "" │
│ ❱ 387 │ │ │ raise InvalidSelectValueError( │
│ 388 │ │ │ │ f"Illegal select value {value!r}." + help_text │
│ 389 │ │ │ ) │
│ 390 │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
InvalidSelectValueError: Illegal select value 'SomeChoiceType.ONE'.
This appears to occur specifically when one of the enum choices is used as a default. If the Enum is used to restrict a required argument, or an option where the default is e.g. None
, Trogon works as expected.
I understand that one would need to inject a .value
there somewhere to get at the actual string that the Enum is referencing, which I imagine is fully within Trogon's control to be doing.
I haven't looked at the parameter conversion code base, so I'm fully willing to hear that knowing about Enums and doing something special with them is against Trogon's design!
Here is a small reproduction if that is helpful.
All tests seem to have broke after changes to UserOptionData
and OptionSchema
. New required attributes are never set in tests, resulting in errors when trying to run tests (using pytest
):
TypeError: __init__() missing X required positional argument: ...
Do you have any CI in place to avoid such errors in the future?
This module is insanely useful in the case of complex command lines.
One aspect that I think would make it even better is some sort of history support. Not shell history like #12, but actual built-in history within TUI where you can recall commands built in previous runs of the TUI, and either re-edit them or re-run them verbatim.
If editing a command, it would be good to also be able to have the option to either overwrite the previous history entry, or create a new one.
This would offer the ability of recalling oft-used recipes or templates without having to rebuild them from scratch each item. One could even foresee named templates in addition to just historical entries.
I'm trying to run my simple python program with trogon, but it's crashing:
Running Ateams.py build
Traceback (most recent call last):
File "F:\Documents\Projects\C++\Ateams\Ateams.py", line 363, in <module>
ateams()
File "C:\Developing\Python\lib\site-packages\click\core.py", line 1157, in __call__
return self.main(*args, **kwargs)
File "C:\Developing\Python\lib\site-packages\click\core.py", line 1078, in main
rv = self.invoke(ctx)
File "C:\Developing\Python\lib\site-packages\click\core.py", line 1688, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
File "C:\Developing\Python\lib\site-packages\click\core.py", line 1434, in invoke
return ctx.invoke(self.callback, **ctx.params)
File "C:\Developing\Python\lib\site-packages\click\core.py", line 783, in invoke
return __callback(*args, **kwargs)
File "C:\Developing\Python\lib\site-packages\click\decorators.py", line 33, in new_func
return f(get_current_context(), *args, **kwargs)
File "C:\Developing\Python\lib\site-packages\trogon\trogon.py", line 296, in wrapped_tui
Trogon(app, app_name=name, command_name=command, click_context=ctx).run()
File "C:\Developing\Python\lib\site-packages\trogon\trogon.py", line 264, in run
os.execvp(program_name, arguments)
File "C:\Developing\Python\lib\os.py", line 575, in execvp
_execvpe(file, args)
File "C:\Developing\Python\lib\os.py", line 616, in _execvpe
raise saved_exc
File "C:\Developing\Python\lib\os.py", line 608, in _execvpe
exec_func(fullname, *argrest)
OSError: [Errno 8] Exec format error
Togon Version: 0.5.0
Python version: 3.10.12
OS: Windows 10 x64
Thanks
It would be nice to use ctrl+h/j/k/l
to navigate the form, or at least the command-tree.
Given a simple CLI with a custom click.ParamType
:
import click
from trogon import tui
@tui()
@click.group()
def main():
pass
class _TestSuiteCases(click.ParamType):
name = "thingy"
def convert(self, value, param, ctx) -> int:
return 37
@main.command()
@click.argument("input", type=_TestSuiteCases())
def foo(input):
pass
main()
Running bar tui
starts up a TUI successfully, but moving the cursor on top of the foo
subcommand crashes with e.g.:
╭────────────────────────────────────────────────────────────────────────────────────────────────────────────── Traceback (most recent call last) ───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ /Users/julian/.dotfiles/.local/share/virtualenvs/bowtie/lib/python3.10/site-packages/textual/widget.py:3100 in _on_compose │
│ │
│ 3097 │ ╭──────────────────────────────── locals ────────────────────────────────╮ │
│ 3098 │ async def _on_compose(self) -> None: │ self = ParameterControls(id='id_192ff4fe', pseudo_classes={'enabled'}) │ │
│ 3099 │ │ try: ╰────────────────────────────────────────────────────────────────────────╯ │
│ ❱ 3100 │ │ │ widgets = compose(self) │
│ 3101 │ │ except TypeError as error: │
│ 3102 │ │ │ raise TypeError( │
│ 3103 │ │ │ │ f"{self!r} compose() method returned an invalid result; {error}" │
│ │
│ /Users/julian/.dotfiles/.local/share/virtualenvs/bowtie/lib/python3.10/site-packages/textual/_compose.py:26 in compose │
│ │
│ 23 │ app._compose_stacks.append(compose_stack) ╭───────────────────────────────────── locals ──────────────────────────────────────╮ │
│ 24 │ app._composed.append(composed) │ app = Trogon(title='Trogon', classes={'-dark-mode'}) │ │
│ 25 │ try: │ child = Label(classes={'command-form-label'}, pseudo_classes={'enabled'}) │ │
│ ❱ 26 │ │ for child in node.compose(): │ compose_stack = [] │ │
│ 27 │ │ │ if composed: │ composed = [ControlGroupsContainer(pseudo_classes={'enabled'})] │ │
│ 28 │ │ │ │ nodes.extend(composed) │ node = ParameterControls(id='id_192ff4fe', pseudo_classes={'enabled'}) │ │
│ 29 │ │ │ │ composed.clear() │ nodes = [] │ │
│ ╰───────────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ /Users/julian/.dotfiles/.local/share/virtualenvs/bowtie/lib/python3.10/site-packages/trogon/widgets/parameter_controls.py:122 in compose │
│ │
│ 119 │ │ │ │ # We always need to display the original group of controls, │
│ 120 │ │ │ │ # regardless of whether there are defaults │
│ 121 │ │ │ │ if multiple or not default.values: │
│ ❱ 122 │ │ │ │ │ widget_group = list(self.make_widget_group()) │
│ 123 │ │ │ │ │ with ControlGroup() as control_group: │
│ 124 │ │ │ │ │ │ if len(widget_group) == 1: │
│ 125 │ │ │ │ │ │ │ control_group.add_class("single-item") │
│ │
│ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────── locals ──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │
│ │ argument_type = <__main__._TestSuiteCases object at 0x105dbb520> │ │
│ │ default = MultiValueParamData(values=[]) │ │
│ │ first_focus_control = None │ │
│ │ help_text = '' │ │
│ │ is_option = False │ │
│ │ label = <text 'input thingy *required' [Span(5, 12, 'dim'), Span(14, 15, 'bold red')]> │ │
│ │ multiple = False │ │
│ │ name = 'input' │ │
│ │ schema = ArgumentSchema(name='input', type=<__main__._TestSuiteCases object at 0x105dbb520>, required=True, key='id_192ff4fe', default=MultiValueParamData(values=[]), choices=None, multiple=False, nargs=1) │ │
│ │ self = ParameterControls(id='id_192ff4fe', pseudo_classes={'enabled'}) │ │
│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ /Users/julian/.dotfiles/.local/share/virtualenvs/bowtie/lib/python3.10/site-packages/trogon/widgets/parameter_controls.py:172 in make_widget_group │
│ │
│ 169 │ │ # At this point we don't care about filling in the default values. │
│ 170 │ │ for _type in parameter_types: │
│ 171 │ │ │ control_method = self.get_control_method(_type) │
│ ❱ 172 │ │ │ control_widgets = control_method( │
│ 173 │ │ │ │ default, label, multiple, schema, schema.key │
│ 174 │ │ │ ) │
│ 175 │ │ │ yield from control_widgets │
│ │
│ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────── locals ────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │
│ │ _type = <__main__._TestSuiteCases object at 0x105dbb520> │ │
│ │ control_method = None │ │
│ │ default = MultiValueParamData(values=[]) │ │
│ │ is_option = False │ │
│ │ label = <text 'input thingy *required' [Span(5, 12, 'dim'), Span(14, 15, 'bold red')]> │ │
│ │ multiple = False │ │
│ │ name = 'input' │ │
│ │ parameter_type = <__main__._TestSuiteCases object at 0x105dbb520> │ │
│ │ parameter_types = [<__main__._TestSuiteCases object at 0x105dbb520>] │ │
│ │ required = True │ │
│ │ schema = ArgumentSchema(name='input', type=<__main__._TestSuiteCases object at 0x105dbb520>, required=True, key='id_192ff4fe', default=MultiValueParamData(values=[]), choices=None, multiple=False, nargs=1) │ │
│ │ self = ParameterControls(id='id_192ff4fe', pseudo_classes={'enabled'}) │ │
│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
TypeError: 'NoneType' object is not callable
The above exception was the direct cause of the following exception:
╭────────────────────────────────────────────────────────────────────────────────────────────────────────────── Traceback (most recent call last) ───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ /Users/julian/.dotfiles/.local/share/virtualenvs/bowtie/lib/python3.10/site-packages/textual/widget.py:3102 in _on_compose │
│ │
│ 3099 │ │ try: ╭──────────────────────────────── locals ────────────────────────────────╮ │
│ 3100 │ │ │ widgets = compose(self) │ self = ParameterControls(id='id_192ff4fe', pseudo_classes={'enabled'}) │ │
│ 3101 │ │ except TypeError as error: ╰────────────────────────────────────────────────────────────────────────╯ │
│ ❱ 3102 │ │ │ raise TypeError( │
│ 3103 │ │ │ │ f"{self!r} compose() method returned an invalid result; {error}" │
│ 3104 │ │ │ ) from error │
│ 3105 │ │ except Exception: │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
TypeError: ParameterControls(id='id_192ff4fe', pseudo_classes={'enabled'}) compose() method returned an invalid result; 'NoneType' object is not callable
(This looks quite nice though! Well done. Certainly looking forward to integrating it into an app or two.)
So far I've really enjoyed using trogon, it's integrated really smoothly into the couple of click CLIs I'm preparing for deployment on a telescope control module. Something I don't vibe with, though, is forcing the use of groups for the scripts. Something I've just been writing is an acquire
scripts with a bunch of options that I like having the TUI for. But having to rework my CLI to look like acquire int <args>
or something like that to allow the acquire tui
group is something I dislike.
What I think I would prefer is a --tui
flag that has eager execution. I think this could be added to the api in a backwards-compatible way with a keyword argument in the decorator
@tui(eager_option=True)
and that way I can call my script with
acquire --tui
and launch into the TUI, whereas
acquire <args>...
would run the command as normal (no forced group)
An option that is configured with hidden=True
should not appear in the TUI.
Minimal example:
@tui()
@click.command()
@click.option("-f", "--foo", default=False, is_flag=True, hidden=False)
@click.option("-b", "--bar", default=False, is_flag=True, hidden=True)
def cli(foo, bar):
pass
In the above, foo
should appear, but bar
shouldn't.
(By the way, keep up the good work, the fixes in 0.4.0 are awesome!)
I'd like to package trogon for Debian.
To make a Debian package I need a tarball that includes the test suite.
PyPI. has a tarball for trogon version 0.4 without the test suite, GitHub has version 0.3 with the tests.
It would be great if somebody could create a GitHub release for version 0.4 or add the tests to the tarball on PyPI.
import click
from trogon import tui
from typing import Any
@tui()
@click.command()
@click.option('--description', prompt="Description of the job: ", default=f"Description of running func on remotes", help="Write something that describes what this job is about.")
@click.option('--update_repo', prompt="Update repo: ", default=False, help="Update the repo on the remote machine.")
@click.pass_context
def get_choices(ctx: Any, description: str, update_repo: bool):
return ctx
if __name__ == '__main__':
res = get_choices(standalone_mode=False)
print(res)
This is producing this error:
$ python C:\Users\aalsaf01\code\crocodile\myresources\crocodile\cluster\template_tui2.py tui
Running template_tui2.py get-choices
Traceback (most recent call last):
File "C:\Users\aalsaf01\code\crocodile\myresources\crocodile\cluster\template_tui2.py", line 20, in <module>
res = get_choices(standalone_mode=False)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\aalsaf01\venvs\ve\Lib\site-packages\click\core.py", line 1157, in __call__
return self.main(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\aalsaf01\venvs\ve\Lib\site-packages\click\core.py", line 1078, in main
rv = self.invoke(ctx)
^^^^^^^^^^^^^^^^
File "C:\Users\aalsaf01\venvs\ve\Lib\site-packages\click\core.py", line 1688, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\aalsaf01\venvs\ve\Lib\site-packages\click\core.py", line 1434, in invoke
return ctx.invoke(self.callback, **ctx.params)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\aalsaf01\venvs\ve\Lib\site-packages\click\core.py", line 783, in invoke
return __callback(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\aalsaf01\venvs\ve\Lib\site-packages\click\decorators.py", line 33, in new_func
return f(get_current_context(), *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\aalsaf01\venvs\ve\Lib\site-packages\trogon\trogon.py", line 296, in wrapped_tui
Trogon(app, app_name=name, command_name=command, click_context=ctx).run()
File "C:\Users\aalsaf01\venvs\ve\Lib\site-packages\trogon\trogon.py", line 264, in run
os.execvp(program_name, arguments)
File "<frozen os>", line 574, in execvp
File "<frozen os>", line 616, in _execvpe
File "<frozen os>", line 607, in _execvpe
FileNotFoundError: [Errno 2] No such file or directory
```
When you try to use the search feature in a command that has a click.option
without a help
text, you'll get an exception as soon as you type a letter in the search bar:
AttributeError: 'NoneType' object has no attribute 'casefold'
Full traceback:
╭───────────────────────────────────────────────────────────────────── Traceback (most recent call last) ──────────────────────────────────────────────────────────────────────╮
│ /Users/samuel/workspace/tmp/trogon/.venv/lib/python3.11/site-packages/trogon/widgets/form.py:222 in apply_filter │
│ │
│ 219 │ │ all_controls = self.query(ParameterControls) │
│ 220 │ │ for control in all_controls: │
│ 221 │ │ │ filter_query = filter_query.casefold() │
│ ❱ 222 │ │ │ control.apply_filter(filter_query) │
│ 223 │
│ │
│ ╭──────────────────────────────────── locals ────────────────────────────────────╮ │
│ │ all_controls = <DOMQuery query='ParameterControls'> │ │
│ │ control = ParameterControls(id='id_1aad92d0', pseudo_classes={'enabled'}) │ │
│ │ event = Changed() │ │
│ │ filter_query = 'f' │ │
│ │ self = CommandForm(pseudo_classes={'focus-within', 'enabled'}) │ │
│ ╰────────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ /Users/samuel/workspace/tmp/trogon/.venv/lib/python3.11/site-packages/trogon/widgets/parameter_controls.py:88 in apply_filter │
│ │
│ 85 │ │ │ │ │ filter_query in name.casefold() for name in self.schema.name │
│ 86 │ │ │ │ ) │
│ 87 │ │ │ │ help_contains_query = ( │
│ ❱ 88 │ │ │ │ │ filter_query in getattr(self.schema, "help", "").casefold() │
│ 89 │ │ │ │ ) │
│ 90 │ │ │ │ should_be_visible = name_contains_query or help_contains_query │
│ 91 │
│ │
│ ╭─────────────────────────────────────── locals ────────────────────────────────────────╮ │
│ │ filter_query = 'f' │ │
│ │ help_text = None │ │
│ │ name = ['--force', '-f'] │ │
│ │ name_contains_query = True │ │
│ │ self = ParameterControls(id='id_1aad92d0', pseudo_classes={'enabled'}) │ │
│ ╰───────────────────────────────────────────────────────────────────────────────────────╯ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
Here's a minimal reproducible example:
requirements.txt
to install:
click==8.1.3
trogon==0.4.0
cli.py
import click
from trogon import tui
@tui()
@click.command()
@click.option("--force", "-f", is_flag=True)
def something(force):
click.echo(f"{force=:}")
something()
Steps:
python cli.py tui
Adding a help text solves the issue:
- @click.option("--force", "-f", is_flag=True)
+ @click.option("--force", "-f", is_flag=True, help="yay")
A search box (at the top?) which refines the list of options.
The search should match against the help text and the switch.
For inspiration, look at Chrome settings.
Currently, if you have an option that sets a callable default, it will simply show the str
format of it, e.g. if you have:
def my_default():
return "hi"
@click.command()
@click.option('-o', default=my_default)
def command(option): ...
it will show as <function my_default at 0x10f0fb040>
Obviously, it would be nice if there was a better representation thanks (or just no default was shown)
(Sorry if it's not the place to ask)
Is there any plan to support argh ? (https://pypi.org/project/argh/)
Thank you,
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.