-
Notifications
You must be signed in to change notification settings - Fork 124
Pyscript updates #405
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Pyscript updates #405
Changes from all commits
ab8194e
371284d
ff89bad
c1fc040
4699451
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,10 +8,9 @@ | |
| """ | ||
|
|
||
| import argparse | ||
| from collections import namedtuple | ||
| import functools | ||
| import sys | ||
| from typing import List, Tuple | ||
| from typing import List, Tuple, Callable | ||
|
|
||
| # Python 3.4 require contextlib2 for temporarily redirecting stderr and stdout | ||
| if sys.version_info < (3, 5): | ||
|
|
@@ -41,54 +40,78 @@ def __bool__(self): | |
|
|
||
| class CopyStream(object): | ||
| """Copies all data written to a stream""" | ||
| def __init__(self, inner_stream): | ||
| def __init__(self, inner_stream, echo: bool = False): | ||
| self.buffer = '' | ||
| self.inner_stream = inner_stream | ||
| self.echo = echo | ||
|
|
||
| def write(self, s): | ||
| self.buffer += s | ||
| self.inner_stream.write(s) | ||
| if self.echo: | ||
| self.inner_stream.write(s) | ||
|
|
||
| def read(self): | ||
| raise NotImplementedError | ||
|
|
||
| def clear(self): | ||
| self.buffer = '' | ||
|
|
||
| def __getattr__(self, item: str): | ||
| if item in self.__dict__: | ||
| return self.__dict__[item] | ||
| else: | ||
| return getattr(self.inner_stream, item) | ||
|
|
||
|
|
||
| def _exec_cmd(cmd2_app, func): | ||
| def _exec_cmd(cmd2_app, func: Callable, echo: bool): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this is intended to be an internal function I think I'd prefer the caller to be explicit about it. |
||
| """Helper to encapsulate executing a command and capturing the results""" | ||
| copy_stdout = CopyStream(sys.stdout) | ||
| copy_stderr = CopyStream(sys.stderr) | ||
| copy_stdout = CopyStream(sys.stdout, echo) | ||
| copy_stderr = CopyStream(sys.stderr, echo) | ||
|
|
||
| copy_cmd_stdout = CopyStream(cmd2_app.stdout, echo) | ||
|
|
||
| cmd2_app._last_result = None | ||
|
|
||
| with redirect_stdout(copy_stdout): | ||
| with redirect_stderr(copy_stderr): | ||
| func() | ||
| try: | ||
| cmd2_app.stdout = copy_cmd_stdout | ||
| with redirect_stdout(copy_stdout): | ||
| with redirect_stderr(copy_stderr): | ||
| func() | ||
| finally: | ||
| cmd2_app.stdout = copy_cmd_stdout.inner_stream | ||
|
|
||
| # if stderr is empty, set it to None | ||
| stderr = copy_stderr if copy_stderr.buffer else None | ||
| stderr = copy_stderr.buffer if copy_stderr.buffer else None | ||
|
|
||
| result = CommandResult(stdout=copy_stdout.buffer, stderr=stderr, data=cmd2_app._last_result) | ||
| outbuf = copy_cmd_stdout.buffer if copy_cmd_stdout.buffer else copy_stdout.buffer | ||
| result = CommandResult(stdout=outbuf, stderr=stderr, data=cmd2_app._last_result) | ||
| return result | ||
|
|
||
|
|
||
| class ArgparseFunctor: | ||
| """ | ||
| Encapsulates translating python object traversal | ||
| """ | ||
| def __init__(self, cmd2_app, item, parser): | ||
| def __init__(self, echo: bool, cmd2_app, command_name: str, parser: argparse.ArgumentParser): | ||
| self._echo = echo | ||
| self._cmd2_app = cmd2_app | ||
| self._item = item | ||
| self._command_name = command_name | ||
| self._parser = parser | ||
|
|
||
| # Dictionary mapping command argument name to value | ||
| self._args = {} | ||
| # argparse object for the current command layer | ||
| self.__current_subcommand_parser = parser | ||
|
|
||
| def __getattr__(self, item): | ||
| def __dir__(self): | ||
| """Returns a custom list of attribute names to match the sub-commands""" | ||
| commands = [] | ||
| for action in self.__current_subcommand_parser._actions: | ||
| if not action.option_strings and isinstance(action, argparse._SubParsersAction): | ||
| commands.extend(action.choices) | ||
| return commands | ||
|
|
||
| def __getattr__(self, item: str): | ||
| """Search for a subcommand matching this item and update internal state to track the traversal""" | ||
| # look for sub-command under the current command/sub-command layer | ||
| for action in self.__current_subcommand_parser._actions: | ||
|
|
@@ -101,7 +124,6 @@ def __getattr__(self, item): | |
| return self | ||
|
|
||
| raise AttributeError(item) | ||
| # return super().__getattr__(item) | ||
|
|
||
| def __call__(self, *args, **kwargs): | ||
| """ | ||
|
|
@@ -182,7 +204,7 @@ def __call__(self, *args, **kwargs): | |
|
|
||
| def _run(self): | ||
| # look up command function | ||
| func = getattr(self._cmd2_app, 'do_' + self._item) | ||
| func = getattr(self._cmd2_app, 'do_' + self._command_name) | ||
|
|
||
| # reconstruct the cmd2 command from the python call | ||
| cmd_str = [''] | ||
|
|
@@ -238,16 +260,16 @@ def traverse_parser(parser): | |
|
|
||
| traverse_parser(self._parser) | ||
|
|
||
| # print('Command: {}'.format(cmd_str[0])) | ||
| return _exec_cmd(self._cmd2_app, functools.partial(func, cmd_str[0]), self._echo) | ||
|
|
||
| return _exec_cmd(self._cmd2_app, functools.partial(func, cmd_str[0])) | ||
|
|
||
| class PyscriptBridge(object): | ||
| """Preserves the legacy 'cmd' interface for pyscript while also providing a new python API wrapper for | ||
| application commands.""" | ||
| def __init__(self, cmd2_app): | ||
| self._cmd2_app = cmd2_app | ||
| self._last_result = None | ||
| self.cmd_echo = False | ||
|
|
||
| def __getattr__(self, item: str): | ||
| """Check if the attribute is a command. If so, return a callable.""" | ||
|
|
@@ -261,13 +283,19 @@ def __getattr__(self, item: str): | |
| except AttributeError: | ||
| # Command doesn't, we will accept parameters in the form of a command string | ||
| def wrap_func(args=''): | ||
| return _exec_cmd(self._cmd2_app, functools.partial(func, args)) | ||
| return _exec_cmd(self._cmd2_app, functools.partial(func, args), self.cmd_echo) | ||
| return wrap_func | ||
| else: | ||
| # Command does use argparse, return an object that can traverse the argparse subcommands and arguments | ||
| return ArgparseFunctor(self._cmd2_app, item, parser) | ||
| return ArgparseFunctor(self.cmd_echo, self._cmd2_app, item, parser) | ||
|
|
||
| raise AttributeError(item) | ||
| return super().__getattr__(item) | ||
|
|
||
| def __dir__(self): | ||
| """Return a custom set of attribute names to match the available commands""" | ||
| commands = list(self._cmd2_app.get_all_commands()) | ||
| commands.insert(0, 'cmd_echo') | ||
| return commands | ||
|
|
||
| def __call__(self, args): | ||
| return _exec_cmd(self._cmd2_app, functools.partial(self._cmd2_app.onecmd_plus_hooks, args + '\n')) | ||
| def __call__(self, args: str): | ||
| return _exec_cmd(self._cmd2_app, functools.partial(self._cmd2_app.onecmd_plus_hooks, args + '\n'), self.cmd_echo) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| app.cmd_echo = True | ||
| app.bar('11', '22') |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| custom.cmd_echo = True | ||
| custom.echo('blah!') |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| app.cmd_echo = True | ||
| app.foo('aaa', 'bbb', counter=3, trueval=True, constval=True) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| app.cmd_echo = True | ||
| app.foo('11', '22', '33', '44', counter=3, trueval=True, constval=True) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| app.cmd_echo = True | ||
| app.foo('11', '22', '33', '44', '55', '66', counter=3, trueval=False, constval=False) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| app.cmd_echo = True | ||
| result = app.foo('aaa', 'bbb', counter=3) | ||
| out_text = 'Fail' | ||
| if result: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| app.help() | ||
| app.cmd_echo = True | ||
| app.help() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| app.cmd_echo = True | ||
| app.help('media') |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| app.cmd_echo = True | ||
| app.media.movies.add('My Movie', 'PG-13', director=('George Lucas', 'J. J. Abrams')) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| app.cmd_echo = True | ||
| app.media.movies.add('My Movie', 'PG-13', actor=('Mark Hamill'), director=('George Lucas', 'J. J. Abrams')) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| app.media.movies.list() | ||
| app.cmd_echo = True | ||
| app.media.movies.list() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| app.media().movies().list() | ||
| app.cmd_echo = True | ||
| app.media().movies().list() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| app('media movies list') | ||
| app.cmd_echo = True | ||
| app('media movies list') |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| app.cmd_echo = True | ||
| app.media.movies.list(actor='Mark Hamill') |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| app.cmd_echo = True | ||
| app.media.movies.list(actor=('Mark Hamill', 'Carrie Fisher')) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| app.cmd_echo = True | ||
| app.media.movies.list(rating='PG') |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| app.cmd_echo = True | ||
| app.media.movies.list(rating=('PG', 'PG-13')) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| out = dir(app) | ||
| out.sort() | ||
| print(out) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| out = dir(app.media) | ||
| out.sort() | ||
| print(out) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -101,7 +101,14 @@ def do_foo(self, args): | |
|
|
||
| @with_argparser(bar_parser) | ||
| def do_bar(self, args): | ||
| print('bar ' + str(args.__dict__)) | ||
| out = 'bar ' | ||
| arg_dict = args.__dict__ | ||
| keys = list(arg_dict.keys()) | ||
| keys.sort() | ||
| out += '{' | ||
| for key in keys: | ||
| out += "'{}':'{}'".format(key, arg_dict[key]) | ||
| print(out) | ||
|
|
||
|
|
||
| @pytest.fixture | ||
|
|
@@ -160,7 +167,7 @@ def test_pyscript_help(ps_app, capsys, request, command, pyscript_file): | |
| ('foo aaa bbb -ccc -t -n', 'foo1.py'), | ||
| ('foo 11 22 33 44 -ccc -t -n', 'foo2.py'), | ||
| ('foo 11 22 33 44 55 66 -ccc', 'foo3.py'), | ||
| ('bar 11 22', 'bar1.py') | ||
| ('bar 11 22', 'bar1.py'), | ||
| ]) | ||
| def test_pyscript_out(ps_app, capsys, request, command, pyscript_file): | ||
| test_dir = os.path.dirname(request.module.__file__) | ||
|
|
@@ -204,11 +211,30 @@ def test_pyscript_results(ps_app, capsys, request, pyscript_file, exp_out): | |
| assert exp_out in expected | ||
|
|
||
|
|
||
| def test_pyscript_custom_name(ps_echo, capsys): | ||
| @pytest.mark.parametrize('expected, pyscript_file', [ | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for adding some unit tests |
||
| ("['_relative_load', 'alias', 'bar', 'cmd_echo', 'edit', 'eof', 'eos', 'foo', 'help', 'history', 'load', 'media', 'py', 'pyscript', 'quit', 'set', 'shell', 'shortcuts', 'unalias']", | ||
| 'pyscript_dir1.py'), | ||
| ("['movies', 'shows']", 'pyscript_dir2.py') | ||
| ]) | ||
| def test_pyscript_dir(ps_app, capsys, request, expected, pyscript_file): | ||
| test_dir = os.path.dirname(request.module.__file__) | ||
| python_script = os.path.join(test_dir, 'pyscript', pyscript_file) | ||
|
|
||
| run_cmd(ps_app, 'pyscript {}'.format(python_script)) | ||
| out, _ = capsys.readouterr() | ||
| out = out.strip() | ||
| assert len(out) > 0 | ||
| assert out == expected | ||
|
|
||
|
|
||
| def test_pyscript_custom_name(ps_echo, capsys, request): | ||
| message = 'blah!' | ||
| run_cmd(ps_echo, 'py custom.echo("{}")'.format(message)) | ||
|
|
||
| test_dir = os.path.dirname(request.module.__file__) | ||
| python_script = os.path.join(test_dir, 'pyscript', 'custom_echo.py') | ||
|
|
||
| run_cmd(ps_echo, 'pyscript {}'.format(python_script)) | ||
| expected, _ = capsys.readouterr() | ||
| assert len(expected) > 0 | ||
| expected = expected.splitlines() | ||
| assert message == expected[0] | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for making the features added by pyscript_bridge available consistently using either the py or ipy commands.