From 8e85a8bbead0b548b9bf56a908a1e607d63f9106 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 19 Mar 2019 00:50:55 -0700 Subject: [PATCH 01/11] Add ability to test against API for CLI methods --- hug/test.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/hug/test.py b/hug/test.py index a9a22766..7ab11645 100644 --- a/hug/test.py +++ b/hug/test.py @@ -76,10 +76,13 @@ def call(method, api_or_module, url, body='', headers=None, params=None, query_s globals()[method.lower()] = tester -def cli(method, *args, **arguments): +def cli(method, *args, api=None, module=None, **arguments): """Simulates testing a hug cli method from the command line""" - collect_output = arguments.pop('collect_output', True) + if api and module: + raise ValueError("Please specify an API OR a Module that contains the API, not both") + elif api or module: + method = API(api or module).cli.commands[method].interface._function command_args = [method.__name__] + list(args) for name, values in arguments.items(): From 7401d1ecd6b3323b266cf02eabd42a2c4e40d988 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 19 Mar 2019 00:51:24 -0700 Subject: [PATCH 02/11] Add initial tests for test module --- tests/test_test.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tests/test_test.py diff --git a/tests/test_test.py b/tests/test_test.py new file mode 100644 index 00000000..74998646 --- /dev/null +++ b/tests/test_test.py @@ -0,0 +1,40 @@ +"""tests/test_test.py. + +Test to ensure basic test functionality works as expected. + +Copyright (C) 2019 Timothy Edmund Crosley + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +""" +import pytest + +import hug + +api = hug.API(__name__) + + +def test_cli(): + """Test to ensure the CLI tester works as intended to allow testing CLI endpoints""" + @hug.cli() + def my_cli_function(): + return 'Hello' + + assert hug.test.cli(my_cli_function) == 'Hello' + assert hug.test.cli('my_cli_function', api=api) == 'Hello' + + # Shouldn't be able to specify both api and module. + with pytest.raises(ValueError): + assert hug.test.cli('my_method', api=api, module=hug) From 0eb8cf904ec7bc4e73663872257826de7576fdac Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 19 Mar 2019 00:51:46 -0700 Subject: [PATCH 03/11] Update test to be API aware --- tests/test_decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_decorators.py b/tests/test_decorators.py index 0d068f1f..fe490cb3 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -825,7 +825,7 @@ def extend_with(): assert hug.test.get(api, '/api/made_up_go').data == 'Going!' assert tests.module_fake_http_and_cli.made_up_go() == 'Going!' - assert hug.test.cli(tests.module_fake_http_and_cli.made_up_go) == 'Going!' + assert hug.test.cli('made_up_go', api=api) def test_cli(): From 77ebd89e5b99cd8ed452e25756b2f79cb0c45034 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 19 Mar 2019 00:52:41 -0700 Subject: [PATCH 04/11] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41519858..20a76f8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Changelog ========= ### 2.4.4 - TBD +- Added optional built-in API aware testing for CLI commands. - Add unit test for `extend_api()` with CLI commands - Fix running tests using `python setup.py test` - Documented the `multiple_files` example From 5582c8761ffa43c92033a1ed439ea13a960cb447 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 19 Mar 2019 01:17:27 -0700 Subject: [PATCH 05/11] Add built-in support for CLI extending --- hug/api.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/hug/api.py b/hug/api.py index 468a427b..34b65848 100644 --- a/hug/api.py +++ b/hug/api.py @@ -156,7 +156,7 @@ def add_exception_handler(self, exception_type, error_handler, versions=(None, ) placement = self._exception_handlers.setdefault(version, OrderedDict()) placement[exception_type] = (error_handler, ) + placement.get(exception_type, tuple()) - def extend(self, http_api, route="", base_url=""): + def extend(self, http_api, route="", base_url="", **kwargs): """Adds handlers from a different Hug API to this one - to create a single API""" self.versions.update(http_api.versions) base_url = base_url or self.base_url @@ -396,6 +396,14 @@ def handlers(self): """Returns all registered handlers attached to this API""" return self.commands.values() + def extend(self, cli_api, prefix="", sub_command="", **kwargs): + """Extends this CLI api with the commands present in the provided cli_api object""" + if sub_command: + self.commands[sub_command] = cli_api + else: + for name, command in cli_api.commands.items(): + self.commands["{}{}".format(prefix, name)] = command + def __str__(self): return "{0}\n\nAvailable Commands:{1}\n".format(self.api.doc or self.api.name, "\n\n\t- " + "\n\t- ".join(self.commands.keys())) @@ -497,12 +505,15 @@ def context(self): self._context = {} return self._context - def extend(self, api, route="", base_url=""): + def extend(self, api, route="", base_url="", http=True, cli=True, **kwargs): """Adds handlers from a different Hug API to this one - to create a single API""" api = API(api) - if hasattr(api, '_http'): - self.http.extend(api.http, route, base_url) + if http and hasattr(api, '_http'): + self.http.extend(api.http, route, base_url, **kwargs) + + if cli and hasattr(api, '_cli'): + self.cli.extend(api.cli, **kwargs) for directive in getattr(api, '_directives', {}).values(): self.add_directive(directive) From 067a4a55eb8cab94010d071ab671a124bb45422d Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 19 Mar 2019 01:17:34 -0700 Subject: [PATCH 06/11] Update changelog to mention CLI extending support --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20a76f8a..e682d32d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Changelog ========= ### 2.4.4 - TBD +- Added the ablity to extend CLI APIs in addition to HTTP APIs issue #744. - Added optional built-in API aware testing for CLI commands. - Add unit test for `extend_api()` with CLI commands - Fix running tests using `python setup.py test` From 6cdd62e77977878f8b8c5a8b971e5db94a7504bf Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 19 Mar 2019 01:28:11 -0700 Subject: [PATCH 07/11] Lay out all desired supported CLI extending behaviour in test case --- tests/test_decorators.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/test_decorators.py b/tests/test_decorators.py index fe490cb3..74b633b2 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -815,6 +815,39 @@ def extend_with(): assert hug.test.get(api, '/api/made_up_hello').data == 'hello' +def test_extending_api_with_http_and_cli(): + """Test to ensure it's possible to extend the current API so both HTTP and CLI APIs are extended""" + import tests.module_fake_http_and_cli + + @hug.extend_api(base_url='/api') + def extend_with(): + return (tests.module_fake_http_and_cli, ) + + assert hug.test.get(api, '/api/made_up_go').data == 'Going!' + assert tests.module_fake_http_and_cli.made_up_go() == 'Going!' + assert hug.test.cli('made_up_go', api=api) + + # Should be able to apply a prefix when extending CLI APIs + @hug.extend_api(command_prefix='prefix_', http=False) + def extend_with(): + return (tests.module_fake_http_and_cli, ) + + assert hug.test.cli('prefix_made_up_go', api=api) + + # OR provide a sub command use to reference the commands + @hug.extend_api(sub_command='sub_api', http=False) + def extend_with(): + return (tests.module_fake_http_and_cli, ) + + assert hug.test.cli('sub_api', 'made_up_go', api=api) + + # But not both + with pytest.raises(ValueError): + @hug.extend_api(sub_command='sub_api', command_prefix='api_', http=False) + def extend_with(): + return (tests.module_fake_http_and_cli, ) + + def test_extending_api_with_http_and_cli(): """Test to ensure it's possible to extend the current API so both HTTP and CLI APIs are extended""" import tests.module_fake_http_and_cli From 03d9f0a9e9a18961cfdf12da8caf7b83f7b7a6aa Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 19 Mar 2019 01:28:52 -0700 Subject: [PATCH 08/11] Ensure a sub_command and prefix can't both be provided when extending --- hug/api.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/hug/api.py b/hug/api.py index 34b65848..f474b98c 100644 --- a/hug/api.py +++ b/hug/api.py @@ -396,13 +396,16 @@ def handlers(self): """Returns all registered handlers attached to this API""" return self.commands.values() - def extend(self, cli_api, prefix="", sub_command="", **kwargs): + def extend(self, cli_api, command_prefix="", sub_command="", **kwargs): """Extends this CLI api with the commands present in the provided cli_api object""" + if sub_command and command_prefix: + raise ValueError('It is not currently supported to provide both a command_prefix and sub_command') + if sub_command: self.commands[sub_command] = cli_api else: for name, command in cli_api.commands.items(): - self.commands["{}{}".format(prefix, name)] = command + self.commands["{}{}".format(command_prefix, name)] = command def __str__(self): return "{0}\n\nAvailable Commands:{1}\n".format(self.api.doc or self.api.name, From 19ed4994c74dd5050a682e285427468192637398 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 19 Mar 2019 01:29:17 -0700 Subject: [PATCH 09/11] Pass along additional options to all interface extenders --- hug/decorators.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hug/decorators.py b/hug/decorators.py index ea3b9cd4..c613ca99 100644 --- a/hug/decorators.py +++ b/hug/decorators.py @@ -169,12 +169,12 @@ def decorator(middleware_class): return decorator -def extend_api(route="", api=None, base_url=""): +def extend_api(route="", api=None, base_url="", **kwargs): """Extends the current api, with handlers from an imported api. Optionally provide a route that prefixes access""" def decorator(extend_with): apply_to_api = hug.API(api) if api else hug.api.from_object(extend_with) for extended_api in extend_with(): - apply_to_api.extend(extended_api, route, base_url) + apply_to_api.extend(extended_api, route, base_url, **kwargs) return extend_with return decorator From dd3f284df32c6691a1fe9d40b9c529d0ca6138e1 Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 19 Mar 2019 01:41:46 -0700 Subject: [PATCH 10/11] Fix install when Cython present --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index 557b18db..7a1eed9d 100755 --- a/setup.py +++ b/setup.py @@ -32,6 +32,7 @@ JYTHON = 'java' in sys.platform ext_modules = [] +cmdclass = {} try: sys.pypy_version_info @@ -96,6 +97,7 @@ def list_modules(dirname): setup_requires=['pytest-runner'], tests_require=['pytest', 'mock', 'marshmallow'], ext_modules=ext_modules, + cmdclass=cmdclass, python_requires=">=3.4", keywords='Web, Python, Python3, Refactoring, REST, Framework, RPC', classifiers=[ From e8a45abdf9b0cbe1a421c78b9afdd152c51fc5fb Mon Sep 17 00:00:00 2001 From: Timothy Crosley Date: Tue, 19 Mar 2019 01:42:05 -0700 Subject: [PATCH 11/11] Add basic CLI example --- examples/multi_file_cli/__init__.py | 0 examples/multi_file_cli/api.py | 17 +++++++++++++++++ examples/multi_file_cli/sub_api.py | 6 ++++++ 3 files changed, 23 insertions(+) create mode 100644 examples/multi_file_cli/__init__.py create mode 100644 examples/multi_file_cli/api.py create mode 100644 examples/multi_file_cli/sub_api.py diff --git a/examples/multi_file_cli/__init__.py b/examples/multi_file_cli/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/multi_file_cli/api.py b/examples/multi_file_cli/api.py new file mode 100644 index 00000000..179f6385 --- /dev/null +++ b/examples/multi_file_cli/api.py @@ -0,0 +1,17 @@ +import hug + +import sub_api + + +@hug.cli() +def echo(text: hug.types.text): + return text + + +@hug.extend_api(sub_command='sub_api') +def extend_with(): + return (sub_api, ) + + +if __name__ == '__main__': + hug.API(__name__).cli() diff --git a/examples/multi_file_cli/sub_api.py b/examples/multi_file_cli/sub_api.py new file mode 100644 index 00000000..dd21eda0 --- /dev/null +++ b/examples/multi_file_cli/sub_api.py @@ -0,0 +1,6 @@ +import hug + + +@hug.cli() +def hello(): + return 'Hello world'