1

The objective: I have a package with submodules that I would like to be accessible in the most straightforward way possible. The submodules contain classes to take advantage of the class structure, but don't need to be initialized (as they contain static and class methods). So, ideally, I would like to access them as follows:

from myPackage.subModule import someMethod
print (someMethod)

from myPackage import subModule
print (subModule.someMethod)

import myPackage
print(myPackage.subModule.someMethod)

Here is the package structure:

myPackage ─┐
    __init__.py
    subModule
    subModule2
    etc.

Example of a typical submodule:

# submodule.py
class SomeClass():

    someAttr = list(range(10))

    @classmethod
    def someMethod(cls):
        pass

    @staticmethod
    def someMethod2():
        pass

Here is the code I have in my '__init __.py': In order to achieve the above; it attempts to set attributes for each class at the package level, and the same for it's methods at the sub-module level.

# __init__.py
def import_submodules(package, filetypes=('py', 'pyc', 'pyd'), ignoreStartingWith='_'):
    '''Import submodules to the given package, expose any classes at the package level
    and their respective class methods at submodule level.

    :Parameters:
        package (str)(obj) = A python package.
        filetypes (str)(tuple) = Filetype extension(s) to include.
        ignoreStartingWith (str)(tuple) = Ignore submodules starting with given chars.
    '''
    if isinstance(package, str):
        package = sys.modules[package]
    if not package:
        return

    pkg_dir = os.path.dirname(os.path.abspath(package.__file__))
    sys.path.append(pkg_dir) #append this dir to the system path.

    for mod_name in os.listdir(pkg_dir):
        if mod_name.startswith(ignoreStartingWith):
            continue

        elif os.path.isfile(os.path.join(pkg_dir, mod_name)):
            mod_name, *mod_ext = mod_name.rsplit('.', 1)
            if filetypes:
                if not mod_ext or mod_ext[0] not in filetypes:
                    continue

        mod = importlib.import_module(mod_name)
        vars(package)[mod_name] = mod

        classes = inspect.getmembers(mod, inspect.isclass)

        for cls_name, clss in classes:
            vars(package)[cls_name] = clss

            methods = inspect.getmembers(clss, inspect.isfunction)

            for method_name, method in methods:
                vars(mod)[method_name] = method

        del mod_name


import_submodules(__name__)

At issue is this line:

vars(mod)[method_name] = method

Which ultimately results in: (indicating that the attribute was not set)

from myPackage.subModule import someMethod
ImportError: cannot import name 'someMethod' from 'myPackage.subModule'

I am able to set the methods as attributes to the module within that module, but setting them from outside (ie. in the package __init __), isn't working as written. I understand this isn't ideal to begin with, but my current logic is; that the ease of use, outweighs any perceived issues with namespace pollution. I am, of course, always open to counter-arguments.

m3trik
  • 333
  • 2
  • 13

4 Answers4

0

I just checked it on my machine. Created a package myPackage with a module subModule that has a function someMethod.

I run a python shell with working directory in the same directory that the myPackage is in, and to get these 3 import statements to work:

from myPackage.subModule import someMethod

from myPackage import subModule

import myPackage

All I had to do was to create an __init__.py with this line in it:

from . import subModule
LITzman
  • 730
  • 1
  • 16
  • First of all, thank you for trying to help. What you describe should indeed work should the submodules not contain classes. What I need, is for each submodule's class methods to be exposed at module level dynamically in the package __init __. – m3trik Nov 25 '22 at 11:12
  • Oh I understand it now. I'm not next to a computer so I can't test a solution but If this code is meant to be used by people who are not aware of the project "unique" structure then I recommend you to create functions that will manage initiating a class and calling it's methods internally, as what you described is very not intuitive, even if those functions are 100% "boilerplate". I'll try to figure out a solution soon anyway – LITzman Nov 25 '22 at 21:38
  • It's more a collection of backend tools that are used all over the place (hens the weight I put on quick, straightforward imports). Still, it's best practice to treat it as standard production code, so I'll pull it out of classes if I'm forced to in the end. Still interested in exploring the formor method as a proof of concept though. Surely it's possible to set a module attribute/ modify it's globals from outside of that module. – m3trik Nov 26 '22 at 11:11
0

Found a nice "hacky" solution -

subModule.py:

class myClass:

    @staticmethod
    def someMethod():
        print("I have a bad feeling about this")

myInstance = myClass()
someMethod = myInstance.someMethod

init.py is empty

LITzman
  • 730
  • 1
  • 16
  • I don't see anything hacky about instantiating a class within it's module, I've seen people do it often enough, it just isn't very helpful to my particular problem. For now, I'm moving on. Thanks anyhow. – m3trik Nov 27 '22 at 12:11
  • The hacky part IMO is creating a variable that holds a function :p Anyway, this allows you to import `someMethod` in all the ways you described, and it can even be automated for every static method in a module using `inspect` to enumerate them – LITzman Nov 27 '22 at 19:08
0

Still scratching my head of why I am unable to do this from the package __init __, but this solution works with the caveat it has to be called at the end of each submodule. Perhaps someone, in the future, someone can chime in as to why this wasn't working when completely contained in the __init __.

def addMembers(module, ignoreStartingWith='_'):
    '''Expose class members at module level.

    :Parameters:
        module (str)(obj) = A python module.
        ignoreStartingWith (str)(tuple) = Ignore class members starting with given chars.

    ex. call: addMembers(__name__)
    '''
    if isinstance(module, str):
        module = sys.modules[module]
    if not module:
        return

    classes = inspect.getmembers(module, inspect.isclass)

    for cls_name, clss in classes:
        cls_members = [(o, getattr(clss, o)) for o in dir(clss) if not o.startswith(ignoreStartingWith)]
        for name, mem in cls_members:
            vars(module)[name] = mem
m3trik
  • 333
  • 2
  • 13
0

This is the solution I ended up going with. It needs to be put at the end of each submodule of your package. But, it is simple and in addition to all the standard ways of importing, allows you to import a method directly:

def __getattr__(attr):
    '''Attempt to get a class attribute.

    :Parameters:
        attr (str): A name of a class attribute.

    :Return:
        (obj) The attribute.
    '''
    try:
        return getattr(Someclass, attr)
    except AttributeError as error:
        raise AttributeError(f'{__file__} in __getattr__\n\t{error} ({type(attr).__name__})')
from somePackage.someModule import someMethod
m3trik
  • 333
  • 2
  • 13