Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

__init__.py required for pkgutil.walk_packages in python3?

PEP420 makes __init__.py files optional: https://docs.python.org/3/whatsnew/3.3.html#pep-420-implicit-namespace-packages

Though it seems without them, pkgutil.walk_packages does not function as desired: https://docs.python.org/3/library/pkgutil.html#pkgutil.walk_packages

Consider the following example:

$ tree foo
foo
├── bar
│   ├── baz.py
│   └── __init__.py
├── __init__.py
└── womp.py

And a test script

# test.py
import pkgutil

import foo


for _, mod, _ in pkgutil.walk_packages(foo.__path__, foo.__name__ + '.'):
    print(mod)

In both python2 and python3 I get the following output:

$ python2.7 test.py
foo.bar
foo.bar.baz
foo.womp
$ python3.5 test.py
foo.bar
foo.bar.baz
foo.womp

Removing the __init__.py files and only using python3, I get this:

$ find -name '__init__.*' -delete
$ python3.5 test.py
foo.bar

The modules are definitely importable:

$ python3.5 -c 'import foo.bar.baz'
$

Is this a bug? Am I forced to create the __init__.py files to achieve what I want?

like image 213
Anthony Sottile Avatar asked Dec 17 '16 22:12

Anthony Sottile


People also ask

Is __ init __ needed in Python 3?

Python 3.3+ has Implicit Namespace Packages that allow it to create a packages without an __init__.py file. Allowing implicit namespace packages means that the requirement to provide an __init__.py file can be dropped completely, and affected ... . The old way with __init__.py files still works as in Python 2.

What is __ init __ py used for in Python?

The __init__.py file makes Python treat directories containing it as modules. Furthermore, this is the first file to be loaded in a module, so you can use it to execute code that you want to run each time a module is loaded, or specify the submodules to be exported.

What should __ init __ py contain?

The gist is that __init__.py is used to indicate that a directory is a python package. (A quick note about packages vs. modules from the python docs: "From a file system perspective, packages are directories and modules are files.").

What is Pkgutil in Python?

Source code: Lib/pkgutil.py. This module provides utilities for the import system, in particular package support.


2 Answers

As a workaround (maybe this will help someone else), I'm using something like this. It isn't perfect (broken if pwd changes or if the packages are not rooted at .) but it does do what I want to do for my simple usecase:

def walk_modules(pkg):
    assert hasattr(pkg, '__path__'), 'This function is for packages'
    path = pkg.__name__.replace('.', '/')
    modules = []
    for root, _, filenames in os.walk(path):
        for filename in filenames:
            if filename.startswith('.') or not filename.endswith('.py'):
                continue
            path = os.path.join(root, filename)
            modules.append(os.path.splitext(path)[0].replace('/', '.'))
    for module in sorted(modules):
        yield __import__(module, fromlist=['__trash'])
like image 182
Anthony Sottile Avatar answered Sep 27 '22 20:09

Anthony Sottile


Another approach that respects the __path__ attribute for merged namespace packages:

import pkgutil
from pathlib import Path

def iter_packages(path, prefix, onerror=None):
    """ Find packages recursively, including PEP420 packages """
    yield from pkgutil.walk_packages(path, prefix, onerror)
    namespace_packages = {}
    for path_root in path:
        for sub_path in Path(path_root).iterdir():
            # TODO: filter to legal package names
            if sub_path.is_dir() and not (sub_path / '__init__.py').exists():
                ns_paths = namespace_packages.setdefault(prefix + sub_path.name, [])
                ns_paths.append(str(sub_path))
    for name, paths in namespace_packages.items():
        # TODO: construct a loader somehow?
        yield pkgutil.ModuleInfo(None, name, True)
        yield from iter_packages(paths, name + '.', onerror)
like image 25
Eric Avatar answered Sep 27 '22 22:09

Eric