Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -71,16 +71,92 @@ class EndpointMethod(Generic[P, SyncReturnT, AsyncReturnT]):
the response type that ``send()`` returns for each endpoint marker.
"""

# Copied from the endpoint in __init__ so help() and autodoc describe the
# endpoint. Declared here so the descriptor's introspection surface is part of
# its type rather than something callers have to discover at runtime.
__wrapped__: Callable[P, PreparedRequest]
__name__: str
__qualname__: str
__doc__: str | None
__module__: str

def __init__(self, endpoint_fn: Callable[P, PreparedRequest]) -> None:
self._endpoint_fn = endpoint_fn
# Per-owning-class callable stubs handed out on *class*-level access, so
# unittest.mock / inspect can classify each endpoint (sync vs coroutine)
# and autospec it as callable. Keyed and cached by objtype so repeated
# class access is stable. See __get__.
self._class_stubs: dict[type | None, Callable[..., object]] = {}
# Carry the endpoint's name, docstring, and annotations onto the descriptor
# so help() and autodoc describe the endpoint rather than the descriptor.
# Set directly rather than via functools.update_wrapper, which expects a
# callable wrapper; a descriptor is not one, and which would also copy
# __dict__ and with it the endpoint's __isabstractmethod__ marker.
#
# inspect.signature(SomeClient.method) works because class-level access
# returns a functools.wraps'd stub (see __get__), not the raw descriptor.
self.__wrapped__ = endpoint_fn
for attr in functools.WRAPPER_ASSIGNMENTS:
try:
setattr(self, attr, getattr(endpoint_fn, attr))
except AttributeError:
pass

def _class_level_stub(self, objtype: type | None) -> Callable[..., object]:
"""Callable stub returned on class-level access, matched to ``objtype``.

``unittest.mock`` (both ``Mock(spec=...)`` and ``create_autospec``) reads
each attribute off the *class* and classifies it with ``callable()`` and
``asyncio.iscoroutinefunction()``. A bare descriptor is neither callable
nor a coroutine function, so every endpoint on an async client would be
mocked as a sync ``MagicMock`` and could not be awaited. This hands back a
real function -- ``async def`` for async clients, ``def`` for sync -- that
wraps the endpoint (so ``inspect.signature`` works and autospec validates
call signatures), but refuses to run unbound: endpoints only mean anything
against a client instance.
"""
cached = self._class_stubs.get(objtype)
if cached is not None:
return cached
is_async = objtype is not None and issubclass(objtype, AsyncNemoClient)
if is_async:

@functools.wraps(self._endpoint_fn)
async def stub(*args: object, **kwargs: object) -> object:
raise TypeError(f"{self.__name__} must be called on a client instance, not the class")
else:

@functools.wraps(self._endpoint_fn)
def stub(*args: object, **kwargs: object) -> object:
raise TypeError(f"{self.__name__} must be called on a client instance, not the class")

# __isabstractmethod__ rides along in the endpoint's __dict__ via wraps;
# drop it so the stub is never mistaken for an abstract member.
stub.__dict__.pop("__isabstractmethod__", None)
self._class_stubs[objtype] = stub
return stub

@property
def endpoint(self) -> Callable[P, PreparedRequest]:
"""The endpoint function this descriptor binds."""
return self._endpoint_fn

@overload
def __get__(self, obj: None, objtype: type | None = None) -> EndpointMethod[P, SyncReturnT, AsyncReturnT]: ...
@overload
def __get__(self, obj: NemoClient, objtype: type | None = None) -> Callable[P, SyncReturnT]: ...
@overload
def __get__(self, obj: AsyncNemoClient, objtype: type | None = None) -> Callable[P, Awaitable[AsyncReturnT]]: ...

def __get__(self, obj: NemoClient | AsyncNemoClient | None, objtype: type | None = None) -> object:
assert obj is not None
if obj is None:
# Class-level access. Anything that inspects a client class rather than
# an instance -- Mock(spec=...), autospec, inspect, help(), autodoc --
# lands here. Hand back a callable stub matched to the owning client
# type so mock classifies sync vs async correctly and autospec sees a
# callable. The raw descriptor is still reachable via
# inspect.getattr_static, which never invokes __get__.
return self._class_level_stub(objtype)
if isinstance(obj, AsyncNemoClient):

@functools.wraps(self._endpoint_fn)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ class NemoResponse(Generic[ResponseT]):
resp.http_response # full httpx.Response

user = resp.data() # raises on non-2xx, otherwise returns body

When several 2xx codes share one typed body (e.g. a delete that returns 202
Accepted for async teardown or 204 No Content when already gone, both typed
``None``), inspect ``resp.http_response.status_code`` to tell them apart.
"""

http_response: httpx.Response
Expand Down
Loading