5

I would like to be able to import a python module which is actually located in a subdirectory of another module.

I am developing a framework with plug-ins. Since I'm expecting to have a few thousands (there's currently >250 already) and I don't want one big directory containing >1000 files I have them ordered in directories like this, where they are grouped by the first letter of their name:

framework\
    __init__.py
    framework.py
    tools.py
    plugins\
        __init__.py
        a\
            __init__.py
            atlas.py
            ...
        b\
            __init__.py
            binary.py
            ...
        c\
            __init__.py
            cmake.py
        ...

Since I would not like to impose a burden on developers of other plugins, or people not needing as many as I have, I would like to put each plugin in the 'framework.plugins' namespace. This way someone adding a bunch of private plugins can just do so by adding them in the folder framework.plugins and there provide a __init__.py file containing:

from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)

however, currently this setup is forcing them to also use the a-z subdirectories. Sometimes a plugin is extending another plugin, so now I have a

from framework.plugins.a import atlas

and I would like to have

from framework.pugins import atlas

Is there any way to declare a namespace where the full name space name actually doesn't map to a folder structure?

I am aware of the pkg_resources package, but this is only available via setuptools, and I'd rather not have an extra dependency.

import pkg_resources
pkg_resources.declare_namespace(__name__)

The solution should work in python 2.4-2.7.3

update: Combining the provided answers I tried to get a list of all plugins imported in the __init__.py from plugins. However, this fails due to dependencies. Since a plugin in the 'c' folder tries to import a plugin starting with 't', and this one has not been added yet.

plugins = [ x[0].find_module(x[1]).load_module(x[1]) for x in pkgutil.walk_packages([ os.path.join(framework.plugins.__path__[0], chr(y)) for y in xrange(ord('a'), ord('z') + 1) ],'framework.plugins.' ) ]

I'm not sure If I'm on the right track here, or just overcomplicating things and better write my own PEP302 importer. However, I can't seem to find any decent examples of how these should work.

Update: I tried to follow the suggesting of wrapping the __getattr__ function in my __init__.py, but this seems to no avail.

import pkgutil
import os
import sys
plugins = [x[1] for x in pkgutil.walk_packages([ os.path.join(__path__[0], chr(y)) for y in xrange(ord('a'), ord('z') + 1) ] )]
import types
class MyWrapper(types.ModuleType):
    def __init__(self, wrapped):
            self.wrapped = wrapped

    def __getattr__(self, name):
           if name in plugins:
                   askedattr =  name[0] + '.' + name
            else:
                    askedattr = name
            attr = getattr(self.wrapped, askedattr)
            return attr


sys.modules[__name__] = MyWrapper(sys.modules[__name__])
4
  • Out of curiosity, do you have a specific reason for which you'd rather not depend on setuptools? Unless you're in a very specific situation, I honestly think you can expect reasonnably expect users to have setuptools / pip installed anyway (And setuptools itself is widely available and easily installed using the egg.). Commented Aug 20, 2012 at 16:34
  • This is a framework for compiling software from source currently counting >16000 lines of code. We don't assume the user has root privileges and/or can easily install software in any other way. So currently the only real dependency is python> 2.4 < 3.0. Commented Aug 20, 2012 at 16:54
  • setuptools/pip will either install to the root installation or to the user's home directory ( or to a virtualenv ). it's not necessary to have root privileges to install python modules. Commented Aug 21, 2012 at 16:27
  • Yes, but if you haven't got setuptools or pip yet, it's not that easy to actually install it. We're going for 1 tarbal, unzipping it, done. Also, I felt there had to be a really simple way, I found it with manually extending __path__ now. So really not needed. Also, declare_namespace still needed you to leave the directory structure to agree with the namespace. Commented Aug 23, 2012 at 8:56

4 Answers 4

1

A simple solution would be to import your a, b... modules in plugins.__init__, like:

from a import atlas
from b import binary
...
Sign up to request clarification or add additional context in comments.

7 Comments

I would be adding new plugins all the day, since this framework is under heavy development by a couple of different developers, adding an import every time is just not an option. I'm looking into a clean solution, preferably only 1-2 lines of python.
And what if, say, you have another atlas.py in module b ?
I'm sorry, the a-z directory structure is there to reflect the first letter of the module name, this is to group the files, so I don't end up with 1 directory containing >1000 files.
Then, what about pkgutil.extend_path, as described here and [there] (docs.python.org/library/pkgutil.html) ?
why don't you want a directory with > 1000 files ? any modern filesystem can handle that number fine. you're also not really safeguarding against any real folder content limits - this assumes there's a relatively even distribution of modules by the first letter - this rarely/never happens. it's not unlikely that a single folder could have over 1000 modules if you double your current size.
|
1

I'd suggest lazy-loading the modules by subclassing types.ModuleType and converting modname to modname[0] + '.' + modname on demand; this should be less work than implementing a module loader.

You can look at apipkg for an example of how to do this.

7 Comments

How would I then let python know to use this new ModuleType for modules in the framework.plugins directory?
@JensTimmerman you replace the module in sys.modules[pkgname]. See bitbucket.org/hpk42/apipkg/src/tip/apipkg.py#cl-14
So I had to repeat this for every module again? This will again give me the dependency problem where I have to first resolve the dependencies. I'd rather just tell python to use the new type for each module in this submodule, but assigning to type.ModuleType does not work...
@JensTimmerman no, just for the module framework.plugins; a reference from framework.plugins modname is a getattr on sys.modules["framework.plugins"].
Yes, I meant this basically is about same as importing each plugin in the framework/plugins/__init__.py ? But doing it lazy.
|
1

Don't use the pkgutil.extend_path function here, it tries to do the opposite of what you're trying to accomplish:

This will add to the package’s __path__ all subdirectories of directories on sys.path named after the package. This is useful if one wants to distribute different parts of a single logical package as multiple directories.

Just extending __path__ with the subdirectories in your framework.plugins.__init__.py works just fine.

So the solution to this problem is: put this in your __init__.py:

__path__.extend([os.path.join(__path__[0],chr(y)) for y in range(ord('a'),ord('z')+1)])

Comments

0

This isn't a particularly fast solution (startup overheads), but what about having plugins.__init__ scrape the filesystem and import each found file into the local namespace?

import glob
import sys

thismodule = sys.modules[__name__]

for plugin in glob.glob("?/*"):
    _temp = __import__(plugin.split("/")[0],
                   globals(),
                   locals(),
                   [plugin.split("/")[1]],
                   -1)
   setattr(thismodule, plugin.split("/")[1], getattr(_temp, plugin.split("/")[1]))

3 Comments

Your code got me thinking, and I came up with this: modules = [ x[0].find_module(x[1]).load_module(x[1]) for x in pkgutil.walk_packages([ os.path.join(framework.plugins.__path__[0],chr(y)) for y in xrange(ord('a'),ord('z')) ],'framework.plugins.' ) ] But this doesn't work because of dependencies. A plugin in the /c/ directory already tries to import one in the /t/ directory, which is not available yet at that time, since I haven't added it as an attribute yet.
Interdependence like that could cause you more problems further on - either import directly (from plugins.t import Thing) or rework the interdependencies to something external to the plugins.
We're currently using the direct import indeed. But I thought it would be relatively easy to just add everything to a namespace, without the directory structure interfering. I just can't seem to find the easiest way.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.