Skip to content

Commit

Permalink
Make consumer method look like the original func
Browse files Browse the repository at this point in the history
This involves copying function attributes (e.g., docstrings) using
`functools.update_wrapper`.

Original post on Gitter:
https://gitter.im/python-uplink/Lobby?at=5efcf94ce0e5673398e865e2
  • Loading branch information
prkumar committed Jul 26, 2020
1 parent 5288f5b commit 814528b
Show file tree
Hide file tree
Showing 4 changed files with 49 additions and 8 deletions.
4 changes: 4 additions & 0 deletions tests/unit/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ def test_method_handler_builder_getter(
self, annotation_handler_builder_mock
):
builder = commands.RequestDefinitionBuilder(
None,
None,
None,
type(annotation_handler_builder_mock)(),
Expand All @@ -143,6 +144,7 @@ def test_build(self, mocker, annotation_handler_builder_mock):
method_handler_builder = annotation_handler_builder_mock
uri_definition_builder = mocker.Mock(spec=commands.URIDefinitionBuilder)
builder = commands.RequestDefinitionBuilder(
None,
"method",
uri_definition_builder,
argument_handler_builder,
Expand All @@ -164,6 +166,7 @@ def test_auto_fill_when_not_done(
method_handler_builder = annotation_handler_builder_mock
uri_definition_builder = mocker.Mock(spec=commands.URIDefinitionBuilder)
builder = commands.RequestDefinitionBuilder(
None,
"method",
uri_definition_builder,
argument_handler_builder,
Expand All @@ -189,6 +192,7 @@ def test_auto_fill_when_not_done_fails(
method_handler_builder = annotation_handler_builder_mock
uri_definition_builder = mocker.Mock(spec=commands.URIDefinitionBuilder)
builder = commands.RequestDefinitionBuilder(
None,
"method",
uri_definition_builder,
argument_handler_builder,
Expand Down
25 changes: 21 additions & 4 deletions uplink/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,18 +193,35 @@ def _build_definition(self):
)

def __get__(self, instance, owner):
# TODO:
# Consider caching by instance/owner using WeakKeyDictionary.
# This will avoid the extra copy/create per attribute reference.
# However, we should do this after investigating for any latent cases
# of unnecessary overhead in the codebase as a whole.
if instance is None:
return self._request_definition_builder.copy()
# This code path is traditionally called when applying a class
# decorator to a Consumer. We should return a copy of the definition
# builder to avoid class decorators on a subclass from polluting
# other siblings (#152).
value = self._request_definition_builder.copy()
else:
return instance.session.create(instance, self._request_definition)
value = instance.session.create(instance, self._request_definition)

# Make the return value look like the original method (e.g., inherit
# docstrings and other function attributes).
# TODO: Ideally, we should wrap once instead of on each reference.
self._request_definition_builder.update_wrapper(value)
return value


class ConsumerMeta(type):
@staticmethod
def _wrap_if_definition(cls_name, key, value):
wrapped_value = value
if isinstance(value, interfaces.RequestDefinitionBuilder):
value = ConsumerMethod(cls_name, key, value)
return value
wrapped_value = ConsumerMethod(cls_name, key, value)
value.update_wrapper(wrapped_value)
return wrapped_value

@staticmethod
def _set_init_handler(namespace):
Expand Down
25 changes: 21 additions & 4 deletions uplink/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,14 @@ def build(self):

class RequestDefinitionBuilder(interfaces.RequestDefinitionBuilder):
def __init__(
self, method, uri, argument_handler_builder, method_handler_builder
self,
func,
method,
uri,
argument_handler_builder,
method_handler_builder,
):
self._func = func
self._method = method
self._uri = uri
self._argument_handler_builder = argument_handler_builder
Expand Down Expand Up @@ -193,15 +199,23 @@ def extend(self, uri=None, args=()):
uri = self.uri.template if uri is None else uri
return factory(uri, args)

def _extend(self, method, uri, arg_handler, _):
def _extend(self, func, method, uri, arg_handler, _):
builder = RequestDefinitionBuilder(
method, uri, arg_handler, self.method_handler_builder.copy()
# Extended consumer methods should only inherit the decorators and
# not any function annotations, since the new method can have a
# different signature than the original.
func,
method,
uri,
arg_handler,
self.method_handler_builder.copy(),
)
builder.return_type = self.return_type
return builder

def copy(self):
builder = RequestDefinitionBuilder(
self._func,
self._method,
self._uri,
self._argument_handler_builder.copy(),
Expand All @@ -224,6 +238,9 @@ def _auto_fill_remaining_arguments(self):
path_vars = dict.fromkeys(matching, arguments.Path)
self.argument_handler_builder.set_annotations(path_vars)

def update_wrapper(self, wrapper):
functools.update_wrapper(wrapper, self._func)

def build(self):
if not self._argument_handler_builder.is_done():
self._auto_fill_remaining_arguments()
Expand Down Expand Up @@ -316,6 +333,7 @@ def __call__(
func, spec.args
)
builder = request_definition_builder_factory(
func,
self._method,
URIDefinitionBuilder(self._uri),
arg_handler,
Expand All @@ -329,7 +347,6 @@ def __call__(
# Use return value type hint as expected return type
if spec.return_annotation is not None:
builder = returns.schema(spec.return_annotation)(builder)
functools.update_wrapper(builder, func)
builder = self._add_args(builder)
return builder

Expand Down
3 changes: 3 additions & 0 deletions uplink/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ def argument_handler_builder(self):
def method_handler_builder(self):
raise NotImplementedError

def update_wrapper(self, wrapper):
raise NotImplementedError

def build(self):
raise NotImplementedError

Expand Down

0 comments on commit 814528b

Please sign in to comment.