Enforce that some modules can't be imported from other modules.
Project description
import-guard
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.
Installation
pip install import-guard
Usage
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)
Rules
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()
Exact match
guard.set_deny_rules({"<stdin>": "decimal"})
# shortcut for mod("decimal")
from decimal import Decimal # shows warning
from enum import Enum # ok
Explicit match
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
Match multiple modules
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
Match by regular expression
guard.set_deny_rules({"<stdin>": mod.matches("log.*")})
# shows multiple warnings
from logging.config import dictConfig
Inversion
guard.set_deny_rules({"<stdin>": ~mod.matches("log.*")})
import io # shows warning
Match only module-level imports
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
Match star import
guard.set_deny_rules({"<stdin>": mod.star("csv")})
from csv import * # shows warning
Complex rules
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(".*"))),
})
Non-strict mode
# not enabled for `prod`
if env == "staging":
# warn on forbidden import
guard.enable(strict=False)
elif env == "local":
# raise ForbiddenImportError
guard.enable(strict=True)
Rules hierarchy
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.
Lazy module
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")})
Custom module matcher
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
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
Unit tests
Testing with current Python interpreter:
$ python -m unittest discover tests -v
Testing with different Python versions and interpreters:
$ tox
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file import-guard-0.1.2.tar.gz
.
File metadata
- Download URL: import-guard-0.1.2.tar.gz
- Upload date:
- Size: 11.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: python-requests/2.25.1
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 5450dad0b72254c67e0a3cf3fd1cd18837cec297f8a6d60a33705c70a1b44227 |
|
MD5 | 8dc90df44db22a7775b47e99bbe9186a |
|
BLAKE2b-256 | cb473c0062f39a9cdb37e53e6e3abee7b71caf6596218abb28101fb2f11102d1 |
File details
Details for the file import_guard-0.1.2-py2.py3-none-any.whl
.
File metadata
- Download URL: import_guard-0.1.2-py2.py3-none-any.whl
- Upload date:
- Size: 9.8 kB
- Tags: Python 2, Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: python-requests/2.25.1
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | c24fe73152120435d29464403d80d0cd77315b4565e07573c5cd941f91c91dda |
|
MD5 | 9cbb1896704f70db7c500f2954f9d654 |
|
BLAKE2b-256 | 2eb24991ddb748f80f112deed86873be92db32d16092d24e88c47d60fa6d67ba |