mahmoud / glom

☄️ Python's nested data operator (and CLI), for all your declarative restructuring needs. Got data? Glom it! ☄️
https://glom.readthedocs.io
Other
1.88k stars 61 forks source link

Assign a target with an object which has a `__setattr__` which returns a callable produces a false positive for `glom.core._has_callable_glomit` #252

Closed lululaplap closed 6 months ago

lululaplap commented 1 year ago

Hello,

I have a dictionary which I am trying to assign values to using glom. Some of the objects I am using have the __setattr__ dunder set to return a callable by default. As such glom falsely believes it has the attribute glomit and assigns the path to the result of the callable which __setattr__ returns. I have produced a minimal example below

import glom

class MyObject:
    def __init__(self):
        self.hello = 'world'

    def __getattr__(self, name):
        def callable_attr(*args, **kwargs):
            return f"called {name} with {args} and {kwargs}"
        return callable_attr

obj = MyObject()
my_dict = glom.assign(obj={'a': 1}, path='a', val=obj)
a = glom.glom(my_dict, "a")
a.hello

produces

AttributeError: 'str' object has no attribute 'hello'

I believe checking that the attribute is callable and is defined on the dir would solve this:

def _has_callable_glomit(obj):
    glomit = getattr(obj, 'glomit', None)
    return callable(glomit)  and "glomit" in dir(obj) and not isinstance(obj, type)

Cheers, Lewis

lululaplap commented 7 months ago

@mahmoud Would a PR implementing the above be welcome? Cheers

mahmoud commented 7 months ago

Oh interesting, I see what you're saying. So, you don't have control over the __getattr__ to, e.g., return None if __getattr__ is called with glomit as a name?

I ask because dir() isn't cheap or even authoritative in my experience. It's more like a help() output than something on which one might base a protocol.

mahmoud commented 7 months ago

Assuming you don't, but do have access to the glom.assign call, I think the best thing would be to wrap the val in a Val spec, like so:

...

obj = MyObject()
my_dict = glom.assign(obj={'a': 1}, path='a', val=Val(obj))  # added Val
a = glom.glom(my_dict, "a")
a.hello
# gets 'world'

What do you think?

mahmoud commented 6 months ago

I think the couple of solutions outlined here should address this issue. Feel free to reopen if they don't work for some reason.