Skip to content

Instantly share code, notes, and snippets.

@outofmbufs
Created August 19, 2024 22:27
Show Gist options
  • Select an option

  • Save outofmbufs/ac3bdc0ac049b51450880307f598e3f5 to your computer and use it in GitHub Desktop.

Select an option

Save outofmbufs/ac3bdc0ac049b51450880307f598e3f5 to your computer and use it in GitHub Desktop.

Revisions

  1. outofmbufs created this gist Aug 19, 2024.
    183 changes: 183 additions & 0 deletions decoclass.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,183 @@
    # MIT License
    #
    # Copyright (c) 2024 Neil Webber
    #
    # Permission is hereby granted, free of charge, to any person obtaining a copy
    # of this software and associated documentation files (the "Software"), to deal
    # in the Software without restriction, including without limitation the rights
    # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    # copies of the Software, and to permit persons to whom the Software is
    # furnished to do so, subject to the following conditions:
    #
    # The above copyright notice and this permission notice shall be included
    # in all copies or substantial portions of the Software.
    #
    # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    # SOFTWARE.

    import functools
    import types


    # A DecoClass abstracts away a few details that are important to get correct
    # when implementing decorators as a class, with callable wrapper objects.
    #
    # To implement a decorator this way:
    #
    # class MyDecorator(DecoClass):
    # def __init__(self, *args, kw1="foo", kw2="bar", **kwargs):
    # super().__init__(*args, **kwargs)
    # self.kw1 = kw1 # args as needed per application
    # self.kw2 = kw2 # ...
    #
    # def __call__(self, *args, **kwargs):
    # # -- decorator-specific stuff goes here --
    # rv = super().__call__(*args, **kwargs)
    # # -- decorator-specific stuff goes here --
    # return rv
    #

    class DecoClass:
    """Base class for implementing a decorator as a class object."""

    # NOTE ON PARAMETERIZED decorators and NAKED decorators
    #
    # If there are no arguments for the DECORATOR ITSELF, it will
    # usually be invoked like this:
    #
    # @DecoClass
    # def foo(): pass
    #
    # which ultimately becomes a call to the class constructor with
    # one argument, the decoration target:
    #
    # DecoClass(foo)
    #
    # If there are KEYWORD arguments for the DECORATOR ITSELF, that
    # looks like this:
    #
    # @DecoClass(clown='bozo')
    # def foo(): pass
    #
    # And the class constructor is called with the keyword arguments
    # given, and is expected to return a callable object 'D' that THEN is
    # invoked with the function as an argument:
    #
    # @DecoClass(clown='bozo') --returns--> D
    # D(foo)
    #
    # In the nested-function implementation of decorators this is handled
    # by yet a third level of function nesting (see any number of python
    # writeups about this topic for examples of that).
    #
    # In this implementation, the parameterized form returns a different
    # object (via __new__ magic) that in turn will be called to return
    # the decorated function object.

    def __new__(cls, f_or_nothing=None, *args, **kwargs):
    if f_or_nothing is None:
    return cls.__Deferred(cls, *args, **kwargs) # PARAMETERIZED case
    else:
    return super().__new__(cls) # NAKED case

    def __init__(self, f, /, **kwargs):
    """subclasses should provide their own and super() this."""
    self.func = f
    functools.wraps(f)(self)

    # the callable object used for PARAMETERIZED decorators
    class __Deferred:
    def __init__(self, cls, *args, **kwargs):
    self.cls = cls
    self.args = args
    self.kwargs = kwargs

    def __call__(self, f):
    return self.cls(f, *self.args, **self.kwargs)

    def __call__(self, *args, **kwargs):
    """Call the wrapped function."""
    return self.func(*args, **kwargs)

    # A decorator implemented as a callable object method will not work
    # for decorating bound methods unless the decorator object is a
    # non-data descriptor (i.e., defines a __get__() method). This does that.
    def __get__(self, instance, owner):
    if instance is None:
    return self
    return types.MethodType(self, instance)


    if __name__ == "__main__":
    import unittest

    # This is also an example of how to write a DecoClass decorator.
    # This decorator counts the nesting level of the decorated function.
    class RecursionCounter(DecoClass):
    def __init__(self, *args, depthlimit=None, **kwargs):
    super().__init__(*args, **kwargs)
    self.depthlimit = depthlimit
    self._count = 0
    self.deepest = 0

    def __call__(self, *args, **kwargs):
    if self.depthlimit is not None and self._count >= self.depthlimit:
    raise RecursionError(f"recursion limit exceeded")
    self._count += 1
    self.deepest = max(self._count, self.deepest)
    try:
    rv = super().__call__(*args, **kwargs)
    finally:
    self._count -= 1
    return rv

    @RecursionCounter
    def nakedfoo(n):
    if n > 1:
    nakedfoo(n-1)
    return nakedfoo.deepest

    @RecursionCounter()
    def nakedparmfoo(n):
    if n > 1:
    nakedparmfoo(n-1)
    return nakedparmfoo.deepest

    foolimit = 5

    @RecursionCounter(depthlimit=foolimit)
    def limitedfoo(n):
    if n > 1:
    limitedfoo(n-1)
    return limitedfoo.deepest

    class TestMethods(unittest.TestCase):
    def test_naked_deco(self):
    self.assertEqual(nakedfoo(17), 17)

    def test_nakedparm_deco(self):
    self.assertEqual(nakedparmfoo(17), 17)

    def test_limited_deco(self):
    self.assertEqual(limitedfoo(foolimit), foolimit)
    with self.assertRaises(RecursionError):
    limitedfoo(foolimit+1)

    # this one is a method
    class Foo:
    @RecursionCounter
    def foo(self, a, b=1, /, *, clown='bozo'):
    return (a, b, clown)

    # commenting out __get__ in DecoClass will demonstrate what this tests
    def test_methodfoo(self):
    f = self.Foo()
    self.assertEqual(f.foo(17), (17, 1, 'bozo'))
    self.assertEqual(f.foo(3, 17, clown='krusty'), (3, 17, 'krusty'))

    unittest.main()