Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion tests/integration/test_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,10 @@ def test_info_shell_commands():
echo "hello world"
""")
script_name = "tome_echo.sh" if platform.system() != "Windows" else "tome_echo.bat"
command_name = "greetings:echo-sh" if platform.system() != "Windows" else "greetings:echo-bat"
c.save({os.path.join(c.current_folder, "greetings", script_name): script})
c.run("install .")
c.run("info greetings:echo")
c.run(f"info {command_name}")
# TODO: Check more things: runner, script
print(c.out)
assert "name: echo" in c.out
20 changes: 14 additions & 6 deletions tests/integration/test_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,15 +313,23 @@ def test_commands_from_script():
echo "hello world"
'''
)
script_name = "tome_echo.sh"
if sys.platform == "win32":
script_name = "tome_echo.bat"
client.save({os.path.join(client.current_folder, "greetings", script_name): script})

client.save({os.path.join(client.current_folder, "greetings", "tome_echo.sh"): script})
client.save({os.path.join(client.current_folder, "greetings", "tome_echo.bat"): script})

client.run("install .")
client.run("list")
assert "greetings:echo" in client.out
client.run("greetings:echo --help")

assert "greetings:echo-bat" in client.out
assert "greetings:echo-sh" in client.out

command_name = "greetings:echo-bat" if sys.platform == "win32" else "greetings:echo-sh"

client.run(f"{command_name} --help")

assert "Command to run the script: " in client.out

script_name = "tome_echo.bat" if sys.platform == "win32" else "tome_echo.sh"
assert script_name in client.out


Expand Down
131 changes: 116 additions & 15 deletions tests/integration/test_list.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import json
import os
import textwrap
from time import sleep

import pytest

from tome.internal.cache import TomePaths
from tome.internal.utils.files import mkdir
from tome.internal.utils.files import mkdir, rmdir

from tests.utils.tools import TestClient


@pytest.fixture
def client():
client = TestClient()
# Editable commands
client.run("new mynamespace:mycommand")
client.run("install . -e")

mkdir(os.path.join(client.current_folder, "greetings"))
mkdir(os.path.join(client.current_folder, "deployments"))

tome_script = textwrap.dedent("""
from tome.command import tome_command

Expand All @@ -36,10 +37,7 @@ def bye(tome_api, parser, *args):
print(f"bye: {args.message}")
""")
client.save({os.path.join(client.current_folder, "greetings", "greetings-commands.py"): tome_script})
# Cache commands
tome_scripts_path = TomePaths(client.cache_folder).scripts_path
cache_commands_folder = os.path.join(tome_scripts_path, "all", "deployments")
mkdir(cache_commands_folder)

tome_script = textwrap.dedent("""
from tome.command import tome_command

Expand All @@ -58,8 +56,16 @@ def release(tome_api, parser, *args):
args = parser.parse_args(*args)
print(f"Release: {args.message}")
""")
client.save({os.path.join(cache_commands_folder, "deployments-commands.py"): tome_script})
client.save({os.path.join("deployments", "deployments-commands.py"): tome_script})
client.run("install .")

rmdir(os.path.join(client.current_folder, "greetings"))
rmdir(os.path.join(client.current_folder, "deployments"))

# Editable commands
client.run("new mynamespace:mycommand")
client.run("install . -e")

return client


Expand Down Expand Up @@ -100,7 +106,7 @@ def test_empty_pattern():
"""
client = TestClient()
client.run("list")
assert "Error: No matches were found for * pattern." in client.out
assert "No matches were found for '*' pattern." in client.out


def test_list_failed_imported():
Expand Down Expand Up @@ -144,15 +150,110 @@ def mycommand(tome_api, parser, *args):

def test_formats_json():
client = TestClient()
client.run(f"new mynamespace:mycommand")
client.run("new mynamespace:mycommand")
client.run("install .")
client.run("list --format json")

origin_key = os.path.abspath(client.current_folder)
namespace_key = "mynamespace"
command_key = "mycommand"

