Skip to content

Commit aee09c8

Browse files
authored
Merge pull request #765 from nolar/refactor-layers-3
Rebalance the dependency tree by moving & tossing the modules
2 parents 7a2ac48 + 22a2d62 commit aee09c8

File tree

230 files changed

+1101
-873
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

230 files changed

+1101
-873
lines changed

.github/workflows/ci.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ jobs:
2020
with:
2121
python-version: "3.9"
2222
- run: pip install -r requirements.txt
23+
- run: lint-imports
2324
- run: isort . --check --diff
2425
continue-on-error: true
2526
- run: isort examples --settings=examples --check --diff

.github/workflows/thorough.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ jobs:
2424
with:
2525
python-version: "3.9"
2626
- run: pip install -r requirements.txt
27+
- run: lint-imports
2728
- run: isort . --check --diff
2829
continue-on-error: true
2930
- run: isort examples --settings=examples --check --diff

.importlinter

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
; Importing constraints for layered layout of modules and packages.
2+
; The goal is higher cohesion and lower coupling of components.
3+
; https://import-linter.readthedocs.io/en/stable/contract_types.html
4+
[importlinter]
5+
root_package = kopf
6+
include_external_packages = True
7+
contract_types =
8+
conditional: _importlinter_conditional.ConditionalImportContract
9+
10+
[importlinter:contract:root-layers]
11+
name = The root framework modules must be layered
12+
type = layers
13+
layers =
14+
kopf.on
15+
kopf._kits
16+
kopf._core
17+
kopf._cogs
18+
19+
[importlinter:contract:core-layers]
20+
name = The internal core must be layered
21+
type = layers
22+
layers =
23+
kopf._core.reactor
24+
kopf._core.engines
25+
kopf._core.intents
26+
kopf._core.actions
27+
28+
[importlinter:contract:cogs-layers]
29+
name = The internal cogs must be layered
30+
type = layers
31+
layers =
32+
kopf._cogs.clients
33+
kopf._cogs.configs
34+
kopf._cogs.structs
35+
kopf._cogs.aiokits
36+
kopf._cogs.helpers
37+
38+
[importlinter:contract:progress-storage]
39+
name = Progress storages must be persistence settings
40+
type = layers
41+
layers =
42+
kopf._cogs.configs.configuration
43+
kopf._cogs.configs.progress
44+
kopf._cogs.configs.conventions
45+
46+
[importlinter:contract:diffbase-storage]
47+
name = Diffbase storages must be persistence settings
48+
type = layers
49+
layers =
50+
kopf._cogs.configs.configuration
51+
kopf._cogs.configs.diffbase
52+
kopf._cogs.configs.conventions
53+
54+
[importlinter:contract:independent-storages]
55+
name = Storage types must be unaware of each other
56+
type = independence
57+
modules =
58+
kopf._cogs.configs.diffbase
59+
kopf._cogs.configs.progress
60+
61+
[importlinter:contract:independent-aiokits]
62+
name = Most asyncio kits must be unaware of each other
63+
type = independence
64+
modules =
65+
kopf._cogs.aiokits.aioadapters
66+
kopf._cogs.aiokits.aiobindings
67+
kopf._cogs.aiokits.aioenums
68+
kopf._cogs.aiokits.aiotoggles
69+
kopf._cogs.aiokits.aiovalues
70+
; but not aiotasks & aiotime!
71+
72+
[importlinter:contract:ban-toolkits]
73+
name = The internals must be unaware of user-facing toolkits
74+
type = forbidden
75+
source_modules =
76+
kopf._cogs
77+
kopf._core
78+
forbidden_modules =
79+
kopf._kits
80+
81+
[importlinter:contract:indenpendent-toolkits]
82+
name = The user-facing toolkits must be unaware of each other
83+
type = independence
84+
modules =
85+
kopf._kits.hierarchies
86+
kopf._kits.runner
87+
kopf._kits.webhooks
88+
89+
[importlinter:contract:allow-3rd-party]
90+
name = 3rd-party clients must be explicitly allowed
91+
type = forbidden
92+
source_modules =
93+
kopf
94+
forbidden_modules =
95+
pykube
96+
kubernetes
97+
ignore_imports =
98+
kopf._core.intents.piggybacking -> pykube
99+
kopf._core.intents.piggybacking -> kubernetes
100+
kopf._cogs.helpers.thirdparty -> pykube
101+
kopf._cogs.helpers.thirdparty -> kubernetes
102+
103+
[importlinter:contract:secure-3rd-party]
104+
name = 3rd-party clients must be secured by conditional imports
105+
type = conditional
106+
source_modules =
107+
kopf
108+
conditional_modules =
109+
pykube
110+
kubernetes

