DEV Community

Discussion on: Snake Eyes: Extension Methods

Collapse
 
kiralappo profile image
Kirill Lappo • Edited

nice article, thanks!

I tried and it works like a charm.

I had to modify extention implementation: I corrected class scope check at (3) and also added check if attribute is callable, so your extension class could have it's own usable attributes, that are acessible via self

def extension(scope_cls):
    def _default(_obj, _name):
        raise AttributeError()

    # (1)
    # we access the base-class of our extension class to get the class we're extending.
    base_cls = scope_cls.__base__
    cls_name = scope_cls.__name__
    original_getattr = getattr(base_cls, '__getattr__', _default)

    def _getattr(obj, name):
        with suppress(AttributeError):
            return original_getattr(obj, name)

        # (2)
        # we check whether the requested attribute exists in our extension class.
        # As you can see, the changes are pretty simple and straight-forward.
        if not hasattr(scope_cls, name):
            raise AttributeError()

        # (3)
        #  we make the most important change -
        #  we check for the extension class in the scope,
        #  not the extension methods.
        #  This is the core of this change!
        if not _is_in_scope(scope_cls, cls_name):
            raise AttributeError()

        # (4)
        # we get the required attribute from out extension class.
        attribute = getattr(scope_cls, name)

        # if it is function, return as function
        if hasattr(attribute, '__call__'):
            return functools.partial(attribute, obj)

        # return as is
        return attribute

    cls.__getattr__ = _getattr

    return scope_cls
Enter fullscreen mode Exit fullscreen mode

and my example of usage:

# no defined method here
class Context:
    def __init__(self):
        self.bag = dict()


@extension
class SpecificContext(Context):
    SPECIFIC_KEY = "SPECIFIC_KEY"

    def get_specific(self) -> Any:
        # bag will be taken from base class
        # SPECIFIC_KEY will be taken from extension class
        return self.bag[self.SPECIFIC_KEY]

    def set_specific(self, value: Any) -> None:
        self.bag[self.SPECIFIC_KEY] = value


def try_use_via_extension(context: SpecificContext) -> bool:
    # without @extension attribute this will fail
    # with exception 'no such attribute "set_specific" in Context class'
    context.set_specific("my-secret-here")

    key = context.get_specific()
    print("my secret is " + key)

    # return true because it does work :)
    return True


base_context = Context()  # of type Context
try_use_via_extension(base_context)  # but will be handled as SpecificContext

Enter fullscreen mode Exit fullscreen mode