class TestSort: @pytest.mark.parametrize(('in_', 'sortkey', 'expected'), [ pytest.param( [forge.arg('b'), forge.arg('a')], None, [forge.arg('a'), forge.arg('b')], id='lexicographical', ), pytest.param( [forge.arg('a', default=None), forge.arg('b')], None, [forge.arg('b'), forge.arg('a', default=None)], id='default', ), pytest.param( [ forge.vkw('e'), forge.kwo('d'), forge.vpo('c'), forge.pok('b'), forge.pos('a'), ], None, [ forge.pos('a'), forge.pok('b'), forge.vpo('c'), forge.kwo('d'), forge.vkw('e'), ], id='kind', ), pytest.param( [forge.arg('x', 'b'), forge.arg('y', 'a')], lambda param: param.interface_name, [forge.arg('y', 'a'), forge.arg('x', 'b')], id='sortkey_interface_name', ), pytest.param( [forge.vpo('a'), forge.vpo('b')], None, [forge.vpo('a'), forge.vpo('b')], id='novalidate', ), ]) def test_revise(self, in_, sortkey, expected): """ Ensure that parameter sorting: - doesn't validate the signature - by default sorts by (kind, has-default, name) - takes advantage of user-supplied sortkey """ rev = sort(sortkey) in_ = FSignature(in_, __validate_parameters__=False) expected = FSignature(expected, __validate_parameters__=False) assert rev.revise(in_) == expected
def _eval_arguments(self, node): NONEXISTANT_DEFAULT = object() # a unique object to contrast with None posonlyargs_and_defaults = [] num_args = len(node.args) if hasattr(node, "posonlyargs"): for (arg, default) in itertools.zip_longest( node.posonlyargs[::-1], node.defaults[::-1][num_args:], fillvalue=NONEXISTANT_DEFAULT, ): if default is NONEXISTANT_DEFAULT: posonlyargs_and_defaults.append(forge.pos(arg.arg)) else: posonlyargs_and_defaults.append( forge.pos(arg.arg, default=self._eval(default))) posonlyargs_and_defaults.reverse() args_and_defaults = [] for (arg, default) in itertools.zip_longest( node.args[::-1], node.defaults[::-1][:num_args], fillvalue=NONEXISTANT_DEFAULT, ): if default is NONEXISTANT_DEFAULT: args_and_defaults.append(forge.arg(arg.arg)) else: args_and_defaults.append( forge.arg(arg.arg, default=self._eval(default))) args_and_defaults.reverse() vpo = (node.vararg and forge.args(node.vararg.arg)) or [] kwonlyargs_and_defaults = [] # kwonlyargs is 1:1 to kw_defaults, no need to jump through hoops for (arg, default) in zip(node.kwonlyargs, node.kw_defaults): if not default: kwonlyargs_and_defaults.append(forge.kwo(arg.arg)) else: kwonlyargs_and_defaults.append( forge.kwo(arg.arg, default=self._eval(default))) vkw = (node.kwarg and forge.kwargs(node.kwarg.arg)) or {} return ( [ *posonlyargs_and_defaults, *args_and_defaults, *vpo, *kwonlyargs_and_defaults, ], vkw, )
def _initialize(self, instance): self.request = instance.request available_parameters = { # Json body is only available for POST/PUT ('create', 'update'): self._params['body'], # Query parameter is only available for GET/DELETE ('get', 'list', 'delete'): self._params['query'], } for method, path in self._paths.items(): # Path parameter would be postional argument sigs = [forge.pos(f) for f in path.fields] for methods, params in available_parameters.items(): if method in methods: # Query/Body parameter would be keyword only argument sigs.extend( forge.kwo( param.name, default=param.default, type=param.type) for param in params) # Any other keyword argument would be matched to 'kwargs' sigs.append(forge.vkw('kwargs')) setattr(self, method, forge.sign(*sigs)(getattr(self, method))) for method in list(self.methods): def unsupported(*args, **kwargs): raise NotImplementedError(f"'{self._name}' has no method " f"what you called") if method not in self._paths: setattr(self, method, unsupported) self.methods.remove(method)
import forge import aiohttp from typing import Dict, List, Union # RestClient signature decorators GET_SIGNATURE = forge.compose( forge.copy(aiohttp.ClientSession.get), forge.modify("url", default=None), forge.returns(aiohttp.ClientResponse), forge.insert( [ forge.kwo("parameters", default={}, type=Dict[str, Union[str, List[str]]]), forge.kwo("headers", default={}, type=Dict[str, str]), ], before=lambda arg: arg.kind == forge.FParameter.VAR_KEYWORD, ), ) MGET_SIGNATURE = forge.compose( forge.copy(aiohttp.ClientSession.get, exclude="url"), forge.returns(List[aiohttp.ClientResponse]), forge.insert( forge.pok( "urls", type=forge.fsignature(aiohttp.ClientSession.get)["url"].type, default=None, ), index=1,
def create_method(client, params, body, method_info): _sig_url_params = [] _sig_params_req = [] _sig_params_opt = [] _sig_body_req = [] _sig_body_opt = [] for _key, _url_param in method_info['url_parameters'].items(): _sig_url_params.append(forge.arg(name=_key, type=_url_param['type'])) if params['data']: for _key, _param_item in params['data'].items(): if _param_item['required']: _sig_params_req.append( forge.arg(name=_key, type=_param_item['type'])) else: _sig_params_opt.append( forge.kwo(name=_key, type=_param_item['type'], default=None)) if body['data']: for _key, _body_item in body['data'].items(): if _body_item['required']: _sig_body_req.append( forge.arg(name=_key, type=_body_item['type'])) else: _sig_body_opt.append( forge.kwo(name=_key, type=_body_item['type'], default=None)) async def inner_method(self, **kwargs): # Verify signature endpoint_signature = {} for url_param, url_param_info in method_info['url_parameters'].items(): if url_param not in kwargs: raise SyntaxError( f'Argument {url_param!r} is a required keyword argument.') if 'validator' in url_param_info: tested = url_param_info['validator'](kwargs[url_param]) if not tested: # TODO: less generic error messages by abstracting this to the endpoint data structure raise ValueError( f'Supplied argument {url_param!r} failed to validate') endpoint_signature[url_param] = kwargs[url_param] endpoint = method_info['endpoint'].format(**endpoint_signature) # Verify params # TODO: DRY params_signature = [] for param in params['required']: item = None if param not in kwargs: if 'default' not in params['data'][param]: raise SyntaxError( f'Parameter {param!r} is a required keyword argument') else: item = params['data'][param] if not item: item = kwargs[param] if 'validator' in params['data'][param]: tested = params['data'][param]['validator'](item) if not tested: # TODO: less generic error messages by abstracting this to the endpoint data structure raise ValueError( f'Supplied argument {param!r} failed to validate') if '_internal_name' in params['data'][param]: params_signature.append( (params['data'][param]['_internal_name'], format_parameter_value(item))) else: params_signature.append((param, format_parameter_value(item))) for param in params['optional']: if param in kwargs and kwargs[param] is not None: if 'validator' in params['data'][param]: tested = params['data'][param]['validator'](kwargs[param]) if not tested: # TODO: less generic error messages by abstracting this to the endpoint data structure raise ValueError( f'Supplied argument {param!r} failed to validate') if '_internal_name' in params['data'][param]: params_signature.append( (params['data'][param]['_internal_name'], format_parameter_value(kwargs[param]))) else: params_signature.append( (param, format_parameter_value(kwargs[param]))) # Verify body # TODO: DRY body_signature = {} for key in body['required']: item = None if key not in kwargs: if 'default' not in body['data'][key]: raise SyntaxError( f'Body key {key!r} is a required keyword argument') else: item = body['data'][key]['default'] if not item: item = kwargs[key] if 'validator' in body['data'][key]: tested = body['data'][key]['validator'](item) if not tested: # TODO: less generic error messages by abstracting this to the endpoint data structure raise ValueError( f'Supplied argument {key!r} failed to validate') if '_internal_name' in body['data'][key]: body_signature[body['data'][key]['_internal_name']] = item if method_info['body_type'] == 'json' else \ format_parameter_value(item) else: body_signature[key] = item if method_info['body_type'] == 'json' else \ format_parameter_value(item) for key in body['optional']: if key in kwargs and kwargs[key] is not None: if 'validator' in body['data'][key]: tested = body['data'][key]['validator'](kwargs[key]) if not tested: # TODO: less generic error messages by abstracting this to the endpoint data structure raise ValueError( f'Supplied argument {key!r} failed to validate') if '_internal_name' in body['data'][key]: body_signature[body['data'][key]['_internal_name']] = kwargs[key] if \ method_info['body_type'] == 'json' else \ format_parameter_value(kwargs[key]) else: body_signature[key] = kwargs[key] if method_info['body_type'] == 'json' else \ format_parameter_value(kwargs[key]) if method_info['http_method'] in ['POST', 'PUT', 'PATCH']: # Have a body if method_info['body_type'] == 'kv': resp = await self.signed_request( method_info['http_method'], endpoint, params=params_signature, data=body_signature, headers={'content-type': method_info['content_type']}) elif method_info['body_type'] == 'json': resp = await self.signed_request( method_info['http_method'], endpoint, params=params_signature, json=body_signature, headers={'content-type': method_info['content_type']}) else: raise RuntimeError( f'Unknown body type {method_info["body_type"]!r} in method {method_info["method_name"]!r}' ) elif method_info['http_method'] in ['GET', 'DELETE']: # No body for these methods, nor a specific content-type resp = await self.signed_request(method_info['http_method'], endpoint, params=params_signature) else: raise NotImplementedError( f'Unsupported HTTP verb {method_info["http_method"]!r}.') return resp revised = forge.sign( forge.pos(name='self'), *_sig_url_params, *_sig_params_req, *_sig_body_req, *_sig_params_opt, *_sig_body_opt, )(inner_method) revised.__name__ = method_info['method_name'] revised.__doc__ = generate_docstring(params, body, method_info) setattr(client, method_info['method_name'], revised)