An open API service indexing awesome lists of open source software.

https://github.com/anexen/import-guard

Enforce that some modules can't be imported from other modules. In runtime!
https://github.com/anexen/import-guard

code-style deny-imports import-rules python-imports

Last synced: 2 months ago
JSON representation

Enforce that some modules can't be imported from other modules. In runtime!

Awesome Lists containing this project

README

        

# 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](https://pypi.org/project/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

```bash
pip install import-guard
```

# Usage

```python
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:

```python
from importlib import reload
from import_guard import guard, mod
# enable guard in advance
guard.enable()
```

#### Exact match

```python
guard.set_deny_rules({"": "decimal"})
# shortcut for mod("decimal")

from decimal import Decimal # shows warning

from enum import Enum # ok
```

#### Explicit match

Consider the following code:

```python
guard.set_deny_rules({"": "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.

```python
guard.set_deny_rules({"": mod.explicit("re")})
reload(csv) # allowed
import re # shows warning
```

#### Match multiple modules

```python
guard.set_deny_rules({"": ["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

```python
guard.set_deny_rules({"": mod.matches("log.*")})

# shows multiple warnings
from logging.config import dictConfig
```

#### Inversion

```python
guard.set_deny_rules({"": ~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.

```python
# deny module-level imports
guard.set_deny_rules({"": mod.top_level("array")})

def some_function():
import array # allowed (lazy import)

some_function()
import array # shows warning
```

#### Match star import

```python
guard.set_deny_rules({"": 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.

```python
mod.explicit(
~mod.top_level(["math", "json"])
| mod.matches("log.*")
)
```

Nice examples:

- deny non-lazy imports in some module:

```python
guard.set_deny_rules({
"test_proj.business_logic": mod.top_level(mod.matches(".*")),
})
```

- deny start imports in project:

```python
guard.set_deny_rules({
"test_proj": mod.star(mod.explicit(mod.matches(".*"))),
})
```

#### Non-strict mode

```python
# 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.

```python
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:

```python
# 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:

```python
guard.set_deny_rules({"tasks": mod.top_level("pandas")})
```

#### Custom module matcher

```python
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:

```python
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="", top_level=False)
```

Testing deny rules through the guard:

```python
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:

```bash
$ python -m unittest discover tests -v
```

Testing with different Python versions and interpreters:

```bash
$ tox
```