simplify module structure, wrap up config, fix bugs

This commit is contained in:
gmega 2024-11-27 11:22:07 -03:00
parent 42cd2e7b1c
commit a286dc5e2a
No known key found for this signature in database
GPG Key ID: 6290D34EAD824B18
15 changed files with 238 additions and 78 deletions

46
benchmarks/core/config.py Normal file
View File

@ -0,0 +1,46 @@
"""Basic utilities for structuring experiment configurations based on Pydantic schemas."""
import re
from abc import abstractmethod
from typing import Annotated
from pydantic import BaseModel, IPvAnyAddress, AfterValidator
from typing_extensions import Generic
from benchmarks.core.experiments.experiment import TExperiment
def drop_config_suffix(name: str) -> str:
return name[:-6] if name.endswith('Config') else name
class ConfigModel(BaseModel):
model_config = {
'alias_generator': drop_config_suffix
}
# This is a simple regex which is not by any means exhaustive but should cover gross syntax errors.
VALID_DOMAIN_NAME = re.compile(r"^localhost$|^(?!-)([A-Za-z0-9-]+\.)+[A-Za-z]{2,6}$")
def is_valid_domain_name(domain_name: str):
stripped = domain_name.strip()
matches = VALID_DOMAIN_NAME.match(stripped)
assert matches is not None
return stripped
DomainName = Annotated[str, AfterValidator(is_valid_domain_name)]
class Host(BaseModel):
address: IPvAnyAddress | DomainName
class ExperimentBuilder(Generic[TExperiment], ConfigModel):
""":class:`ExperimentBuilders` can build real :class:`Experiment`s out of :class:`ConfigModel`s. """
@abstractmethod
def build(self) -> TExperiment:
pass

View File

@ -0,0 +1,17 @@
"""Basic definitions for structuring experiments."""
from abc import ABC, abstractmethod
from mypy.graph_utils import TypeVar
class Experiment(ABC):
"""An :class:`Experiment` is an arbitrary piece of code that can be run and measured."""
@abstractmethod
def run(self):
"""Synchronously runs the experiment, blocking the current thread until it's done."""
pass
TExperiment = TypeVar('TExperiment', bound=Experiment)

View File

