From bc6057c59da04b2737569010d3854b4ee5f23021 Mon Sep 17 00:00:00 2001 From: Ayuto22 Date: Sun, 11 Dec 2016 11:05:42 +0100 Subject: [PATCH 1/7] Updated PluginInfo to have some static attributes Updated documentation. Added some attributes to LoadedPlugin. Removed redundant hasattr() checks. Updated output of "sp plugin list". --- .../source-python/core/command/__init__.py | 34 +++-- .../packages/source-python/plugins/command.py | 10 +- .../packages/source-python/plugins/info.py | 129 ++++++++++++++---- .../source-python/plugins/instance.py | 44 +++--- .../packages/source-python/plugins/manager.py | 30 ++-- 5 files changed, 171 insertions(+), 76 deletions(-) diff --git a/addons/source-python/packages/source-python/core/command/__init__.py b/addons/source-python/packages/source-python/core/command/__init__.py index 6323abc8d..df619e4d5 100644 --- a/addons/source-python/packages/source-python/core/command/__init__.py +++ b/addons/source-python/packages/source-python/core/command/__init__.py @@ -56,7 +56,7 @@ class _CoreCommandManager(SubCommandManager): def print_plugins(self): """List all currently loaded plugins. - + .. todo:: Move this to :class:`plugins.command.SubCommandManager`? """ # Get header messages @@ -69,24 +69,30 @@ def print_plugins(self): # Was an PluginInfo instance found? if info is not None: + message += plugin_name + ' ({}):\n'.format(info.verbose_name) - # Add message with the current plugin's name - message += plugin_name + ':\n' + if info.author is not None: + message += ' author: {}\n'.format(info.author) + + if info.description is not None: + message += ' description: {}\n'.format(info.description) + + if info.version != 'unversioned': + message += ' version: {}\n'.format(info.version) - # Loop through all items in the PluginInfo instance - for item, value in info.items(): + if info.url is not None: + message += ' url: {}\n'.format(info.url) - # Is the value a ConVar? - if isinstance(value, ConVar): + if info.permissions: + message += ' permissions:\n' + for permission, description in info.permissions: + message += ' {}:'.format(permission).ljust(30) + description + '\n' - # Get the ConVar's text - value = '{0}:\n\t\t\t{1}: {2}'.format( - value.name, - value.help_text, - value.get_string()) + if info.public_convar is not None: + message += ' public convar: {}\n'.format(info.public_convar.name) - # Add message for the current item and its value - message += '\t{0}:\n\t\t{1}\n'.format(item, value) + for attr in info.display_in_listing: + message += ' {}:'.format(attr).ljust(20) + str(getattr(info, attr)) + '\n' # Was no PluginInfo instance found? else: diff --git a/addons/source-python/packages/source-python/plugins/command.py b/addons/source-python/packages/source-python/plugins/command.py index a0c8f46ee..f88adc6ee 100644 --- a/addons/source-python/packages/source-python/plugins/command.py +++ b/addons/source-python/packages/source-python/plugins/command.py @@ -55,17 +55,11 @@ def __init__(self, command, description='', prefix=''): super().__init__() # Does the class have a proper manager object assigned? - if not (hasattr(self, 'manager') and - isinstance(self.manager, PluginManager)): - - # If not, raise an error + if not isinstance(self.manager, PluginManager): raise PluginManagerError(PluginManagerError.__doc__) # Does the class have a proper instance class assigned? - if not (hasattr(self, 'instance') and - issubclass(self.instance, LoadedPlugin)): - - # If not, raise an error + if not issubclass(self.instance, LoadedPlugin): raise PluginInstanceError(PluginInstanceError.__doc__) # Store the command diff --git a/addons/source-python/packages/source-python/plugins/info.py b/addons/source-python/packages/source-python/plugins/info.py index 079ac4f5a..d353236d1 100644 --- a/addons/source-python/packages/source-python/plugins/info.py +++ b/addons/source-python/packages/source-python/plugins/info.py @@ -5,9 +5,9 @@ # ============================================================================= # >> IMPORTS # ============================================================================= -# Python Imports -# Collections -from collections import OrderedDict +# Source.Python Imports +# Cvars +from cvars.public import PublicConVar # ============================================================================= @@ -20,32 +20,111 @@ # ============================================================================= # >> CLASSES # ============================================================================= -class PluginInfo(OrderedDict): - """Stores information for a plugin.""" +class PluginInfo(dict): + """Store information for a plugin.""" - def __getattr__(self, attribute): - """Redirect to __getitem__.""" - # Is the attribute private? - if attribute.startswith('_'): + def __init__(self, verbose_name=None, author=None, description=None, + version=None, url=None, permissions=None, public_convar=True, + display_in_listing=None, **kwargs): + """Initialize the instance. - # Raise an error - # This is done to fix an issue with OrderedDict.__init__ - raise AttributeError('Private attributes not allowed') + :param str verbose_name: + A verbose name for the plugin (e.g. GunGame). + :param str author: + Name of the author. + :param str description: + A short description of what the plugin does. + :param str version: + Current version of the plugin. + :param str url: + A link to a thread in the 'Plugin Releases' forum section or the + plugin's SPPM link. + :param list permissions: + A list of permissions defined or used by the plugin. The list + should contain tuples that define the permission and a short + description of the permission. + :param public_convar: + If set to ``True``, a public convar will be generated based on the + plugin name, version and description. Set it to ``False`` if you + don't want a public convar or set it to your own + :class:`cvars.public.PublicConVar` instance. + :param list display_in_listing: + A list that contains custom attributes that should appear in the + plugin listing (e.g. sp plugin list). + :param kwargs: + Any additional attributes you want to set. If you want those + attributes to appear in the plugin listing, update + :attr:`display_in_listing`. + """ + self._verbose_name = verbose_name + self.author = author + self.description = description + self._version = version + self.url = url - # Redirect to __getitem__ - return self[attribute] + # All permissions defined by this plugin + # A list that contains tuples: + # Example: + # [('test1.kick', 'Permission to kick players.'), + # ('test1.ban', 'Permission to ban players.'), + # ('test1.start_vote', 'Permission to start a vote.')] + self.permissions = [] if permissions is None else permissions + self.public_convar = public_convar + + self.display_in_listing = [] if display_in_listing is None else display_in_listing - def __setattr__(self, attribute, value): - """Redirect to __setitem__.""" - # Is the attribute private? - if attribute.startswith('_'): + # A reference to its LoadedPlugin instance. Will be set by the + # LoadedPlugin instance itself + self.plugin = None + + for key, value in kwargs.items(): + self[key] = value - # Re-call __setattr__ - # This is done to fix an issue with OrderedDict.__init__ - super().__setattr__(attribute, value) - - # No need to go further + def _create_public_convar(self): + """Create a public convar if :attr:`public_convar` is set to True.""" + if self.public_convar is not True: return - # Redirect to __setitem__ - self[attribute] = value + self.public_convar = PublicConVar( + '{}_version'.format(self.plugin.plugin_name), + self.version, + '{} version.'.format(self.verbose_name) + ) + + def get_verbose_name(self): + """Return the verbose name of the plugin. + + If no verbose name has been set, the plugin name will be titled. + + :rtype: str + """ + if self._verbose_name is None: + return self.plugin.plugin_name.replace('_', ' ').title() + + return self._verbose_name + + def set_verbose_name(self, value): + """Set the verbose name of the plugin.""" + self._verbose_name = value + + verbose_name = property(get_verbose_name, set_verbose_name) + + def get_version(self): + """Return the plugin's version. + + :rtype: str + """ + if self._version is None: + return 'unversioned' + + return self._version + + def set_version(self, value): + """Set the plugin's version.""" + self._version = value + + version = property(get_version, set_version) + + # Redirect __getitem__ and __setitem__ to __getattr__ and __setattr__ + __getattr__ = dict.__getitem__ + __setattr__ = dict.__setitem__ diff --git a/addons/source-python/packages/source-python/plugins/instance.py b/addons/source-python/packages/source-python/plugins/instance.py index ca22449e8..3e2418ad0 100644 --- a/addons/source-python/packages/source-python/plugins/instance.py +++ b/addons/source-python/packages/source-python/plugins/instance.py @@ -42,16 +42,19 @@ class LoadedPlugin(object): def __init__(self, plugin_name, base_import): """Called when a plugin's instance is initialized.""" - # Does the object have a logger set? - if not hasattr(self, 'logger'): - - # If not, set the default logger + self.file_path = None + self.import_name = None + self.globals = None + self.translations = None + self.base_import = base_import + self.plugin_name = plugin_name + + # Fall back to the default logger if none was set + if self.logger is None: self.logger = plugins_instance_logger - # Does the object have a translations value set? - if not hasattr(self, 'translations'): - - # If not, set the default translations + # Fall back to the default translations if none was set + if self.translations is None: self.translations = _plugin_strings # Print message that the plugin is going to be loaded @@ -59,16 +62,16 @@ def __init__(self, plugin_name, base_import): 'Loading'].get_string(plugin=plugin_name)) # Get the plugin's main file - file_path = PLUGIN_PATH.joinpath(*tuple( + self.file_path = PLUGIN_PATH.joinpath(*tuple( base_import.split('.')[:~0] + [plugin_name, plugin_name + '.py'])) # Does the plugin's main file exist? - if not file_path.isfile(): + if not self.file_path.isfile(): # Print a message that the plugin's main file was not found self.logger.log_message(self.prefix + self.translations[ 'No Module'].get_string( - plugin=plugin_name, file=file_path.replace( + plugin=plugin_name, file=self.file_path.replace( GAME_PATH, '').replace('\\', '/'))) # Raise an error so that the plugin @@ -76,25 +79,28 @@ def __init__(self, plugin_name, base_import): raise PluginFileNotFoundError # Get the base import - import_name = base_import + plugin_name + '.' + plugin_name + self.import_name = base_import + plugin_name + '.' + plugin_name # Import the plugin - self._plugin = import_module(import_name) + self._plugin = import_module(self.import_name) # Set the globals value - self._globals = { + self.globals = { x: getattr(self._plugin, x) for x in dir(self._plugin)} - @property - def globals(self): - """Return the plugin's globals.""" - return self._globals + # Add this instance to the plugin info for easier access + info = self.info + if info is not None: + info.plugin = self + info._create_public_convar() @property def info(self): - """Return the plugin's PluginInfo object. + """Return the plugin's PluginInfo instance. If no PluginInfo was found, None will be returned. + + :rtype: PluginInfo """ for obj in self.globals.values(): if isinstance(obj, PluginInfo): diff --git a/addons/source-python/packages/source-python/plugins/manager.py b/addons/source-python/packages/source-python/plugins/manager.py index 7d8b80a9b..ba3ace5a2 100644 --- a/addons/source-python/packages/source-python/plugins/manager.py +++ b/addons/source-python/packages/source-python/plugins/manager.py @@ -50,20 +50,18 @@ def __init__(self, base_import=''): """Called when the class instance is initialized.""" # Re-call OrderedDict's __init__ to properly setup the object super().__init__() + self.logger = None + self.translations = None # Store the base import path self._base_import = base_import # Does the object have a logger set? - if not hasattr(self, 'logger'): - - # If not, set the default logger + if self.logger is None: self.logger = plugins_manager_logger # Does the object have a translations value set? - if not hasattr(self, 'translations'): - - # If not, set the default translations + if self.translations is None: self.translations = _plugin_strings def __missing__(self, plugin_name): @@ -157,15 +155,24 @@ def __delitem__(self, plugin_name): @property def base_import(self): - """Return the base import path for the manager.""" + """Return the base import path for the manager. + + :rtype: str + """ return self._base_import def is_loaded(self, plugin_name): - """Return whether or not a plugin is loaded.""" + """Return whether or not a plugin is loaded. + + :rtype: bool + """ return plugin_name in self def get_plugin_instance(self, plugin_name): - """Return a plugin's instance, if it is loaded.""" + """Return a plugin's instance, if it is loaded. + + :rtype: LoadedPlugin + """ # Is the plugin loaded? if plugin_name in self: @@ -204,7 +211,10 @@ def _remove_modules(self, plugin_name): @staticmethod def _is_related_module(base_name, module): - """Check if a plugin's base name is related to a module name.""" + """Check if a plugin's base name is related to a module name. + + :rtype: bool + """ return (module.startswith('{}.'.format(base_name)) or module == base_name) From 9a7827a66af33e4f1cd1bc9b45c0460984ec3507 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sun, 11 Dec 2016 09:11:04 -0500 Subject: [PATCH 2/7] Changed PluginInfo.__init__ to call super instead of looping through kwargs to add items to the dictionary. --- addons/source-python/packages/source-python/plugins/info.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/addons/source-python/packages/source-python/plugins/info.py b/addons/source-python/packages/source-python/plugins/info.py index d353236d1..b434ff4e9 100644 --- a/addons/source-python/packages/source-python/plugins/info.py +++ b/addons/source-python/packages/source-python/plugins/info.py @@ -56,6 +56,7 @@ def __init__(self, verbose_name=None, author=None, description=None, attributes to appear in the plugin listing, update :attr:`display_in_listing`. """ + super().__init__(**kwargs) self._verbose_name = verbose_name self.author = author self.description = description @@ -70,15 +71,12 @@ def __init__(self, verbose_name=None, author=None, description=None, # ('test1.start_vote', 'Permission to start a vote.')] self.permissions = [] if permissions is None else permissions self.public_convar = public_convar - + self.display_in_listing = [] if display_in_listing is None else display_in_listing # A reference to its LoadedPlugin instance. Will be set by the # LoadedPlugin instance itself self.plugin = None - - for key, value in kwargs.items(): - self[key] = value def _create_public_convar(self): """Create a public convar if :attr:`public_convar` is set to True.""" From 5ae678d242562ae8b3cd15f9fd407078db43a848 Mon Sep 17 00:00:00 2001 From: Stephen Toon Date: Sun, 11 Dec 2016 17:44:39 -0500 Subject: [PATCH 3/7] Added class attribute defaults for prefix, instance, translations, and logger. Removed unused description argument for SubCommandManager. --- .../packages/source-python/core/command/__init__.py | 4 +--- .../packages/source-python/plugins/command.py | 2 +- .../packages/source-python/plugins/instance.py | 5 ++++- .../packages/source-python/plugins/manager.py | 11 +++++++---- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/addons/source-python/packages/source-python/core/command/__init__.py b/addons/source-python/packages/source-python/core/command/__init__.py index df619e4d5..c6ff2c266 100644 --- a/addons/source-python/packages/source-python/core/command/__init__.py +++ b/addons/source-python/packages/source-python/core/command/__init__.py @@ -16,8 +16,6 @@ from core import core_logger from core.manager import core_plugin_manager from core.version import VERSION -# Cvars -from cvars import ConVar # Engines from engines.server import execute_server_command from engines.server import queue_command_string @@ -139,7 +137,7 @@ def print_credits(self): self._log_message(message + '=' * 61 + '\n\n') # Get the _CoreCommandManager instance -_core_command = _CoreCommandManager('sp', 'Source.Python base command.') +_core_command = _CoreCommandManager('sp') # ============================================================================= diff --git a/addons/source-python/packages/source-python/plugins/command.py b/addons/source-python/packages/source-python/plugins/command.py index f88adc6ee..23e62fe9b 100644 --- a/addons/source-python/packages/source-python/plugins/command.py +++ b/addons/source-python/packages/source-python/plugins/command.py @@ -49,7 +49,7 @@ class SubCommandManager(AutoUnload, list): logger = plugins_command_logger translations = _plugin_strings - def __init__(self, command, description='', prefix=''): + def __init__(self, command, prefix=''): """Called on instance initialization.""" # Re-call OrderedDict's __init__ to properly setup the object super().__init__() diff --git a/addons/source-python/packages/source-python/plugins/instance.py b/addons/source-python/packages/source-python/plugins/instance.py index 3e2418ad0..d32d41d4f 100644 --- a/addons/source-python/packages/source-python/plugins/instance.py +++ b/addons/source-python/packages/source-python/plugins/instance.py @@ -40,12 +40,15 @@ class LoadedPlugin(object): """Stores a plugin's instance.""" + logger = None + translations = None + prefix = None + def __init__(self, plugin_name, base_import): """Called when a plugin's instance is initialized.""" self.file_path = None self.import_name = None self.globals = None - self.translations = None self.base_import = base_import self.plugin_name = plugin_name diff --git a/addons/source-python/packages/source-python/plugins/manager.py b/addons/source-python/packages/source-python/plugins/manager.py index ba3ace5a2..342bcd081 100644 --- a/addons/source-python/packages/source-python/plugins/manager.py +++ b/addons/source-python/packages/source-python/plugins/manager.py @@ -46,6 +46,9 @@ class PluginManager(OrderedDict): """Stores plugins and their instances.""" + instance = None + prefix = None + def __init__(self, base_import=''): """Called when the class instance is initialized.""" # Re-call OrderedDict's __init__ to properly setup the object @@ -156,21 +159,21 @@ def __delitem__(self, plugin_name): @property def base_import(self): """Return the base import path for the manager. - + :rtype: str """ return self._base_import def is_loaded(self, plugin_name): """Return whether or not a plugin is loaded. - + :rtype: bool """ return plugin_name in self def get_plugin_instance(self, plugin_name): """Return a plugin's instance, if it is loaded. - + :rtype: LoadedPlugin """ # Is the plugin loaded? @@ -212,7 +215,7 @@ def _remove_modules(self, plugin_name): @staticmethod def _is_related_module(base_name, module): """Check if a plugin's base name is related to a module name. - + :rtype: bool """ return (module.startswith('{}.'.format(base_name)) From af91fbe7d157846adb0960b91827df6724c0cfc9 Mon Sep 17 00:00:00 2001 From: Ayuto22 Date: Mon, 2 Jan 2017 18:06:36 +0100 Subject: [PATCH 4/7] PluginInfo is now file based --- .../packages/source-python/plugins/info.py | 40 ++++---- .../source-python/plugins/instance.py | 43 ++++----- .../packages/source-python/plugins/manager.py | 94 +++++++++++++++++-- 3 files changed, 126 insertions(+), 51 deletions(-) diff --git a/addons/source-python/packages/source-python/plugins/info.py b/addons/source-python/packages/source-python/plugins/info.py index b434ff4e9..02ebc4bc6 100644 --- a/addons/source-python/packages/source-python/plugins/info.py +++ b/addons/source-python/packages/source-python/plugins/info.py @@ -23,11 +23,13 @@ class PluginInfo(dict): """Store information for a plugin.""" - def __init__(self, verbose_name=None, author=None, description=None, + def __init__(self, name, verbose_name=None, author=None, description=None, version=None, url=None, permissions=None, public_convar=True, display_in_listing=None, **kwargs): """Initialize the instance. + :param str name: + Name of the plugin on the file system. :param str verbose_name: A verbose name for the plugin (e.g. GunGame). :param str author: @@ -45,9 +47,10 @@ def __init__(self, verbose_name=None, author=None, description=None, description of the permission. :param public_convar: If set to ``True``, a public convar will be generated based on the - plugin name, version and description. Set it to ``False`` if you - don't want a public convar or set it to your own - :class:`cvars.public.PublicConVar` instance. + plugin name, verbose name and version. Set it to ``False`` if you + don't want a public convar or set it to a dictionary containing + the parameters to create a :class:`cvars.public.PublicConvar` + instance. :param list display_in_listing: A list that contains custom attributes that should appear in the plugin listing (e.g. sp plugin list). @@ -57,6 +60,7 @@ def __init__(self, verbose_name=None, author=None, description=None, :attr:`display_in_listing`. """ super().__init__(**kwargs) + self.name = name self._verbose_name = verbose_name self.author = author self.description = description @@ -74,20 +78,22 @@ def __init__(self, verbose_name=None, author=None, description=None, self.display_in_listing = [] if display_in_listing is None else display_in_listing - # A reference to its LoadedPlugin instance. Will be set by the - # LoadedPlugin instance itself - self.plugin = None - def _create_public_convar(self): """Create a public convar if :attr:`public_convar` is set to True.""" - if self.public_convar is not True: - return - - self.public_convar = PublicConVar( - '{}_version'.format(self.plugin.plugin_name), - self.version, - '{} version.'.format(self.verbose_name) - ) + name = '{}_version'.format(self.name) + description = '{} version.'.format(self.verbose_name) + if self.public_convar is True: + self.public_convar = PublicConVar( + name, + self.version, + description + ) + elif isinstance(self.public_convar, dict): + self.public_convar = PublicConVar( + self.public_convar.pop('name', name), + self.public_convar.pop('value', self.version), + self.public_convar.pop('description', description), + **self.public_convar) def get_verbose_name(self): """Return the verbose name of the plugin. @@ -97,7 +103,7 @@ def get_verbose_name(self): :rtype: str """ if self._verbose_name is None: - return self.plugin.plugin_name.replace('_', ' ').title() + return self.name.replace('_', ' ').title() return self._verbose_name diff --git a/addons/source-python/packages/source-python/plugins/instance.py b/addons/source-python/packages/source-python/plugins/instance.py index d32d41d4f..93c29794c 100644 --- a/addons/source-python/packages/source-python/plugins/instance.py +++ b/addons/source-python/packages/source-python/plugins/instance.py @@ -44,13 +44,21 @@ class LoadedPlugin(object): translations = None prefix = None - def __init__(self, plugin_name, base_import): - """Called when a plugin's instance is initialized.""" + def __init__(self, plugin_name, manager): + """Called when a plugin's instance is initialized. + + :param str plugin_name: + Name of the plugin to load. + :param PluginManager manager: + A plugin manager instance. + """ + self.manager = manager self.file_path = None self.import_name = None self.globals = None - self.base_import = base_import self.plugin_name = plugin_name + self.directory = self.manager.get_plugin_directory(plugin_name) + self.file_path = self.directory / plugin_name + '.py' # Fall back to the default logger if none was set if self.logger is None: @@ -64,10 +72,6 @@ def __init__(self, plugin_name, base_import): self.logger.log_message(self.prefix + self.translations[ 'Loading'].get_string(plugin=plugin_name)) - # Get the plugin's main file - self.file_path = PLUGIN_PATH.joinpath(*tuple( - base_import.split('.')[:~0] + [plugin_name, plugin_name + '.py'])) - # Does the plugin's main file exist? if not self.file_path.isfile(): @@ -81,8 +85,9 @@ def __init__(self, plugin_name, base_import): # is not added to the PluginManager raise PluginFileNotFoundError - # Get the base import - self.import_name = base_import + plugin_name + '.' + plugin_name + # Get the import name + self.import_name = (self.manager.base_import + plugin_name + + '.' + plugin_name) # Import the plugin self._plugin = import_module(self.import_name) @@ -92,21 +97,5 @@ def __init__(self, plugin_name, base_import): x: getattr(self._plugin, x) for x in dir(self._plugin)} # Add this instance to the plugin info for easier access - info = self.info - if info is not None: - info.plugin = self - info._create_public_convar() - - @property - def info(self): - """Return the plugin's PluginInfo instance. - - If no PluginInfo was found, None will be returned. - - :rtype: PluginInfo - """ - for obj in self.globals.values(): - if isinstance(obj, PluginInfo): - return obj - - return None + self.info = self.manager._create_plugin_info(self.plugin_name) + self.info._create_public_convar() diff --git a/addons/source-python/packages/source-python/plugins/manager.py b/addons/source-python/packages/source-python/plugins/manager.py index 342bcd081..f32cea95c 100644 --- a/addons/source-python/packages/source-python/plugins/manager.py +++ b/addons/source-python/packages/source-python/plugins/manager.py @@ -8,6 +8,9 @@ # Python Imports # Collections from collections import OrderedDict +# Configobj +from configobj import ConfigObj +from configobj import Section # Sys import sys @@ -20,10 +23,13 @@ # Listeners from listeners import on_plugin_loaded_manager from listeners import on_plugin_unloaded_manager +# Paths +from paths import PLUGIN_PATH # Plugins from plugins import plugins_logger from plugins import _plugin_strings from plugins.errors import PluginFileNotFoundError +from plugins.info import PluginInfo # ============================================================================= @@ -55,8 +61,6 @@ def __init__(self, base_import=''): super().__init__() self.logger = None self.translations = None - - # Store the base import path self._base_import = base_import # Does the object have a logger set? @@ -73,7 +77,7 @@ def __missing__(self, plugin_name): try: # Get the plugin's instance - instance = self.instance(plugin_name, self.base_import) + instance = self.instance(plugin_name, self) # Does the plugin have a load function? if 'load' in instance.globals: @@ -164,6 +168,14 @@ def base_import(self): """ return self._base_import + @property + def plugins_directory(self): + """Return the directory where the plugins are stored. + + :rtype: path.Path + """ + return PLUGIN_PATH.joinpath(*tuple(self.base_import.split('.')[:~0])) + def is_loaded(self, plugin_name): """Return whether or not a plugin is loaded. @@ -171,20 +183,88 @@ def is_loaded(self, plugin_name): """ return plugin_name in self + def plugin_exists(self, plugin_name): + """Return whether of not a plugin exists. + + :rtype: bool + """ + return self.get_plugin_directory(plugin_name).isdir() + def get_plugin_instance(self, plugin_name): """Return a plugin's instance, if it is loaded. :rtype: LoadedPlugin """ - # Is the plugin loaded? if plugin_name in self: - - # Return the plugin's instance return self[plugin_name] - # Return None if the plugin is not loaded return None + def get_plugin_directory(self, plugin_name): + """Return the directory of the given plugin. + + :rtype: path.Path + """ + return self.plugins_directory / plugin_name + + def get_plugin_info(self, plugin_name): + """Return information about the given plugin. + + :rtype: PluginInfo + """ + plugin = self.get_plugin_instance(plugin_name) + if plugin is not None: + return plugin.info + + return self._create_plugin_info(plugin_name) + + def _create_plugin_info(self, plugin_name): + """Create a new :class:`plugins.info.PluginInfo` instance. + + :param str plugin_name: + Name of the plugin whose plugin info should be created. + :rtype: PluginInfo + """ + if not self.plugin_exists(plugin_name): + raise ValueError( + 'Plugin "{}" does not exist.'.format(plugin_name)) + + info_file = self.get_plugin_directory(plugin_name) / 'info.ini' + if not info_file.isfile(): + # Just return an "empty" PluginInfo instance. We don't have more + # information. + return PluginInfo(plugin_name) + + info = ConfigObj(info_file) + return PluginInfo( + plugin_name, + info.pop('verbose_name', None), + info.pop('author', None), + info.pop('description', None), + info.pop('version', None), + info.pop('url', None), + tuple(info.pop('permissions', dict()).items()), + self._get_public_convar_from_info_file(info), + self._get_display_in_listing_from_info_file(info), + **info.dict() + ) + + @staticmethod + def _get_public_convar_from_info_file(info): + data = info.pop('public_convar', True) + if isinstance(data, Section): + return data.dict() + + return data + + @staticmethod + def _get_display_in_listing_from_info_file(info): + data = info.pop('display_in_listing', []) + if isinstance(data, (tuple, list)): + return list(data) + + return [data] + def _remove_modules(self, plugin_name): """Remove all modules from the plugin.""" # Get the plugins import path From 9582497032f7ccb474932a9a81210d1b153cc186 Mon Sep 17 00:00:00 2001 From: Ayuto22 Date: Mon, 2 Jan 2017 21:46:22 +0100 Subject: [PATCH 5/7] Fixed not being able to get the PluginInfo instance during load on module level or in load --- .../source-python/plugins/instance.py | 24 ++++++--- .../packages/source-python/plugins/manager.py | 53 +++++++++---------- 2 files changed, 42 insertions(+), 35 deletions(-) diff --git a/addons/source-python/packages/source-python/plugins/instance.py b/addons/source-python/packages/source-python/plugins/instance.py index 93c29794c..cb78d20a7 100644 --- a/addons/source-python/packages/source-python/plugins/instance.py +++ b/addons/source-python/packages/source-python/plugins/instance.py @@ -59,6 +59,9 @@ def __init__(self, plugin_name, manager): self.plugin_name = plugin_name self.directory = self.manager.get_plugin_directory(plugin_name) self.file_path = self.directory / plugin_name + '.py' + self.info = self.manager._create_plugin_info(self.plugin_name) + self.info._create_public_convar() + self._plugin = None # Fall back to the default logger if none was set if self.logger is None: @@ -86,16 +89,23 @@ def __init__(self, plugin_name, manager): raise PluginFileNotFoundError # Get the import name - self.import_name = (self.manager.base_import + plugin_name + + self.import_name = (self.manager.base_import + plugin_name + '.' + plugin_name) - # Import the plugin + def _load(self): + """Actually load the plugin.""" self._plugin = import_module(self.import_name) - - # Set the globals value self.globals = { x: getattr(self._plugin, x) for x in dir(self._plugin)} - # Add this instance to the plugin info for easier access - self.info = self.manager._create_plugin_info(self.plugin_name) - self.info._create_public_convar() + if 'load' in self.globals: + self.globals['load']() + + def _unload(self): + """Actually unload the plugin.""" + if 'unload' in self.globals: + # Use a try/except here to still allow the plugin to be unloaded + try: + self.globals['unload']() + except: + except_hooks.print_exception() diff --git a/addons/source-python/packages/source-python/plugins/manager.py b/addons/source-python/packages/source-python/plugins/manager.py index f32cea95c..39def5184 100644 --- a/addons/source-python/packages/source-python/plugins/manager.py +++ b/addons/source-python/packages/source-python/plugins/manager.py @@ -54,13 +54,13 @@ class PluginManager(OrderedDict): instance = None prefix = None + logger = None + translations = None def __init__(self, base_import=''): """Called when the class instance is initialized.""" # Re-call OrderedDict's __init__ to properly setup the object super().__init__() - self.logger = None - self.translations = None self._base_import = base_import # Does the object have a logger set? @@ -71,19 +71,30 @@ def __init__(self, base_import=''): if self.translations is None: self.translations = _plugin_strings + def _create_plugin_instance(self, plugin_name): + """Create a new plugin instance. + + :rtype: LoadedPlugin + """ + # TODO: + # Rename "instance" to a better name? Perphaps completely remove it? + # Subclasses should implement this method instead. + return self.instance(plugin_name, self) + def __missing__(self, plugin_name): """Try to load a plugin that is not loaded.""" # Try to get the plugin's instance try: + instance = self._create_plugin_instance(plugin_name) - # Get the plugin's instance - instance = self.instance(plugin_name, self) + # Add the instance here, so we can use get_plugin_instance() etc. + # within the plugin itself before the plugin has been fully + # loaded. This is also required e.g. for retrieving the PluginInfo + # instance. + self[plugin_name] = instance - # Does the plugin have a load function? - if 'load' in instance.globals: - - # Call the plugin's load function - instance.globals['load']() + # Actually load the plugin + instance._load() # Was the file not found? # We use this check because we already printed the error to console @@ -94,6 +105,10 @@ def __missing__(self, plugin_name): # Was a different error encountered? except: + try: + super().__delitem__(plugin_name) + except KeyError: + pass # Get the error error = sys.exc_info() @@ -120,8 +135,6 @@ def __missing__(self, plugin_name): # Return None as the value to show the addon was not loaded return None - # Add the plugin to the dictionary with its instance - self[plugin_name] = instance on_plugin_loaded_manager.notify(plugin_name) return instance @@ -129,29 +142,13 @@ def __delitem__(self, plugin_name): """Remove a plugin from the manager.""" # Is the plugin in the dictionary? if plugin_name not in self: - - # Do nothing return # Print a message about the plugin being unloaded self.logger.log_message(self.prefix + self.translations[ 'Unloading'].get_string(plugin=plugin_name)) - # Does the plugin have an unload function? - if 'unload' in self[plugin_name].globals: - - # Use a try/except here to still allow the plugin to be unloaded - try: - - # Call the plugin's unload function - self[plugin_name].globals['unload']() - - # Was an exception raised? - except: - - # Print the error to console, but - # allow the plugin to still be unloaded - except_hooks.print_exception() + self[plugin_name]._unload() # Remove all modules from sys.modules self._remove_modules(plugin_name) From ea727307905751499eb6b4280fca7a79df265736 Mon Sep 17 00:00:00 2001 From: Ayuto22 Date: Mon, 2 Jan 2017 22:12:44 +0100 Subject: [PATCH 6/7] Added possibility to use __name__ to retrieve its own plugin instace --- .../packages/source-python/plugins/manager.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/addons/source-python/packages/source-python/plugins/manager.py b/addons/source-python/packages/source-python/plugins/manager.py index 39def5184..6ab30ad8e 100644 --- a/addons/source-python/packages/source-python/plugins/manager.py +++ b/addons/source-python/packages/source-python/plugins/manager.py @@ -176,6 +176,8 @@ def plugins_directory(self): def is_loaded(self, plugin_name): """Return whether or not a plugin is loaded. + :param str plugin_name: + The plugin to check. :rtype: bool """ return plugin_name in self @@ -183,6 +185,8 @@ def is_loaded(self, plugin_name): def plugin_exists(self, plugin_name): """Return whether of not a plugin exists. + :param str plugin_name: + The plugin to check. :rtype: bool """ return self.get_plugin_directory(plugin_name).isdir() @@ -190,8 +194,16 @@ def plugin_exists(self, plugin_name): def get_plugin_instance(self, plugin_name): """Return a plugin's instance, if it is loaded. + :param str plugin_name: + The plugin to check. You can pass ``__name__`` from one of your + plugin files to retrieve its own plugin instance. :rtype: LoadedPlugin """ + # This allows passing __name__ to this method + if plugin_name.startswith(self.base_import): + plugin_name = plugin_name.replace( + self.base_import, '', 1).split('.', 1)[0] + if plugin_name in self: return self[plugin_name] @@ -207,6 +219,9 @@ def get_plugin_directory(self, plugin_name): def get_plugin_info(self, plugin_name): """Return information about the given plugin. + :param str plugin_name: + The plugin to check. You can pass ``__name__`` from one of your + plugin files to retrieve its own plugin instance. :rtype: PluginInfo """ plugin = self.get_plugin_instance(plugin_name) From aaadc2d1f6a3bb60e972158981c5e2b2a10d11fe Mon Sep 17 00:00:00 2001 From: Ayuto22 Date: Mon, 2 Jan 2017 22:36:46 +0100 Subject: [PATCH 7/7] Fixed parsing public_convar --- .../source-python/packages/source-python/plugins/manager.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/addons/source-python/packages/source-python/plugins/manager.py b/addons/source-python/packages/source-python/plugins/manager.py index 6ab30ad8e..4c2f9e8d5 100644 --- a/addons/source-python/packages/source-python/plugins/manager.py +++ b/addons/source-python/packages/source-python/plugins/manager.py @@ -263,11 +263,12 @@ def _create_plugin_info(self, plugin_name): @staticmethod def _get_public_convar_from_info_file(info): - data = info.pop('public_convar', True) + data = info.pop('public_convar', 'True') if isinstance(data, Section): return data.dict() - return data + # False and 0 result in False, everything else is True + return data.lower() not in ('false', '0') @staticmethod def _get_display_in_listing_from_info_file(info):