I'm attempting to add an item getter (__getitem__, to provide the [] syntax) to a class method so that I can use some unique-ish syntax to provide types to functions outside the normal parentheses, like the following. The syntax on the last line (of this first snippet) is really the goal for this whole endeavor.
class MyClass:
@typedmethod
def compute_typed_value(self, value, *args, **kwargs):
print(self, args, kwargs)
result = TypedMethod.requested_type(kwargs)(value)
self.my_other_method()
return result
def my_other_method(self):
print('Doing some other things!')
return 3
a = MyClass()
a.compute_typed_value[int]('12345') # returns int value 12345
Additionally, I'd like to retain the intuitive behavior that a defined function can be called like a function, potentially with a default value for the type, like so:
a = MyClass()
a.compute_typed_value('12345')
# should return whatever the default type is, with the value of '12345',
# or allow some other default behavior
In a broader context, this would be implemented as a piece of an API adapter that implements a generic request processor, and I'd like the data to come out of the API adapter in a specific format. So the way that this might look in actual use could be something like the following:
@dataclass
class MyAPIData:
property_a: int = 0
property_b: int = 0
class MyAPIAdapter:
_session
def __init__(self, token):
self._init_session(token)
@typedmethod
def request_json(self, url, **kwargs):
datatype = TypedMethod.requested_type(kwargs)
response_data = self._session.get(url).json()
if datatype:
response_data = datatype(**response_data)
return response_data
def fetch_myapidata(self, search):
return self.request_json[MyAPIData](f"/myapi?q={search}")
I'm attempting to achieve this kind of behavior with a decorator that I can throw onto any function that I want to enable this behavior. Here is my current full implementation:
from functools import partial
class TypedMethod:
_REQUESTED_TYPE_ATTR = '__requested_type'
def __init__(self, method):
self._method = method
print(method)
self.__call__ = method.__call__
def __getitem__(self, specified_type, *args, **kwargs):
print(f'getting typed value: {specified_type}')
if not isinstance(specified_type, type):
raise TypeError("Only Type Accessors are supported - must be an instance of `type`")
return partial(self.__call__, **{self.__class__._REQUESTED_TYPE_ATTR: specified_type})
def __call__(self, *args, **kwargs):
print(args, kwargs)
return self._method(self, *args, **kwargs)
@classmethod
def requested_type(cls, foo_kwargs):
return foo_kwargs[cls._REQUESTED_TYPE_ATTR] if cls._REQUESTED_TYPE_ATTR in foo_kwargs else None
def typedmethod(foo):
print(f'wrapping {foo.__name__} with a Typed Method: {foo}')
_typed_method = TypedMethod(foo)
def wrapper(self, *args, **kwargs):
print('WRAPPER', self, args, kwargs)
return _typed_method(self, *args, **kwargs)
_typed_method.__call__ = wrapper
return _typed_method
class MyClass:
@typedmethod
def compute_typed_value(self, value, *args, **kwargs):
print(self, args, kwargs)
result = TypedMethod.requested_type(kwargs)(value)
print(result)
self.my_other_method()
return result
def my_other_method(self):
print('Doing some other things!')
return 3
a = MyClass()
a.compute_typed_value[int]('12345')
If you run this code, it will fail stating that 'TypedMethod' object has no attribute 'my_other_method'. Further inspection reveals that the first line of compute_typed_value is not printing what one would intuitively expect from the code:
<__main__.TypedMethod object at 0x10754e790> () {'__requested_type': <class 'int'>}
Specifically, the first item printed, which is a TypedMethod instead of a MyClass instance
Basically, the idea is use the __getitem__ callout to generate a functools.partial so that the subsequent call to the resulting function contains the __getitem__ key in a known "magic" kwargs value, which should hypothetically work, except that now the self reference that is available to MyClass.compute_typed_value is actually a reference to the TypedMethod instance generated by the wrapper instead of the expected MyClass instance. I've attempted a number of things to get the MyClass instance passed as self, but since it's implemented as a decorator, the instance isn't available at the time of decoration, meaning that somehow it needs to be a bound method at the time of function execution, I think.
I know I could just pass this value in as like the first positional argument, but I want it to work with the square bracket annotation because I think it'd be cool and more readable. This is mostly a learning exercise to understand more of Python's inner workings, so the answer could ultimately be "no".
Your code is doing some odd stuff with __call__ that doesn't quite work. Fixing those issues will likely make self refer to what you expect in compute_typed_value.
The main problems:
__call__ attribute of an instance doesn't work to change the object's behavior when it's actually called. You attempt this twice, but the TypedMethod object's hard-coded __call__ method is getting called instead of any of the other things you try (you first set _method.__call__ and separately wrapper to be called, neither of which make much sense to me).typed_method decorator returns the TypedMethod object it creates, rather than the wrapper function. Because TypedMethod is not a descriptor, there's no binding logic for MyClass.compute_typed_value, so there's no good way for the instance of MyClass to get passed in anywhere. Normally this works because functions are descriptors, returning bound method objects. However, it's going to be a bit complicated to make that work here, since you want a __getattr__ to work on the bound object.So, I think you should change things up to use two different classes.
The first is a descriptor class, that when looked up, has binding behavior so that you can get the self value to pass in to the method. When bound, it returns an instance of the second class.
The second class handles the indexing by type. It has a __getitem__ method, which returns a partial that passes both the self value that the first class captured, and the type that it has been indexed with (as a secret keyword argument).
Here's what that looks like:
class typedmethod:
def __init__(self, method):
self.method = method
def __get__(self, instance, owner=None):
if instance is None: return self # class lookup
return TypeIndexer(instance, self.method)
class TypeIndexer:
def __init__(self, instance, method):
self.instance = instance
self.method = method
def __getitem__(self, type):
return partial(method, self.instance, _secret_kwarg=type)
I've left out the logic to hide the name _secret_kwarg in a class variable somewhere, and to have a public API for getting it out of a kwargs dict. It would actually be a whole lot easier if you just passed the type in to the method as a public argument. Maybe make it the first positional argument after self, or a kwarg with a meaningful name? The fact that the user doesn't actually supply it directly wouldn't be much more confusing than TypedMethod.requested_type(kwargs)(value) is now.
Of course, if we follow that logic to its conclusion, you could rewrite the whole obj.method[type](args) pattern to be obj.method(type, args) and it would be a whole lot easier.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With