Skip to content

Commit 2c9b0b4

Browse files
authored
use pkgutil.iter_modules to find PrinterModules (#786)
This subtly changes loading behavior. Previously, only name.py or name/__init__.py would be loaded. By iterating modules, this will also find compiled .so modules as as well as pre-compiled .pyc.
1 parent ada9eb5 commit 2c9b0b4

1 file changed

Lines changed: 28 additions & 149 deletions

File tree

klippy/printer.py

Lines changed: 28 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,16 @@
88
import collections
99
import gc
1010
import importlib
11-
import importlib.util
1211
import logging
1312
import optparse
1413
import os
14+
import pkgutil
1515
import sys
1616
import time
1717
from collections import defaultdict
18-
from enum import Enum
1918
from pathlib import Path
2019
from types import ModuleType
21-
from typing import Any, Callable, Optional, Union
20+
from typing import Any, Callable, Generator, Optional, Union
2221

2322
from klippy.configfile import ConfigWrapper
2423

@@ -84,56 +83,6 @@ class WaitInterruption(gcode.CommandError):
8483
pass
8584

8685

87-
class PrinterModuleType(Enum):
88-
EXTRA = "klippy.extras."
89-
PLUGIN = ("klippy.extras.", True)
90-
PLUGIN_OVERRIDE_EXTRA = ("klippy.extras.", True, True)
91-
PLUGIN_DIRECTORY = ("klippy.plugins.", True)
92-
PLUGIN_DIRECTORY_OVERRIDE_EXTRA = ("klippy.plugins.", True, True)
93-
94-
def __init__(
95-
self,
96-
module_root,
97-
custom_loading: bool = False,
98-
is_override: bool = False,
99-
):
100-
self.module_root = module_root
101-
self.custom_loading = custom_loading
102-
self.is_override = is_override
103-
104-
def import_module(self, module_name: str, module_path: Path) -> ModuleType:
105-
full_name = self.module_root + module_name
106-
if self.custom_loading:
107-
return self._module_from_spec(full_name, module_path)
108-
return self._import_module(full_name)
109-
110-
@staticmethod
111-
def _import_module(module_name: str) -> ModuleType:
112-
"""
113-
Import a module when its physical path on disk matches it module path
114-
All extras and plugins in a directory
115-
"""
116-
return importlib.import_module(module_name)
117-
118-
@staticmethod
119-
def _module_from_spec(module_name: str, module_path: Path) -> ModuleType:
120-
"""
121-
Import a module when its module path doesn't match its physical path
122-
Default for plugin files
123-
"""
124-
path = module_path
125-
if path.is_dir():
126-
path = module_path.joinpath("__init__.py")
127-
mod_spec = importlib.util.spec_from_file_location(module_name, path)
128-
if mod_spec is None:
129-
raise ModuleNotFoundError(f"Module {module_name} failed to load")
130-
module = importlib.util.module_from_spec(mod_spec)
131-
mod_spec.loader.exec_module(module)
132-
# TODO: insert into sys_modules?
133-
# sys.modules[module_name] = module
134-
return module
135-
136-
13786
class SubsystemComponentCollection:
13887
def __init__(self, config_error):
13988
self._subsystems: dict[str, dict[str, Any]] = defaultdict(dict)
@@ -163,38 +112,26 @@ def register_component(
163112

164113

165114
class PrinterModule:
166-
path: Path
167115
name: str
168-
module_type: PrinterModuleType
169-
exception: Optional[Exception] = None
116+
module_info: pkgutil.ModuleInfo
170117
module: Optional[ModuleType] = None
171-
allow_plugin_override: bool
172-
config_error: Callable
118+
exception: Optional[Exception] = None
173119

174-
def __init__(
175-
self,
176-
path: Path,
177-
module_type: PrinterModuleType,
178-
allow_plugin_override: bool,
179-
config_error: Callable,
180-
):
181-
self.path = path
182-
self.name = path.stem
183-
self.module_type = module_type
184-
self.allow_plugin_override = allow_plugin_override
185-
self.config_error = config_error
120+
def __init__(self, name: str, module_info: pkgutil.ModuleInfo):
121+
self.name = name
122+
self.module_info = module_info
186123

187124
def load(self):
188125
try:
189-
self.module = self.module_type.import_module(self.name, self.path)
126+
self.module = importlib.import_module(self.module_info.name)
190127
except Exception as ex:
191128
logging.exception(f"Failed to load module '{self.name}'.")
192129
self.exception = ex
193130

194131
def get_init_function(self, section: str):
195132
# if loading failed, raise that exception now
196-
self.verify_loaded()
197-
self.validate_plugin_overrides()
133+
if self.exception is not None:
134+
raise self.exception
198135
# find the right init function
199136
is_prefix = self.name != section
200137
init_func_name = "load_config_prefix" if is_prefix else "load_config"
@@ -208,22 +145,8 @@ def register_components(self, collector: SubsystemComponentCollection):
208145
register_func = self.get_method("register_components")
209146
if register_func is None:
210147
return
211-
# only validate now that the call will actually happen
212-
self.validate_plugin_overrides()
213148
register_func(collector)
214149

215-
def validate_plugin_overrides(self):
216-
if not self.module_type.is_override:
217-
return
218-
if not self.allow_plugin_override:
219-
raise self.config_error(
220-
f"Module '{self.name}' found in both extras and plugins!"
221-
)
222-
223-
def verify_loaded(self):
224-
if self.exception is not None:
225-
raise self.exception
226-
227150
def get_method(self, function_name):
228151
if self.module is None:
229152
return None
@@ -255,74 +178,31 @@ def __init__(self, main_reactor, bglogger, start_args):
255178
m.add_early_printer_objects(self)
256179

257180
@staticmethod
258-
def _list_modules(search_path: str) -> list[Path]:
259-
"""
260-
list files + directories and filter to only those that could be a module
261-
"""
262-
path_list: list[Path] = []
263-
for path_string in os.listdir(search_path):
264-
path = Path(os.path.join(search_path, path_string))
265-
# don't include hidden files or directories
266-
# don't include __init__.py
267-
if path.name.startswith(".") or path.name.startswith("__"):
268-
continue
269-
# only include files that are .py files
270-
if path.is_file() and not path.name.endswith(".py"):
271-
continue
272-
path_list.append(path)
273-
return path_list
181+
def _iter_modules(prefix: str, path: Path) -> Generator[PrinterModule]:
182+
for module_info in pkgutil.iter_modules([str(path)], prefix=prefix):
183+
name = module_info.name.rsplit(".", 1)[-1]
184+
yield PrinterModule(name, module_info)
274185

275186
def _load_modules(self, config: ConfigWrapper):
276187
allow_overrides = self._allow_plugin_override(config)
277-
extra_modules: dict[str, PrinterModule] = {}
278-
extras_path = os.path.join(os.path.dirname(__file__), "extras")
279-
extras = self._list_modules(extras_path)
280-
extra_names = [extra.stem for extra in extras]
281-
plugin_modules: dict[str, PrinterModule] = {}
282-
plugins_path = os.path.join(os.path.dirname(__file__), "plugins")
283-
plugins = self._list_modules(plugins_path)
284-
plugin_names = [plugin.stem for plugin in plugins]
285-
286-
for plugin in plugins:
287-
is_dir = plugin.is_dir()
288-
is_override = plugin.name in extra_names
289-
if is_override:
290-
if is_dir:
291-
module_type = (
292-
PrinterModuleType.PLUGIN_DIRECTORY_OVERRIDE_EXTRA
293-
)
294-
else:
295-
module_type = PrinterModuleType.PLUGIN_OVERRIDE_EXTRA
296-
else:
297-
if is_dir:
298-
module_type = PrinterModuleType.PLUGIN_DIRECTORY
299-
else:
300-
module_type = PrinterModuleType.PLUGIN
301-
pm = PrinterModule(
302-
plugin, module_type, allow_overrides, self.config_error
303-
)
304-
plugin_modules[pm.name] = pm
305-
pm.load()
188+
klippy_dir = Path(__file__).parent
306189

307-
for extra in extras:
308-
# don't load extras that were overridden by plugins
309-
if extra.name in plugin_names:
310-
continue
311-
pm = PrinterModule(
312-
extra,
313-
PrinterModuleType.EXTRA,
314-
allow_overrides,
315-
self.config_error,
316-
)
317-
pm.load()
318-
extra_modules[pm.name] = pm
190+
for pm in self._iter_modules("klippy.extras.", klippy_dir / "extras"):
191+
self.printer_modules[pm.name] = pm
319192

320-
# plugins override extras:
321-
self.printer_modules = extra_modules | plugin_modules
193+
for pm in self._iter_modules("klippy.plugins.", klippy_dir / "plugins"):
194+
if pm.name in self.printer_modules and not allow_overrides:
195+
raise configfile.error(
196+
f"Module '{pm.name}' found in both extras and plugins!"
197+
)
198+
self.printer_modules[pm.name] = pm
199+
200+
for pm in self.printer_modules.values():
201+
pm.load()
322202

323203
def _register_subsystem_components(self):
324-
for name, module in self.printer_modules.items():
325-
module.register_components(self.components)
204+
for printer_module in self.printer_modules.values():
205+
printer_module.register_components(self.components)
326206

327207
@staticmethod
328208
def _allow_plugin_override(config) -> bool:
@@ -402,7 +282,6 @@ def load_object(
402282
module_name = module_parts[0]
403283
if module_name in self.printer_modules:
404284
printer_module = self.printer_modules[module_name]
405-
printer_module.verify_loaded()
406285
init_func = printer_module.get_init_function(section)
407286
if init_func is None:
408287
if default is not configfile.sentinel:

0 commit comments

Comments
 (0)