From d69e6747ef2fa343ba315bc6fd992c21fd0a67fe Mon Sep 17 00:00:00 2001 From: Ayuto22 Date: Sat, 14 Jan 2017 11:41:28 +0100 Subject: [PATCH 01/12] Cleanup, ease and improve plugins API --- .../packages/source-python/__init__.py | 7 +- .../packages/source-python/commands/typed.py | 10 + .../source-python/core/command/__init__.py | 47 ++-- .../source-python/core/command/auth.py | 55 ++-- .../source-python/core/command/docs.py | 12 +- .../source-python/core/command/dump.py | 18 +- .../source-python/core/command/plugin.py | 22 +- .../packages/source-python/core/manager.py | 39 --- .../source-python/listeners/__init__.py | 18 ++ .../packages/source-python/plugins/command.py | 228 ++++++++-------- .../packages/source-python/plugins/errors.py | 33 --- .../source-python/plugins/instance.py | 62 ++--- .../packages/source-python/plugins/manager.py | 251 +++++++++++------- 13 files changed, 387 insertions(+), 415 deletions(-) delete mode 100644 addons/source-python/packages/source-python/core/manager.py delete mode 100644 addons/source-python/packages/source-python/plugins/errors.py diff --git a/addons/source-python/packages/source-python/__init__.py b/addons/source-python/packages/source-python/__init__.py index 823ddc953..b4d64b05a 100644 --- a/addons/source-python/packages/source-python/__init__.py +++ b/addons/source-python/packages/source-python/__init__.py @@ -308,11 +308,10 @@ def unload_plugins(): """Unload all plugins.""" _sp_logger.log_debug('Unloading plugins...') - from core.manager import core_plugin_manager - from core.command import _core_command + from plugins.manager import plugin_manager - for plugin_name in list(core_plugin_manager): - _core_command.unload_plugin(plugin_name) + for plugin in list(plugin_manager.values()): + plugin.unload() # ============================================================================= diff --git a/addons/source-python/packages/source-python/commands/typed.py b/addons/source-python/packages/source-python/commands/typed.py index b97ab4711..2c87031a2 100644 --- a/addons/source-python/packages/source-python/commands/typed.py +++ b/addons/source-python/packages/source-python/commands/typed.py @@ -322,6 +322,16 @@ def get_node(self, commands): return store + def set_node_description(self, commands, description): + """Set the description of a node. + + :param str/list/tuple: + Node to seach. + :raise ValueError: + Raised if the node does not exist. + """ + self.get_node(commands).description = description + def get_command(self, commands): """Return a command. 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 c6ff2c266..ed7452636 100644 --- a/addons/source-python/packages/source-python/core/command/__init__.py +++ b/addons/source-python/packages/source-python/core/command/__init__.py @@ -14,7 +14,6 @@ from commands.typed import TypedServerCommand # Core from core import core_logger -from core.manager import core_plugin_manager from core.version import VERSION # Engines from engines.server import execute_server_command @@ -24,11 +23,19 @@ # Plugins from plugins import _plugin_strings from plugins.command import SubCommandManager -from plugins.instance import LoadedPlugin +from plugins.manager import plugin_manager # Tick from listeners.tick import Delay +# ============================================================================= +# >> ALL DECLARATION +# ============================================================================= +__all__ = ('CoreCommandManager', + 'core_command', + ) + + # ============================================================================= # >> GLOBAL VARIABLES # ============================================================================= @@ -39,17 +46,10 @@ # ============================================================================= # >> CLASSES # ============================================================================= -class _CoreLoadedPlugin(LoadedPlugin): - """Plugin instance class used to create "sp" loaded plugins.""" - - logger = core_command_logger - - -class _CoreCommandManager(SubCommandManager): +class CoreCommandManager(SubCommandManager): """Class used for executing "sp" sub-command functionality.""" - manager = core_plugin_manager - instance = _CoreLoadedPlugin + manager = plugin_manager logger = core_command_logger def print_plugins(self): @@ -58,7 +58,7 @@ def print_plugins(self): .. todo:: Move this to :class:`plugins.command.SubCommandManager`? """ # Get header messages - message = self.prefix + _plugin_strings[ + message = _plugin_strings[ 'Plugins'].get_string() + '\n' + '=' * 61 + '\n\n' # Loop through all loaded plugins @@ -105,12 +105,12 @@ def print_plugins(self): message += '=' * 61 # Print the message - self._log_message(message) + self.log_message(message) def print_credits(self): """List all credits for Source.Python.""" # Get header messages - message = self.prefix + _plugin_strings[ + message = _plugin_strings[ 'Credits'].get_string() + '\n' + '=' * 61 + '\n\n' # Get the credits information @@ -134,35 +134,34 @@ def print_credits(self): message += '\n' # Print the message - self._log_message(message + '=' * 61 + '\n\n') + self.log_message(message + '=' * 61 + '\n\n') -# Get the _CoreCommandManager instance -_core_command = _CoreCommandManager('sp') +core_command = CoreCommandManager('sp') # ============================================================================= # >> sp # ============================================================================= -@_core_command.server_sub_command(['delay']) +@core_command.server_sub_command(['delay']) def _sp_delay(command_info, delay:float, command, *args): """Execute a command after a given delay.""" Delay(delay, queue_command_string, (command + ' ' + ' '.join(args), )) -@_core_command.server_sub_command(['version']) +@core_command.server_sub_command(['version']) def _sp_version(command_info): """Display Source.Python version information.""" core_command_logger.log_message( 'Current Source.Python version: {0}'.format(VERSION)) -@_core_command.server_sub_command(['credits']) +@core_command.server_sub_command(['credits']) def _sp_credits(command_info): """List all credits for Source.Python.""" - _core_command.print_credits() + core_command.print_credits() -@_core_command.server_sub_command(['help']) +@core_command.server_sub_command(['help']) def _sp_help(command_info, command=None, *server_sub_commands): """Print all sp sub-commands or help for a specific command.""" if command is None: @@ -185,5 +184,5 @@ def _sp_help(command_info, command=None, *server_sub_commands): # ============================================================================= # >> DESCRIPTION # ============================================================================= -_sp = TypedServerCommand.parser.get_node('sp') -_sp.description = 'Source.Python main command.' +TypedServerCommand.parser.set_node_description( + 'sp', 'Source.Python main command.') diff --git a/addons/source-python/packages/source-python/core/command/auth.py b/addons/source-python/packages/source-python/core/command/auth.py index d3eeed18f..e8523760e 100644 --- a/addons/source-python/packages/source-python/core/command/auth.py +++ b/addons/source-python/packages/source-python/core/command/auth.py @@ -12,7 +12,7 @@ from commands.typed import filter_str from commands.typed import TypedServerCommand # Core -from core.command import _core_command +from core.command import core_command from core.command import core_command_logger # Filters from filters.players import PlayerIter @@ -27,7 +27,7 @@ # ============================================================================= # >> sp auth backend # ============================================================================= -@_core_command.server_sub_command(['auth', 'backend', 'set']) +@core_command.server_sub_command(['auth', 'backend', 'set']) def _sp_auth_load(command_info, backend): """Set the active backend.""" try: @@ -38,7 +38,7 @@ def _sp_auth_load(command_info, backend): logger.log_message( 'Backend "{}" has been loaded sucessfully!'.format(backend)) -@_core_command.server_sub_command(['auth', 'backend', 'list']) +@core_command.server_sub_command(['auth', 'backend', 'list']) def _sp_auth_list(command_info): """List all available backends and marks the active backend.""" if not auth_manager: @@ -57,7 +57,7 @@ def _sp_auth_list(command_info): # ============================================================================= # >> sp auth permission player # ============================================================================= -@_core_command.server_sub_command(['auth', 'permission', 'player', 'add']) +@core_command.server_sub_command(['auth', 'permission', 'player', 'add']) def _sp_auth_permission_player_add( command_info, players:filter_str, permission, server_id:int=None): """Grant a permission to players.""" @@ -72,7 +72,7 @@ def _sp_auth_permission_player_add( logger.log_message('Granted permission "{}" to {}.'.format( permission, player.name)) -@_core_command.server_sub_command(['auth', 'permission', 'player', 'remove']) +@core_command.server_sub_command(['auth', 'permission', 'player', 'remove']) def _sp_auth_permission_player_remove( command_info, players:filter_str, permission, server_id:int=None): """Remove a permission from players.""" @@ -88,7 +88,7 @@ def _sp_auth_permission_player_remove( 'Removed permission "{}" from {}.'.format( permission, player.name)) -@_core_command.server_sub_command([ +@core_command.server_sub_command([ 'auth', 'permission', 'player', 'add_parent' ]) def _sp_auth_permission_player_add_parent( @@ -105,7 +105,7 @@ def _sp_auth_permission_player_add_parent( logger.log_message( 'Added parent "{}" to {}.'.format(parent, player.name)) -@_core_command.server_sub_command([ +@core_command.server_sub_command([ 'auth', 'permission', 'player', 'remove_parent' ]) def _sp_auth_permission_player_remove_parent( @@ -122,7 +122,7 @@ def _sp_auth_permission_player_remove_parent( logger.log_message( 'Removed parent "{}" from {}.'.format(parent, player.name)) -@_core_command.server_sub_command(['auth', 'permission', 'player', 'test']) +@core_command.server_sub_command(['auth', 'permission', 'player', 'test']) def _sp_auth_permission_player_test(command_info, permission): """Test which players are granted a permission.""" logger.log_message( @@ -148,7 +148,7 @@ def _sp_auth_permission_player_test(command_info, permission): # ============================================================================= # >> sp auth permission parent # ============================================================================= -@_core_command.server_sub_command(['auth', 'permission', 'parent', 'add']) +@core_command.server_sub_command(['auth', 'permission', 'parent', 'add']) def _sp_auth_permission_parent_add( command_info, parent, permission, server_id:int=None): """Add a permission to a parent.""" @@ -156,7 +156,7 @@ def _sp_auth_permission_parent_add( logger.log_message( 'Added permission "{}" to parent "{}".'.format(permission, parent)) -@_core_command.server_sub_command(['auth', 'permission', 'parent', 'remove']) +@core_command.server_sub_command(['auth', 'permission', 'parent', 'remove']) def _sp_auth_permission_parent_remove( command_info, parent, permission, server_id:int=None): """Remove a permission from a parent.""" @@ -164,7 +164,7 @@ def _sp_auth_permission_parent_remove( logger.log_message('Removed permission "{}" from parent "{}".'.format( permission, parent)) -@_core_command.server_sub_command([ +@core_command.server_sub_command([ 'auth', 'permission', 'parent', 'add_parent' ]) def _sp_auth_permission_parent_add_parent( @@ -174,7 +174,7 @@ def _sp_auth_permission_parent_add_parent( logger.log_message( 'Added parent "{}" to parent "{}".'.format(parent_to_add, parent)) -@_core_command.server_sub_command([ +@core_command.server_sub_command([ 'auth', 'permission', 'parent', 'remove_parent' ]) def _sp_auth_permission_parent_remove_parent( @@ -186,7 +186,7 @@ def _sp_auth_permission_parent_remove_parent( 'Removed parent "{}" from parent "{}".'.format( parent_to_remove, parent)) -@_core_command.server_sub_command([ +@core_command.server_sub_command([ 'auth', 'permission', 'parent', 'list' ]) def _sp_auth_permission_parent_list( @@ -212,21 +212,22 @@ def _sp_auth_permission_parent_list( # ============================================================================= # >> DESCRIPTIONS # ============================================================================= -_sp_auth = TypedServerCommand.parser.get_node(['sp', 'auth']) -_sp_auth.description = 'Authorization specific commands.' +TypedServerCommand.parser.set_node_description( + ['sp', 'auth'], + 'Authorization specific commands.') -_sp_auth_backend = TypedServerCommand.parser.get_node( - ['sp', 'auth', 'backend']) -_sp_auth_backend.description = 'Authorization backend specific commands.' +TypedServerCommand.parser.set_node_description( + ['sp', 'auth', 'backend'], + 'Authorization backend specific commands.') -_sp_auth_permission = TypedServerCommand.parser.get_node( - ['sp', 'auth', 'permission']) -_sp_auth_permission.description = 'Commands to modify permissions.' +TypedServerCommand.parser.set_node_description( + ['sp', 'auth', 'permission'], + 'Commands to modify permissions.') -_sp_auth_permission_parent = TypedServerCommand.parser.get_node( - ['sp', 'auth', 'permission', 'parent']) -_sp_auth_permission_parent.description = 'Commands to modify parent permissions.' +TypedServerCommand.parser.set_node_description( + ['sp', 'auth', 'permission', 'parent'], + 'Commands to modify parent permissions.') -_sp_auth_permission_player = TypedServerCommand.parser.get_node( - ['sp', 'auth', 'permission', 'player']) -_sp_auth_permission_player.description = 'Commands to modify player permissions.' +TypedServerCommand.parser.set_node_description( + ['sp', 'auth', 'permission', 'player'], + 'Commands to modify player permissions.') diff --git a/addons/source-python/packages/source-python/core/command/docs.py b/addons/source-python/packages/source-python/core/command/docs.py index 3bed7f7db..b9ba67050 100644 --- a/addons/source-python/packages/source-python/core/command/docs.py +++ b/addons/source-python/packages/source-python/core/command/docs.py @@ -15,7 +15,7 @@ # Commands from commands.typed import TypedServerCommand # Core -from core.command import _core_command +from core.command import core_command from core.command import core_command_logger from core.version import VERSION # Paths @@ -37,17 +37,17 @@ # ============================================================================= # >> sp docs create/generate/build # ============================================================================= -@_core_command.server_sub_command(['docs', 'create']) +@core_command.server_sub_command(['docs', 'create']) def _sp_docs_create(command_info, package): """Create a Sphinx project.""" _create_sphinx_project(package) -@_core_command.server_sub_command(['docs', 'generate']) +@core_command.server_sub_command(['docs', 'generate']) def _sp_docs_generate(command_info, package): """Generate a Sphinx project.""" _generate_sphinx_project(package) -@_core_command.server_sub_command(['docs', 'build']) +@core_command.server_sub_command(['docs', 'build']) def _sp_docs_build(command_info, package): """Build a Sphinx project.""" _build_sphinx_project(package) @@ -429,5 +429,5 @@ def is_plugin(package): # ============================================================================= # >> DESCRIPTIONS # ============================================================================= -_sp_docs = TypedServerCommand.parser.get_node(['sp', 'docs']) -_sp_docs.description = 'Documentation specific commands.' +TypedServerCommand.parser.set_node_description( + ['sp', 'docs'], 'Documentation specific commands.') diff --git a/addons/source-python/packages/source-python/core/command/dump.py b/addons/source-python/packages/source-python/core/command/dump.py index 021aacdb9..38837acf9 100644 --- a/addons/source-python/packages/source-python/core/command/dump.py +++ b/addons/source-python/packages/source-python/core/command/dump.py @@ -10,39 +10,39 @@ from commands.typed import TypedServerCommand # Core from core import dumps -from core.command import _core_command +from core.command import core_command # ============================================================================= # >> sp dump # ============================================================================= #: .. todo:: Make file_name optional -@_core_command.server_sub_command(['dump', 'class_info']) +@core_command.server_sub_command(['dump', 'class_info']) def _sp_dump_class_info(command_info, file_name): """Dump class info.""" dumps.dump_class_info(file_name) -@_core_command.server_sub_command(['dump', 'convars']) +@core_command.server_sub_command(['dump', 'convars']) def _sp_dump_convars(command_info, file_name): """Dump convars.""" dumps.dump_convars(file_name) -@_core_command.server_sub_command(['dump', 'datamaps']) +@core_command.server_sub_command(['dump', 'datamaps']) def _sp_dump_datamaps(command_info, file_name): """Dump datamaps.""" dumps.dump_datamaps(file_name) -@_core_command.server_sub_command(['dump', 'server_classes']) +@core_command.server_sub_command(['dump', 'server_classes']) def _sp_dump_server_classes(command_info, file_name): """Dump server classes.""" dumps.dump_server_classes(file_name) -@_core_command.server_sub_command(['dump', 'string_tables']) +@core_command.server_sub_command(['dump', 'string_tables']) def _sp_dump_string_tables(command_info, file_name): """Dump string tables.""" dumps.dump_string_tables(file_name) -@_core_command.server_sub_command(['dump', 'weapon_scripts']) +@core_command.server_sub_command(['dump', 'weapon_scripts']) def _sp_dump_weapon_scripts(command_info, file_name): """Dump weapon scripts.""" dumps.dump_weapon_scripts(file_name) @@ -51,5 +51,5 @@ def _sp_dump_weapon_scripts(command_info, file_name): # ============================================================================= # >> DESCRIPTIONS # ============================================================================= -_sp_dump = TypedServerCommand.parser.get_node(['sp', 'dump']) -_sp_dump.description = 'Dump various data to files.' +TypedServerCommand.parser.set_node_description( + ['sp', 'dump'], 'Dump various data to files.') diff --git a/addons/source-python/packages/source-python/core/command/plugin.py b/addons/source-python/packages/source-python/core/command/plugin.py index 5f9ffe416..0c3928411 100644 --- a/addons/source-python/packages/source-python/core/command/plugin.py +++ b/addons/source-python/packages/source-python/core/command/plugin.py @@ -9,7 +9,7 @@ # Commands from commands.typed import TypedServerCommand # Core -from core.command import _core_command +from core.command import core_command from core.command import core_command_logger @@ -22,29 +22,29 @@ # ============================================================================= # >> sp plugin # ============================================================================= -@_core_command.server_sub_command(['plugin', 'load']) +@core_command.server_sub_command(['plugin', 'load']) def _sp_plugin_load(command_info, plugin): """Load a plugin.""" - _core_command.load_plugin(plugin) + core_command.load_plugin(plugin) -@_core_command.server_sub_command(['plugin', 'unload']) +@core_command.server_sub_command(['plugin', 'unload']) def _sp_plugin_unload(command_info, plugin): """Unload a plugin.""" - _core_command.unload_plugin(plugin) + core_command.unload_plugin(plugin) -@_core_command.server_sub_command(['plugin', 'reload']) +@core_command.server_sub_command(['plugin', 'reload']) def _sp_plugin_reload(command_info, plugin): """Reload a plugin.""" - _core_command.reload_plugin(plugin) + core_command.reload_plugin(plugin) -@_core_command.server_sub_command(['plugin', 'list']) +@core_command.server_sub_command(['plugin', 'list']) def _sp_plugin_list(command_info): """List all currently loaded plugins.""" - _core_command.print_plugins() + core_command.print_plugins() # ============================================================================= # >> DESCRIPTIONS # ============================================================================= -_sp_plugin = TypedServerCommand.parser.get_node(['sp', 'plugin']) -_sp_plugin.description = 'Plugin specific commands.' +TypedServerCommand.parser.set_node_description( + ['sp', 'plugin'], 'Plugin specific commands.') diff --git a/addons/source-python/packages/source-python/core/manager.py b/addons/source-python/packages/source-python/core/manager.py deleted file mode 100644 index cb9356200..000000000 --- a/addons/source-python/packages/source-python/core/manager.py +++ /dev/null @@ -1,39 +0,0 @@ -# ../core/manager.py - -"""Provides access to all plugins loaded via "sp plugin load".""" - -# ============================================================================= -# >> IMPORTS -# ============================================================================= -# Source.Python Imports -# Core -from core import core_logger -# Plugins -from plugins.manager import PluginManager - - -# ============================================================================= -# >> ALL DECLARATION -# ============================================================================= -__all__ = ('_CorePluginManager', - 'core_plugin_manager', - ) - - -# ============================================================================= -# >> GLOBAL VARIABLES -# ============================================================================= -# Get the sp.core.manager logger -core_manager_logger = core_logger.manager - - -# ============================================================================= -# >> CLASSES -# ============================================================================= -class _CorePluginManager(PluginManager): - """Plugin Manager class used to load "sp" plugins.""" - - logger = core_manager_logger - -# The singleton object of the :class:`_CorePluginManager` class -core_plugin_manager = _CorePluginManager() diff --git a/addons/source-python/packages/source-python/listeners/__init__.py b/addons/source-python/packages/source-python/listeners/__init__.py index dcbb46e44..cee9a6ac0 100644 --- a/addons/source-python/packages/source-python/listeners/__init__.py +++ b/addons/source-python/packages/source-python/listeners/__init__.py @@ -90,7 +90,9 @@ 'OnButtonStateChanged', 'OnPlayerRunCommand', 'OnPluginLoaded', + 'OnPluginLoading', 'OnPluginUnloaded', + 'OnPluginUnloading', 'OnQueryCvarValueFinished', 'OnServerActivate', 'OnTick', @@ -118,7 +120,9 @@ 'on_level_shutdown_listener_manager', 'on_network_id_validated_listener_manager', 'on_plugin_loaded_manager', + 'on_plugin_loading_manager', 'on_plugin_unloaded_manager', + 'on_plugin_unloading_manager', 'on_query_cvar_value_finished_listener_manager', 'on_server_activate_listener_manager', 'on_tick_listener_manager', @@ -136,6 +140,8 @@ on_convar_changed_listener_manager = ListenerManager() on_plugin_loaded_manager = ListenerManager() on_plugin_unloaded_manager = ListenerManager() +on_plugin_loading_manager = ListenerManager() +on_plugin_unloading_manager = ListenerManager() on_level_end_listener_manager = ListenerManager() on_player_run_command_listener_manager = ListenerManager() on_button_state_changed_listener_manager = ListenerManager() @@ -376,12 +382,24 @@ class OnPluginLoaded(ListenerManagerDecorator): manager = on_plugin_loaded_manager +class OnPluginLoading(ListenerManagerDecorator): + """Register/unregister a plugin loading listener.""" + + manager = on_plugin_loading_manager + + class OnPluginUnloaded(ListenerManagerDecorator): """Register/unregister a plugin unloaded listener.""" manager = on_plugin_unloaded_manager +class OnPluginUnloading(ListenerManagerDecorator): + """Register/unregister a plugin unloading listener.""" + + manager = on_plugin_unloading_manager + + class OnLevelEnd(ListenerManagerDecorator): """Register/unregister a map end listener.""" diff --git a/addons/source-python/packages/source-python/plugins/command.py b/addons/source-python/packages/source-python/plugins/command.py index 23e62fe9b..a7a64b72e 100644 --- a/addons/source-python/packages/source-python/plugins/command.py +++ b/addons/source-python/packages/source-python/plugins/command.py @@ -5,10 +5,6 @@ # ============================================================================= # >> IMPORTS # ============================================================================= -# Python Imports -# Re -import re - # Source.Python Imports # Commands from commands.typed import ( @@ -16,13 +12,18 @@ ) # Core from core import AutoUnload +# Hooks +from hooks.exceptions import except_hooks +# Paths +from paths import GAME_PATH # Plugins from plugins import plugins_logger from plugins import _plugin_strings -from plugins.errors import PluginInstanceError -from plugins.errors import PluginManagerError -from plugins.instance import LoadedPlugin -from plugins.manager import PluginManager +from plugins.manager import PluginFileNotFoundError +from plugins.manager import InvalidPluginName +from plugins.manager import PluginAlreadyLoaded +from plugins.manager import PluginHasBuiltInName +from plugins.manager import PluginNotLoaded # ============================================================================= @@ -50,37 +51,37 @@ class SubCommandManager(AutoUnload, list): translations = _plugin_strings def __init__(self, command, prefix=''): - """Called on instance initialization.""" + """Initializes the sub-command manager. + + :param str command: + Command to register. + :param str prefix: + Prefix used for printing to the console. + """ # Re-call OrderedDict's __init__ to properly setup the object super().__init__() - - # Does the class have a proper manager object assigned? - if not isinstance(self.manager, PluginManager): - raise PluginManagerError(PluginManagerError.__doc__) - - # Does the class have a proper instance class assigned? - if not issubclass(self.instance, LoadedPlugin): - raise PluginInstanceError(PluginInstanceError.__doc__) - - # Store the command self._command = command - - # Store the prefix self._prefix = prefix if prefix else '[{0}] '.format( self.command.upper()) - # Set the prefix for the manager and instance classes - self.manager.prefix = self.instance.prefix = self.prefix + @property + def manager(self): + """Return a plugin manager. - # Set the instance class for the manager class - self.manager.instance = self.instance + :rtype: PluginManager + """ + raise NotImplementedError('No manager attribute defined for class.') def _unload_instance(self): + """Unload all sub-commands.""" for item in self: item._unload_instance() + # Probably not necessary, but just in case... + self.clear() + def server_sub_command(self, commands): - """Add a sub-command. + """Add a server sub-command. .. seealso:: :class:`commands.typed.TypedServerCommand` """ @@ -91,6 +92,10 @@ def server_sub_command(self, commands): return command def client_sub_command(self, commands, permission=None): + """Add a client sub-command. + + .. seealso:: :class:`commands.typed.TypedClientCommand` + """ if isinstance(commands, str): commands = [commands] command = TypedClientCommand( @@ -101,6 +106,10 @@ def client_sub_command(self, commands, permission=None): return command def say_sub_command(self, commands, permission=None): + """Add a say sub-command. + + .. seealso:: :class:`commands.typed.TypedSayCommand` + """ if isinstance(commands, str): commands = [commands] command = TypedSayCommand( @@ -110,116 +119,97 @@ def say_sub_command(self, commands, permission=None): self.append(command) return command - @property - def manager(self): - """Raise an error if the inheriting class does not have their own.""" - raise NotImplementedError('No manager attribute defined for class.') - - @property - def instance(self): - """Raise an error if the inheriting class does not have their own.""" - raise NotImplementedError('No instance attribute defined for class.') - @property def command(self): - """Return the server command registered to the class.""" + """Return the server command registered to the class. + + :rtype: str + """ return self._command @property def prefix(self): - """Return the prefix to use in log messages.""" + """Return the prefix to use in log messages. + + :rtype: str + """ return self._prefix def load_plugin(self, plugin_name): - """Load a plugin by name.""" - # Is the given plugin name a proper name? - if not self._is_valid_plugin_name(plugin_name): + """Load a plugin by name. - # Log a message that the given name is invalid - self._log_message(self.prefix + self.translations[ + :param str plugin_name: + Name of the plugin to load. + :return: + Return the loaded plugin. Return ``None`` on failure. + :rtype: LoadedPlugin + """ + plugin = None + self.log_message(self.translations[ + 'Loading'].get_string(plugin=plugin_name)) + try: + plugin = self.manager.load(plugin_name) + except InvalidPluginName: + self.log_message(self.translations[ 'Invalid Name'].get_string(plugin=plugin_name)) - - # No need to go further - return - - # Is the plugin already loaded? - if plugin_name in self.manager: - - # Log a message that the plugin is already loaded - self._log_message(self.prefix + self.translations[ + except PluginAlreadyLoaded: + self.log_message(self.translations[ 'Already Loaded'].get_string(plugin=plugin_name)) - - # No need to go further - return - - # Load the plugin and get its instance - plugin = self.manager[plugin_name] - - # Was the plugin unable to be loaded? - if plugin is None: - - # Log a message that the plugin was not loaded - self._log_message(self.prefix + self.translations[ + except PluginFileNotFoundError: + self.log_message(self.translations[ + 'No Module'].get_string( + plugin=plugin_name, file=GAME_PATH.relpathto( + self.manager.get_plugin_file_path( + plugin_name)).replace('\\', '/'))) + except PluginHasBuiltInName: + self.log_message(self.translations[ + 'Built-in'].get_string(plugin=plugin_name)) + except: + except_hooks.print_exception() + self.log_message(self.translations[ 'Unable to Load'].get_string(plugin=plugin_name)) + else: + self.log_message(self.translations[ + 'Successful Load'].get_string(plugin=plugin_name)) - # No need to go further - return - - # Log a message that the plugin was loaded - self._log_message(self.prefix + self.translations[ - 'Successful Load'].get_string(plugin=plugin_name)) + return plugin def unload_plugin(self, plugin_name): - """Unload a plugin by name.""" - # Is the given plugin name a proper name? - if not self._is_valid_plugin_name(plugin_name): + """Unload a plugin by name. - # Send a message that the given name is invalid - self._log_message(self.prefix + self.translations[ + :param str plugin_name: + Name of the plugin to unload. + """ + self.log_message(self.translations[ + 'Unloading'].get_string(plugin=plugin_name)) + try: + self.manager.unload(plugin_name) + except InvalidPluginName: + self.log_message(self.translations[ 'Invalid Name'].get_string(plugin=plugin_name)) - - # No need to go further - return - - # Is the plugin loaded? - if plugin_name not in self.manager: - - # Send a message that the plugin is not loaded - self._log_message(self.prefix + self.translations[ + except PluginNotLoaded: + self.log_message(self.translations[ 'Not Loaded'].get_string(plugin=plugin_name)) - - # No need to go further - return - - # Unload the plugin - del self.manager[plugin_name] - - # Send a message that the plugin was unloaded - self._log_message(self.prefix + self.translations[ - 'Successful Unload'].get_string(plugin=plugin_name)) + else: + self.log_message(self.translations[ + 'Successful Unload'].get_string(plugin=plugin_name)) def reload_plugin(self, plugin_name): - """Reload a plugin by name.""" - # Is the given plugin name a proper name? - if not self._is_valid_plugin_name(plugin_name): - - # Send a message that the given name is invalid - self._log_message(self.prefix + self.translations[ - 'Invalid Name'].get_string(plugin=plugin_name)) - - # No need to go further - return + """Reload a plugin by name. - # Unload the plugin + :param str plugin_name: + Name of the plugin to reload. + :return: + Return the loaded plugin. Return ``None`` on failure. + :rtype: LoadedPlugin + """ self.unload_plugin(plugin_name) - - # Load the plugin - self.load_plugin(plugin_name) + return self.load_plugin(plugin_name) def print_plugins(self): """Print all currently loaded plugins.""" # Get the header message - message = self.prefix + self.translations[ + message = self.translations[ 'Plugins'].get_string() + '\n' + '=' * 61 + '\n\n\t' # Add all loaded plugins to the message @@ -229,18 +219,12 @@ def print_plugins(self): message += '\n\n' + '=' * 61 # Send the message - self._log_message(message) - - def _log_message(self, message): - """Log a message.""" - # Log the message - self.logger.log_message(message) + self.log_message(message) - @staticmethod - def _is_valid_plugin_name(plugin_name): - """Return whether or not the given plugin name is valid.""" - # Get the regular expression match for the given plugin name - match = re.match('([A-Za-z][A-Za-z0-9_]*[A-Za-z0-9])$', plugin_name) + def log_message(self, message): + """Log a message. The prefix will be added automatically. - # Return whether it is valid or not - return False if match is None else match.group() == plugin_name + :param str message: + Message to log. + """ + self.logger.log_message(self.prefix + message) diff --git a/addons/source-python/packages/source-python/plugins/errors.py b/addons/source-python/packages/source-python/plugins/errors.py deleted file mode 100644 index fcb280319..000000000 --- a/addons/source-python/packages/source-python/plugins/errors.py +++ /dev/null @@ -1,33 +0,0 @@ -# ../plugins/errors.py - -"""Contains custom exceptions that can be raised within the plugins package.""" - -# ============================================================================= -# >> ALL DECLARATION -# ============================================================================= -__all__ = ('PluginFileNotFoundError', - 'PluginInstanceError', - 'PluginManagerError', - ) - - -# ============================================================================= -# >> CLASSES -# ============================================================================= -class PluginFileNotFoundError(FileNotFoundError): - """Plugin file not found.""" - - -class PluginInstanceError(TypeError): - """Improper plugin instance class assigned.""" - -PluginInstanceError.__doc__ += ( - ' Class must be inherited from plugins.instance.LoadedPlugin') - - -class PluginManagerError(TypeError): - """Improper plugin manager assigned.""" - -PluginManagerError.__doc__ += ( - ' Object must be an instance of a class that inherits ' + - 'from plugins.manager.PluginManager') diff --git a/addons/source-python/packages/source-python/plugins/instance.py b/addons/source-python/packages/source-python/plugins/instance.py index cb78d20a7..72df27d7c 100644 --- a/addons/source-python/packages/source-python/plugins/instance.py +++ b/addons/source-python/packages/source-python/plugins/instance.py @@ -10,13 +10,8 @@ from importlib import import_module # Source.Python Imports -# Paths -from paths import GAME_PATH -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 @@ -40,10 +35,6 @@ class LoadedPlugin(object): """Stores a plugin's instance.""" - logger = None - translations = None - prefix = None - def __init__(self, plugin_name, manager): """Called when a plugin's instance is initialized. @@ -53,47 +44,28 @@ def __init__(self, plugin_name, manager): A plugin manager instance. """ self.manager = manager - self.file_path = None - self.import_name = None self.globals = None - self.plugin_name = plugin_name + self.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.file_path = self.manager.get_plugin_file_path(plugin_name) + self.info = self.manager._create_plugin_info(plugin_name) self._plugin = None - - # Fall back to the default logger if none was set - if self.logger is None: - self.logger = plugins_instance_logger - - # 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 - self.logger.log_message(self.prefix + self.translations[ - 'Loading'].get_string(plugin=plugin_name)) - - # Does the plugin's main file exist? - 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=self.file_path.replace( - GAME_PATH, '').replace('\\', '/'))) - - # Raise an error so that the plugin - # is not added to the PluginManager - raise PluginFileNotFoundError - - # Get the import name self.import_name = (self.manager.base_import + plugin_name + '.' + plugin_name) + def unload(self): + """Unload the plugin.""" + # Convenience method + self.manager.unload(self.name) + + def reload(self): + """Reload the plugin.""" + # Convenience method + self.manager.reload(self.name) + def _load(self): """Actually load the plugin.""" + self.info._create_public_convar() self._plugin = import_module(self.import_name) self.globals = { x: getattr(self._plugin, x) for x in dir(self._plugin)} @@ -104,8 +76,4 @@ def _load(self): 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() + self.globals['unload']() diff --git a/addons/source-python/packages/source-python/plugins/manager.py b/addons/source-python/packages/source-python/plugins/manager.py index 4c2f9e8d5..7a45dd2a9 100644 --- a/addons/source-python/packages/source-python/plugins/manager.py +++ b/addons/source-python/packages/source-python/plugins/manager.py @@ -11,8 +11,12 @@ # Configobj from configobj import ConfigObj from configobj import Section +# Importlib +from importlib.util import find_spec # Sys import sys +# Re +import re # Source.Python Imports # Core @@ -23,13 +27,14 @@ # Listeners from listeners import on_plugin_loaded_manager from listeners import on_plugin_unloaded_manager +from listeners import on_plugin_loading_manager +from listeners import on_plugin_unloading_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 +from plugins.instance import LoadedPlugin # ============================================================================= @@ -49,13 +54,29 @@ # ============================================================================= # >> CLASSES # ============================================================================= +class PluginError(ValueError): + pass + +class PluginFileNotFoundError(PluginError): + pass + +class InvalidPluginName(PluginError): + pass + +class PluginAlreadyLoaded(PluginError): + pass + +class PluginHasBuiltInName(PluginError): + pass + +class PluginNotLoaded(PluginError): + pass + + class PluginManager(OrderedDict): """Stores plugins and their instances.""" - instance = None - prefix = None - logger = None - translations = None + RE_VALID_PLUGIN = re.compile('^([A-Za-z][A-Za-z0-9_]*[A-Za-z0-9])$') def __init__(self, base_import=''): """Called when the class instance is initialized.""" @@ -63,115 +84,133 @@ def __init__(self, base_import=''): super().__init__() self._base_import = base_import - # Does the object have a logger set? - if self.logger is None: - self.logger = plugins_manager_logger - - # Does the object have a translations value set? - if self.translations is None: - self.translations = _plugin_strings - def _create_plugin_instance(self, plugin_name): """Create a new plugin instance. + Overwrite this method if you wish to use your own LoadedPlugin + subclass. + :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) + return LoadedPlugin(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 - - # Actually load the plugin - instance._load() + @property + def base_import(self): + """Return the base import path for the manager. - # Was the file not found? - # We use this check because we already printed the error to console - except PluginFileNotFoundError: + :rtype: str + """ + return self._base_import - # Return None as the value to show the plugin was not loaded - return None + @property + def plugins_directory(self): + """Return the directory where the plugins are stored. - # Was a different error encountered? - except: - try: - super().__delitem__(plugin_name) - except KeyError: - pass + :rtype: path.Path + """ + return PLUGIN_PATH.joinpath(*tuple(self.base_import.split('.')[:~0])) - # Get the error - error = sys.exc_info() + def load(self, plugin_name): + """Load a plugin by name. - # Is the error due to "No module named '.'"? - if (len(error[1].args) and error[1].args[0] == - "No module named '{0}.{0}'".format(plugin_name)): + :param str plugin_name: + Name of the plugin to load. + :raise InvalidPluginName: + Raised if the given plugin name is invalid. + :raise PluginAlreadyLoaded: + Raised if the plugin was already loaded. + :raise PluginFileNotFoundError: + Raised if the plugin's main file wasn't found. + :raise PluginHasBuiltInName: + Raised if the plugin has the name of a built-in module. + :raise Exception: + Any other exceptions raised by the plugin during the load process. + :rtype: LoadedPlugin + """ + if self.is_loaded(plugin_name): + raise PluginAlreadyLoaded( + 'Plugin "{}" is already loaded.'.format(plugin_name)) - # Print a message about not using built-in module names - # We already know the path exists, so the only way this error - # could occur is if it shares its name with a built-in module - self.logger.log_message(self.prefix + self.translations[ - 'Built-in'].get_string(plugin=plugin_name)) + if not self.is_valid_plugin_name(plugin_name): + raise InvalidPluginName( + '"{}" is an invalid plugin name.'.format(plugin_name)) - # Otherwise - else: + plugin = self._create_plugin_instance(plugin_name) + if not plugin.file_path.isfile(): + raise PluginFileNotFoundError - # Print the exception to the console - except_hooks.print_exception(*error) + spec = find_spec(plugin.import_name) + if spec is None or spec.origin != plugin.file_path: + raise PluginHasBuiltInName - # Remove all modules from sys.modules - self._remove_modules(plugin_name) + # 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] = plugin + on_plugin_loading_manager.notify(plugin) - # Return None as the value to show the addon was not loaded - return None + try: + # Actually load the plugin + plugin._load() + except: + self.pop(plugin_name, 0) + self._remove_modules(plugin_name) + raise - on_plugin_loaded_manager.notify(plugin_name) - return instance + on_plugin_loaded_manager.notify(plugin) + return plugin - def __delitem__(self, plugin_name): - """Remove a plugin from the manager.""" - # Is the plugin in the dictionary? - if plugin_name not in self: - return + def unload(self, plugin_name): + """Unload a plugin by name. - # Print a message about the plugin being unloaded - self.logger.log_message(self.prefix + self.translations[ - 'Unloading'].get_string(plugin=plugin_name)) + :param str plugin_name: + Name of the plugin to unload. + :raise PluginNotLoaded: + Raised if the plugin is not loaded. + """ + if not self.is_loaded(plugin_name): + raise PluginNotLoaded( + 'Plugin "{}" is not loaded.'.format(plugin_name)) - self[plugin_name]._unload() + plugin = self[plugin_name] + on_plugin_unloading_manager.notify(plugin) + try: + plugin._unload() + except: + except_hooks.print_exceptions() - # Remove all modules from sys.modules self._remove_modules(plugin_name) - - # Remove the plugin from the dictionary - super().__delitem__(plugin_name) + del self[plugin_name] on_plugin_unloaded_manager.notify(plugin_name) - @property - def base_import(self): - """Return the base import path for the manager. + def reload(self, plugin_name): + """Reload a plugin by name. - :rtype: str + :param str plugin_name: + Name of the plugin to reload. + :raise PluginNotLoaded: + Raised if the plugin is not loaded. + :raise InvalidPluginName: + Raised if the given plugin name is invalid. + :raise PluginFileNotFoundError: + Raised if the plugin's main file wasn't found. + :raise PluginHasBuiltInName: + Raised if the plugin has the name of a built-in module. + :raise Exception: + Any other exceptions raised by the plugin during the load process. + :rtype: LoadedPlugin """ - return self._base_import + self.unload(plugin_name) + return self.load(plugin_name) - @property - def plugins_directory(self): - """Return the directory where the plugins are stored. + def is_valid_plugin_name(self, plugin_name): + """Return whether or not the given plugin name is valid. - :rtype: path.Path + :param str plugin_name: + Name to check. + :rtype: bool """ - return PLUGIN_PATH.joinpath(*tuple(self.base_import.split('.')[:~0])) + return self.RE_VALID_PLUGIN.match(plugin_name) is not None def is_loaded(self, plugin_name): """Return whether or not a plugin is loaded. @@ -185,6 +224,9 @@ def is_loaded(self, plugin_name): def plugin_exists(self, plugin_name): """Return whether of not a plugin exists. + This method only checks for the existance of the plugin directory, but + not for the existance of the main plugin file. + :param str plugin_name: The plugin to check. :rtype: bool @@ -199,16 +241,25 @@ def get_plugin_instance(self, plugin_name): 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: + plugin_name = self.get_plugin_name(plugin_name) + if self.is_loaded(plugin_name): return self[plugin_name] return None + def get_plugin_name(self, plugin_name): + """Return the plugin's name. + + :param str plugin_name: + The plugin's real name (will be passed through) or the + ``__name__`` variable of one of the plugin's files. + :rtype: str + """ + if not plugin_name.startswith(self.base_import): + return plugin_name + + return plugin_name.replace(self.base_import, '', 1).split('.', 1)[0] + def get_plugin_directory(self, plugin_name): """Return the directory of the given plugin. @@ -216,6 +267,15 @@ def get_plugin_directory(self, plugin_name): """ return self.plugins_directory / plugin_name + def get_plugin_file_path(self, plugin_name): + """Return the path to the plugin's main file. + + :param str plugin_name: + Name of the plugin. + :rtype: path.Path + """ + return self.get_plugin_directory(plugin_name) / plugin_name + '.py' + def get_plugin_info(self, plugin_name): """Return information about the given plugin. @@ -235,10 +295,12 @@ def _create_plugin_info(self, plugin_name): :param str plugin_name: Name of the plugin whose plugin info should be created. + :raise PluginFileNotFoundError: + Raised if the plugin's main directory wasn't found. :rtype: PluginInfo """ if not self.plugin_exists(plugin_name): - raise ValueError( + raise PluginFileNotFoundError( 'Plugin "{}" does not exist.'.format(plugin_name)) info_file = self.get_plugin_directory(plugin_name) / 'info.ini' @@ -325,3 +387,6 @@ def _unload_auto_unload_instances(instances): # other AutoUnload instances to be unloaded # and the plugin to be fully unloaded itself except_hooks.print_exception() + +# The singleton instance of the :class:`PluginManager` class +plugin_manager = PluginManager() From a61bc6d2b47bb2fb886604eaeb0f4b9c02fb965a Mon Sep 17 00:00:00 2001 From: Ayuto22 Date: Sat, 14 Jan 2017 11:45:30 +0100 Subject: [PATCH 02/12] No need to check for the existance of a PluginInfo instance anymore --- .../source-python/core/command/__init__.py | 42 ++++++++----------- 1 file changed, 17 insertions(+), 25 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 ed7452636..c19c5487f 100644 --- a/addons/source-python/packages/source-python/core/command/__init__.py +++ b/addons/source-python/packages/source-python/core/command/__init__.py @@ -65,38 +65,30 @@ def print_plugins(self): for plugin_name in sorted(self.manager): info = self.manager[plugin_name].info - # Was an PluginInfo instance found? - if info is not None: - message += plugin_name + ' ({}):\n'.format(info.verbose_name) + message += plugin_name + ' ({}):\n'.format(info.verbose_name) - if info.author is not None: - message += ' author: {}\n'.format(info.author) + 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.description is not None: + message += ' description: {}\n'.format(info.description) - if info.version != 'unversioned': - message += ' version: {}\n'.format(info.version) + if info.version != 'unversioned': + message += ' version: {}\n'.format(info.version) - if info.url is not None: - message += ' url: {}\n'.format(info.url) + if info.url is not None: + message += ' url: {}\n'.format(info.url) - if info.permissions: - message += ' permissions:\n' - for permission, description in info.permissions: - message += ' {}:'.format(permission).ljust(30) + description + '\n' + if info.permissions: + message += ' permissions:\n' + for permission, description in info.permissions: + message += ' {}:'.format(permission).ljust(30) + description + '\n' - if info.public_convar is not None: - message += ' public convar: {}\n'.format(info.public_convar.name) + if info.public_convar is not None: + message += ' public convar: {}\n'.format(info.public_convar.name) - for attr in info.display_in_listing: - message += ' {}:'.format(attr).ljust(20) + str(getattr(info, attr)) + '\n' - - # Was no PluginInfo instance found? - else: - - # Add message with the current plugin's name - message += plugin_name + '\n' + for attr in info.display_in_listing: + message += ' {}:'.format(attr).ljust(20) + str(getattr(info, attr)) + '\n' # Add 1 blank line between each plugin message += '\n' From 105ab577ed1f75dd58d5f573c21e4d0f9a53d5fb Mon Sep 17 00:00:00 2001 From: Ayuto22 Date: Sat, 14 Jan 2017 12:25:52 +0100 Subject: [PATCH 03/12] Updated documentation files --- .../source/developing/modules/core.manager.rst | 7 ------- .../source/developing/modules/core.rst | 1 - .../modules/entities.engines.csgo.csgo.rst | 7 +++++++ .../modules/entities.engines.csgo.rst | 10 ++++++++++ .../developing/modules/entities.engines.rst | 18 ++++++++++++++++++ .../developing/modules/players.engines.bms.rst | 10 ++++++++++ .../modules/players.engines.l4d2.rst | 10 ++++++++++ .../developing/modules/plugins.errors.rst | 7 ------- .../source/developing/modules/plugins.rst | 1 - .../developing/modules/source-python.rst | 1 + .../modules/weapons.engines.csgo.csgo.rst | 7 +++++++ 11 files changed, 63 insertions(+), 16 deletions(-) delete mode 100644 addons/source-python/docs/source-python/source/developing/modules/core.manager.rst create mode 100644 addons/source-python/docs/source-python/source/developing/modules/entities.engines.csgo.csgo.rst create mode 100644 addons/source-python/docs/source-python/source/developing/modules/entities.engines.csgo.rst create mode 100644 addons/source-python/docs/source-python/source/developing/modules/entities.engines.rst create mode 100644 addons/source-python/docs/source-python/source/developing/modules/players.engines.bms.rst create mode 100644 addons/source-python/docs/source-python/source/developing/modules/players.engines.l4d2.rst delete mode 100644 addons/source-python/docs/source-python/source/developing/modules/plugins.errors.rst create mode 100644 addons/source-python/docs/source-python/source/developing/modules/weapons.engines.csgo.csgo.rst diff --git a/addons/source-python/docs/source-python/source/developing/modules/core.manager.rst b/addons/source-python/docs/source-python/source/developing/modules/core.manager.rst deleted file mode 100644 index 6763a98b8..000000000 --- a/addons/source-python/docs/source-python/source/developing/modules/core.manager.rst +++ /dev/null @@ -1,7 +0,0 @@ -core.manager module -==================== - -.. automodule:: core.manager - :members: - :undoc-members: - :show-inheritance: diff --git a/addons/source-python/docs/source-python/source/developing/modules/core.rst b/addons/source-python/docs/source-python/source/developing/modules/core.rst index c8bbc2bc2..6567a3002 100644 --- a/addons/source-python/docs/source-python/source/developing/modules/core.rst +++ b/addons/source-python/docs/source-python/source/developing/modules/core.rst @@ -9,7 +9,6 @@ Submodules core.command core.dumps - core.manager core.settings core.table core.version diff --git a/addons/source-python/docs/source-python/source/developing/modules/entities.engines.csgo.csgo.rst b/addons/source-python/docs/source-python/source/developing/modules/entities.engines.csgo.csgo.rst new file mode 100644 index 000000000..815567661 --- /dev/null +++ b/addons/source-python/docs/source-python/source/developing/modules/entities.engines.csgo.csgo.rst @@ -0,0 +1,7 @@ +entities.engines.csgo.csgo module +================================== + +.. automodule:: entities.engines.csgo.csgo + :members: + :undoc-members: + :show-inheritance: diff --git a/addons/source-python/docs/source-python/source/developing/modules/entities.engines.csgo.rst b/addons/source-python/docs/source-python/source/developing/modules/entities.engines.csgo.rst new file mode 100644 index 000000000..be709464e --- /dev/null +++ b/addons/source-python/docs/source-python/source/developing/modules/entities.engines.csgo.rst @@ -0,0 +1,10 @@ +entities.engines.csgo package +============================== + +Module contents +--------------- + +.. automodule:: entities.engines.csgo + :members: + :undoc-members: + :show-inheritance: diff --git a/addons/source-python/docs/source-python/source/developing/modules/entities.engines.rst b/addons/source-python/docs/source-python/source/developing/modules/entities.engines.rst new file mode 100644 index 000000000..be5f64c36 --- /dev/null +++ b/addons/source-python/docs/source-python/source/developing/modules/entities.engines.rst @@ -0,0 +1,18 @@ +entities.engines package +========================= + +Subpackages +----------- + +.. toctree:: + :titlesonly: + + entities.engines.csgo + +Module contents +--------------- + +.. automodule:: entities.engines + :members: + :undoc-members: + :show-inheritance: diff --git a/addons/source-python/docs/source-python/source/developing/modules/players.engines.bms.rst b/addons/source-python/docs/source-python/source/developing/modules/players.engines.bms.rst new file mode 100644 index 000000000..f4c7dced5 --- /dev/null +++ b/addons/source-python/docs/source-python/source/developing/modules/players.engines.bms.rst @@ -0,0 +1,10 @@ +players.engines.bms package +============================ + +Module contents +--------------- + +.. automodule:: players.engines.bms + :members: + :undoc-members: + :show-inheritance: diff --git a/addons/source-python/docs/source-python/source/developing/modules/players.engines.l4d2.rst b/addons/source-python/docs/source-python/source/developing/modules/players.engines.l4d2.rst new file mode 100644 index 000000000..8be8d9e41 --- /dev/null +++ b/addons/source-python/docs/source-python/source/developing/modules/players.engines.l4d2.rst @@ -0,0 +1,10 @@ +players.engines.l4d2 package +============================= + +Module contents +--------------- + +.. automodule:: players.engines.l4d2 + :members: + :undoc-members: + :show-inheritance: diff --git a/addons/source-python/docs/source-python/source/developing/modules/plugins.errors.rst b/addons/source-python/docs/source-python/source/developing/modules/plugins.errors.rst deleted file mode 100644 index 9462f5d2d..000000000 --- a/addons/source-python/docs/source-python/source/developing/modules/plugins.errors.rst +++ /dev/null @@ -1,7 +0,0 @@ -plugins.errors module -====================== - -.. automodule:: plugins.errors - :members: - :undoc-members: - :show-inheritance: diff --git a/addons/source-python/docs/source-python/source/developing/modules/plugins.rst b/addons/source-python/docs/source-python/source/developing/modules/plugins.rst index a98597672..1610d2e1c 100644 --- a/addons/source-python/docs/source-python/source/developing/modules/plugins.rst +++ b/addons/source-python/docs/source-python/source/developing/modules/plugins.rst @@ -8,7 +8,6 @@ Submodules :titlesonly: plugins.command - plugins.errors plugins.info plugins.instance plugins.manager diff --git a/addons/source-python/docs/source-python/source/developing/modules/source-python.rst b/addons/source-python/docs/source-python/source/developing/modules/source-python.rst index 87794f4eb..4a7ea9565 100644 --- a/addons/source-python/docs/source-python/source/developing/modules/source-python.rst +++ b/addons/source-python/docs/source-python/source/developing/modules/source-python.rst @@ -40,6 +40,7 @@ Submodules bitbuffers colors effects + filesystem keyvalues loggers mathlib diff --git a/addons/source-python/docs/source-python/source/developing/modules/weapons.engines.csgo.csgo.rst b/addons/source-python/docs/source-python/source/developing/modules/weapons.engines.csgo.csgo.rst new file mode 100644 index 000000000..24483b9c5 --- /dev/null +++ b/addons/source-python/docs/source-python/source/developing/modules/weapons.engines.csgo.csgo.rst @@ -0,0 +1,7 @@ +weapons.engines.csgo.csgo module +================================= + +.. automodule:: weapons.engines.csgo.csgo + :members: + :undoc-members: + :show-inheritance: From 583580e85ef63426e4cf9fb4ecc0e20675540a1d Mon Sep 17 00:00:00 2001 From: Ayuto22 Date: Sat, 14 Jan 2017 12:32:05 +0100 Subject: [PATCH 04/12] Moved print_plugins() to base class Also updated __init__ method --- .../source-python/core/command/__init__.py | 53 +----------- .../packages/source-python/plugins/command.py | 84 ++++++++++++++----- 2 files changed, 64 insertions(+), 73 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 c19c5487f..8981b02c2 100644 --- a/addons/source-python/packages/source-python/core/command/__init__.py +++ b/addons/source-python/packages/source-python/core/command/__init__.py @@ -49,56 +49,6 @@ class CoreCommandManager(SubCommandManager): """Class used for executing "sp" sub-command functionality.""" - manager = plugin_manager - logger = core_command_logger - - def print_plugins(self): - """List all currently loaded plugins. - - .. todo:: Move this to :class:`plugins.command.SubCommandManager`? - """ - # Get header messages - message = _plugin_strings[ - 'Plugins'].get_string() + '\n' + '=' * 61 + '\n\n' - - # Loop through all loaded plugins - for plugin_name in sorted(self.manager): - info = self.manager[plugin_name].info - - message += plugin_name + ' ({}):\n'.format(info.verbose_name) - - 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) - - if info.url is not None: - message += ' url: {}\n'.format(info.url) - - if info.permissions: - message += ' permissions:\n' - for permission, description in info.permissions: - message += ' {}:'.format(permission).ljust(30) + description + '\n' - - if info.public_convar is not None: - message += ' public convar: {}\n'.format(info.public_convar.name) - - for attr in info.display_in_listing: - message += ' {}:'.format(attr).ljust(20) + str(getattr(info, attr)) + '\n' - - # Add 1 blank line between each plugin - message += '\n' - - # Add the ending separator - message += '=' * 61 - - # Print the message - self.log_message(message) - def print_credits(self): """List all credits for Source.Python.""" # Get header messages @@ -128,7 +78,8 @@ def print_credits(self): # Print the message self.log_message(message + '=' * 61 + '\n\n') -core_command = CoreCommandManager('sp') +core_command = CoreCommandManager( + plugin_manager, 'sp', logger=core_command_logger) # ============================================================================= diff --git a/addons/source-python/packages/source-python/plugins/command.py b/addons/source-python/packages/source-python/plugins/command.py index a7a64b72e..076dc6693 100644 --- a/addons/source-python/packages/source-python/plugins/command.py +++ b/addons/source-python/packages/source-python/plugins/command.py @@ -46,31 +46,42 @@ class SubCommandManager(AutoUnload, list): """Class used for executing sub-commands for the given console command.""" - # Set the default class values for base attributes - logger = plugins_command_logger - translations = _plugin_strings - - def __init__(self, command, prefix=''): + def __init__(self, manager, command, prefix='', + logger=plugins_command_logger, translations=_plugin_strings): """Initializes the sub-command manager. + :param PluginManager manager: + A plugin manager. :param str command: Command to register. :param str prefix: - Prefix used for printing to the console. + Prefix used for printing messages to the console. + :param Logger logger: + A logger that is used for printing messages to the console. + :param LangStrings translations: + Translations used for printing messages to the console. The + translations have to define the following messages: + + * Loading + * Invalid Name + * Already Loaded + * No Module + * Built-in + * Unable to Load + * Successful Load + * Unloading + * Not Loaded + * Successful Unload + * Plugins """ # Re-call OrderedDict's __init__ to properly setup the object super().__init__() + self.manager = manager self._command = command self._prefix = prefix if prefix else '[{0}] '.format( self.command.upper()) - - @property - def manager(self): - """Return a plugin manager. - - :rtype: PluginManager - """ - raise NotImplementedError('No manager attribute defined for class.') + self.logger = logger + self.translations = translations def _unload_instance(self): """Unload all sub-commands.""" @@ -207,18 +218,47 @@ def reload_plugin(self, plugin_name): return self.load_plugin(plugin_name) def print_plugins(self): - """Print all currently loaded plugins.""" - # Get the header message + """List all currently loaded plugins.""" + # Get header messages message = self.translations[ - 'Plugins'].get_string() + '\n' + '=' * 61 + '\n\n\t' + 'Plugins'].get_string() + '\n' + '=' * 61 + '\n\n' + + # Loop through all loaded plugins + for plugin_name in sorted(self.manager): + info = self.manager[plugin_name].info + + message += plugin_name + ' ({}):\n'.format(info.verbose_name) + + 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) + + if info.url is not None: + message += ' url: {}\n'.format(info.url) + + if info.permissions: + message += ' permissions:\n' + for permission, description in info.permissions: + message += ' {}:'.format(permission).ljust(30) + description + '\n' + + if info.public_convar is not None: + message += ' public convar: {}\n'.format(info.public_convar.name) + + for attr in info.display_in_listing: + message += ' {}:'.format(attr).ljust(20) + str(getattr(info, attr)) + '\n' - # Add all loaded plugins to the message - message += '\n\t'.join(self.manager) + # Add 1 blank line between each plugin + message += '\n' - # Add a breaker at the end of the message - message += '\n\n' + '=' * 61 + # Add the ending separator + message += '=' * 61 - # Send the message + # Print the message self.log_message(message) def log_message(self, message): From d1256ab58edf458bec8dae67d8ee49a8ebadd1c1 Mon Sep 17 00:00:00 2001 From: Ayuto22 Date: Sat, 14 Jan 2017 13:13:04 +0100 Subject: [PATCH 05/12] Removed "globals" attribute from LoadedPlugin --- .../packages/source-python/plugins/instance.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/addons/source-python/packages/source-python/plugins/instance.py b/addons/source-python/packages/source-python/plugins/instance.py index 72df27d7c..3804ffad1 100644 --- a/addons/source-python/packages/source-python/plugins/instance.py +++ b/addons/source-python/packages/source-python/plugins/instance.py @@ -43,37 +43,31 @@ def __init__(self, plugin_name, manager): :param PluginManager manager: A plugin manager instance. """ + self.module = None self.manager = manager - self.globals = None self.name = plugin_name self.directory = self.manager.get_plugin_directory(plugin_name) self.file_path = self.manager.get_plugin_file_path(plugin_name) self.info = self.manager._create_plugin_info(plugin_name) - self._plugin = None self.import_name = (self.manager.base_import + plugin_name + '.' + plugin_name) def unload(self): """Unload the plugin.""" - # Convenience method self.manager.unload(self.name) def reload(self): """Reload the plugin.""" - # Convenience method self.manager.reload(self.name) def _load(self): """Actually load the plugin.""" self.info._create_public_convar() - self._plugin = import_module(self.import_name) - self.globals = { - x: getattr(self._plugin, x) for x in dir(self._plugin)} - - if 'load' in self.globals: - self.globals['load']() + self.module = import_module(self.import_name) + if hasattr(self.module, 'load'): + self.module.load() def _unload(self): """Actually unload the plugin.""" - if 'unload' in self.globals: - self.globals['unload']() + if hasattr(self.module, 'unload'): + self.module.unload() From 65b9a293b9c5c64e1dcbfefcf86a9ee691f153b5 Mon Sep 17 00:00:00 2001 From: Ayuto22 Date: Sat, 14 Jan 2017 13:22:36 +0100 Subject: [PATCH 06/12] Added documentation for the two new listeners --- .../developing/module_tutorials/listeners.rst | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/addons/source-python/docs/source-python/source/developing/module_tutorials/listeners.rst b/addons/source-python/docs/source-python/source/developing/module_tutorials/listeners.rst index 718cbfd45..ac492fdc4 100644 --- a/addons/source-python/docs/source-python/source/developing/module_tutorials/listeners.rst +++ b/addons/source-python/docs/source-python/source/developing/module_tutorials/listeners.rst @@ -372,14 +372,29 @@ Called when a plugin has been loaded successfully. from listeners import OnPluginLoaded @OnPluginLoaded - def on_plugin_loaded(plugin_name): + def on_plugin_loaded(plugin): + pass + + +OnPluginLoading +--------------- + +Called right before a plugin is imported and loaded. All checks (e.g. plugin +file exists, etc.) have been done at this point. + +.. code-block:: python + + from listeners import OnPluginLoading + + @OnPluginLoading + def on_plugin_loading(plugin): pass OnPluginUnloaded ---------------- -Called when a plugin has been unloaded. +Called when a plugin has been unloaded sucessfully. .. code-block:: python @@ -390,6 +405,20 @@ Called when a plugin has been unloaded. pass +OnPluginUnloading +----------------- + +Called right before a loaded plugin is unloaded. + +.. code-block:: python + + from listeners import OnPluginUnloading + + @OnPluginUnloading + def on_plugin_unloading(plugin): + pass + + OnQueryCvarValueFinished ------------------------ From 4825bc4a864a97bd7afa4165e7837398042294b4 Mon Sep 17 00:00:00 2001 From: Ayuto22 Date: Sat, 14 Jan 2017 14:42:03 +0100 Subject: [PATCH 07/12] Added "plugins" tutorial --- .../developing/module_tutorials/plugins.rst | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 addons/source-python/docs/source-python/source/developing/module_tutorials/plugins.rst diff --git a/addons/source-python/docs/source-python/source/developing/module_tutorials/plugins.rst b/addons/source-python/docs/source-python/source/developing/module_tutorials/plugins.rst new file mode 100644 index 000000000..ea324764e --- /dev/null +++ b/addons/source-python/docs/source-python/source/developing/module_tutorials/plugins.rst @@ -0,0 +1,159 @@ +plugins +======= + +This page contains tutorials about the :mod:`plugins` package. + + +PluginInfo +---------- + +For every plugin a :class:`plugins.info.PluginInfo` instance can be retrieved, +even if the plugin isn't loaded. The :class:`plugins.info.PluginInfo` instance +is created based on a file called ``info.ini`` that has to reside in the +plugin's main directory. This file is completely optional. So, if it doesn't +exist, the :class:`plugins.info.PluginInfo` instance will still be created, +but it won't contain much more information beside the name of the plugin. + +Here is an example ``info.ini`` file containing the most basic options: + +.. code:: ini + + # A verbose name of the plugin. + # If this option is not defined, the plugin's name will be used, all + # underscores are replaced with spaces and the first letter of every word is + # being capitalized. + verbose_name = "Paintball" + + # Name of the Author. + # If this option is not defined, the plugin info will contain 'None'. + author = "Ayuto" + + # A description of what the plugin does. + # If this option is not defined, the plugin info will contain 'None'. + description = "Adds paintball effects to the game." + + # Version of the plugin. + # If this option is not defined, the plugin info will contain 'unversioned'. + version = "1.3" + + # An link to the 'Plugin Releases' forum or the plugin's SPPM link (hopefully + # coming some day). + # If this option is not defined, the plugin info will contain 'None'. + url = "http://forums.sourcepython.com/viewtopic.php?f=7&t=502" + + +You can also note which permissions are defined by your plugin. This helps +server owners to configure their authorization settings properly. See the +example below: + +.. code:: ini + + # Permissions defined by the plugin. + # If this option is not defined, the plugin info will contain an empty list. + [permissions] + admin.kick = "Ability to kick a player." + admin.ban = "Ability to ban a player." + + +As soon as a plugin is being loaded, a public console variable is created that +contains the following information: + +* Name: ``_version`` +* Value: ```` +* Description: `` version.`` + + +If you don't want a public console variable, you can simply use the following +option in your info file to disable that feature: + +.. code:: ini + + public_convar = False + + +If you wish to use different values to create the public console variable, you +can use the following in your info file: + +.. code:: ini + + [public_convar] + # All of these options are optional. + name = "my_plugin_version" + value = "My custom value." + description = "My custom description." + + +Sometimes you might also want to define some custom options for the plugin +info. Adding those is quite easy. You just need to define them: + +.. code:: ini + + my_custom_option = "something" + my_custom_option2 = "something else" + + +Since those are custom options, they are not displayed when the list of loaded +plugins is printed (e.g. via ``sp plugin list``). If you want to change that +behaviour, you can define the ``display_in_listing`` option: + +.. code:: ini + + display_in_listing = "my_custom_option", "my_custom_option2" + + +Retrieving a PluginInfo instance +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. note:: + + If you retrieve a :class:`plugins.info.PluginInfo` instance of a plugin + that isn't loaded, the :class:`plugins.info.PluginInfo` instance is + recreated everytime you retrieve it. Only loaded plugins will cache the + instance. + + +The following example will show how to retrieve a +:class:`plugins.info.PluginInfo` instance for a specific plugin. + +.. code:: python + + from plugins.manager import plugin_manager + + # Retrieve the plugin info of the paintball plugin. This is case sensitive! + info = plugin_manager.get_plugin_info('paintball') + + # Print the plugin's description + print(info.description) + + +You are not only restricted to the plugin's name, but you can also use the +plugin's import path. See the example below: + +.. code:: python + + from plugins.manager import plugin_manager + + info = plugin_manager.get_plugin_info('paintball.paintball') + + +Obviously, this doesn't make much sense as the first example is shorter and +both result in the same. But this feature has been added, so plugin's can pass +their own ``__name__`` variable, which contains their import path. Thus, you +can use the following snippet to retrieve the plugin info of your own plugin, +without directly specifying the plugin's name. + +.. code:: python + + from plugins.manager import plugin_manager + + info = plugin_manager.get_plugin_info(__name__) + + +You can also use this snippet outside of your plugin's main file (e.g. in +other sub-modules or sub-packages). + + +Adding sub-plugins +------------------ + +.. todo:: Show how to add sub-plugins From 4e7a25fdb7f105442b4d6407ebd4c1b7074e550b Mon Sep 17 00:00:00 2001 From: Ayuto22 Date: Sat, 14 Jan 2017 16:05:58 +0100 Subject: [PATCH 08/12] Added exception messages --- .../packages/source-python/plugins/manager.py | 7 +++++-- 1 file changed, 5 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 7a45dd2a9..91a81e4fd 100644 --- a/addons/source-python/packages/source-python/plugins/manager.py +++ b/addons/source-python/packages/source-python/plugins/manager.py @@ -137,11 +137,14 @@ def load(self, plugin_name): plugin = self._create_plugin_instance(plugin_name) if not plugin.file_path.isfile(): - raise PluginFileNotFoundError + raise PluginFileNotFoundError( + 'File {} does not exist.'.format(plugin.file_path)) spec = find_spec(plugin.import_name) if spec is None or spec.origin != plugin.file_path: - raise PluginHasBuiltInName + raise PluginHasBuiltInName( + 'Plugin "{}" has the name of a built-in module.'.format( + plugin_name)) # Add the instance here, so we can use get_plugin_instance() etc. # within the plugin itself before the plugin has been fully loaded. From 644edb891ed65b3439941392173bb57c36cfe1b0 Mon Sep 17 00:00:00 2001 From: Ayuto22 Date: Sat, 14 Jan 2017 17:58:07 +0100 Subject: [PATCH 09/12] Added "plugins" property to PluginManager plugin_exists() now checks for the existance of the plugin's main file. --- .../packages/source-python/plugins/manager.py | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/addons/source-python/packages/source-python/plugins/manager.py b/addons/source-python/packages/source-python/plugins/manager.py index 91a81e4fd..fdb82b3ee 100644 --- a/addons/source-python/packages/source-python/plugins/manager.py +++ b/addons/source-python/packages/source-python/plugins/manager.py @@ -110,6 +110,24 @@ def plugins_directory(self): """ return PLUGIN_PATH.joinpath(*tuple(self.base_import.split('.')[:~0])) + @property + def plugins(self): + """Return a generator to iterate over all existing plugins. + + :return: + The generator yields the plugin names. + :rtype: generator + """ + for path in self.plugins_directory.dirs(): + plugin_name = path.namebase + if not self.is_valid_plugin_name(plugin_name): + continue + + if not self.plugin_exists(plugin_name): + continue + + yield plugin_name + def load(self, plugin_name): """Load a plugin by name. @@ -227,14 +245,11 @@ def is_loaded(self, plugin_name): def plugin_exists(self, plugin_name): """Return whether of not a plugin exists. - This method only checks for the existance of the plugin directory, but - not for the existance of the main plugin file. - :param str plugin_name: The plugin to check. :rtype: bool """ - return self.get_plugin_directory(plugin_name).isdir() + return self.get_plugin_file_path(plugin_name).isfile() def get_plugin_instance(self, plugin_name): """Return a plugin's instance, if it is loaded. @@ -299,7 +314,7 @@ def _create_plugin_info(self, plugin_name): :param str plugin_name: Name of the plugin whose plugin info should be created. :raise PluginFileNotFoundError: - Raised if the plugin's main directory wasn't found. + Raised if the plugin's main file wasn't found. :rtype: PluginInfo """ if not self.plugin_exists(plugin_name): From a01c0794581c07b0b23dfcfaf27816214dc09a5a Mon Sep 17 00:00:00 2001 From: Ayuto22 Date: Sat, 14 Jan 2017 21:53:44 +0100 Subject: [PATCH 10/12] Added LoadedPlugin.sub_module attribute Updated OnPluginUnloaded listener. --- .../source/developing/module_tutorials/listeners.rst | 2 +- .../source-python/packages/source-python/plugins/instance.py | 3 +++ addons/source-python/packages/source-python/plugins/manager.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/addons/source-python/docs/source-python/source/developing/module_tutorials/listeners.rst b/addons/source-python/docs/source-python/source/developing/module_tutorials/listeners.rst index ac492fdc4..701bc8044 100644 --- a/addons/source-python/docs/source-python/source/developing/module_tutorials/listeners.rst +++ b/addons/source-python/docs/source-python/source/developing/module_tutorials/listeners.rst @@ -401,7 +401,7 @@ Called when a plugin has been unloaded sucessfully. from listeners import OnPluginUnloaded @OnPluginUnloaded - def on_plugin_unloaded(plugin_name): + def on_plugin_unloaded(plugin): pass diff --git a/addons/source-python/packages/source-python/plugins/instance.py b/addons/source-python/packages/source-python/plugins/instance.py index 3804ffad1..fe7ae5a1f 100644 --- a/addons/source-python/packages/source-python/plugins/instance.py +++ b/addons/source-python/packages/source-python/plugins/instance.py @@ -46,6 +46,7 @@ def __init__(self, plugin_name, manager): self.module = None self.manager = manager self.name = plugin_name + self.sub_plugin = manager.base_import != '' self.directory = self.manager.get_plugin_directory(plugin_name) self.file_path = self.manager.get_plugin_file_path(plugin_name) self.info = self.manager._create_plugin_info(plugin_name) @@ -71,3 +72,5 @@ def _unload(self): """Actually unload the plugin.""" if hasattr(self.module, 'unload'): self.module.unload() + + self.module = None diff --git a/addons/source-python/packages/source-python/plugins/manager.py b/addons/source-python/packages/source-python/plugins/manager.py index fdb82b3ee..ba3f72990 100644 --- a/addons/source-python/packages/source-python/plugins/manager.py +++ b/addons/source-python/packages/source-python/plugins/manager.py @@ -202,7 +202,7 @@ def unload(self, plugin_name): self._remove_modules(plugin_name) del self[plugin_name] - on_plugin_unloaded_manager.notify(plugin_name) + on_plugin_unloaded_manager.notify(plugin) def reload(self, plugin_name): """Reload a plugin by name. From 187c4f7952d27fa39f9411438aefa2cc9ce478d3 Mon Sep 17 00:00:00 2001 From: Ayuto22 Date: Sat, 14 Jan 2017 22:05:20 +0100 Subject: [PATCH 11/12] Renamed LoadedPlugin to Plugin --- .../packages/source-python/plugins/command.py | 4 ++-- .../packages/source-python/plugins/instance.py | 4 ++-- .../packages/source-python/plugins/manager.py | 15 +++++++-------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/addons/source-python/packages/source-python/plugins/command.py b/addons/source-python/packages/source-python/plugins/command.py index 076dc6693..782dbe6f6 100644 --- a/addons/source-python/packages/source-python/plugins/command.py +++ b/addons/source-python/packages/source-python/plugins/command.py @@ -153,7 +153,7 @@ def load_plugin(self, plugin_name): Name of the plugin to load. :return: Return the loaded plugin. Return ``None`` on failure. - :rtype: LoadedPlugin + :rtype: Plugin """ plugin = None self.log_message(self.translations[ @@ -212,7 +212,7 @@ def reload_plugin(self, plugin_name): Name of the plugin to reload. :return: Return the loaded plugin. Return ``None`` on failure. - :rtype: LoadedPlugin + :rtype: Plugin """ self.unload_plugin(plugin_name) return self.load_plugin(plugin_name) diff --git a/addons/source-python/packages/source-python/plugins/instance.py b/addons/source-python/packages/source-python/plugins/instance.py index fe7ae5a1f..25d5cf32a 100644 --- a/addons/source-python/packages/source-python/plugins/instance.py +++ b/addons/source-python/packages/source-python/plugins/instance.py @@ -18,7 +18,7 @@ # ============================================================================= # >> ALL DECLARATION # ============================================================================= -__all__ = ('LoadedPlugin', +__all__ = ('Plugin', ) @@ -32,7 +32,7 @@ # ============================================================================= # >> CLASSES # ============================================================================= -class LoadedPlugin(object): +class Plugin(object): """Stores a plugin's instance.""" def __init__(self, plugin_name, manager): diff --git a/addons/source-python/packages/source-python/plugins/manager.py b/addons/source-python/packages/source-python/plugins/manager.py index ba3f72990..3704a44ec 100644 --- a/addons/source-python/packages/source-python/plugins/manager.py +++ b/addons/source-python/packages/source-python/plugins/manager.py @@ -34,7 +34,7 @@ # Plugins from plugins import plugins_logger from plugins.info import PluginInfo -from plugins.instance import LoadedPlugin +from plugins.instance import Plugin # ============================================================================= @@ -87,12 +87,11 @@ def __init__(self, base_import=''): def _create_plugin_instance(self, plugin_name): """Create a new plugin instance. - Overwrite this method if you wish to use your own LoadedPlugin - subclass. + Overwrite this method if you wish to use your own Plugin subclass. - :rtype: LoadedPlugin + :rtype: Plugin """ - return LoadedPlugin(plugin_name, self) + return Plugin(plugin_name, self) @property def base_import(self): @@ -143,7 +142,7 @@ def load(self, plugin_name): Raised if the plugin has the name of a built-in module. :raise Exception: Any other exceptions raised by the plugin during the load process. - :rtype: LoadedPlugin + :rtype: Plugin """ if self.is_loaded(plugin_name): raise PluginAlreadyLoaded( @@ -219,7 +218,7 @@ def reload(self, plugin_name): Raised if the plugin has the name of a built-in module. :raise Exception: Any other exceptions raised by the plugin during the load process. - :rtype: LoadedPlugin + :rtype: Plugin """ self.unload(plugin_name) return self.load(plugin_name) @@ -257,7 +256,7 @@ def get_plugin_instance(self, plugin_name): :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 + :rtype: Plugin """ plugin_name = self.get_plugin_name(plugin_name) if self.is_loaded(plugin_name): From 21f62362f471394968a16a0cf01dc03647440071 Mon Sep 17 00:00:00 2001 From: Ayuto22 Date: Sat, 14 Jan 2017 23:23:29 +0100 Subject: [PATCH 12/12] Added sub-plugins tutorial --- .../developing/module_tutorials/plugins.rst | 113 +++++++++++++++++- 1 file changed, 112 insertions(+), 1 deletion(-) diff --git a/addons/source-python/docs/source-python/source/developing/module_tutorials/plugins.rst b/addons/source-python/docs/source-python/source/developing/module_tutorials/plugins.rst index ea324764e..475dbe578 100644 --- a/addons/source-python/docs/source-python/source/developing/module_tutorials/plugins.rst +++ b/addons/source-python/docs/source-python/source/developing/module_tutorials/plugins.rst @@ -156,4 +156,115 @@ other sub-modules or sub-packages). Adding sub-plugins ------------------ -.. todo:: Show how to add sub-plugins +Adding sub-plugins to your plugin is done a very few steps. All you actually +need is a new instance of the :class:`plugins.manager.PluginManager` class. +This instance allows you to load plugins from a specifc directory. + +Imagine your plugin resides in ``../addons/source-python/plugins/my_plugin`` +and within that directory you have created a new directory called ``plugins``, +which should contain all sub-plugins of ``my_plugin``. Then the plugin manager +could be created using the following code: + +.. code:: python + + from plugins.manager import PluginManager + + my_plugin_manager = PluginManager('my_plugin.plugins.') + + +That's all you need! Now you can load sub-plugins using ``my_plugin_manager`` +from your sub-plugins directory with the following code: + +.. code:: python + + # Load the plugin 'my_sub_plugin' from + # ../addons/source-python/plugins/my_plugin/plugins + my_plugin_manager.load('my_sub_plugin') + + +However, this doesn't print any messages like Source.Python does when you load +a plugin via ``sp plugin load``. If you would like to have those messages as +well, without implementing them on your own, you can simply create an instance +of ``plugins.command.SubCommandManager``. + +.. code:: python + + from plugins.command import SubCommandManager + + my_sub_command_manager = SubCommandManager( + # Tell the sub command manager to use this plugin manager to load plugins + my_plugin_manager, + + # If you create sub-commands, they will use 'my_plugin' as the base + # command like Source.Python uses 'sp' + 'my_plugin' + ) + + +Now, you can load your sub-plugin using the following code: + +.. code:: python + + my_sub_command_manager.load_plugin('my_sub_plugin') + + +So far, so good. But what if you want to load your plugins via a server +command? Well, just add it using the following code: + +.. code:: python + + @my_sub_command_manager.server_sub_command(['plugin', 'load']) + def plugin_load(command_info, plugin): + my_sub_command_manager.load_plugin(plugin) + + @my_sub_command_manager.server_sub_command(['plugin', 'unload']) + def plugin_unload(command_info, plugin): + my_sub_command_manager.unload_plugin(plugin) + + +Now you can also load your sub-plugins using ``my_plugin plugin load`` and +unload them using ``my_plugin plugin unload``. + +There is only one last thing left to do. If your main plugin is being +unloaded, you should also unload all of your sub-plugins. It doesn't cause any +problems with Source.Python if you don't do that, because Source.Python also +unloads all :class:`core.AutoUnload` and :class:`core.WeakAutoUnload` +instances of your sub-plugins. But if you don't do that the ``unload`` +functions of your sub-plugins are never getting called. To avoid this issue +use the following code: + +.. code:: python + + def unload(): + for plugin in tuple(my_plugin_manager.values()): + plugin.unload() + + +Here is the full example code to implement sub-plugins: + +.. code:: python + + from plugins.manager import PluginManager + from plugins.command import SubCommandManager + + my_plugin_manager = PluginManager('my_plugin.plugins.') + my_sub_command_manager = SubCommandManager( + # Tell the sub command manager to use this plugin manager to load plugins + my_plugin_manager, + + # If you create sub-commands, they will use 'my_plugin' as the base + # command like Source.Python uses 'sp' + 'my_plugin' + ) + + @my_sub_command_manager.server_sub_command(['plugin', 'load']) + def plugin_load(command_info, plugin): + my_sub_command_manager.load_plugin(plugin) + + @my_sub_command_manager.server_sub_command(['plugin', 'unload']) + def plugin_unload(command_info, plugin): + my_sub_command_manager.unload_plugin(plugin) + + def unload(): + for plugin in tuple(my_plugin_manager.values()): + plugin.unload()