6161
6262if TYPE_CHECKING :
6363 from sopel .bot import Sopel
64+ from types import ModuleType
6465
6566
6667class AbstractPluginHandler (abc .ABC ):
@@ -259,6 +260,12 @@ def __init__(self, name, package=None):
259260
260261 self ._module = None
261262
263+ @property
264+ def module (self ) -> ModuleType :
265+ if self ._module is None :
266+ raise RuntimeError ('No module for plugin %s' % self .name )
267+ return self ._module
268+
262269 def get_label (self ):
263270 """Retrieve a display label for the plugin.
264271
@@ -269,12 +276,11 @@ def get_label(self):
269276 docstring, its first line is used as the plugin's label.
270277 """
271278 default_label = '%s plugin' % self .name
272- module_doc = getattr (self ._module , '__doc__' , None )
273279
274- if not self .is_loaded () or not module_doc :
280+ if not self .is_loaded () or not hasattr ( self . module , '__doc__' ) :
275281 return default_label
276282
277- lines = inspect .cleandoc (module_doc ).splitlines ()
283+ lines = inspect .cleandoc (self . module . __doc__ ).splitlines ()
278284 return default_label if not lines else lines [0 ]
279285
280286 def get_meta_description (self ):
@@ -317,8 +323,8 @@ def get_version(self) -> Optional[str]:
317323 :rtype: Optional[str]
318324 """
319325 version : Optional [str ] = None
320- if hasattr (self ._module , "__version__" ):
321- version = str (self ._module .__version__ )
326+ if self . is_loaded () and hasattr (self .module , "__version__" ):
327+ version = str (self .module .__version__ )
322328 elif self .module_name .startswith ("sopel." ):
323329 version = release
324330
@@ -336,14 +342,14 @@ def reload(self):
336342
337343 This method assumes the plugin is already loaded.
338344 """
339- self ._module = importlib .reload (self ._module )
345+ self ._module = importlib .reload (self .module )
340346
341347 def is_loaded (self ):
342348 return self ._module is not None
343349
344350 def setup (self , bot ):
345351 if self .has_setup ():
346- self ._module .setup (bot )
352+ self .module .setup (bot )
347353
348354 def has_setup (self ):
349355 """Tell if the plugin has a setup action.
@@ -354,12 +360,12 @@ def has_setup(self):
354360 The plugin has a setup action if its module has a ``setup`` attribute.
355361 This attribute is expected to be a callable.
356362 """
357- return hasattr (self ._module , 'setup' )
363+ return hasattr (self .module , 'setup' )
358364
359365 def get_capability_requests (self ) -> List [plugin_decorators .capability ]:
360366 return [
361367 module_attribute
362- for module_attribute in vars (self ._module ).values ()
368+ for module_attribute in vars (self .module ).values ()
363369 if isinstance (module_attribute , plugin_decorators .capability )
364370 ]
365371
@@ -369,7 +375,7 @@ def register(self, bot: Sopel) -> None:
369375 bot .cap_requests .register (self .name , cap_request )
370376
371377 # plugin callables go through ``bot.add_plugin``
372- relevant_parts = loader .clean_module (self ._module , bot .config )
378+ relevant_parts = loader .clean_module (self .module , bot .config )
373379 for part in itertools .chain (* relevant_parts ):
374380 # annotate all callables in relevant_parts with `plugin_name`
375381 # attribute to make per-channel config work; see #1839
@@ -379,12 +385,12 @@ def register(self, bot: Sopel) -> None:
379385 bot .add_plugin (self , * relevant_parts )
380386
381387 def unregister (self , bot ):
382- relevant_parts = loader .clean_module (self ._module , bot .config )
388+ relevant_parts = loader .clean_module (self .module , bot .config )
383389 bot .remove_plugin (self , * relevant_parts )
384390
385391 def shutdown (self , bot ):
386392 if self .has_shutdown ():
387- self ._module .shutdown (bot )
393+ self .module .shutdown (bot )
388394
389395 def has_shutdown (self ):
390396 """Tell if the plugin has a shutdown action.
@@ -396,11 +402,11 @@ def has_shutdown(self):
396402 The plugin has a shutdown action if its module has a ``shutdown``
397403 attribute. This attribute is expected to be a callable.
398404 """
399- return hasattr (self ._module , 'shutdown' )
405+ return hasattr (self .module , 'shutdown' )
400406
401407 def configure (self , settings ):
402408 if self .has_configure ():
403- self ._module .configure (settings )
409+ self .module .configure (settings )
404410
405411 def has_configure (self ):
406412 """Tell if the plugin has a configure action.
@@ -412,7 +418,7 @@ def has_configure(self):
412418 The plugin has a configure action if its module has a ``configure``
413419 attribute. This attribute is expected to be a callable.
414420 """
415- return hasattr (self ._module , 'configure' )
421+ return hasattr (self .module , 'configure' )
416422
417423
418424class PyFilePlugin (PyModulePlugin ):
@@ -465,6 +471,9 @@ def __init__(self, filename):
465471 else :
466472 raise exceptions .PluginError ('Invalid Sopel plugin: %s' % filename )
467473
474+ if spec is None :
475+ raise exceptions .PluginError ('Could not determine spec for plugin: %s' % filename )
476+
468477 self .filename = filename
469478 self .path = filename
470479 self .module_spec = spec
@@ -474,6 +483,8 @@ def __init__(self, filename):
474483 def _load (self ):
475484 module = importlib .util .module_from_spec (self .module_spec )
476485 sys .modules [self .name ] = module
486+ if not self .module_spec .loader :
487+ raise exceptions .PluginError ('Could not determine loader for plugin: %s' % self .filename )
477488 self .module_spec .loader .exec_module (module )
478489 return module
479490
@@ -595,9 +606,9 @@ def get_version(self) -> Optional[str]:
595606 """
596607 version : Optional [str ] = super ().get_version ()
597608
598- if version is None and hasattr (self ._module , "__package__" ):
609+ if version is None and hasattr (self .module , "__package__" ):
599610 try :
600- version = importlib_metadata .version (self ._module .__package__ )
611+ version = importlib_metadata .version (self .module .__package__ )
601612 except ValueError :
602613 # package name is probably empty-string; just give up
603614 pass
0 commit comments