_importlinter_conditional.py

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"""
2+
A contract for the import linter to secure 3rd-party clients importing.
3+
4+
Wrong::
5+
6+
import kubernetes
7+
8+
Right::
9+
10+
try:
11+
import kubernetes
12+
except ImportError:
13+
...
14+
15+
https://import-linter.readthedocs.io/en/stable/custom_contract_types.html
16+
"""
17+
import os.path
18+
19+
import astpath
20+
from importlinter import Contract, ContractCheck, fields, output
21+
22+
23+
class ConditionalImportContract(Contract):
24+
"""
25+
Contract that defines a single forbidden import between
26+
two modules.
27+
"""
28+
source_modules = fields.ListField(subfield=fields.ModuleField())
29+
conditional_modules = fields.ListField(subfield=fields.ModuleField())
30+
31+
def check(self, graph):
32+
failed_details = []
33+
34+
# Combine all source x all target (secured) modules.
35+
conditional_modules = [m for m in self.conditional_modules if m.name in graph.modules]
36+
for source_module in self.source_modules:
37+
for conditional_module in conditional_modules:
38+
39+
# For every pair of source & target, find all import chains.
40+
chains = graph.find_shortest_chains(
41+
importer=source_module.name,
42+
imported=conditional_module.name,
43+
)
44+
for chain in chains:
45+
# Of each chain, we only need the tail for our analysis.
46+
# A sample chain: ('kopf.on', 'kopf._core.intents.registries', 'pykube')
47+
importer, imported = chain[-2:]
48+
details = graph.get_import_details(
49+
importer=importer,
50+
imported=imported
51+
)
52+
53+
# For each import (possible several per file), get its line number and check it.
54+
for detail in details:
55+
ok = self._check_secured_import(detail['importer'], detail['line_number'])
56+
if not ok:
57+
failed_details.append(detail)
58+
59+
return ContractCheck(
60+
kept=not failed_details,
61+
metadata={'failed_details': failed_details},
62+
)
63+
64+
def render_broken_contract(self, check):
65+
for detail in check.metadata['failed_details']:
66+
importer = detail['importer']
67+
imported = detail['imported']
68+
line_number = detail['line_number']
69+
line_contents = detail['line_contents']
70+
output.print_error(
71+
f'{importer} is not allowed to import {imported} without try-except-ImportError:',
72+
bold=True,
73+
)
74+
output.new_line()
75+
output.indent_cursor()
76+
output.print_error(f'{importer}:{line_number}: {line_contents}')
77+
78+
def _check_secured_import(self, mod: str, lno: int) -> bool:
79+
80+
# Some hard-coded heuristics because importlib fails on circular imports.
81+
# TODO: switch to: importlib.util.find_spec(mod)?.origin
82+
path = os.path.join(os.path.dirname(__file__), mod.replace('.', '/')) + '.py'
83+
with open(path, 'rt', encoding='utf-8') as f:
84+
text = f.read()
85+
xtree = astpath.file_contents_to_xml_ast(text)
86+
87+
# For every "import" of interest, find any surrounding "try-except-ImportError" clauses.
88+
for node in xtree.xpath(f'''//Import[@lineno={lno!r}]'''):
89+
tries = node.xpath('''../parent::Try[//ExceptHandler/type/Name/@id="ImportError"]''')
90+
if not tries:
91+
return False
92+
return True

0 commit comments

Comments
 (0)