@ -1,10 +1,11 @@
from typing_extensions import Generic, List
from benchmarks.core.experiments.experiment import Experiment
from benchmarks.core.network import TInitialMetadata, TNetworkHandle, Node
from benchmarks.core.utils import ExperimentData
class StaticDisseminationExperiment(Generic[TNetworkHandle, TInitialMetadata]):
class StaticDisseminationExperiment(Generic[TNetworkHandle, TInitialMetadata], Experiment):
def __init__(
self,
network: List[Node[TNetworkHandle, TInitialMetadata]],

View File

@ -3,7 +3,7 @@ from ipaddress import IPv4Address, IPv6Address
import pytest
from pydantic import ValidationError
from benchmarks.deluge.config.host import Host, DomainName
from benchmarks.core.config import Host, DomainName
def test_should_parse_ipv4_address():
@ -26,6 +26,14 @@ def test_should_parse_localhost():
assert h.address == DomainName('localhost')
def test_should_return_correct_string_representation_for_addresses():
h = Host(address='localhost')
assert str(h.address) == 'localhost'
h = Host(address='192.168.1.1')
assert str(h.address) == '192.168.1.1'
def test_should_fail_invalid_names():
invalid_names = [
'-node-1.local.svc',

View File

@ -65,7 +65,8 @@ def sample(n: int) -> Iterator[int]:
for i in range(n - 1):
j = i + random.randint(0, n - i)
tmp = p[j]
p[j], p[j + 1] = p[j + 1], tmp
p[j] = p[i]
p[i] = tmp
yield p[i]

View File

@ -2,58 +2,65 @@ from itertools import islice
from pathlib import Path
from typing import List
from pydantic import BaseModel, Field, model_validator
from pydantic import BaseModel, Field, model_validator, HttpUrl
from torrentool.torrent import Torrent
from urllib3.util import parse_url
from benchmarks.core.config import Host, ExperimentBuilder
from benchmarks.core.experiments.static_experiment import StaticDisseminationExperiment
from benchmarks.core.utils import sample
from benchmarks.deluge.config.host import Host
from benchmarks.core.utils import sample, RandomTempData
from benchmarks.deluge.deluge_node import DelugeMeta, DelugeNode as RealDelugeNode
class DelugeNode(BaseModel):
class DelugeNodeConfig(BaseModel):
address: Host
daemon_port: int
listen_ports: list[int] = Field(min_length=2, max_length=2)
class DelugeNodeSet(BaseModel):
class DelugeNodeSetConfig(BaseModel):
network_size: int = Field(gt=2)
address: str
daemon_port: int
listen_ports: list[int] = Field(min_length=2, max_length=2)
nodes: List[DelugeNode] = []
nodes: List[DelugeNodeConfig] = []
@model_validator(mode='after')
def expand_nodes(self):
self.nodes = [
DelugeNode(
DelugeNodeConfig(
address=Host(address=self.address.format(node_index=str(i))),
daemon_port=self.daemon_port,
listen_ports=self.listen_ports,
)
for i in range(1, self.network_size + 1)
]
return self
class DelugeExperiment(BaseModel):
DelugeDisseminationExperiment = StaticDisseminationExperiment[Torrent, DelugeMeta]
class DelugeExperimentConfig(ExperimentBuilder[DelugeDisseminationExperiment]):
file_size: int = Field(gt=0)
repetitions: int = Field(gt=0)
seeders: int = Field(gt=0)
shared_volume_path: Path
nodes: List[DelugeNode] | DelugeNodeSet
tracker_announce_url: HttpUrl
nodes: List[DelugeNodeConfig] | DelugeNodeSetConfig
def build(self) -> StaticDisseminationExperiment[Torrent, DelugeMeta]:
def build(self) -> DelugeDisseminationExperiment:
nodes = self.nodes.nodes if isinstance(self.nodes, DelugeNodeSetConfig) else self.nodes
return StaticDisseminationExperiment(
network=[
RealDelugeNode(
name=f'deluge-{i}',
volume=self.shared_volume_path / f'deluge-{i}',
daemon_port=node.daemon_port,
daemon_address=node.address,
daemon_address=str(node.address.address),
)
for i, node in enumerate(self.nodes)
for i, node in enumerate(nodes)
],
seeders=list(islice(sample(len(self.nodes)), self.seeders)),
data=self.data
seeders=list(islice(sample(len(nodes)), self.seeders)),
data=RandomTempData(size=self.file_size,
meta=DelugeMeta('dataset-1', announce_url=parse_url(str(self.tracker_announce_url))))
)

View File

@ -1,21 +0,0 @@
import re
from typing import Annotated
from pydantic import BaseModel, StringConstraints, IPvAnyAddress, AfterValidator
# This is a simple regex which is not by any means exhaustive but should cover gross syntax errors.
VALID_DOMAIN_NAME = re.compile(r"^localhost$|^(?!-)([A-Za-z0-9-]+\.)+[A-Za-z]{2,6}$")
def is_valid_domain_name(domain_name: str):
stripped = domain_name.strip()
matches = VALID_DOMAIN_NAME.match(stripped)
assert matches is not None
return stripped
DomainName = Annotated[str, AfterValidator(is_valid_domain_name)]
class Host(BaseModel):
address: IPvAnyAddress | DomainName

View File

@ -1,36 +0,0 @@
from io import StringIO
from benchmarks.deluge.config.deluge import DelugeNodeSet, DelugeNode
from benchmarks.deluge.config.host import Host
def test_should_expand_node_sets_into_simple_nodes():
nodeset = DelugeNodeSet(
address='deluge-{node_index}.local.svc',
network_size=4,
daemon_port=6080,
listen_ports=[6081, 6082]
)
assert nodeset.nodes == [
DelugeNode(
address=Host(address='deluge-1.local.svc'),
daemon_port=6080,
listen_ports=[6081, 6082],
),
DelugeNode(
address=Host(address='deluge-2.local.svc'),
daemon_port=6080,
listen_ports=[6081, 6082],
),
DelugeNode(
address=Host(address='deluge-3.local.svc'),
daemon_port=6080,
listen_ports=[6081, 6082],
),
DelugeNode(
address=Host(address='deluge-4.local.svc'),
daemon_port=6080,
listen_ports=[6081, 6082],
),
]

View File

@ -52,8 +52,6 @@ class DelugeNode(SharedFSNode[Torrent, DelugeMeta]):
super().__init__(self.downloads_root)
self._init_folders()
def wipe_all_torrents(self):
torrent_ids = list(self.rpc.core.get_torrents_status({}, []).keys())
if torrent_ids:
@ -109,6 +107,7 @@ class DelugeNode(SharedFSNode[Torrent, DelugeMeta]):
@property
def rpc(self) -> DelugeRPCClient:
if self._rpc is None:
self._init_folders()
self.connect()
return self._rpc

View File

@ -0,0 +1,61 @@
from io import StringIO
import yaml
from benchmarks.core.config import Host
from benchmarks.deluge.config import DelugeNodeSetConfig, DelugeNodeConfig, DelugeExperimentConfig
def test_should_expand_node_sets_into_simple_nodes():
nodeset = DelugeNodeSetConfig(
address='deluge-{node_index}.local.svc',
network_size=4,
daemon_port=6080,
listen_ports=[6081, 6082]
)
assert nodeset.nodes == [
DelugeNodeConfig(
address=Host(address='deluge-1.local.svc'),
daemon_port=6080,
listen_ports=[6081, 6082],
),
DelugeNodeConfig(
address=Host(address='deluge-2.local.svc'),
daemon_port=6080,
listen_ports=[6081, 6082],
),
DelugeNodeConfig(
address=Host(address='deluge-3.local.svc'),
daemon_port=6080,
listen_ports=[6081, 6082],
),
DelugeNodeConfig(
address=Host(address='deluge-4.local.svc'),
daemon_port=6080,
listen_ports=[6081, 6082],
),
]
def test_should_build_experiment_from_config():
config_file = StringIO("""
deluge_experiment:
seeders: 3
tracker_announce_url: http://localhost:2020/announce
file_size: 1024
shared_volume_path: /var/lib/deluge
nodes:
network_size: 10
address: 'node-{node_index}.deluge.codexbenchmarks.svc.cluster.local'
daemon_port: 6890
listen_ports: [ 6891, 6892 ]
""")
config = DelugeExperimentConfig.model_validate(yaml.safe_load(config_file)['deluge_experiment'])
experiment = config.build()
assert len(experiment.nodes) == 10

View File

@ -1,4 +1,6 @@
deluge_experiment:
seeders: ${SEEDERS}
tracker_announce_url: ${TRACKER_ANNOUNCE_URL}
file_size: ${FILE_SIZE}
repetitions: ${REPETITIONS}
shared_volume_path: ${SHARED_VOLUME_PATH}

75
poetry.lock generated
View File

@ -301,6 +301,68 @@ pluggy = ">=1.5,<2"
[package.extras]
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "pyyaml"
version = "6.0.2"
description = "YAML parser and emitter for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"},
{file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"},
{file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"},
{file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"},
{file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"},
{file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"},
{file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"},
{file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"},
{file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"},
{file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"},
{file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"},
{file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"},
{file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"},
{file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"},
{file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"},
{file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"},
{file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"},
{file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"},
{file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"},
{file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"},
{file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"},
{file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"},
{file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"},
{file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"},
{file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"},
{file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"},
{file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"},
{file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"},
{file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"},
{file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"},
{file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"},
{file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"},
{file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"},
{file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"},
{file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"},
{file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"},
{file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"},
{file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"},
{file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"},
{file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"},
{file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"},
{file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"},
{file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"},
{file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"},
{file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"},
{file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"},
{file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"},
{file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"},
{file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"},
{file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"},
{file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"},
{file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"},
{file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"},
]
[[package]]
name = "torrentool"
version = "1.2.0"
@ -315,6 +377,17 @@ files = [
[package.extras]
cli = ["click"]
[[package]]
name = "types-pyyaml"
version = "6.0.12.20240917"
description = "Typing stubs for PyYAML"
optional = false
python-versions = ">=3.8"
files = [
{file = "types-PyYAML-6.0.12.20240917.tar.gz", hash = "sha256:d1405a86f9576682234ef83bcb4e6fff7c9305c8b1fbad5e0bcd4f7dbdc9c587"},
{file = "types_PyYAML-6.0.12.20240917-py3-none-any.whl", hash = "sha256:392b267f1c0fe6022952462bf5d6523f31e37f6cea49b14cee7ad634b6301570"},
]
[[package]]
name = "typing-extensions"
version = "4.12.2"
@ -329,4 +402,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
content-hash = "cc71833eb49981fca809cecda62c182668934f3a100ba6165a14ff895600d954"
content-hash = "c10ab6006a3097ae8fcbac02448e98cf18f61146ab311979e1e9d5e735e2369d"

View File

@ -13,10 +13,12 @@ deluge-client = "^1.10.2"
pathvalidate = "^3.2.1"
torrentool = "^1.2.0"
pydantic = "^2.10.2"
pyyaml = "^6.0.2"
[tool.poetry.group.test.dependencies]
pytest = "^8.3.3"
mypy = "^1.13.0"
types-pyyaml = "^6.0.12.20240917"
[tool.mypy]
ignore_missing_imports = true