set-package-attribute

Allow modules which are located inside packages to be run as scripts, even when explicit relative imports are used.

Description

Modules inside packages cannot ordinarily be run as scripts if they use explicit relative imports or if they import any module which uses explicit relative imports. Setting the __package__ attribute of the module allows these imports to work as usual, but it is not quite as simple as just setting the attribute.

Importing set_package_attribute from such a script and running its init function will set the __package__ attribute of the script’s module (which is always __main__). This is intended for use in any module inside a package which might ever be run as a script and which either uses intra-package imports or else imports other modules from within the same package which do so.

To use set_package_attribute just import it before any of the non-system files, inside any module that you might want to run as a script, and call the init function. This should be done inside a guard conditional, so that it only runs when the module is executed as a script:

if __name__ == "__main__":
    import set_package_attribute
    set_package_attribute.init()

The guard conditional is not required if the module is only ever run as a script, i.e., if it is never imported by any module. The init function must be called before any within-package explicit relative imports, and before importing any modules from within the same package which themselves use such imports. Any previously-set __package__ attribute (other than None) will be left unchanged, so running the init twice is the same as running it once.

If you are happy with the default values to the init arguments then as a shortcut you can perform a single import which will call init automatically:

if __name__ == "__main__":
    import set_package_attribute_magic

The init function takes one optional boolean parameter, modify_syspath. If modify_syspath is true then whenever the __package__ attribute is set by init the first element of sys.path is also deleted. This avoids some of the potential aliasing and shadowing problems that can arise when directories inside packages are added to sys.path (since Python automatically inserts a script’s directory as the first element of sys.path). This is not guaranteed not to create other problems, but it works in test cases. The default is true, i.e., sys.path has the first element deleted by default on a call to init. If such a deletion is performed the sys.path entry is saved as set_package_attribute.deleted_sys_path_0_value for informational purposes (replacing the default None value).

Even the use of absolute intra-package imports within a script requires that the package itself be discoverable on sys.path. This module also takes care of that, temporarily adding the directory containing the package’s root directory to sys.path and then restoring the original sys.path after doing the import.

Another use of the set_package_attribute module is that it allows explicit relative imports to be used for intra-package imports in the main module of a Python application (i.e., in a Python application’s entry-point script). Usually, as described in the Python documentation, these imports should always be absolute imports. That is, without the __package__ attribute being set such modules should generally only import intra-package modules by their full, package-qualified names (with the package itself being discoverable on sys.path). The guard conditional would not be required in this case, assuming the entry-point module is only ever used to start the application and is not imported from another Python file.

Installation

The simplest way to install is to use pip:

pip install set-package-attribute

The module can also be installed by downloading it or cloning it from GitHub and running its setup.py file in the usual way. The clone command is:

git clone https://github.com/abarker/set-package-attribute

The distribution currently consists of a single module, which could also simply be copied to somewhere in the Python path (to avoid adding a dependency).

Some technical notes

  • Internally, this module also needs to import the package directory containing the script module (under its full package-qualified name). A side-effect of this is that any __init__.py files in the package path down to the script (from the top package level) will be executed. Similarly, any modules you import as intra-package imports will cause the init files down to them to be run. This could give unexpected results as compared with simply running the script not as a part of the package, depending on how __init__.py files are used in a given package. The effect is essentially the same as if the script file had been imported using its full, package-qualified module name.

  • The basic mechanism still works if the guard conditional is left off. Without it, though, if a script in a different package/project were to explicitly or implicity import a module which itself imports and uses set_package_attribute, a potential problem would occur. This includes importing that module as part of its full package, say if the __init__.py of that imported package imports the module (which happens quite often). This would have the side-effect of setting the package attribute of the __main__ module, which in this case is the module for a script in an entirely different package. Often this would not cause a problem, since it would at least be set correctly, but it might result in unexpected behavior that could be difficult to trace.

  • This only works for intra-package imports, i.e., for a module importing another module from within its own package. You still cannot directly import a module from inside a different package and expect its intra-package imports to work.

  • An alternative approach is to always execute scripts inside packages with the -m flag set. For example, to execute a script module_name.py, which is in a subdirectory inside a package pkg_toplevel, you would use:

    python -m pkg_toplevel.pkg_subdir.module_name
    

    This requires the full package name to be used, however, and has a different invocation method than other scripts. Also, the directory containing the top-level package directory pkg_toplevel (i.e., its parent directory) needs to be in Python’s package search path in order for this approach to work.

Further details

When init is run this module searches for the module __main__ in sys.modules. If that module is not found then nothing is done. If __main__ is found then the __package__ attribute for the __main__ module is computed by going up the directory tree from its source file, looking for __init__.py files. The __package__ attribute is then set in the __main__ module’s namespace. Only the __main__ module is ever modified. If there is already a __package__ attribute in the namespace of __main__ then nothing is done.

After setting the __package__ attribute in the __main__ module the package directory containing the __main__ module is then imported, using its fully-qualified name. An entry for the __main__ module is also added to sys.modules under its full package name.

See also

This module is based on the basic method described in the answers on this StackOverflow page: http://stackoverflow.com/questions/2943847/

Note

As of 2007 Guido van Rossum viewed running scripts that are inside packages as an anti-pattern (see https://mail.python.org/pipermail/python-3000/2007-April/006793.html). Nevertheless, it can be a convenient and useful pattern in certain situations. He did later approve PEP 366, which defined the __package__ attribute to handle the situation.

Functions

set_package_attribute.init(modify_syspath=True)[source]

Set the __package__ attribute of the module __main__ if it is not already set.

If modify_syspath is true then whenever the __package__ attribute is set the first element of sys.path (the current directory of the script) is also deleted from the path list.