Skip to content

Commit 08d3e55

Browse files
committed
feat: lazy load ansible imports
1 parent 0084e7b commit 08d3e55

File tree

4 files changed

+125
-33
lines changed

4 files changed

+125
-33
lines changed

tdp/cli/commands/default_diff.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
from typing import TYPE_CHECKING, Optional
1111

1212
import click
13-
from ansible.utils.vars import merge_hash
1413

1514
from tdp.cli.params import collections_option, vars_option
15+
from tdp.core.ansible_loader import AnsibleLoader
1616
from tdp.core.constants import DEFAULT_VARS_DIRECTORY_NAME
1717
from tdp.core.variables import ClusterVariables, Variables
1818

@@ -73,7 +73,7 @@ def service_diff(collections, service):
7373
with Variables(default_service_vars_filepath).open(
7474
"r"
7575
) as default_variables:
76-
default_service_varfile = merge_hash(
76+
default_service_varfile = AnsibleLoader.load_merge_hash()(
7777
default_service_varfile, default_variables
7878
)
7979

tdp/core/ansible_loader.py

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Copyright 2022 TOSIT.IO
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
5+
class AnsibleLoader:
6+
"""Lazy loader for Ansible classes and functions.
7+
8+
This class is required as ansible automatically generate a config when imported.
9+
"""
10+
11+
_merge_hash = None
12+
_from_yaml = None
13+
_AnsibleDumper = None
14+
_InventoryCLI = None
15+
_InventoryReader = None
16+
_InventoryManager = None
17+
_CustomInventoryCLI = None
18+
19+
@classmethod
20+
def load_merge_hash(cls):
21+
"""Load the merge_hash function from ansible."""
22+
if cls._merge_hash is None:
23+
from ansible.utils.vars import merge_hash
24+
25+
cls._merge_hash = merge_hash
26+
27+
return cls._merge_hash
28+
29+
@classmethod
30+
def load_from_yaml(cls):
31+
"""Load the from_yaml function from ansible."""
32+
if cls._from_yaml is None:
33+
from ansible.parsing.utils.yaml import from_yaml
34+
35+
cls._from_yaml = from_yaml
36+
37+
return cls._from_yaml
38+
39+
@classmethod
40+
def load_AnsibleDumper(cls):
41+
"""Load the AnsibleDumper class from ansible."""
42+
if cls._AnsibleDumper is None:
43+
from ansible.parsing.yaml.dumper import AnsibleDumper
44+
45+
cls._AnsibleDumper = AnsibleDumper
46+
47+
return cls._AnsibleDumper
48+
49+
@classmethod
50+
def load_InventoryCLI(cls):
51+
"""Load the InventoryCLI class from ansible."""
52+
if cls._InventoryCLI is None:
53+
from ansible.cli.inventory import InventoryCLI
54+
55+
cls._InventoryCLI = InventoryCLI
56+
57+
return cls._InventoryCLI
58+
59+
@classmethod
60+
def load_InventoryReader(cls):
61+
"""Load the InventoryReader class from ansible."""
62+
if cls._InventoryReader is None:
63+
from tdp.core.inventory_reader import InventoryReader
64+
65+
cls._InventoryReader = InventoryReader
66+
67+
return cls._InventoryReader
68+
69+
@classmethod
70+
def load_InventoryManager(cls):
71+
"""Load the InventoryManager class from ansible."""
72+
if cls._InventoryManager is None:
73+
from ansible.inventory.manager import InventoryManager
74+
75+
cls._InventoryManager = InventoryManager
76+
77+
return cls._InventoryManager
78+
79+
@classmethod
80+
def get_CustomInventoryCLI(cls):
81+
if cls._CustomInventoryCLI is None:
82+
83+
class CustomInventoryCLI(cls.load_InventoryCLI()):
84+
"""Represent a custom Ansible inventory CLI which does nothing.
85+
This is used to load inventory files with Ansible code.
86+
"""
87+
88+
def __init__(self):
89+
super().__init__(["program", "--list"])
90+
# "run" must be called from CLI (the parent of InventoryCLI), to
91+
# initialize context (reading ansible.cfg for example).
92+
super(cls.load_InventoryCLI(), self).run()
93+
# Get InventoryManager instance
94+
_, self.inventory, _ = self._play_prereqs()
95+
96+
# Avoid call InventoryCLI "run", we do not need to run InventoryCLI
97+
def run(self):
98+
pass
99+
100+
cls._CustomInventoryCLI = CustomInventoryCLI()
101+
102+
return cls._CustomInventoryCLI

tdp/core/inventory_reader.py

+9-25
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,29 @@
11
# Copyright 2022 TOSIT.IO
22
# SPDX-License-Identifier: Apache-2.0
33

4-
from typing import Optional, TextIO
4+
from typing import TYPE_CHECKING, Optional, TextIO
55

66
import yaml
7-
from ansible.cli.inventory import InventoryCLI
8-
from ansible.inventory.manager import InventoryManager
7+
8+
from tdp.core.ansible_loader import AnsibleLoader
99

1010
try:
1111
from yaml import CLoader as Loader
1212
except ImportError:
1313
from yaml import Loader
1414

1515

16-
# From ansible/cli/inventory.py
17-
class _CustomInventoryCLI(InventoryCLI):
18-
"""Represent a custom Ansible inventory CLI which does nothing.
19-
This is used to load inventory files with Ansible code.
20-
"""
21-
22-
def __init__(self):
23-
super().__init__(["program", "--list"])
24-
# "run" must be called from CLI (the parent of InventoryCLI), to
25-
# initialize context (reading ansible.cfg for example).
26-
super(InventoryCLI, self).run()
27-
# Get InventoryManager instance
28-
_, self.inventory, _ = self._play_prereqs()
29-
30-
# Avoid call InventoryCLI "run", we do not need to run InventoryCLI
31-
def run(self):
32-
pass
33-
34-
35-
custom_inventory_cli_instance = _CustomInventoryCLI()
16+
if TYPE_CHECKING:
17+
from ansible.inventory.manager import InventoryManager
3618

3719

3820
class InventoryReader:
3921
"""Represent an Ansible inventory reader."""
4022

41-
def __init__(self, inventory: Optional[InventoryManager] = None):
42-
self.inventory = inventory or custom_inventory_cli_instance.inventory
23+
def __init__(self, inventory: Optional["InventoryManager"] = None):
24+
if inventory is None:
25+
inventory = AnsibleLoader.get_CustomInventoryCLI().inventory
26+
self.inventory = inventory
4327

4428
def get_hosts(self, *args, **kwargs) -> list[str]:
4529
"""Takes a pattern or list of patterns and returns a list of matching

tdp/core/variables/variables.py

+12-6
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,8 @@
1010
from weakref import proxy
1111

1212
import yaml
13-
from ansible.parsing.utils.yaml import from_yaml
14-
from ansible.parsing.yaml.dumper import AnsibleDumper
15-
from ansible.utils.vars import merge_hash
1613

14+
from tdp.core.ansible_loader import AnsibleLoader
1715
from tdp.core.types import PathLike
1816

1917

@@ -90,7 +88,7 @@ def merge(self, mapping: MutableMapping) -> None:
9088
Args:
9189
mapping: Mapping to merge.
9290
"""
93-
self._content = merge_hash(self._content, mapping)
91+
self._content = AnsibleLoader.load_merge_hash()(self._content, mapping)
9492

9593
def __getitem__(self, key):
9694
return self._content.__getitem__(key)
@@ -131,7 +129,10 @@ def __init__(self, path: Path, mode: Optional[str] = None):
131129
self._file_path = path
132130
self._file_descriptor = open(self._file_path, mode or "r+")
133131
# Initialize the content of the variables file
134-
super().__init__(content=from_yaml(self._file_descriptor) or {}, name=path.name)
132+
super().__init__(
133+
content=AnsibleLoader.load_from_yaml()(self._file_descriptor) or {},
134+
name=path.name,
135+
)
135136

136137
def __enter__(self) -> _VariablesIOWrapper:
137138
return proxy(self)
@@ -152,7 +153,12 @@ def _flush_on_disk(self) -> None:
152153
# Write the content of the variables file on disk
153154
self._file_descriptor.seek(0)
154155
self._file_descriptor.write(
155-
yaml.dump(self._content, Dumper=AnsibleDumper, sort_keys=False, width=1000)
156+
yaml.dump(
157+
self._content,
158+
Dumper=AnsibleLoader.load_AnsibleDumper(),
159+
sort_keys=False,
160+
width=1000,
161+
)
156162
)
157163
self._file_descriptor.truncate()
158164
self._file_descriptor.flush()

0 commit comments

Comments
 (0)