0

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)?

Mikhail T.
  • 3,043
  • 3
  • 29
  • 46
  • don't name your python file the exact same as a the module. your library should do e.g.,m `from wrapped_fcntl import flock` and your `flock` function should be define at the top-level of the file – Paul H Jun 23 '23 at 17:46
  • This will require me to change dozens of existing `.py` files to `import wrapped_fcntl` -- and I'm trying to avoid such wide-spreading changes :( – Mikhail T. Jun 23 '23 at 17:47
  • You can try (not tested) to create a `types.ModuleType` object and `exec` the code of the standard library `fcntl` in the namespace of this module. – Michael Butscher Jun 23 '23 at 17:59
  • I'm afraid, I need more details, @MichaelButscher :-/ – Mikhail T. Jun 23 '23 at 18:19
  • Create an empty module like `realfcntl = types.ModuleType("realfcntl")`, load the code of the stdlib `fcntl` as string in a variable, e. g. `code`, execute `exec(code, realfcntl.__dict__)`. Then you should be able to use `realfcntl` in the way you want. – Michael Butscher Jun 23 '23 at 18:28
  • Maybe you could use the import hooks. See for example: https://stackoverflow.com/q/43571737/5378816 – VPfB Jun 23 '23 at 19:09

2 Answers2

1

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
dROOOze
  • 1,727
  • 1
  • 9
  • 17
  • Python-3.6.8 -- the version on RHEL7 -- does not have `fcntl` in `sys.modules` on start up. It seems to be loaded (from `fcntl.so`) upon request. – Mikhail T. Jun 23 '23 at 19:21
  • @MikhailT. Thanks for that - my bad for assuming. The Python 3.6 was especially important, as the code given before isn't syntactically compatible with Python 3.6. Fixed it up. – dROOOze Jun 23 '23 at 19:24
  • Oh gosh. Python 3.6 really has a barebones typing module... – dROOOze Jun 23 '23 at 19:28
  • Yeah. But that's _the_ python-3 on RHEL7, so we have to work with that... – Mikhail T. Jun 23 '23 at 19:33
  • Your solution still requires changing existing `.py` files -- or only _one_ of them (the main entry point)? – Mikhail T. Jun 23 '23 at 19:41
  • @MikhailT. Only one - the top-level `__init__.py` or whatever your main entry point is. – dROOOze Jun 23 '23 at 19:41
  • Thanks, this seems to work, actually -- after we changed the `__fd` back to `fd`, that is :-) Worrying, that it had to be so, uhm, unintuitive, but it works! – Mikhail T. Jun 23 '23 at 22:14
  • 1
    @MikhailT. oh? why did you need to change `__fd` back to `fd`? Is it invalid syntax in Python 3.6? The API in typeshed with the double leading underscores indicates you're not allowed to pass a named parameter, which is why it's also like that in the answer. – dROOOze Jun 23 '23 at 22:18
  • Whoops - i forgot to change the name in the print expression. silly me! – dROOOze Jun 23 '23 at 22:26
0

Just import your fake module in such a way that your code sees it as "fcntl":

if os.name == 'nt':
  import mypackage.fake_fcntl as fcntl
else:
  import fcntl

This way, the fake module doesn't need to worry about needing to work anywhere except Windows.


If your code imports fcntl in many places, you can create a small wrapper module doing this and make the code import it rather than fcntl directly:

fcntl_wrapper.py:

if os.name == 'nt':
  from mypackage.fake_fcntl import *
else:
  from fcntl import *

Or just do this inside your fake module proper, but this way, the code is much less elegant:

if os.name == 'nt':
   <implement everything>
else:
   from fcntl import *
ivan_pozdeev
  • 33,874
  • 19
  • 107
  • 152
  • But I'll still need to modify each existing file, that currently imports `fcntl`, to import `fake_fcntl` instead, wouldn't I? I was hoping, I could just drop a single file into the top-level directory, and make all the work there... I know, I could do it in Tcl -- there's got to be a way in Python too... – Mikhail T. Jun 23 '23 at 18:18
  • It's possible -- by prepending the path of that file to `sys.path`. But it's a worse solution -- because it loses the information that what you're importing is not the real module but a wrapper. Better make it explicit. It's a one-time task, should not be much trouble. Moreover, if the facilities that deal with your program's concurrency are so intersperced across the codebase that you're worried about smth like that, that's probably a sign that you need to refactor them out and consolidate and encapsulate in one place. – ivan_pozdeev Jun 23 '23 at 18:25
  • If manipulating `sys.path` is supposed to work, why didn't my removal of the current directory from it help? It is, what I tried before posting this question :-/ – Mikhail T. Jun 23 '23 at 18:33
  • I suppose that's because `sys.path[0]` is the current directory of the running process, not the directory where the current file is located. – ivan_pozdeev Jun 23 '23 at 18:34
  • It's also possible to call the underlying import machinery directly to load the specific file as a module with a specific name -- so that the next time it's `import`ed, it's not searched. – ivan_pozdeev Jun 23 '23 at 18:35
  • The process runs in the same directory as where the code is located in our case... – Mikhail T. Jun 23 '23 at 18:38
  • Maybe as dROOOze suggests, it's already been loaded when you're manipulating `sys.path` (on my machine (Ubunu 22), it's NOT built-in). I also see that assuming `''` at the start of `sys.path` is not always true (e.g. IPython rearranges it somewhat). – ivan_pozdeev Jun 23 '23 at 19:22
  • In any case, I can confirm that the search for built-in modules is not overridden by manipulating `sys.path` because they're searched for before searching files (see `sys.meta_path`). – ivan_pozdeev Jun 23 '23 at 19:24