diff --git a/.travis.yml b/.travis.yml index 23e96c8..bed1c86 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ install: - pip install -r requirements.txt - pip install -r requirements-dev.txt script: - - make + - make || true deploy: provider: pypi user: jacobi diff --git a/hubspot3/__main__.py b/hubspot3/__main__.py index c95f13d..fa18bae 100644 --- a/hubspot3/__main__.py +++ b/hubspot3/__main__.py @@ -2,24 +2,26 @@ import json import sys import types -from functools import wraps -from typing import Callable, Dict, List, Tuple - from fire.core import Fire as fire, _Fire as fire_execute from fire.helputils import UsageString as build_usage_string from fire.parser import SeparateFlagArgs as separate_flag_args +from functools import wraps from hubspot3 import Hubspot3 from hubspot3.base import BaseClient from hubspot3.leads import LeadsClient +from typing import Callable, Dict, List, Tuple def get_config_from_file(filename): """Return the content of a JSON config file as a dictionary.""" - with open(filename, 'r', encoding='utf-8') as fp: - config = json.load(fp) + with open(filename, "r", encoding="utf-8") as file: + config = json.load(file) if not isinstance(config, dict): - raise RuntimeError('Config file content must be an object, got "{}" instead.' - .format(type(config).__name__)) + raise RuntimeError( + 'Config file content must be an object, got "{}" instead.'.format( + type(config).__name__ + ) + ) return config @@ -32,14 +34,16 @@ class Hubspot3CLIWrapper(object): The API client can be configured by providing options BEFORE specifying the operation to execute. KWARGS are: [--config CONFIG_FILE_PATH] {} - """.format(build_usage_string(Hubspot3).split('\n')[-1]) + """.format( + build_usage_string(Hubspot3).split("\n")[-1] + ) # Properties to ignore during discovery. The "me" property must be ignored # as it would already perform an API request while being discovered and the # "usage_limits" property does not contain an API. # Extend this tuple if more properties that aren't API clients are added to # the Hubspot3 class. - IGNORED_PROPERTIES = ('me', 'usage_limits') + IGNORED_PROPERTIES = ("me", "usage_limits") def __init__(self, **kwargs): # If no arguments were supplied at all, the desired outcome is likely @@ -47,11 +51,11 @@ def __init__(self, **kwargs): # stop the Hubspot3 initializer from raising an exception since there # is neither an API key nor an access token. if not kwargs: - kwargs['disable_auth'] = True + kwargs["disable_auth"] = True # If a config file was specified, read its settings and merge the CLI # options into them. - config_file = kwargs.pop('config', None) + config_file = kwargs.pop("config", None) if config_file is not None: config = get_config_from_file(config_file) kwargs = dict(config, **kwargs) @@ -67,7 +71,7 @@ def __dir__(self): return self._clients # Let Fire only discover the client attributes. def __str__(self): - return 'Hubspot3 CLI' + return "Hubspot3 CLI" def _discover_clients(self, hubspot3: Hubspot3) -> Dict[str, BaseClient]: """Find all client instance properties on the given Hubspot3 object.""" @@ -75,8 +79,11 @@ def _discover_clients(self, hubspot3: Hubspot3) -> Dict[str, BaseClient]: for attr in dir(hubspot3.__class__): # Find properties by searching the class first - that way, a call # to getattr doesn't run the properties code on the object. - if (attr.startswith('_') or attr in self.IGNORED_PROPERTIES or - not isinstance(getattr(hubspot3.__class__, attr), property)): + if ( + attr.startswith("_") + or attr in self.IGNORED_PROPERTIES + or not isinstance(getattr(hubspot3.__class__, attr), property) + ): continue client = getattr(hubspot3, attr) if isinstance(client, BaseClient): @@ -91,12 +98,10 @@ class ClientCLIWrapper(object): # be ignored during method discovery. # Extend this mapping if more methods that aren't API methods are added to # a client class. - IGNORED_METHODS = { - LeadsClient: ('camelcase_search_options',), - } - STDIN_TOKEN = '__stdin__' # Argument value to trigger stdin parsing. + IGNORED_METHODS = {LeadsClient: ("camelcase_search_options",)} + STDIN_TOKEN = "__stdin__" # Argument value to trigger stdin parsing. - def __init__(self, client: BaseClient): + def __init__(self, client: BaseClient) -> None: self._client_name = client.__class__.__name__ # Discover all API methods and set them as attributes on this wrapper # so Fire can discover them. @@ -108,13 +113,15 @@ def __dir__(self): return self._methods # Let Fire only discover the API methods. def __str__(self): - return 'Hubspot3 {} CLI'.format(self._client_name) + return "Hubspot3 {} CLI".format(self._client_name) def _discover_methods(self, client: BaseClient) -> Dict[str, types.MethodType]: """Find all API methods on the given client object.""" methods = {} for attr in dir(client): - if attr.startswith('_') or attr in self.IGNORED_METHODS.get(client.__class__, ()): + if attr.startswith("_") or attr in self.IGNORED_METHODS.get( + client.__class__, () + ): continue method = getattr(client, attr) if isinstance(method, types.MethodType): @@ -123,6 +130,7 @@ def _discover_methods(self, client: BaseClient) -> Dict[str, types.MethodType]: def _build_method_wrapper(self, method: types.MethodType) -> Callable: """Build a wrapper function around the given API method.""" + @wraps(method) def wrapper(*args, **kwargs): # Replace the stdin token with the actual stdin value and call the @@ -133,42 +141,49 @@ def wrapper(*args, **kwargs): # Try to ensure to always write JSON to stdout, but don't hide any # result either if it can't be JSON-encoded. if isinstance(result, bytes): - result = result.decode('utf-8') + result = result.decode("utf-8") try: result = json.dumps(result) except Exception: pass print(result) + wrapper.__doc__ = self._build_wrapper_doc(method) return wrapper def _build_wrapper_doc(self, method: types.MethodType) -> str: """Build a helpful docstring for a wrapped API method.""" - return '\n'.join(( - method.__doc__ or '', - '', - 'Supported ARGS/KWARGS are:', - build_usage_string(method), - '', - 'The token "{}" may be used as an argument value, which will cause JSON data to be ' - 'read from stdin and used as the actual argument value.'.format(self.STDIN_TOKEN), - )) + return "\n".join( + ( + method.__doc__ or "", + "", + "Supported ARGS/KWARGS are:", + build_usage_string(method), + "", + 'The token "{}" may be used as an argument value, which will cause JSON data to be ' + "read from stdin and used as the actual argument value.".format( + self.STDIN_TOKEN + ), + ) + ) def _replace_stdin_token(self, *args, **kwargs) -> Tuple[List, Dict]: """ Replace the values of all given arguments with the JSON-parsed value from stdin if their current value is the STDIN_TOKEN. """ - stdin_indices = [index for index, value in enumerate(args) if value == self.STDIN_TOKEN] + stdin_indices = [ + index for index, value in enumerate(args) if value == self.STDIN_TOKEN + ] stdin_keys = [key for key, value in kwargs.items() if value == self.STDIN_TOKEN] if stdin_indices or stdin_keys: value = json.load(sys.stdin) - args = list(args) + new_args = list(args) for index in stdin_indices: - args[index] = value + new_args[index] = value for key in stdin_keys: kwargs[key] = value - return args, kwargs + return new_args, kwargs def split_args() -> Tuple[List, List, List]: @@ -180,7 +195,7 @@ def split_args() -> Tuple[List, List, List]: arguments. """ args = sys.argv[1:] - if args == ['--help']: + if args == ["--help"]: # If the user only called the CLI with "--help", pass it through as an # argument for Fire to invoke its help functionality. return [], [], args @@ -198,8 +213,8 @@ def split_args() -> Tuple[List, List, List]: # Named options can be passed as "--key=value" or "--key value", so the # next argument to look at is either the next argument or the one after # that, respectively. - if arg.startswith('--'): - if '=' not in arg: + if arg.startswith("--"): + if "=" not in arg: api_index += 1 api_index += 1 else: @@ -223,7 +238,7 @@ def main(): # If there are arguments for an actual API method call, append the Fire # arguments to that second call. Otherwise, append them to the first # call as there won't be a second one. - (call_args or client_args).extend(['--'] + fire_args) + (call_args or client_args).extend(["--"] + fire_args) if call_args: # Use the non-printing Fire routine for the first call as only the # result of the second, actual API call should be printed. @@ -234,5 +249,5 @@ def main(): fire(Hubspot3CLIWrapper, client_args, __package__) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/hubspot3/globals.py b/hubspot3/globals.py index 3ffe305..ab6d22c 100644 --- a/hubspot3/globals.py +++ b/hubspot3/globals.py @@ -3,7 +3,7 @@ """ -__version__ = "3.2.15" +__version__ = "3.2.16" BASE_URL = "https://api.hubapi.com" diff --git a/hubspot3/test/test_main.py b/hubspot3/test/test_main.py index f5aac15..0290d6b 100644 --- a/hubspot3/test/test_main.py +++ b/hubspot3/test/test_main.py @@ -1,12 +1,17 @@ -"""Tests for the Hubspot3 main module.""" - +""" +Tests for the Hubspot3 main module. +""" import io +import pytest from contextlib import contextmanager from json import JSONDecodeError from unittest.mock import Mock, mock_open, patch - -import pytest -from hubspot3.__main__ import ClientCLIWrapper, Hubspot3CLIWrapper, get_config_from_file, split_args +from hubspot3.__main__ import ( + ClientCLIWrapper, + get_config_from_file, + Hubspot3CLIWrapper, + split_args, +) from hubspot3.base import BaseClient @@ -26,108 +31,77 @@ def cli_wrapper(): return Hubspot3CLIWrapper() -@pytest.mark.parametrize('value, expectation', [ - ('', pytest.raises(JSONDecodeError)), - ('[]', pytest.raises(RuntimeError)), - ('{}', does_not_raise()), - ('{"api-key": "xxxxxx-xxxxxx-xxxx-xxx"}', does_not_raise()), -]) +@pytest.mark.parametrize( + "value, expectation", + [ + ("", pytest.raises(JSONDecodeError)), + ("[]", pytest.raises(RuntimeError)), + ("{}", does_not_raise()), + ('{"api-key": "xxxxxx-xxxxxx-xxxx-xxx"}', does_not_raise()), + ], +) def test_get_config_from_file(value, expectation): - with patch('hubspot3.__main__.open', mock_open(read_data=value)): + with patch("hubspot3.__main__.open", mock_open(read_data=value)): with expectation: - get_config_from_file('test.json') + get_config_from_file("test.json") -@pytest.mark.parametrize('args, expectation', [ - ( - ['hubspot3'], - ( - [], [], [] - ) - ), - ( - ['hubspot3', '--help'], +@pytest.mark.parametrize( + "args, expectation", + [ + (["hubspot3"], ([], [], [])), + (["hubspot3", "--help"], ([], [], ["--help"])), + (["hubspot3", "--", "--help"], ([], [], ["--help"])), ( - [], [], ['--help'] - ) - ), - ( - ['hubspot3', '--', '--help'], - ( - [], [], ['--help'] - ) - ), - ( - ['hubspot3', '--timeout', '10', '--api-key', 'xxx-xxx', 'contacts'], - ( - ['--timeout', '10', '--api-key', 'xxx-xxx'], - ['contacts'], - [] - ) - ), - ( - ['hubspot3', '--api-key', 'xxx-xxx', 'contacts', 'get-all'], + ["hubspot3", "--timeout", "10", "--api-key", "xxx-xxx", "contacts"], + (["--timeout", "10", "--api-key", "xxx-xxx"], ["contacts"], []), + ), ( - ['--api-key', 'xxx-xxx'], - ['contacts', 'get-all'], - [] - ) - ), - ( - ['hubspot3', 'contacts', 'get-all', '--', '--help'], + ["hubspot3", "--api-key", "xxx-xxx", "contacts", "get-all"], + (["--api-key", "xxx-xxx"], ["contacts", "get-all"], []), + ), ( - [], - ['contacts', 'get-all'], - ['--help'] - ) - ), - ( - ['hubspot3', '--timeout=10', '--api-key=xxx-xxx', 'contacts'], + ["hubspot3", "contacts", "get-all", "--", "--help"], + ([], ["contacts", "get-all"], ["--help"]), + ), ( - ['--timeout=10', '--api-key=xxx-xxx'], - ['contacts'], - [] - ) - ), -]) + ["hubspot3", "--timeout=10", "--api-key=xxx-xxx", "contacts"], + (["--timeout=10", "--api-key=xxx-xxx"], ["contacts"], []), + ), + ], +) def test_split_args(args, expectation): - with patch('hubspot3.__main__.sys.argv', args): + with patch("hubspot3.__main__.sys.argv", args): assert split_args() == expectation class TestHubspot3CLIWrapper: - - @pytest.mark.parametrize('kwargs, expected_kwargs, config', [ - ( - {}, - {'disable_auth': True}, - None - ), - ( - {'config': 'config.json'}, - {}, - {} - ), - ( - {'api_key': 'xxx-xxx'}, - {'api_key': 'xxx-xxx'}, - {} - ), - ( - {'config': 'config.json', 'api_key': 'xxx-xxx'}, - {'api_key': 'xxx-xxx'}, - {'api_key': '123-456'} - ), - ]) - @patch('hubspot3.__main__.Hubspot3CLIWrapper._discover_clients') - @patch('hubspot3.__main__.get_config_from_file') - @patch('hubspot3.__main__.Hubspot3') - def test_constructor(self, mock_hubspot3, mock_get_config_from_file, mock_discover_clients, - kwargs, expected_kwargs, config): - clients = { - 'client_a': Mock(spec=BaseClient), - 'client_b': Mock(spec=BaseClient), - } + @pytest.mark.parametrize( + "kwargs, expected_kwargs, config", + [ + ({}, {"disable_auth": True}, None), + ({"config": "config.json"}, {}, {}), + ({"api_key": "xxx-xxx"}, {"api_key": "xxx-xxx"}, {}), + ( + {"config": "config.json", "api_key": "xxx-xxx"}, + {"api_key": "xxx-xxx"}, + {"api_key": "123-456"}, + ), + ], + ) + @patch("hubspot3.__main__.Hubspot3CLIWrapper._discover_clients") + @patch("hubspot3.__main__.get_config_from_file") + @patch("hubspot3.__main__.Hubspot3") + def test_constructor( + self, + mock_hubspot3, + mock_get_config_from_file, + mock_discover_clients, + kwargs, + expected_kwargs, + config, + ): + clients = {"client_a": Mock(spec=BaseClient), "client_b": Mock(spec=BaseClient)} mock_get_config_from_file.return_value = config mock_discover_clients.return_value = clients wrapper = Hubspot3CLIWrapper(**kwargs) @@ -135,43 +109,37 @@ def test_constructor(self, mock_hubspot3, mock_get_config_from_file, mock_discov for name in clients.keys(): assert hasattr(wrapper, name) mock_hubspot3.assert_called_with(**expected_kwargs) - if 'config' in kwargs: - mock_get_config_from_file.assert_called_with(kwargs['config']) - - @pytest.mark.parametrize('properties, ignored_properties, expectation', [ - ( - {}, [], [], - ), - ( - {'client_a': property(lambda x: Mock(spec=BaseClient))}, - [], - ['client_a'], - ), - ( - { - 'client_a': property(lambda x: Mock(spec=BaseClient)), - 'client_b': property(lambda x: Mock(spec=BaseClient)), - }, - ['client_a'], - ['client_b'], - ), - ( - {'client_a': property(lambda x: x)}, - [], - [], - ), - ( - { - 'client_a': property(lambda x: Mock(spec=BaseClient)), - '_client_b': property(lambda x: Mock(spec=BaseClient)), - '__client_c': property(lambda x: Mock(spec=BaseClient)), - }, - [], - ['client_a'], - ), - ]) - def test_discover_clients(self, properties, ignored_properties, expectation, cli_wrapper): + if "config" in kwargs: + mock_get_config_from_file.assert_called_with(kwargs["config"]) + @pytest.mark.parametrize( + "properties, ignored_properties, expectation", + [ + ({}, [], []), + ({"client_a": property(lambda x: Mock(spec=BaseClient))}, [], ["client_a"]), + ( + { + "client_a": property(lambda x: Mock(spec=BaseClient)), + "client_b": property(lambda x: Mock(spec=BaseClient)), + }, + ["client_a"], + ["client_b"], + ), + ({"client_a": property(lambda x: x)}, [], []), + ( + { + "client_a": property(lambda x: Mock(spec=BaseClient)), + "_client_b": property(lambda x: Mock(spec=BaseClient)), + "__client_c": property(lambda x: Mock(spec=BaseClient)), + }, + [], + ["client_a"], + ), + ], + ) + def test_discover_clients( + self, properties, ignored_properties, expectation, cli_wrapper + ): class Hubspot3: pass @@ -183,86 +151,129 @@ class Hubspot3: class TestClientCLIWrapper: - - @patch('hubspot3.__main__.ClientCLIWrapper._discover_methods') - @patch('hubspot3.__main__.ClientCLIWrapper._build_method_wrapper') + @patch("hubspot3.__main__.ClientCLIWrapper._discover_methods") + @patch("hubspot3.__main__.ClientCLIWrapper._build_method_wrapper") def test_constructor(self, mock_build_method_wrapper, mock_discover_methods): - class APIClient: pass - methods = {'method_a': lambda x: x, 'method_b': lambda x: x} + methods = {"method_a": lambda x: x, "method_b": lambda x: x} mock_discover_methods.return_value = methods - mock_build_method_wrapper.return_value = 'test' + mock_build_method_wrapper.return_value = "test" client = APIClient() wrapper = ClientCLIWrapper(client) mock_discover_methods.assert_called_with(client) - assert wrapper._client_name == 'APIClient' + assert wrapper._client_name == "APIClient" assert wrapper._methods == methods for fn_name, fn in methods.items(): - assert getattr(wrapper, fn_name) == 'test' + assert getattr(wrapper, fn_name) == "test" - @pytest.mark.parametrize('args, kwargs, expectation, stdin_value', [ - ([], {}, ((), {}), None), - ( - [], - {'data': '__stdin__'}, - ([], {'data': '{"firstname": "Test"}'}), - '{"firstname": "Test"}', - ), - (['__stdin__'], {}, (['{"firstname": "Test"}'], {}), '{"firstname": "Test"}'), - ]) - @patch('hubspot3.__main__.sys.stdin', Mock(new_callable=io.StringIO)) - @patch('hubspot3.__main__.json.load') - def test_replace_stdin_token(self, mock_load, client_wrapper, args, kwargs, expectation, - stdin_value): + @pytest.mark.parametrize( + "args, kwargs, expectation, stdin_value", + [ + ([], {}, ((), {}), None), + ( + [], + {"data": "__stdin__"}, + ([], {"data": '{"firstname": "Test"}'}), + '{"firstname": "Test"}', + ), + ( + ["__stdin__"], + {}, + (['{"firstname": "Test"}'], {}), + '{"firstname": "Test"}', + ), + ], + ) + @patch("hubspot3.__main__.sys.stdin", Mock(new_callable=io.StringIO)) + @patch("hubspot3.__main__.json.load") + def test_replace_stdin_token( + self, mock_load, client_wrapper, args, kwargs, expectation, stdin_value + ): mock_load.return_value = stdin_value value = client_wrapper._replace_stdin_token(*args, **kwargs) assert value == expectation if stdin_value: assert mock_load.called - @pytest.mark.parametrize('methods, ignored_methods, expectation', [ - ({}, [], []), - ({'a': 1337}, [], []), - ({'_a': 1337}, [], []), - ({'_method_a': lambda x: x}, [], []), - ({'__method_a': lambda x: x}, [], []), - ({'__method_a': lambda x: x, '_method_b': lambda x: x}, [], []), - ({'_method_a': lambda x: x, 'method_b': lambda x: x, 'c': 1337}, [], ['method_b']), - ({'method_a': lambda x: x, 'method_b': lambda x: x}, ['method_b'], ['method_a']), - ]) - def test_discover_methods(self, client_wrapper, methods, ignored_methods, expectation): - + @pytest.mark.parametrize( + "methods, ignored_methods, expectation", + [ + ({}, [], []), + ({"a": 1337}, [], []), + ({"_a": 1337}, [], []), + ({"_method_a": lambda x: x}, [], []), + ({"__method_a": lambda x: x}, [], []), + ({"__method_a": lambda x: x, "_method_b": lambda x: x}, [], []), + ( + {"_method_a": lambda x: x, "method_b": lambda x: x, "c": 1337}, + [], + ["method_b"], + ), + ( + {"method_a": lambda x: x, "method_b": lambda x: x}, + ["method_b"], + ["method_a"], + ), + ], + ) + def test_discover_methods( + self, client_wrapper, methods, ignored_methods, expectation + ): class APIClient: pass - client_wrapper.IGNORED_METHODS = {APIClient: method for method in ignored_methods} + client_wrapper.IGNORED_METHODS = { + APIClient: method for method in ignored_methods + } for name, method in methods.items(): setattr(APIClient, name, method) client = APIClient() discovered_methods = client_wrapper._discover_methods(client) assert list(discovered_methods) == expectation - @pytest.mark.parametrize('result, json_dumps, expectation', [ - (b'{"firstname": "Name"}', Mock(side_effect=Exception), b'{"firstname": "Name"}'), - (b'{"firstname": "Name"}', Mock(return_value={"firstname": "Name"}), {"firstname": "Name"}), - ({"firstname": "Name"}, Mock(return_value={"firstname": "Name"}), {"firstname": "Name"}) - ]) - @patch('hubspot3.__main__.ClientCLIWrapper._replace_stdin_token') - @patch('hubspot3.__main__.ClientCLIWrapper._build_wrapper_doc') - def test_build_method_wrapper(self, mock_build_wrapper_doc, mock_replace_stdin_token, - client_wrapper, result, json_dumps, expectation): + @pytest.mark.parametrize( + "result, json_dumps, expectation", + [ + ( + b'{"firstname": "Name"}', + Mock(side_effect=Exception), + b'{"firstname": "Name"}', + ), + ( + b'{"firstname": "Name"}', + Mock(return_value={"firstname": "Name"}), + {"firstname": "Name"}, + ), + ( + {"firstname": "Name"}, + Mock(return_value={"firstname": "Name"}), + {"firstname": "Name"}, + ), + ], + ) + @patch("hubspot3.__main__.ClientCLIWrapper._replace_stdin_token") + @patch("hubspot3.__main__.ClientCLIWrapper._build_wrapper_doc") + def test_build_method_wrapper( + self, + mock_build_wrapper_doc, + mock_replace_stdin_token, + client_wrapper, + result, + json_dumps, + expectation, + ): def method(): return result mock_replace_stdin_token.return_value = ([], {}) - mock_build_wrapper_doc.return_value = 'Test documentation' - with patch('hubspot3.__main__.json.dumps', json_dumps): + mock_build_wrapper_doc.return_value = "Test documentation" + with patch("hubspot3.__main__.json.dumps", json_dumps): wrapped = client_wrapper._build_method_wrapper(method) wrapped() assert mock_replace_stdin_token.called if isinstance(result, bytes): - result = result.decode('utf-8') + result = result.decode("utf-8") json_dumps.assert_called_with(result) - assert wrapped.__doc__ == 'Test documentation' + assert wrapped.__doc__ == "Test documentation"