diff --git a/http_prompt/completer.py b/http_prompt/completer.py index 8e97ae2..8c150f3 100644 --- a/http_prompt/completer.py +++ b/http_prompt/completer.py @@ -29,7 +29,7 @@ # '/foo/bar' => ('/foo/bar', 'bar') # '/foo/bar/' => ('/foo/bar/', '') # 'foo/bar' => ('foo/bar', 'bar') - (r'(ls|cd)\s+(/?(?:[^/]+/)*([^/]*)/?)$', 'urlpaths'), + (r'(ls|cd|tree)\s+(/?(?:[^/]+/)*([^/]*)/?)$', 'urlpaths'), (r'^\s*[^\s]*$', 'root_commands') ] diff --git a/http_prompt/completion.py b/http_prompt/completion.py index 598db30..ecba090 100644 --- a/http_prompt/completion.py +++ b/http_prompt/completion.py @@ -24,6 +24,7 @@ ('rm -q', 'Remove querystring parameter'), ('rm -q *', 'Remove all querystring parameters'), ('source', 'Load environment from a file'), + ('tree', 'Show tree structure'), ]) ACTIONS = OrderedDict([ diff --git a/http_prompt/execution.py b/http_prompt/execution.py index 9a66d7a..671aa5e 100644 --- a/http_prompt/execution.py +++ b/http_prompt/execution.py @@ -35,7 +35,8 @@ command = mutation / immutation mutation = concat_mut+ / nonconcat_mut - immutation = preview / action / ls / env / help / exit / exec / source / clear / _ + immutation = preview / action / ls / env / help / + exit / exec / source / clear / tree /_ concat_mut = option_mut / full_quoted_mut / value_quoted_mut / unquoted_mut nonconcat_mut = cd / rm @@ -49,6 +50,7 @@ help = _ "help" _ exit = _ "exit" _ ls = _ "ls" _ (urlpath _)? (redir_out)? + tree = _ "tree" _ (urlpath _)? (redir_out)? env = _ "env" _ (redir_out)? source = _ "source" _ filepath _ exec = _ "exec" _ filepath _ @@ -352,6 +354,55 @@ def visit_ls(self, node, children): self.output.write('\n'.join(lines)) return node + def _formattreename(self, node, color=False): + if color: + col = Name + if node.data.get("type") == "dir": + col = String + return self._colorize(node.name, col) + else: + return node.name + + def _fetchtreenode(self, node, children, filtertypes=[], prepend="", + colorize=True): + ret = [] + ppsym = " " + + cnodes = sorted(sorted([c for c in node.children if c.data.get("type") + not in filtertypes], key=lambda x: x.name), + key=lambda y: y.data.get("type"), reverse=True) + + for i, c in enumerate(cnodes): + if i == len(cnodes)-1: + ppsym = "└── " + postsym = " " + else: + ppsym = "├── " + postsym = "│ " + ret.append(prepend + ppsym + self._formattreename(c, colorize)) + ret += self._fetchtreenode(c, + children, + filtertypes, + prepend + postsym, + colorize) + + return ret + + def visit_tree(self, node, children): + path = urlparse(self.context_override.url).path + path = filter(None, path.split('/')) + topnode = self.context.root.findtopnode(*path) + lines = [] + lines.append(self._formattreename(topnode, self.output.isatty())) + lines += self._fetchtreenode(topnode, + children, + ["file"], + "", + self.output.isatty()) + if lines: + self.output.write('\n'.join(lines)) + return node + def visit_env(self, node, children): text = format_to_http_prompt(self.context) self.output.write(text) diff --git a/http_prompt/tree.py b/http_prompt/tree.py index 345541f..2bf3706 100644 --- a/http_prompt/tree.py +++ b/http_prompt/tree.py @@ -57,7 +57,7 @@ def find_child(self, name, wildcard=True): return None - def ls(self, *path): + def findtopnode(self, *path): success = True cur = self for name in path: @@ -74,5 +74,11 @@ def ls(self, *path): success = False break if success: - for node in sorted(cur.children): - yield node + return cur + + def ls(self, *path): + topnode = self.findtopnode(*path) + if not topnode: + return + for node in sorted(topnode.children): + yield node diff --git a/tests/context/test_context.py b/tests/context/test_context.py index 7c46f43..8d33389 100644 --- a/tests/context/test_context.py +++ b/tests/context/test_context.py @@ -149,13 +149,19 @@ def test_override(): orgs_methods = list(sorted(list(root_children)[0].children)) # path parameters are used even if no method parameter assert len(orgs_methods) == 2 - assert next(filter(lambda i:i.name == 'username', orgs_methods), None) is not None - assert next(filter(lambda i:i.name == 'Accept', orgs_methods), None) is not None + assert next(filter(lambda i: i.name == 'username', orgs_methods), None)\ + is not None + assert next(filter(lambda i: i.name == 'Accept', orgs_methods), None)\ + is not None users_methods = list(sorted(list(root_children)[1].children)) # path and methods parameters are merged assert len(users_methods) == 4 - assert next(filter(lambda i:i.name == 'username', users_methods), None) is not None - assert next(filter(lambda i:i.name == 'custom1', users_methods), None) is not None - assert next(filter(lambda i:i.name == 'custom2', users_methods), None) is not None - assert next(filter(lambda i:i.name == 'Accept', users_methods), None) is not None + assert next(filter(lambda i: i.name == 'username', users_methods), None)\ + is not None + assert next(filter(lambda i: i.name == 'custom1', users_methods), None)\ + is not None + assert next(filter(lambda i: i.name == 'custom2', users_methods), None)\ + is not None + assert next(filter(lambda i: i.name == 'Accept', users_methods), None)\ + is not None diff --git a/tests/test_cli.py b/tests/test_cli.py index 73e7ef4..10d2517 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -186,7 +186,9 @@ def test_spec_basePath(self): self.assertEqual(lv3_names, set(['users', 'orgs'])) def test_spec_from_http(self): - spec_url = 'https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json' + spec_url = 'https://raw.githubusercontent.com/github/'\ + 'rest-api-description/main/descriptions/api.github.com/'\ + 'api.github.com.json' result, context = run_and_exit(['https://api.github.com', '--spec', spec_url]) self.assertEqual(result.exit_code, 0) diff --git a/tests/test_execution.py b/tests/test_execution.py index ecb2063..610229e 100644 --- a/tests/test_execution.py +++ b/tests/test_execution.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- import hashlib -import io import json import shutil import os @@ -767,6 +766,81 @@ def test_reset(self): self.assertFalse(self.context.body_json_params) +class TestExecution_tree(ExecutionTestCase): + + def test_root(self): + execute('tree', self.context) + self.assert_stdout("root\n" + + "├── orgs\n" + + "│ └── {org}\n" + + "│ ├── events\n" + + "│ └── members\n" + + "└── users\n" + + " └── {username}\n" + + " ├── events\n" + + " └── orgs\n") + + def test_relative_path(self): + self.context.url = 'http://localhost/users' + execute('tree 101', self.context) + self.assert_stdout("{username}\n" + + "├── events\n" + + "└── orgs\n") + + def test_absolute_path(self): + self.context.url = 'http://localhost/users' + execute('tree /orgs/1', self.context) + self.assert_stdout("{org}\n" + + "├── events\n" + + "└── members\n") + + def test_redirect_write(self): + filename = self.make_tempfile() + + # Write something first to make sure it's a full overwrite + with open(filename, 'w', encoding="utf-8") as f: + f.write('hello world\n') + + execute('tree > %s' % filename, self.context) + + with open(filename, encoding="utf-8") as f: + content = f.read() + self.assertEqual(content, "root\n" + + "├── orgs\n" + + "│ └── {org}\n" + + "│ ├── events\n" + + "│ └── members\n" + + "└── users\n" + + " └── {username}\n" + + " ├── events\n" + + " └── orgs") + + def test_redirect_append(self): + filename = self.make_tempfile() + + # Write something first to make sure it's an append + with open(filename, 'w', encoding="utf-8") as f: + f.write('hello world\n') + + execute('tree >> %s' % filename, self.context) + + with open(filename, encoding="utf-8") as f: + content = f.read() + self.assertEqual(content, "hello world\nroot\n" + + "├── orgs\n" + + "│ └── {org}\n" + + "│ ├── events\n" + + "│ └── members\n" + + "└── users\n" + + " └── {username}\n" + + " ├── events\n" + + " └── orgs") + + def test_grep(self): + execute('tree | grep users', self.context) + self.assert_stdout('└── users\n') + + class TestExecution_ls(ExecutionTestCase): def test_root(self):