Enforce that some modules can't be imported from other modules. In runtime!
If you need a static analysis tools, take a look at flake8-import-graph.
Features:
- works in runtime
- checks dynamic imports
- customizable rules
This library has some performance overhead. In some cases it may lead to 1.5-2x slower import time (and startup time respectively). It's recommended to enable import_guard only during the development.
pip install import-guard
from import_guard import guard, mod
guard.set_deny_rules({
# deny `csv` import from `test_proj` and submodules
"test_proj": "csv", # the same as mod("csv")
# deny `selenium` and top_level `test_proj.tasks` imports from test_proj.api
# but allow `test_proj.tasks` import inside the function (lazy import)
# the same as mod("selenium") | (mod("test_proj.tasks") & Flags.TopLevel)
"test_proj.api": ["selenium", mod.top_level("test_proj.tasks")],
# deny `test_proj.api` and `test_proj.business_logic` imports from `test_proj.core`
"test_proj.core": mod.matches(r"test_proj\.(api|business_logic)"),
# deny all imports except `logging` and `yaml`
"test_proj.logging": ~mod.explicit(["logging", "yaml"]),
})
# raise ForbiddenImportError
guard.enable(strict=True)
the code below is copy-pastable into the Python interpreter and assumes the following imports:
from importlib import reload
from import_guard import guard, mod
# enable guard in advance
guard.enable()
guard.set_deny_rules({"<stdin>": "decimal"})
# shortcut for mod("decimal")
from decimal import Decimal # shows warning
from enum import Enum # ok
Consider the following code:
guard.set_deny_rules({"<stdin>": "re"})
import csv # shows warning!
What happened?
csv
imports some modules under the hood, e.g. re
or io
.
We implicitly initiated loading of the re
module through the csv
module (rule matches at depth = 1).
This is the default behavior. You can check only explicit imports using mod.explicit("re")
function.
guard.set_deny_rules({"<stdin>": mod.explicit("re")})
reload(csv) # allowed
import re # shows warning
guard.set_deny_rules({"<stdin>": ["logging", "json"]})
# the same as mod.any(["logging", "json'])
# the same as mod("logging") | mod("json")
import json # shows warning
from logging import getLogger # shows warning
guard.set_deny_rules({"<stdin>": mod.matches("log.*")})
# shows multiple warnings
from logging.config import dictConfig
guard.set_deny_rules({"<stdin>": ~mod.matches("log.*")})
import io # shows warning
It's common practice doing a local import instead of a global one to break a cycle import or to postpone importing until you run code that actually needs the module you're importing.
# deny module-level imports
guard.set_deny_rules({"<stdin>": mod.top_level("array")})
def some_function():
import array # allowed (lazy import)
some_function()
import array # shows warning
guard.set_deny_rules({"<stdin>": mod.star("csv")})
from csv import * # shows warning
Rules are very flexible. You can combine them together in a different ways and build very complex conditions.
mod.explicit(
~mod.top_level(["math", "json"])
| mod.matches("log.*")
)
Nice examples:
- deny non-lazy imports in some module:
guard.set_deny_rules({
"test_proj.business_logic": mod.top_level(mod.matches(".*")),
})
- deny start imports in project:
guard.set_deny_rules({
"test_proj": mod.star(mod.explicit(mod.matches(".*"))),
})
# not enabled for `prod`
if env == "staging":
# warn on forbidden import
guard.enable(strict=False)
elif env == "local":
# raise ForbiddenImportError
guard.enable(strict=True)
The set of deny rule for a module also affects its submodules.
guard.set_deny_rules({
"test_proj": "json",
"test_proj.api": ["selenum", "pandas"],
"test_proj.core": "celery"
})
test_proj.core
disallows json
and celery
imports.
test_proj.api.views
disallows json
, selenium
, pandas
imports.
Consider the following project structure:
# main.py
import api
# api.py
def view():
import tasks
# tasks.py
import pandas
Here main.py
imports api
, which imports tasks
lazily, which imports pandas
at module level.
import_guard
handles this case as lazy module import and will think that pandas being imported lazily.
Thus, in this case, the following rules do not raise a warning:
guard.set_deny_rules({"tasks": mod.top_level("pandas")})
def is_relative_import(import_info, caller_info):
return import_info.level > 1
# deny relative import
guard.set_deny_rules({"proj": mod.hook(is_relative_import)})
from .api import view # shows warning
from proj.api import view # ok
Testing rules directly:
rule = mod.top_level(mod.matches(".*"))
# True; mod1 imported at the module level in mod2
rule.test("mod1", caller="mod2")
# False; mod1 doesn't match the top_level constraint
rule.test("mod1", caller="<stdin>", top_level=False)
Testing deny rules through the guard:
guard.is_import_allowed("csv", caller="test_proj.api") # False
guard.is_import_allowed("logging", caller="test_proj.api") # True
guard.is_import_allowed("selenium", caller="test_proj.api") # False
guard.is_import_allowed(
"test_proj.tasks", caller="test_proj.api"
) # False
guard.is_import_allowed(
"test_proj.tasks", caller="test_proj.api", top_level=False
) # True
Testing with current Python interpreter:
$ python -m unittest discover tests -v
Testing with different Python versions and interpreters:
$ tox