88import collections
99import gc
1010import importlib
11- import importlib .util
1211import logging
1312import optparse
1413import os
14+ import pkgutil
1515import sys
1616import time
1717from collections import defaultdict
18- from enum import Enum
1918from pathlib import Path
2019from types import ModuleType
21- from typing import Any , Callable , Optional , Union
20+ from typing import Any , Callable , Generator , Optional , Union
2221
2322from 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-
13786class 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
165114class 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