Skip to content

Commit 3f01565

Browse files
authored
[WIP] some improvements (#8)
* organizing listing commands a bit * wip * wip * update test * fix shell commands * fix tests * sort scripts * better uninstall * minor changes * wip
1 parent 261333b commit 3f01565

File tree

14 files changed

+351
-189
lines changed

14 files changed

+351
-189
lines changed

tests/integration/test_info.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,10 @@ def test_info_shell_commands():
6666
echo "hello world"
6767
""")
6868
script_name = "tome_echo.sh" if platform.system() != "Windows" else "tome_echo.bat"
69+
command_name = "greetings:echo-sh" if platform.system() != "Windows" else "greetings:echo-bat"
6970
c.save({os.path.join(c.current_folder, "greetings", script_name): script})
7071
c.run("install .")
71-
c.run("info greetings:echo")
72+
c.run(f"info {command_name}")
7273
# TODO: Check more things: runner, script
7374
print(c.out)
7475
assert "name: echo" in c.out

tests/integration/test_install.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -313,15 +313,23 @@ def test_commands_from_script():
313313
echo "hello world"
314314
'''
315315
)
316-
script_name = "tome_echo.sh"
317-
if sys.platform == "win32":
318-
script_name = "tome_echo.bat"
319-
client.save({os.path.join(client.current_folder, "greetings", script_name): script})
316+
317+
client.save({os.path.join(client.current_folder, "greetings", "tome_echo.sh"): script})
318+
client.save({os.path.join(client.current_folder, "greetings", "tome_echo.bat"): script})
319+
320320
client.run("install .")
321321
client.run("list")
322-
assert "greetings:echo" in client.out
323-
client.run("greetings:echo --help")
322+
323+
assert "greetings:echo-bat" in client.out
324+
assert "greetings:echo-sh" in client.out
325+
326+
command_name = "greetings:echo-bat" if sys.platform == "win32" else "greetings:echo-sh"
327+
328+
client.run(f"{command_name} --help")
329+
324330
assert "Command to run the script: " in client.out
331+
332+
script_name = "tome_echo.bat" if sys.platform == "win32" else "tome_echo.sh"
325333
assert script_name in client.out
326334

327335

tests/integration/test_list.py

Lines changed: 116 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
import json
22
import os
33
import textwrap
4+
from time import sleep
45

56
import pytest
67

78
from tome.internal.cache import TomePaths
8-
from tome.internal.utils.files import mkdir
9+
from tome.internal.utils.files import mkdir, rmdir
910

1011
from tests.utils.tools import TestClient
1112

1213

1314
@pytest.fixture
1415
def client():
1516
client = TestClient()
16-
# Editable commands
17-
client.run("new mynamespace:mycommand")
18-
client.run("install . -e")
17+
1918
mkdir(os.path.join(client.current_folder, "greetings"))
19+
mkdir(os.path.join(client.current_folder, "deployments"))
20+
2021
tome_script = textwrap.dedent("""
2122
from tome.command import tome_command
2223
@@ -36,10 +37,7 @@ def bye(tome_api, parser, *args):
3637
print(f"bye: {args.message}")
3738
""")
3839
client.save({os.path.join(client.current_folder, "greetings", "greetings-commands.py"): tome_script})
39-
# Cache commands
40-
tome_scripts_path = TomePaths(client.cache_folder).scripts_path
41-
cache_commands_folder = os.path.join(tome_scripts_path, "all", "deployments")
42-
mkdir(cache_commands_folder)
40+
4341
tome_script = textwrap.dedent("""
4442
from tome.command import tome_command
4543
@@ -58,8 +56,16 @@ def release(tome_api, parser, *args):
5856
args = parser.parse_args(*args)
5957
print(f"Release: {args.message}")
6058
""")
61-
client.save({os.path.join(cache_commands_folder, "deployments-commands.py"): tome_script})
59+
client.save({os.path.join("deployments", "deployments-commands.py"): tome_script})
6260
client.run("install .")
61+
62+
rmdir(os.path.join(client.current_folder, "greetings"))
63+
rmdir(os.path.join(client.current_folder, "deployments"))
64+
65+
# Editable commands
66+
client.run("new mynamespace:mycommand")
67+
client.run("install . -e")
68+
6369
return client
6470

6571

@@ -100,7 +106,7 @@ def test_empty_pattern():
100106
"""
101107
client = TestClient()
102108
client.run("list")
103-
assert "Error: No matches were found for * pattern." in client.out
109+
assert "No matches were found for '*' pattern." in client.out
104110

105111

106112
def test_list_failed_imported():
@@ -144,15 +150,110 @@ def mycommand(tome_api, parser, *args):
144150

145151
def test_formats_json():
146152
client = TestClient()
147-
client.run(f"new mynamespace:mycommand")
153+
client.run("new mynamespace:mycommand")
148154
client.run("install .")
149155
client.run("list --format json")
150156

157+
origin_key = os.path.abspath(client.current_folder)
158+
namespace_key = "mynamespace"
159+
command_key = "mycommand"
160+
151161
expected_output = {
152-
"results": {
153-
"mynamespace": {"mycommand": {"doc": "Description of the command.", "type": "cache", "error": None}}
154-
},
155-
"pattern": "*",
162+
origin_key: {
163+
namespace_key: {command_key: {"doc": "Description of the command.", "type": "cache", "error": None}}
164+
}
156165
}
157166

158167
assert json.loads(client.out) == expected_output
168+
169+
170+
def test_grouped_output():
171+
client = TestClient()
172+
client.run(f"new namespace1:mycommand1")
173+
client.run("install .")
174+
175+
rmdir(os.path.join(client.current_folder, "namespace1"))
176+
177+
with client.chdir(os.path.join(client.current_folder, "editable-commands")):
178+
client.run(f"new namespace2:mycommand-editable")
179+
client.run("install . -e")
180+
181+
git_repo_folder = os.path.join(client.current_folder, "git_repo")
182+
with client.chdir(git_repo_folder):
183+
client.run("new namespace3:mycommand-git")
184+
185+
client.init_git_repo(folder=git_repo_folder)
186+
187+
install_source = f"{os.path.join(client.current_folder, git_repo_folder)}/.git"
188+
client.run(f"install '{install_source}'")
189+
190+
expected = {
191+
os.path.abspath(client.current_folder): {
192+
"namespace1": {"mycommand1": {"doc": "Description of the command.", "type": "cache", "error": None}}
193+
},
194+
os.path.abspath(os.path.join(client.current_folder, "git_repo", ".git")): {
195+
"namespace3": {"mycommand-git": {"doc": "Description of the command.", "type": "cache", "error": None}}
196+
},
197+
os.path.abspath(os.path.join(client.current_folder, "editable-commands")): {
198+
"namespace2": {
199+
"mycommand-editable": {"doc": "Description of the command.", "type": "editable", "error": None}
200+
}
201+
},
202+
}
203+
204+
client.run("list --format=json")
205+
assert json.loads(client.out) == expected
206+
207+
208+
def test_overlapped_commands():
209+
client = TestClient()
210+
211+
# FIXME: right now if command names overlap the first one that was installed will be the one used
212+
# should we error out if commands overlap? should we allow multiple commands with the same name?
213+
# let's wait for the user to ask for this feature
214+
215+
with client.chdir(os.path.join(client.current_folder, "someorigin")):
216+
client.run(f"new namespace:mycommand")
217+
client.run("install .")
218+
219+
sleep(0.1)
220+
with client.chdir(os.path.join(client.current_folder, "anotherorigin")):
221+
client.run(f"new namespace:mycommand")
222+
client.run("install .")
223+
224+
expected = {
225+
os.path.abspath(os.path.join(client.current_folder, "someorigin")): {
226+
"namespace": {"mycommand": {"doc": "Description of the command.", "type": "cache", "error": None}}
227+
}
228+
}
229+
230+
client.run("list --format=json")
231+
assert json.loads(client.out) == expected
232+
233+
sleep(0.1)
234+
with client.chdir(os.path.join(client.current_folder, "yetanotherorigin")):
235+
client.run(f"new namespace:mycommand")
236+
client.run("install .")
237+
238+
client.run("list --format=json")
239+
assert json.loads(client.out) == expected
240+
241+
sleep(0.1)
242+
with client.chdir(os.path.join(client.current_folder, "lastorigin")):
243+
client.run(f"new namespace:mycommand")
244+
client.run("install .")
245+
246+
client.run("list --format=json")
247+
assert json.loads(client.out) == expected
248+
249+
# let's uninstall the first one, then the next should be used
250+
251+
expected = {
252+
os.path.abspath(os.path.join(client.current_folder, "anotherorigin")): {
253+
"namespace": {"mycommand": {"doc": "Description of the command.", "type": "cache", "error": None}}
254+
}
255+
}
256+
257+
client.run(f"uninstall '{os.path.abspath(os.path.join(client.current_folder, 'someorigin'))}'")
258+
client.run("list --format=json")
259+
assert json.loads(client.out) == expected

tests/integration/test_new.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ def test_new_oneliner_script():
3333
client.run("install .")
3434
# testing this way because client.run wont return the things outputed by echo
3535
client.run("list")
36-
assert "mynamespace:mycommand Description of the command." in client.out
37-
client.run("mynamespace:mycommand") # it runs without failing
36+
assert f"mynamespace:mycommand-{script_type} Description of the command." in client.out
37+
client.run(f"mynamespace:mycommand-{script_type}") # it runs without failing
3838

3939

4040
def test_new_oneliner_script_other():
@@ -51,8 +51,8 @@ def test_new_oneliner_script_other():
5151
client.run("list")
5252
assert "otherscript" not in client.out
5353
assert "myshellscript" not in client.out
54-
assert "mynamespace:mycommand Description of the command." in client.out
55-
client.run("mynamespace:mycommand") # it runs without failing
54+
assert f"mynamespace:mycommand-{script_type} Description of the command." in client.out
55+
client.run(f"mynamespace:mycommand-{script_type}") # it runs without failing
5656

5757

5858
def test_new_with_hyphen():

tests/integration/test_run_commands.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def test_not_run_shell_when_help():
1919
script_type = "bat"
2020
client.run(f"new greetings:hello --type={script_type}")
2121
client.run("install .")
22-
client.run("greetings:hello --help")
22+
client.run(f"greetings:hello-{script_type} --help")
2323
# The shell scripts were being executed by just calling to tome command --help
2424
assert "Hello, world!" not in client.out
2525
assert "Description of the command." in client.out
@@ -42,14 +42,17 @@ def test_run_shell_with_args():
4242
echo Second arg: %2
4343
'''
4444
)
45+
4546
if sys.platform == "win32":
4647
script_name = "tome_echo.bat"
4748
client.save({os.path.join(client.current_folder, "greetings", script_name): script_bat})
4849
else:
4950
script_name = "tome_echo.sh"
5051
client.save({os.path.join(client.current_folder, "greetings", script_name): script_sh})
5152

53+
script_type = "bat" if sys.platform == "win32" else "sh"
54+
5255
client.run("install .")
53-
client.run("greetings:echo value1 value2")
56+
client.run(f"greetings:echo-{script_type} value1 value2")
5457
assert "First arg: value1" in str(client.stdout)
5558
assert "Second arg: value2" in str(client.stdout)

tests/integration/test_uninstall.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,11 @@ def test_install_empty_argument():
1111
client = TestClient()
1212
client.run("uninstall", assert_error=True)
1313
assert "Error: No installation source provided." in client.out
14+
15+
16+
def test_uninstall_with_command_name():
17+
client = TestClient()
18+
client.run(f"new namespace:mycommand")
19+
client.run("install .")
20+
client.run("uninstall namespace:mycommand", assert_error=True)
21+
assert "You are trying to uninstall a command 'namespace:mycommand'" in client.out

tome/api/subapi/list.py

Lines changed: 40 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fnmatch
22
import re
3+
from collections import defaultdict
34

45
from tome.command import CommandType
56
from tome.errors import TomeException
@@ -11,57 +12,54 @@ def __init__(self, tome_api):
1112
self.tome_api = tome_api
1213
self.cli = None
1314

14-
# TODO: it'd be great if we could highlight the matches
15-
def filter_cli_commands(self, pattern, include):
15+
def filter_commands(self, pattern, types=None):
1616
"""
17-
Filtering all the available commands, and even help documentation. By default, built-in commands
18-
are excluded.
17+
Filter commands based on a search pattern and allowed command types.
1918
20-
:param pattern: pattern (str-like) to perform a search through the command names, and even the docs.
21-
:param include: list of CommandType values to be included in the final list. If None, all the types
22-
will be included.
19+
:param pattern: The search pattern to filter command names and documentation.
20+
:param types: List CommandType values. If not provided, all command types are considered.
21+
:return: A list of CommandInfo objects that match the search pattern.
2322
"""
2423
from tome.cli import Cli
2524

2625
if not isinstance(self.cli, Cli):
2726
raise TomeException(f"Expected 'Cli' type, got '{type(self.cli).__name__}'")
2827

29-
included = include or list(CommandType)
28+
included_types = types or list(CommandType)
29+
result = []
3030

31-
# Check for exact command match first
32-
if pattern in self.cli.commands and self.cli.commands[pattern].type in included:
33-
namespace, command = pattern.split(":")
34-
return {pattern: self.cli.commands[pattern]}, {namespace: [pattern]}
31+
commands = {
32+
name: command_info
33+
for name, command_info in self.cli.commands.items()
34+
if command_info.type in included_types
35+
}
3536

36-
# If no exact match, proceed with existing filtering logic
37-
regex = re.compile(fnmatch.translate(pattern), flags=re.IGNORECASE) # optimizing the match
38-
filtered_commands = {}
39-
filtered_namespaces = {}
37+
# Exact match: if the pattern exactly matches a command's full name, return it immediately.
38+
if pattern in commands:
39+
return [commands[pattern]]
4040

41-
for namespace, commands in sorted(self.cli.namespaces.items()):
42-
# First search in namespace name, if match all the commands are included
43-
if regex.search(namespace):
44-
matched_commands = commands
41+
regex = re.compile(fnmatch.translate(pattern), flags=re.IGNORECASE)
42+
43+
for command_name, command_info in commands.items():
44+
if regex.search(command_name):
45+
result.append(command_info)
46+
elif command_info.doc and regex.search(command_info.doc):
47+
result.append(command_info)
48+
49+
return result
50+
51+
def group_commands(self, commands_list):
52+
grouped_data = defaultdict(lambda: defaultdict(list))
53+
54+
if not commands_list:
55+
return {}
56+
57+
for command_info in commands_list:
58+
if command_info.type == CommandType.built_in:
59+
source_uri = None
4560
else:
46-
# Second search in command names
47-
matched_commands = [name for name in commands if regex.search(name)]
48-
# Third search in command docstrings
49-
matched_commands += [
50-
name
51-
for name in commands
52-
if self.cli.commands[name].doc and regex.search(self.cli.commands[name].doc)
53-
]
54-
55-
# Filter commands by their type
56-
filtered_commands_in_namespace = [
57-
name
58-
for name in matched_commands
59-
if self.cli.commands.get(name) and self.cli.commands.get(name).type in included
60-
]
61-
62-
if filtered_commands_in_namespace:
63-
filtered_namespaces[namespace] = filtered_commands_in_namespace
64-
for name in filtered_commands_in_namespace:
65-
filtered_commands[name] = self.cli.commands[name]
66-
67-
return filtered_commands, filtered_namespaces
61+
source_uri = command_info.source.uri if command_info.source else command_info.base_folder
62+
63+
grouped_data[source_uri][command_info.namespace].append(command_info)
64+
65+
return grouped_data

0 commit comments

Comments
 (0)