expected_output = {
"results": {
"mynamespace": {"mycommand": {"doc": "Description of the command.", "type": "cache", "error": None}}
},
"pattern": "*",
origin_key: {
namespace_key: {command_key: {"doc": "Description of the command.", "type": "cache", "error": None}}
}
}

assert json.loads(client.out) == expected_output


def test_grouped_output():
client = TestClient()
client.run(f"new namespace1:mycommand1")
client.run("install .")

rmdir(os.path.join(client.current_folder, "namespace1"))

with client.chdir(os.path.join(client.current_folder, "editable-commands")):
client.run(f"new namespace2:mycommand-editable")
client.run("install . -e")

git_repo_folder = os.path.join(client.current_folder, "git_repo")
with client.chdir(git_repo_folder):
client.run("new namespace3:mycommand-git")

client.init_git_repo(folder=git_repo_folder)

install_source = f"{os.path.join(client.current_folder, git_repo_folder)}/.git"
client.run(f"install '{install_source}'")

expected = {
os.path.abspath(client.current_folder): {
"namespace1": {"mycommand1": {"doc": "Description of the command.", "type": "cache", "error": None}}
},
os.path.abspath(os.path.join(client.current_folder, "git_repo", ".git")): {
"namespace3": {"mycommand-git": {"doc": "Description of the command.", "type": "cache", "error": None}}
},
os.path.abspath(os.path.join(client.current_folder, "editable-commands")): {
"namespace2": {
"mycommand-editable": {"doc": "Description of the command.", "type": "editable", "error": None}
}
},
}

client.run("list --format=json")
assert json.loads(client.out) == expected


def test_overlapped_commands():
client = TestClient()

# FIXME: right now if command names overlap the first one that was installed will be the one used
# should we error out if commands overlap? should we allow multiple commands with the same name?
# let's wait for the user to ask for this feature

with client.chdir(os.path.join(client.current_folder, "someorigin")):
client.run(f"new namespace:mycommand")
client.run("install .")

sleep(0.1)
with client.chdir(os.path.join(client.current_folder, "anotherorigin")):
client.run(f"new namespace:mycommand")
client.run("install .")

expected = {
os.path.abspath(os.path.join(client.current_folder, "someorigin")): {
"namespace": {"mycommand": {"doc": "Description of the command.", "type": "cache", "error": None}}
}
}

client.run("list --format=json")
assert json.loads(client.out) == expected

sleep(0.1)
with client.chdir(os.path.join(client.current_folder, "yetanotherorigin")):
client.run(f"new namespace:mycommand")
client.run("install .")

client.run("list --format=json")
assert json.loads(client.out) == expected

sleep(0.1)
with client.chdir(os.path.join(client.current_folder, "lastorigin")):
client.run(f"new namespace:mycommand")
client.run("install .")

client.run("list --format=json")
assert json.loads(client.out) == expected

# let's uninstall the first one, then the next should be used

expected = {
os.path.abspath(os.path.join(client.current_folder, "anotherorigin")): {
"namespace": {"mycommand": {"doc": "Description of the command.", "type": "cache", "error": None}}
}
}

client.run(f"uninstall '{os.path.abspath(os.path.join(client.current_folder, 'someorigin'))}'")
client.run("list --format=json")
assert json.loads(client.out) == expected
8 changes: 4 additions & 4 deletions tests/integration/test_new.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ def test_new_oneliner_script():
client.run("install .")
# testing this way because client.run wont return the things outputed by echo
client.run("list")
assert "mynamespace:mycommand Description of the command." in client.out
client.run("mynamespace:mycommand") # it runs without failing
assert f"mynamespace:mycommand-{script_type} Description of the command." in client.out
client.run(f"mynamespace:mycommand-{script_type}") # it runs without failing


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


