Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Prevent Python packages from re-exporting imported names

Tags:

python

module

In a Python package, I have the file structure

package/     __init__.py     import_me.py 

The file import_me.py is thought to provide snippets of functionality:

import re import sys  def hello():     pass 

so that package.import_me.hello can be imported dynamically via import. Unfortunately, this also allows to import re and sys as package.import_me.re and package.import_me.sys, respectively.

Is there a way to prevent the imported modules in import_me.py to be re-exported again? Preferably this should go beyond name mangling or underscore-prefixing imported modules, since in my case it might pose a security problem under certain instances.

like image 686
Boldewyn Avatar asked Mar 15 '16 15:03

Boldewyn


People also ask

How do you organize imports in Python?

Organize imports into groups: first standard library imports, then third-party imports, and finally local application or library imports. Order imports alphabetically within each group. Prefer absolute imports over relative imports. Avoid wildcard imports like from module import * .

Why is the use of import all statement not recommended in Python?

Using import * in python programs is considered a bad habit because this way you are polluting your namespace, the import * statement imports all the functions and classes into your own namespace, which may clash with the functions you define or functions of other libraries that you import.


2 Answers

There is no easy way to forbid importing a global name from a module; Python simply is not built that way.

While you could possibly achieve the forbidding goal if you wrote your own __import__ function and shadowed the built-in one, but I doubt the cost in time and testing would be worth it nor completely effective.

What you can do is import the dependent modules with a leading underscore, which is a standard Python idiom for communicating "implementation detail, use at your own risk":

import re as _re import sys as _sys  def hello():     pass 

Note

While just deleting the imported modules as a way of not allowing them to be imported seems like it might work, it actually does not:

import re import sys  def hello():     sys     print('hello')  del re del sys 

and then importing and using hello:

>>> import del_mod >>> del_mod.hello() Traceback (most recent call last):   File "<stdin>", line 1, in <module>   File "del_mod.py", line 5, in hello     sys NameError: global name 'sys' is not defined 
like image 96
Ethan Furman Avatar answered Sep 27 '22 17:09

Ethan Furman


Update. Some experience later, I'd strongly encourage the use of __all__, and discourage the initializer-function idea. There is a lot of tooling, that will be confused by it.

1. Initializer function

An alternative might be wrapping definitions into an initializer function.

## --- exporttest.py --- def _init():     import os                       # effectively hidden      global get_ext                  # effectively exports it     def get_ext(filename):         return _pointless_subfunc(filename)                                        # underscore optional, but good      def _pointless_subfunc(filename): # for the sake of documentation         return os.path.splitext(filename)[1]      if __name__ == '__main__':      # for interactive debugging (or doctest)           globals().update(locals())  # we want all definitions accessible         import doctest         doctest.testmod()          _init()  print('is ``get_ext`` accessible?           ', 'get_ext' in globals()) print('is ``_pointless_subfunc`` accessible?', '_pointless_subfunc' in globals()) print('is ``os`` accessible?                ', 'os' in globals()) 

For comparison:

>>> python3 -m exporttest is ``get_ext`` accessible?            True is ``_pointless_subfunc`` accessible? True is ``os`` accessible?                 True  >>> python3 -c "import exporttest" is ``get_ext`` accessible?            True is ``_pointless_subfunc`` accessible? False is ``os`` accessible?                 False 

1.1. Advantages

  • Actual hiding of the imports.
  • More convenient for interactive code-exploration, as dir(exporttest) is clutter-free.

1.2. Disadvantages

  • Sadly, unlike the import MODULE as _MODULE pattern, it doesn't play nicely with pylint.

    C:  4, 4: Invalid constant name "get_ext" (invalid-name) W:  4, 4: Using global for 'get_ext' but no assignment is done (global-variable-not-assigned) W:  5, 4: Unused variable 'get_ext' (unused-variable) 
  • It also doesn't play nicely with IDE intellisense features.

2. Embrace __all__

Upon further reading, I've found that the pythonic way to do it is to rely on __all__. It controls not only what is exported on from MODULE import *, but also what shows up in help(MODULE), and according to the "We are all adults here" mantra, it is the users own fault if he uses anything not documented as public.

2.1. Advantages

Tooling has best support for this approach (e.g. through editor support for autoimports through the importmagic library).

2.2. Disadvantages

Personally, I find that whole "we are all adults" mantra quite naive; When working under time pressure with no chance to fully understand a code-base before delivering a change, we can do with any help we can get to prevent "shot your own foot" scenarios. Plus, even many popular packages don't really follow best practices like providing useful interactive docstrings, or defining __all__. But it is the pythonic way.

like image 25
kdb Avatar answered Sep 27 '22 17:09

kdb