Search code examples
pythonpython-import

How to wrap an existing module?


Our code makes extensive use of the fcntl-module to lock files -- to prevent multiple instances of the program from stepping over each other. This works fine on Linux, but, as we just discovered, there is no fcntl on Windows...

Fortunately, we don't need the multiple instance-safety on Windows and could just fake it there. But replacing the existing use of fcntl-module, its constants (like fcntl.LOCK_EX) and function (fcntl.flock) is something, I want to avoid.

So I tried creating an fcntl.py of my own with something like:

import sys
import os

if os.name == 'posix':
    syspath = sys.path
    sys.path = sys.path[1:] # Remove current directory from search path
    import fcntl as realfcntl
    LOCK_EX = realfcntl.LOCK_EX
    LOCK_SH = realfcntl.LOCK_SH
    LOCK_UN = realfcntl.LOCK_UN
    sys.path = syspath

    def flock(fd, mode):
        return realfcntl.flock(fd, mode)
else:
    # Fake it:
    LOCK_EX = -1
    LOCK_SH = -1
    LOCK_UN = -1
    def flock(fd, mode):
        print('Pretending to manipulate locking on FD %s' % fd, file = sys.stderr)

To my dismay, this fails at the import time on Unix, on line 8: module 'fcntl' has no attribute 'LOCK_EX', which tells me, my attempt to trick it into loading the real fcntl failed.

How can the wrapper like mine load the real module (being wrapped)?


Solution

  • I believe the easiest way to do this is to simply force a module called fcntl to exist in the entry point of your code:

    YOUR_REPOSITORY/
        YOUR_PACKAGE/
            __init__.py
            _activate_fcntl.py
    
    # YOUR_PACKAGE/__init__.py
    
    # This import must be before all other import statements which reference `YOUR_PACKAGE`
    from . import _activate_fcntl
    
    # Other imports
    ...
    
    # YOUR_PACKAGE/_activate_fcntl.py
    
    from __future__ import annotations
    
    import sys
    
    
    try:
        # Don't need to mess around with checking `os.name` here, if it exists, then it
        # exists.
        import fcntl
    except ModuleNotFoundError:
        from inspect import getdoc
        from types import ModuleType
    
        class fake_fcntl_module_class(ModuleType):
    
            """
            This will be your fake `fcntl` module's docstring, if you need it.
            """
    
            def __init__(self) -> None:
                """Class constructor - initialise your module with a name and docstring"""
                super().__init__(name="fcntl", doc=getdoc(self))
    
            # =============================================================================        
            # Treat the remaining contents of this class as the body of your `fcntl` module
            # =============================================================================
    
            LOCK_EX = -1
            LOCK_SH = -1
            LOCK_UN = -1
    
            # Typing definitions taken from
            # https://github.com/python/typeshed/blob/main/stdlib/fcntl.pyi
            # `flock` is a module-level function, so imitate this using a `@staticmethod`
            @staticmethod
            def flock(__fd: int, __mode: int) -> None:
                print("Pretending to manipulate locking on FD %s" % __fd, file=sys.stderr)
        
        # Create the `fcntl` module object
        fcntl = fake_fcntl_module_class()
    
    
    # Forcefully replace whatever `fcntl` is available
    sys.modules["fcntl"] = fcntl