Search code examples
pythonclassnamespacesinstance

Encapsulate the scope of an imported module


Using the hideous RoboClaw Python "library" in a project, I can't seem to get past the fact that they threw a bunch of functions and global variables that deal with interfacing with physical hardware into a file. Unfortunately, I am stuck using this library because when the vendor releases a new firmware for their board, they also update this file on their website; porting it to something useful would be a continuous effort.

The issue arises when I attempt to import it multiple times, once for each board that I have attached to USB. Something (conceptually) like this would be ideal:

import roboclaw
class Board:
    def __init__(self):
        self.rc = roboclaw

Since the Python interpreter seems to maintain the same module reference in memory with every import, I can't seem to get it to create instances that exist in separate namespaces, essentially spitting out all kinds of I/O conflict errors when all boards incorrectly become assigned to the same /dev/ttyACM device file. The closest that I seem to be able to get is this answer provided by Noctis Skytower, but it still isn't creating a separate namespaces for each Board instance.

Additionally, I have tried setting up dynamic imports using imp (like this) and importlib (like this) though both of these fail to import because they can't find the roboclaw.py file that sits in the same directory.

At a bit of a loss for the direction that I should be going on this, as I've never had to deal with this before.


Solution

  • Ok normally I would condemn hacks like the one I am suggesting but it seems you don't have much choice as the author of that package clearly did not understand the meaning of object oriented programming, there is a way to create a copy of a function with a different global scope by directly calling the FunctionType() contructor:

    from types import FunctionType
    from functools import wraps
    
    def copy_func(func,global_namespace):
        "copies a function object with the new specified global scope"
        c = FunctionType(func.__code__,global_namespace,func.__name__,func.__defaults__,func.__closure__)
        try:
            c.__kwdefaults__ = func.__kwdefaults__
        except AttributeError:pass #this is only for python 3
        c = wraps(func)(c) #copy all metadata although there doesn't seem to be any for roboclaw
        return c
    

    This way it does not affect the functions in the module at all but still uses your own namespace, you can then spoof a copy module with this:

    class CopyScope:
        def __init__(self,module):
            own_scope = self.__dict__
            for name,thing in vars(module).items():
                if isinstance(thing,FunctionType):
                    setattr(self, name, copy_func(thing, own_scope))
                else:
                    setattr(self, name, thing)
                    #you could also do own_scope[name] = .. instead of setattr() but I prefer setattr()
    

    Although this only runs copy_func for all the module level function and not any methods that may use the global statement but I'd imagine that if the author understood what methods were you wouldn't need this at all.

    I was able to test this with the following code:

    import roboclaw
    x = CopyScope(roboclaw)
    
    x.crc_clear()
    x.crc_update(4)
    
    print(x._crc)
    print(roboclaw._crc) #this will actually raise an error because it isn't defined in the original module yet.
    

    EDIT: If you have not tried this it may be preferable to the above work around:

    import sys
    import roboclaw as robo1
    del sys.modules["roboclaw"]
    import roboclaw as robo2
    assert robo1 is not robo2