def test_new_with_hyphen():
Expand Down
7 changes: 5 additions & 2 deletions tests/integration/test_run_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def test_not_run_shell_when_help():
script_type = "bat"
client.run(f"new greetings:hello --type={script_type}")
client.run("install .")
client.run("greetings:hello --help")
client.run(f"greetings:hello-{script_type} --help")
# The shell scripts were being executed by just calling to tome command --help
assert "Hello, world!" not in client.out
assert "Description of the command." in client.out
Expand All @@ -42,14 +42,17 @@ def test_run_shell_with_args():
echo Second arg: %2
'''
)

if sys.platform == "win32":
script_name = "tome_echo.bat"
client.save({os.path.join(client.current_folder, "greetings", script_name): script_bat})
else:
script_name = "tome_echo.sh"
client.save({os.path.join(client.current_folder, "greetings", script_name): script_sh})

script_type = "bat" if sys.platform == "win32" else "sh"

client.run("install .")
client.run("greetings:echo value1 value2")
client.run(f"greetings:echo-{script_type} value1 value2")
assert "First arg: value1" in str(client.stdout)
assert "Second arg: value2" in str(client.stdout)
8 changes: 8 additions & 0 deletions tests/integration/test_uninstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,11 @@ def test_install_empty_argument():
client = TestClient()
client.run("uninstall", assert_error=True)
assert "Error: No installation source provided." in client.out


def test_uninstall_with_command_name():
client = TestClient()
client.run(f"new namespace:mycommand")
client.run("install .")
client.run("uninstall namespace:mycommand", assert_error=True)
assert "You are trying to uninstall a command 'namespace:mycommand'" in client.out
82 changes: 40 additions & 42 deletions tome/api/subapi/list.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fnmatch
import re
from collections import defaultdict

from tome.command import CommandType
from tome.errors import TomeException
Expand All @@ -11,57 +12,54 @@ def __init__(self, tome_api):
self.tome_api = tome_api
self.cli = None

# TODO: it'd be great if we could highlight the matches
def filter_cli_commands(self, pattern, include):
def filter_commands(self, pattern, types=None):
"""
Filtering all the available commands, and even help documentation. By default, built-in commands
are excluded.
Filter commands based on a search pattern and allowed command types.

:param pattern: pattern (str-like) to perform a search through the command names, and even the docs.
:param include: list of CommandType values to be included in the final list. If None, all the types
will be included.
:param pattern: The search pattern to filter command names and documentation.
:param types: List CommandType values. If not provided, all command types are considered.
:return: A list of CommandInfo objects that match the search pattern.
"""
from tome.cli import Cli

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

included = include or list(CommandType)
included_types = types or list(CommandType)
result = []

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

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

for namespace, commands in sorted(self.cli.namespaces.items()):
# First search in namespace name, if match all the commands are included
if regex.search(namespace):
matched_commands = commands
regex = re.compile(fnmatch.translate(pattern), flags=re.IGNORECASE)

for command_name, command_info in commands.items():
if regex.search(command_name):
result.append(command_info)
elif command_info.doc and regex.search(command_info.doc):
result.append(command_info)

return result

def group_commands(self, commands_list):
grouped_data = defaultdict(lambda: defaultdict(list))

if not commands_list:
return {}

for command_info in commands_list:
if command_info.type == CommandType.built_in:
source_uri = None
else:
# Second search in command names
matched_commands = [name for name in commands if regex.search(name)]
# Third search in command docstrings
matched_commands += [
name
for name in commands
if self.cli.commands[name].doc and regex.search(self.cli.commands[name].doc)
]

# Filter commands by their type
filtered_commands_in_namespace = [
name
for name in matched_commands
if self.cli.commands.get(name) and self.cli.commands.get(name).type in included
]

if filtered_commands_in_namespace:
filtered_namespaces[namespace] = filtered_commands_in_namespace
for name in filtered_commands_in_namespace:
filtered_commands[name] = self.cli.commands[name]

return filtered_commands, filtered_namespaces
source_uri = command_info.source.uri if command_info.source else command_info.base_folder

grouped_data[source_uri][command_info.namespace].append(command_info)

return grouped_data
Loading
Loading