Duke Dougal
Duke Dougal

Reputation: 26406

Python decorating / importing .... how can I eliminate the __init__.py in this solution?

Python 3.8

So I have two versions of my software - a standard edition and a pro edition.

I want to be able to have precisely the same code base, but simply by dropping in some additional files, pro features are enabled.

I am doing this by decorating/wrapping the standard version Python functions, so the pro edition functions are seamlessly extending the behaviour of the standard functions.

I've made a simple example of this below.

The fooble.py imports "do_something" from the foo module.

The init.py tries to import the pro functions from test_decorate_pro.py. If it succeeds in the import, then it wraps the standard edition function "do_something" with the pro edition function of the same name "do_something".

All this works.

The problem is that I wanted to NOT have two init.py files. This solution only works if the standard edition has its own init.py that must be overwritten by the new init.py -

standard edition init.py

import sys
from .test_decorate import do_something

So my question is.... is there a solution for this in which I can eliminate the standard edition init.py somehow? I want my pro features to be added by simply dropping in additional files. I don't want to be overwriting anything in the standard edition - I'd like the pro edition to add its features in to the codebase simply by adding, with no overwriting or deleting. And I want there to be no pro edition related code at all in the standard edition codebase, so I don't want to just use the pro edition init.py Hopefully that makes sense.

the directory structure:

fooble.py
foo/__init__.py
foo/test_decorate.py
foo/test_decorate_pro.py

fooble.py:

from foo import do_something

do_something('xxx')

foo/init.py

import sys
from .test_decorate import do_something
try:
    import foo.test_decorate_pro
except ImportError as e:
    pass

if 'foo.test_decorate_pro' in sys.modules:
    do_something = foo.test_decorate_pro.do_something(do_something)

foo/test_decorate.py

def do_something(name):
   print(f"STANDARD do something {name}")

foo/test_decorate_pro.py

import wrapt

@wrapt.decorator
def do_something(wrapped, instance, args, kwargs):
   print(f"PRO do something args[0]", args, kwargs)
   return wrapped(*args, **kwargs)

Upvotes: 0

Views: 375

Answers (1)

Blckknght
Blckknght

Reputation: 104792

The only good way to avoid needing to overwrite the __init__.py file is to put the logic that imports the pro edition (if it's available), in the standard edition. You could simplify this quite a bit if you made the do_something function from the pro-edition a drop-in replacement of the function of the same name from the standard edition. You can still implement it with a decorator if that makes sense, you'd just go about it differently (and expose fewer details in __init__.py).

Standard edition test_decorate.py:

def do_something(name):
    print(f"STANDARD do something {name}")

Pro edition test_decorate_pro.py:

from .test_decorate import do_something as do_something_standard

def do_something(name):
    print(f"PRO do something {name}")
    do_something_standard(name)    
    print("done with PRO stuff")

Standard edition __init__.py file, which should work for the pro edition too:

try:
    from .test_decorate_pro import do_something  # try to get the pro version first
except ImportError:
    from .test_decorate import do_something      # fall back to the standard edition

Note that I defined the pro-version of the function directly, without using decorator style code. You don't need to make that change, the only important thing is that the do_stuff function that you want to expose is the final result of the decoration, not the decorator. There's not really any good reason to use a higher order function like a decorator in this situation, just call the standard version of the function directly, since we can import it from its module in the pro code.

Upvotes: 1

Related Questions