tgzr.cli 0.1.2__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.
- tgzr/cli/__init__.py +0 -0
- tgzr/cli/__main__.py +7 -0
- tgzr/cli/_version.py +34 -0
- tgzr/cli/add_plugins.py +109 -0
- tgzr/cli/install.py +147 -0
- tgzr/cli/install_cli.py +143 -0
- tgzr/cli/main.py +7 -0
- tgzr/cli/main_cli.py +31 -0
- tgzr/cli/utils.py +99 -0
- tgzr_cli-0.1.2.dist-info/METADATA +168 -0
- tgzr_cli-0.1.2.dist-info/RECORD +14 -0
- tgzr_cli-0.1.2.dist-info/WHEEL +4 -0
- tgzr_cli-0.1.2.dist-info/entry_points.txt +2 -0
- tgzr_cli-0.1.2.dist-info/licenses/LICENSE +674 -0
tgzr/cli/__init__.py
ADDED
|
File without changes
|
tgzr/cli/__main__.py
ADDED
tgzr/cli/_version.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '0.1.2'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 2)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
tgzr/cli/add_plugins.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import traceback
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def add_plugins(group):
|
|
9
|
+
|
|
10
|
+
if not isinstance(group, click.Group):
|
|
11
|
+
raise TypeError(
|
|
12
|
+
f"plugins can only be attached to an instance of"
|
|
13
|
+
f" 'click.Group()' not: {repr(group)}"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
entry_point_group = "tgzr.cli.plugin"
|
|
17
|
+
|
|
18
|
+
all_entry_points = importlib.metadata.entry_points(group=entry_point_group)
|
|
19
|
+
|
|
20
|
+
for ep in all_entry_points:
|
|
21
|
+
try:
|
|
22
|
+
installer = ep.load()
|
|
23
|
+
installer(group)
|
|
24
|
+
# group.add_command(ep.load())
|
|
25
|
+
|
|
26
|
+
# Catch all exceptions (technically not 'BaseException') and
|
|
27
|
+
# instead register a special 'BrokenCommand()'. Otherwise, a single
|
|
28
|
+
# plugin that fails to load and/or register will make the CLI
|
|
29
|
+
# inoperable. 'BrokenCommand()' explains the situation to users.
|
|
30
|
+
except Exception as e:
|
|
31
|
+
group.add_command(BrokenCommand(ep, e))
|
|
32
|
+
|
|
33
|
+
return group
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class BrokenCommand(click.Command):
|
|
37
|
+
"""Represents a plugin ``click.Command()`` that failed to load.
|
|
38
|
+
|
|
39
|
+
Can be executed just like a ``click.Command()``, but prints information
|
|
40
|
+
for debugging and exits with an error code.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, entry_point, exception):
|
|
44
|
+
"""
|
|
45
|
+
:param importlib.metadata.EntryPoint entry_point:
|
|
46
|
+
Entry point that failed to load.
|
|
47
|
+
:param Exception exception:
|
|
48
|
+
Raised when attempting to load the entry point associated with
|
|
49
|
+
this instance.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
super().__init__(entry_point.name)
|
|
53
|
+
|
|
54
|
+
# There are several ways to get a traceback from an exception, but
|
|
55
|
+
# 'TracebackException()' seems to be the most portable across actively
|
|
56
|
+
# supported versions of Python.
|
|
57
|
+
tbe = traceback.TracebackException.from_exception(exception)
|
|
58
|
+
|
|
59
|
+
# A message for '$ cli command --help'. Contains full traceback and a
|
|
60
|
+
# helpful note. The intention is to nudge users to figure out which
|
|
61
|
+
# project should get a bug report since users are likely to report the
|
|
62
|
+
# issue to the developers of the CLI utility they are directly
|
|
63
|
+
# interacting with. These are not necessarily the right developers.
|
|
64
|
+
self.help = (
|
|
65
|
+
"{ls}ERROR: entry point '{module}:{name}' could not be loaded."
|
|
66
|
+
" Contact its author for help.{ls}{ls}{tb}"
|
|
67
|
+
).format(
|
|
68
|
+
module=entry_point.module,
|
|
69
|
+
name=entry_point.name,
|
|
70
|
+
ls=os.linesep,
|
|
71
|
+
tb="".join(tbe.format()),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Replace the broken command's summary with a warning about how it
|
|
75
|
+
# was not loaded successfully. The idea is that '$ cli --help' should
|
|
76
|
+
# include a clear indicator that a subcommand is not functional, and
|
|
77
|
+
# a little hint for what to do about it. U+2020 is a "dagger", whose
|
|
78
|
+
# modern use typically indicates a footnote.
|
|
79
|
+
self.short_help = (
|
|
80
|
+
f"\u2020 Warning: could not load plugin. Invoke command with"
|
|
81
|
+
f" '--help' for traceback."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def invoke(self, ctx):
|
|
85
|
+
"""Print traceback and debugging message.
|
|
86
|
+
|
|
87
|
+
:param click.Context ctx:
|
|
88
|
+
Active context.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
click.echo(self.help, color=ctx.color, err=True)
|
|
92
|
+
ctx.exit(1)
|
|
93
|
+
|
|
94
|
+
def parse_args(self, ctx, args):
|
|
95
|
+
"""Pass arguments along without parsing.
|
|
96
|
+
|
|
97
|
+
:param click.Context ctx:
|
|
98
|
+
Active context.
|
|
99
|
+
:param list args:
|
|
100
|
+
List of command line arguments.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
# Do not attempt to parse these arguments. We do not know why the
|
|
104
|
+
# entry point failed to load, but it is reasonable to assume that
|
|
105
|
+
# argument parsing will not work. Ultimately the goal is to get the
|
|
106
|
+
# 'Command.invoke()' method (overloaded in this class) to execute
|
|
107
|
+
# and provide the user with a bit of debugging information.
|
|
108
|
+
|
|
109
|
+
return args
|
tgzr/cli/install.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from typing import Callable
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import tempfile
|
|
7
|
+
import platform
|
|
8
|
+
|
|
9
|
+
import uv
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def install(
|
|
13
|
+
home: Path | str,
|
|
14
|
+
studio_name: str,
|
|
15
|
+
python_version: str | None = "3.12",
|
|
16
|
+
default_index: str | None = None,
|
|
17
|
+
find_links: str | None = None,
|
|
18
|
+
allow_prerelease: bool = False,
|
|
19
|
+
echo: Callable[[str], None] | None = None,
|
|
20
|
+
):
|
|
21
|
+
"""
|
|
22
|
+
Install TGZR by creating a tmp venv with tgzr.shell, then
|
|
23
|
+
using this tgzr.cli to create a Studio at the requested location.
|
|
24
|
+
|
|
25
|
+
May raise: FileExistsError, ChildProcessError.
|
|
26
|
+
"""
|
|
27
|
+
if echo is None:
|
|
28
|
+
echo = lambda message: print(message)
|
|
29
|
+
|
|
30
|
+
home = Path(home)
|
|
31
|
+
studio_path = home / "Workspace" / studio_name
|
|
32
|
+
if studio_path.exists():
|
|
33
|
+
raise FileExistsError(f"The Studio {studio_path} already exists. Aborting.")
|
|
34
|
+
|
|
35
|
+
echo(f"Installing tgzr at {home}, creating Studio {studio_name}")
|
|
36
|
+
|
|
37
|
+
# We need to create the `.tgzr` file at requested home first, or
|
|
38
|
+
# the cli may discover existing locations and create the Studio
|
|
39
|
+
# in an existing installation:
|
|
40
|
+
home.mkdir(exist_ok=True, parents=True)
|
|
41
|
+
(home / ".tgzr").touch()
|
|
42
|
+
|
|
43
|
+
venv_path = tempfile.mkdtemp(prefix="tgzr_install_tmp_venv")
|
|
44
|
+
echo(f"Creating temp venv: {venv_path}")
|
|
45
|
+
|
|
46
|
+
# Clean up PATH
|
|
47
|
+
# (we've seen situations where things in the PATH would mess up the installation )
|
|
48
|
+
PATH = os.environ.get("PATH", "")
|
|
49
|
+
path = PATH.split(os.pathsep)
|
|
50
|
+
banned_words = ["python", ".poetry"]
|
|
51
|
+
clean_path = []
|
|
52
|
+
for i in path:
|
|
53
|
+
keep = True
|
|
54
|
+
for word in banned_words:
|
|
55
|
+
if word in i.lower():
|
|
56
|
+
keep = False
|
|
57
|
+
break
|
|
58
|
+
if keep:
|
|
59
|
+
clean_path.append(i)
|
|
60
|
+
os.environ["PATH"] = os.pathsep.join(clean_path)
|
|
61
|
+
|
|
62
|
+
if 0:
|
|
63
|
+
# This does not work as pyinstaller script: sys.executable is wrong so venv is messed up
|
|
64
|
+
print(sys.executable)
|
|
65
|
+
cmd = f"{sys.executable} -m uv venv --prompt TGZR_installer {venv_path}"
|
|
66
|
+
echo(f"EXEC: {cmd}")
|
|
67
|
+
ret = os.system(cmd)
|
|
68
|
+
print("--->", ret)
|
|
69
|
+
elif 0:
|
|
70
|
+
# This does not work as pyinstaller script: sys.executable is wrong so venv is messed up
|
|
71
|
+
import venv
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
venv.main(["--without-pip", "--prompt", "TGZR-Installer", venv_path])
|
|
75
|
+
except Exception:
|
|
76
|
+
raise
|
|
77
|
+
else:
|
|
78
|
+
# This does work as pyinstaller script: we are delegating everything to uv
|
|
79
|
+
|
|
80
|
+
# Use this to inspect the content of the pyinstaller archive when
|
|
81
|
+
# we run as a pysintaller script:
|
|
82
|
+
# if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
|
|
83
|
+
# ROOT = sys._MEIPASS
|
|
84
|
+
# print("FROZEN CONTENT:", os.listdir(ROOT))
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
# NOTE: this also works when we're a pysintaller script thanks to the
|
|
88
|
+
# data arg of the Analysis in the pyinstall spec file:
|
|
89
|
+
# it keeps the uv executable installed in your current venv
|
|
90
|
+
# (it exists because we have uv in the project requirement)
|
|
91
|
+
# and place it in a "bin" folder inside the pysintaller archive.
|
|
92
|
+
# This bin folder is looked up by uv.find_uv_bin() so we're
|
|
93
|
+
# good.
|
|
94
|
+
uv_exe = uv.find_uv_bin()
|
|
95
|
+
except Exception as err:
|
|
96
|
+
# This should not occur ¯\\_(ツ)_/¯
|
|
97
|
+
echo(f"Oops, could not find uv: {err}")
|
|
98
|
+
else:
|
|
99
|
+
cmd = (
|
|
100
|
+
f"{uv_exe} venv -p {python_version} --prompt TGZR-Installer {venv_path}"
|
|
101
|
+
)
|
|
102
|
+
echo(f"EXEC: {cmd}")
|
|
103
|
+
ret = os.system(cmd)
|
|
104
|
+
if ret:
|
|
105
|
+
raise Exception("Error creating venv with cmd: {cmd}")
|
|
106
|
+
|
|
107
|
+
default_index_options = ""
|
|
108
|
+
if default_index:
|
|
109
|
+
default_index_options = f"--default-index {default_index}"
|
|
110
|
+
|
|
111
|
+
find_links_options = ""
|
|
112
|
+
if find_links:
|
|
113
|
+
find_links_options = f"--find-links {find_links}"
|
|
114
|
+
|
|
115
|
+
prerelease_options = ""
|
|
116
|
+
if allow_prerelease:
|
|
117
|
+
prerelease_options = "--prerelease=allow"
|
|
118
|
+
|
|
119
|
+
# Install tgzr.shell in the temp venv:
|
|
120
|
+
cmd = f"{uv_exe} pip install {default_index_options} {find_links_options} {prerelease_options} --python {venv_path} tgzr.shell"
|
|
121
|
+
echo(f"EXEC: {cmd}")
|
|
122
|
+
ret = os.system(cmd)
|
|
123
|
+
if ret:
|
|
124
|
+
raise ChildProcessError("Error installing packages in venv with cmd: {cmd}")
|
|
125
|
+
|
|
126
|
+
# Use tgzr.cli from the temp venv to create the Studio
|
|
127
|
+
# ! Don't forget to pass the index related options there !
|
|
128
|
+
index_options = ""
|
|
129
|
+
if default_index:
|
|
130
|
+
index_options += f" --default-index {default_index}"
|
|
131
|
+
if find_links:
|
|
132
|
+
index_options += f" --find-links {find_links}"
|
|
133
|
+
if allow_prerelease:
|
|
134
|
+
index_options += " --allow-prerelease"
|
|
135
|
+
if 0:
|
|
136
|
+
# IDKW but this does not work, it runs the orignal tgzr cmd instead of the venv one :[
|
|
137
|
+
cmd = f"{uv_exe} run --python {venv_path} tgzr --home {home} studio create {index_options} {studio_name}"
|
|
138
|
+
else:
|
|
139
|
+
if platform.system() == "Windows":
|
|
140
|
+
tgzr_exe = f"{venv_path}/Scripts/tgzr"
|
|
141
|
+
else:
|
|
142
|
+
tgzr_exe = f"{venv_path}/bin/tgzr"
|
|
143
|
+
cmd = f"{tgzr_exe} --home {home} studio create {index_options} {studio_name}"
|
|
144
|
+
ret = os.system(cmd)
|
|
145
|
+
echo(f"EXEC: {cmd}")
|
|
146
|
+
if ret:
|
|
147
|
+
raise ChildProcessError(f"Error creating Studio with cmd: {cmd}")
|
tgzr/cli/install_cli.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from ._version import __version__
|
|
6
|
+
from .install import install
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.command("install")
|
|
10
|
+
@click.option(
|
|
11
|
+
"-S",
|
|
12
|
+
"--studio",
|
|
13
|
+
default=None,
|
|
14
|
+
help="Name of the Studio to create.",
|
|
15
|
+
)
|
|
16
|
+
@click.option(
|
|
17
|
+
"-H",
|
|
18
|
+
"--home",
|
|
19
|
+
help="Path to install to. Default to current dir. Created if needed.",
|
|
20
|
+
)
|
|
21
|
+
@click.option(
|
|
22
|
+
"-p",
|
|
23
|
+
"--python-version",
|
|
24
|
+
default="3.12",
|
|
25
|
+
help="The python version to use. Defaults to 3.12 which in the minmum (3.14 is know to cause issue with nicegui on windows)",
|
|
26
|
+
)
|
|
27
|
+
@click.option(
|
|
28
|
+
"--default-index",
|
|
29
|
+
help="The URL of the default package index (by default: <https://pypi.org/simple>).",
|
|
30
|
+
)
|
|
31
|
+
@click.option(
|
|
32
|
+
"-f",
|
|
33
|
+
"--find-links",
|
|
34
|
+
help="path a folder containing packages to install. Usefull in no-internet situations.",
|
|
35
|
+
)
|
|
36
|
+
@click.option(
|
|
37
|
+
"--allow-prerelease",
|
|
38
|
+
is_flag=True,
|
|
39
|
+
help="Allow installing tgzr using pre-release packages. Default is False.",
|
|
40
|
+
)
|
|
41
|
+
@click.option(
|
|
42
|
+
"-i",
|
|
43
|
+
"--info",
|
|
44
|
+
is_flag=True,
|
|
45
|
+
help="Show info about this installer.",
|
|
46
|
+
)
|
|
47
|
+
def install_cmd(
|
|
48
|
+
studio: str | None,
|
|
49
|
+
home: str | None,
|
|
50
|
+
python_version,
|
|
51
|
+
default_index,
|
|
52
|
+
find_links,
|
|
53
|
+
allow_prerelease,
|
|
54
|
+
info,
|
|
55
|
+
):
|
|
56
|
+
"""
|
|
57
|
+
Install a new tgzr environment.
|
|
58
|
+
"""
|
|
59
|
+
if info:
|
|
60
|
+
import tgzr.cli
|
|
61
|
+
import sys
|
|
62
|
+
import uv
|
|
63
|
+
|
|
64
|
+
click.echo("TGZR Installer:")
|
|
65
|
+
click.echo(f"{tgzr.cli._version.__version__=}")
|
|
66
|
+
click.echo(f"{sys.version=}")
|
|
67
|
+
click.echo(f"{sys.executable=}")
|
|
68
|
+
click.echo(f"{uv.find_uv_bin()=}")
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
if studio is None:
|
|
72
|
+
studio = str(click.prompt(f"Studio name", default="MyStudio"))
|
|
73
|
+
studio = studio.strip().replace(" ", "_") # overall better + avoids code injection
|
|
74
|
+
|
|
75
|
+
if home is None:
|
|
76
|
+
cwd = Path.cwd().resolve()
|
|
77
|
+
home = str(
|
|
78
|
+
click.prompt(f"Install path (can be relative to current dir)", default=cwd)
|
|
79
|
+
)
|
|
80
|
+
home = home.strip()
|
|
81
|
+
if not Path(home).is_absolute():
|
|
82
|
+
home = str((cwd / home).resolve())
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
install(
|
|
86
|
+
home,
|
|
87
|
+
studio,
|
|
88
|
+
python_version,
|
|
89
|
+
default_index,
|
|
90
|
+
find_links,
|
|
91
|
+
allow_prerelease,
|
|
92
|
+
click.echo,
|
|
93
|
+
)
|
|
94
|
+
except (FileExistsError, ChildProcessError) as err:
|
|
95
|
+
click.echo(f"\nInstallation failed: {err}\nPlease contact your adminitrator.")
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
# Some shell won't allow unicodes :/ so:
|
|
99
|
+
try:
|
|
100
|
+
click.echo("\n\n✨ tgzr successfully installed ✨")
|
|
101
|
+
except:
|
|
102
|
+
click.echo("\n\n tgzr successfully installed!")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@click.command("install")
|
|
106
|
+
def install_help():
|
|
107
|
+
click.echo("# Installing TGZR with `tgzr install`")
|
|
108
|
+
click.echo(
|
|
109
|
+
"""
|
|
110
|
+
TGZR installs itself in a folder called "home".
|
|
111
|
+
This folder will contain:
|
|
112
|
+
- System : a folder with technical bits managed by TGZR.
|
|
113
|
+
- Workspace: a folder for everything work related.
|
|
114
|
+
- `.tgzr` : a configuration file for the installation.
|
|
115
|
+
|
|
116
|
+
Everything you will do with TGZR will be related to a Studio.
|
|
117
|
+
Your Studios are located in the Workspace folder.
|
|
118
|
+
|
|
119
|
+
When installing TGZR, you need to know:
|
|
120
|
+
- Where you want your "home" to be
|
|
121
|
+
- The name of you studio
|
|
122
|
+
|
|
123
|
+
The easiest way to install TGZR with the command line
|
|
124
|
+
is to go into the folder you chose as "home" and enter:
|
|
125
|
+
`tgzr install`
|
|
126
|
+
You will be prompter for a Studio name, and you will
|
|
127
|
+
be prompter for an install path with a default value of the
|
|
128
|
+
current folder.
|
|
129
|
+
|
|
130
|
+
You can also specify the home path and the studio name
|
|
131
|
+
with arguments:
|
|
132
|
+
`tgzr install --home /path/to/tgzr/home --studio MyStudioName`
|
|
133
|
+
|
|
134
|
+
# Advanced
|
|
135
|
+
|
|
136
|
+
During installation, tgzr will fetch packages from PyPI.
|
|
137
|
+
If you need to use custom packages instead of official ones,
|
|
138
|
+
you can override the default package index with
|
|
139
|
+
`--default-index` and/or specify an local folder containing
|
|
140
|
+
packages with `--find-links`.
|
|
141
|
+
|
|
142
|
+
"""
|
|
143
|
+
)
|
tgzr/cli/main.py
ADDED
tgzr/cli/main_cli.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from ._version import __version__
|
|
5
|
+
from .add_plugins import add_plugins
|
|
6
|
+
from .install_cli import install_cmd, install_help
|
|
7
|
+
|
|
8
|
+
from .utils import TGZRCliGroup
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.group(
|
|
12
|
+
cls=TGZRCliGroup,
|
|
13
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
14
|
+
)
|
|
15
|
+
@click.version_option(version=__version__, prog_name="tgzr")
|
|
16
|
+
def tgzr_cli(
|
|
17
|
+
**kwargs, # needed for plugin installing custom options
|
|
18
|
+
):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@tgzr_cli.group(cls=TGZRCliGroup, help="Documentations and tooltips.")
|
|
23
|
+
def help():
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
tgzr_cli.add_command(install_cmd)
|
|
28
|
+
help.add_command(install_help)
|
|
29
|
+
tgzr_cli.set_default_command(install_cmd)
|
|
30
|
+
|
|
31
|
+
add_plugins(tgzr_cli)
|
tgzr/cli/utils.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TGZRCliGroup(click.Group):
|
|
7
|
+
"""
|
|
8
|
+
This Group recognizes commands with less
|
|
9
|
+
than their full name when there is no ambiguity.
|
|
10
|
+
For example: 'wo'->'workspace' if no other command
|
|
11
|
+
starts with 'wo'
|
|
12
|
+
|
|
13
|
+
This Group can be configured with a default command.
|
|
14
|
+
This command will be invoked if no command is provided.
|
|
15
|
+
|
|
16
|
+
This Group can find a sub-group for you with
|
|
17
|
+
`find_group(group_name)`, which may be used by cli plugins
|
|
18
|
+
to install their commands and groups where they want
|
|
19
|
+
to.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
23
|
+
kwargs["result_callback"] = self.__result_callback
|
|
24
|
+
super().__init__(*args, **kwargs)
|
|
25
|
+
self.no_args_is_help = True
|
|
26
|
+
self.invoke_without_command = False
|
|
27
|
+
self._looked_up_cmds: list[str] = []
|
|
28
|
+
|
|
29
|
+
self._default_command: click.Command | None = None
|
|
30
|
+
self._default_command_kwargs: dict | None = None
|
|
31
|
+
self._default_command_setter_module: str | None = None
|
|
32
|
+
|
|
33
|
+
def set_default_command(self, cmd: click.Command | None, **kwargs) -> None:
|
|
34
|
+
frame: inspect.FrameInfo = inspect.stack()[1]
|
|
35
|
+
setter_module = inspect.getmodule(frame[0])
|
|
36
|
+
self._default_command_setter_module = (
|
|
37
|
+
setter_module and setter_module.__name__ or None
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if cmd is None:
|
|
41
|
+
self.no_args_is_help = True
|
|
42
|
+
self.invoke_without_command = False
|
|
43
|
+
self._default_command = None
|
|
44
|
+
self._default_command_kwargs = None
|
|
45
|
+
return
|
|
46
|
+
else:
|
|
47
|
+
self.no_args_is_help = False
|
|
48
|
+
self.invoke_without_command = True
|
|
49
|
+
self._default_command = cmd
|
|
50
|
+
self._default_command_kwargs = kwargs
|
|
51
|
+
|
|
52
|
+
def get_default_command(
|
|
53
|
+
self,
|
|
54
|
+
) -> tuple[click.Command | None, dict | None, str | None]:
|
|
55
|
+
return (
|
|
56
|
+
self._default_command,
|
|
57
|
+
self._default_command_kwargs,
|
|
58
|
+
self._default_command_setter_module,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:
|
|
62
|
+
self._looked_up_cmds.append(cmd_name)
|
|
63
|
+
known_commands = self.list_commands(ctx)
|
|
64
|
+
if cmd_name not in known_commands:
|
|
65
|
+
found = [name for name in known_commands if name.startswith(cmd_name)]
|
|
66
|
+
if len(found) > 1:
|
|
67
|
+
raise click.UsageError(
|
|
68
|
+
f'Ambiuous command "{cmd_name}" (could be {' or '.join(found)}).'
|
|
69
|
+
)
|
|
70
|
+
elif found:
|
|
71
|
+
cmd_name = found[0]
|
|
72
|
+
|
|
73
|
+
return super().get_command(ctx, cmd_name)
|
|
74
|
+
|
|
75
|
+
def find_group(self, named: str) -> click.Group | None:
|
|
76
|
+
"""
|
|
77
|
+
Find a click group under this group.
|
|
78
|
+
Usefull to install plugin under sub commands.
|
|
79
|
+
"""
|
|
80
|
+
for name, value in self.commands.items():
|
|
81
|
+
if not isinstance(value, click.Group):
|
|
82
|
+
continue
|
|
83
|
+
if name == named:
|
|
84
|
+
return value
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
@click.pass_context
|
|
88
|
+
@staticmethod
|
|
89
|
+
def __result_callback(ctx: click.Context, self, *result, **kwargs):
|
|
90
|
+
# See that "self" arg on a staticmethod ? Don't ask!!!! :p
|
|
91
|
+
|
|
92
|
+
if self._looked_up_cmds:
|
|
93
|
+
# Only invoke default command when no other command
|
|
94
|
+
# was specified. We use self._looked_up_cmds to
|
|
95
|
+
# find out if a command was given.
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
if self._default_command is not None:
|
|
99
|
+
ctx.invoke(self._default_command, **self._default_command_kwargs)
|