7

I am trying to create a class (MySerial) that instantiates a serial object so that I can write/read to a serial device (UART). There is an instance method that is a decorator which wraps around a function that belongs to a completely different class (App). So decorator is responsible for writing and reading to the serial buffer.

If I create an instance of MySerial inside the App class, I can't use the decorator instance method that is created from MySerial. I have tried foregoing instance methods and using class methods as explained in this second answer, but I really need to instantiate MySerial, thus create an instance using __init__.

How can this be accomplished? Is it impossible?

  • Create a decorator that is an instance method.
  • Use this decorator within another class

class MySerial():
    def __init__(self):
        pass # I have to have an __init__
    def write(self):
        pass # write to buffer
    def read(self):
        pass # read to buffer
    def decorator(self, func):
        def func_wrap(*args, **kwargs):
            self.write(func(*args, **kwars))
            return self.read()
        return func_wrap

class App():
    def __init__(self):
        self.ser = MySerial()

    @self.ser.decorator  # <-- does not work here.
    def myfunc(self):
        # 'yummy_bytes' is written to the serial buffer via 
        # MySerial's decorator method
        return 'yummy_bytes'

if __name__ == '__main__':
    app = App()
Jordan Lee
  • 515
  • 1
  • 5
  • 16

3 Answers3

2

You can use a staticmethod to wrap decorator. The inner func_wrap function of decorator contains an additional parameter in its signature: cls. cls can be used to access the ser attribute of the instance of App, and then the desired methods write and read can be called from cls.ser. Also, note that in your declarations, MySerial.write takes no paramters, but is passed the result of the wrapped function. The code below uses *args to prevent the TypeError which would otherwise be raised:

class MySerial():
   def __init__(self):
     pass # I have to have an __init__
   def write(self, *args):
     pass # write to buffer
   def read(self):
     pass # read to buffer
   @staticmethod
   def decorator(func):
     def func_wrap(cls, *args, **kwargs):
        cls.ser.write(func(cls, *args, **kwargs))
        return cls.ser.read()
     return func_wrap

class App():
  def __init__(self):
     self.ser = MySerial()
  @MySerial.decorator 
  def myfunc(self):
    # 'yummy_bytes' is written to the serial buffer via 
    # MySerial's decorator method
    return 'yummy_bytes'

App().myfunc()
Ajax1234
  • 69,937
  • 8
  • 61
  • 102
1

The reason this does not work is because you are refering to self in the class body, where it is not defined. Here are two solutions.

Store the serial object as class attribute

If you store the MySerial instance as a class attribute, then it sill be accessible in the class body:

class App():
    ser = MySerial()

    @ser.decorator
    def myfunc(self):
        return 'yummy_bytes'

Decorate on each instantiation

Or if you need a different MySerial instance for every App instance, then you will need to wait for the instance to be created to define an instance attribute my_func. This means the function is decorated dynamically on every instance creation, in which case, the @ decorator syntax must be replaced by a function call.

class App():
    def __init__(self):
        self.ser = MySerial()
        self.my_func = self.ser.decorator(self.myfunc)

    def myfunc(self):
        return 'yummy_bytes'

This solution generalizes to decorating multiple methods or conditionally deactivating serializing, say in a test environment.

import env

class App():
    def __init__(self):
        self.ser = MySerial()

        to_decorate = [] if env.test else ['myfunc']

        for fn_name in to_decorate:
            fn = getattr(self, fn_name)
            setattr(self, fn_name, self.ser.decorator(fn))
Olivier Melançon
  • 21,584
  • 4
  • 41
  • 73
  • I'm actually already implementing your're first solution, but I've run into a huge headache when needing to unit test when I asked my other [question](https://stackoverflow.com/questions/50476632/mock-a-class-but-not-one-of-its-functions) – Jordan Lee Jun 01 '18 at 00:12
  • I'm leaning more toward your second solution, but I have over 60+ functions to take care of. – Jordan Lee Jun 01 '18 at 00:17
  • The comment from Martijn Pieters in that question is a really good point. Maybe you could write a new class which is your mocked version of MySerial and replace App.ser by this mocked instance before every test? – Olivier Melançon Jun 01 '18 at 00:18
  • As for the second solution, I actually think it makes more sense to have a different serial for each app. So I'd recommend that one. Anyway, you'll be removing 60 `@decorator` and adding 60 instance attributes. It that's really an issue for you, you could write a helper function that decorates functions from a list, but having a 60 line `__init__` function is not necessarly a problem. – Olivier Melançon Jun 01 '18 at 00:20
  • @JordanLee I am also working on a metaclass approach for you if you are willing to wait a moment – Olivier Melançon Jun 01 '18 at 00:22
  • Yeah that would be great. – Jordan Lee Jun 01 '18 at 00:30
  • @JordanLee Here it is. Let me know if you have question or are not familiar with metaclass. In this solution, setting back the metaclass to `type` removes all decorator, which might help you with your tests. – Olivier Melançon Jun 01 '18 at 00:37
  • I really appreciate it. I'm not sure how setting the metaclass to type is performed. – Jordan Lee Jun 01 '18 at 00:46
  • @JordanLee I removed the metaclass approach, a bit overkill for what you want to achieve and simply suggested a way to update the __init__ method to efficiently decorate many functions or to deactivate serialization in a test environment. – Olivier Melançon Jun 01 '18 at 01:20
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/172268/discussion-between-jordan-lee-and-olivier-melancon). – Jordan Lee Jun 01 '18 at 18:25
0

There's a lot of hidden pitfalls that make this a risky design, however it is a great learning example.

First off, the call to 'self' when decorating fails because there is no self at that scope. It only exists inside the methods. Now that the easy one is out of the way...

myfunc is an attribute of App class. When you create an instance of App, it is always that one function that gets called. Even when it becomes methodfied, that only happens once.

a1 = App()
a2 = App()
assert a1.myfunc.__func__ is a2.myfunc.__func__
assert id(a1.myfunc) is id(a2.myfunc)  # Methods have some weirdness that means that won't equate but id's show they are the same 

This is why self is needed to get a unique namespace for the instance. It is also why you won't be able to get decorator that is unique to the instance in this way. Another way to think about it is that Class must be defined before you can produce instances. Therefore, you can't use an instance in the defination of a Class.

Solution

The decorator needs to be written in a way that it won't store any instance attributes. It will access the App instance attributes instead.

class MySerial():
    def __init__(self):
        pass # Possibly don't need to have an __init__
    def write(self, serial_config):
        pass # write to buffer
    def read(self, serial_config):
        pass # read to buffer
    def decorator(self, func):
        def func_wrap(self_app: App, *args, **kwargs):
            self.write(func(self_app, *args, **kwars), self_app.serial_config)
            return self.read(self_app.serial_config)
        return func_wrap

ser = MySerial()

class App():
    def __init__(self, serial_config):
        self.serial_config = serial_config  # This is the instance data for     MySerial

    @ser.decorator
    def myfunc(self):
        # 'yummy_bytes' is written to the serial buffer via 
        # MySerial's decorator method
        return 'yummy_bytes'

if __name__ == '__main__':
    app = App()

Now I'm assuming MySerial was going to have a unique file, or port or something per instance of App. This is what would be recorded in serial_config. This may not be elegant if the stream is opening an closing but you should be able to improve this for your exact application.

Guy Gangemi
  • 1,533
  • 1
  • 13
  • 25