From addd517ba7379306c8de1f32971dd55b43876651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krist=C3=B3f=20T=C3=B3th?= Date: Wed, 18 Apr 2018 19:44:26 +0200 Subject: [PATCH] Add a huge bunch of docstrings --- lib/tfw/components/history_monitor.py | 24 ++++++++++ .../process_managing_event_handler.py | 12 +++++ lib/tfw/components/terminado_event_handler.py | 25 ++++++++++ lib/tfw/components/terminal_commands.py | 18 ++++++++ lib/tfw/components/webide_event_handler.py | 46 +++++++++++++++++++ lib/tfw/event_handler_base.py | 10 ++++ lib/tfw/fsm_base.py | 7 +++ lib/tfw/linear_fsm.py | 6 +++ lib/tfw/mixins/callback_mixin.py | 6 +++ .../event_handlers/server_connector.py | 12 +++++ lib/tfw/networking/message_sender.py | 9 ++++ lib/tfw/networking/server/tfw_server.py | 8 ++++ 12 files changed, 183 insertions(+) diff --git a/lib/tfw/components/history_monitor.py b/lib/tfw/components/history_monitor.py index 7f3965c..d5586a7 100644 --- a/lib/tfw/components/history_monitor.py +++ b/lib/tfw/components/history_monitor.py @@ -24,6 +24,17 @@ class CallbackEventHandler(PatternMatchingEventHandler, ABC): class HistoryMonitor(CallbackMixin, ObserverMixin, ABC): + """ + Abstract class capable of monitoring and parsing a history file such as + bash HISTFILEs. Monitoring means detecting when the file was changed and + notifying subscribers about new content in the file. + + This is useful for monitoring CLI sessions. + + To specify a custom HistoryMonitor inherit from this class and override the + command pattern property and optionally the sanitize_command method. + See examples below. + """ def __init__(self, histfile): CallbackMixin.__init__(self) ObserverMixin.__init__(self) @@ -61,6 +72,15 @@ class HistoryMonitor(CallbackMixin, ObserverMixin, ABC): class BashMonitor(HistoryMonitor): + """ + HistoryMonitor for monitoring bash CLI sessions. + This requires the following to be set in bash + (note that this is done automatically by TFW): + PROMPT_COMMAND="history -a" + shopt -s cmdhist + shopt -s histappend + unset HISTCONTROL + """ @property def command_pattern(self): return r'.+' @@ -70,6 +90,10 @@ class BashMonitor(HistoryMonitor): class GDBMonitor(HistoryMonitor): + """ + HistoryMonitor to monitor GDB sessions. + For this to work "set trace-commands on" must be set in GDB. + """ @property def command_pattern(self): return r'(?<=\n)\+(.+)\n' diff --git a/lib/tfw/components/process_managing_event_handler.py b/lib/tfw/components/process_managing_event_handler.py index 8ec5c80..958ae81 100644 --- a/lib/tfw/components/process_managing_event_handler.py +++ b/lib/tfw/components/process_managing_event_handler.py @@ -23,6 +23,18 @@ class ProcessManager(SupervisorMixin): class ProcessManagingEventHandler(EventHandlerBase): + """ + Event handler that can manage processes managed by supervisor. + + This EventHandler accepts messages that have a data["command"] key specifying + a command to be executed. + Every message must contain a data["process_name"] field with the name of the + process to manage. This is the name specified in supervisor config files like so: + [program:someprogram] + + Commands available: start, stop, restart, readlog + (the names are as self-documenting as it gets) + """ def __init__(self, key, dirmonitor=None): super().__init__(key) self.key = key diff --git a/lib/tfw/components/terminado_event_handler.py b/lib/tfw/components/terminado_event_handler.py index c0351c9..e2501bc 100644 --- a/lib/tfw/components/terminado_event_handler.py +++ b/lib/tfw/components/terminado_event_handler.py @@ -11,7 +11,20 @@ LOG = logging.getLogger(__name__) class TerminadoEventHandler(EventHandlerBase): + """ + Event handler responsible for managing terminal sessions for frontend xterm + sessions to connect to. You need to instanciate this in order for frontend + terminals to work. + + This EventHandler accepts messages that have a data["command"] key specifying + a command to be executed. + The API of each command is documented in their respective handlers. + """ def __init__(self, key, monitor): + """ + :param key: key this EventHandler listens to + :param monitor: tfw.components.HistoryMonitor instance to read command history from + """ super().__init__(key) self.working_directory = TFWENV.TERMINADO_DIR self._historymonitor = monitor @@ -37,9 +50,21 @@ class TerminadoEventHandler(EventHandlerBase): LOG.error('IGNORING MESSAGE: Invalid message received: %s', message) def write(self, data): + """ + Writes a string to the terminal session (on the pty level). + Useful for pre-typing and executing commands for the user. + + :param data['shellcmd']: command to be written to the pty + """ self.terminado_server.pty.write(data['shellcmd']) def read(self, data): + """ + Reads the history of commands executed. + + :param data['count']: the number of history elements to return + :return: message with list of commands in data['history'] + """ data['count'] = int(data.get('count', 1)) if self.historymonitor: data['history'] = self.historymonitor.history[-data['count']:] diff --git a/lib/tfw/components/terminal_commands.py b/lib/tfw/components/terminal_commands.py index e95e3bc..bf7b416 100644 --- a/lib/tfw/components/terminal_commands.py +++ b/lib/tfw/components/terminal_commands.py @@ -10,6 +10,24 @@ LOG = logging.getLogger(__name__) class TerminalCommands(ABC): + """ + A class you can use to define hooks for terminal commands. This means that you can + have python code executed when the user enters a specific command to the terminal on + our frontend. + + To receive events you need to subscribe TerminalCommand.callback to a HistoryMonitor + instance. + + Inherit from this class and define methods which start with "command_". When the user + executes the command specified after the underscore, your method will be invoked. All + such commands must expect the parameter *args which will contain the arguments of the + command. + + For example to define a method that runs when someone starts vim in the terminal + you have to define a method like: "def command_vim(self, *args)" + + You can also use this class to create new commands similarly. + """ def __init__(self, bashrc=None): self._command_method_regex = r'^command_(.+)$' self.command_implemetations = self._build_command_to_implementation_dict() diff --git a/lib/tfw/components/webide_event_handler.py b/lib/tfw/components/webide_event_handler.py index 5ebd6e9..0e4f859 100644 --- a/lib/tfw/components/webide_event_handler.py +++ b/lib/tfw/components/webide_event_handler.py @@ -93,7 +93,23 @@ class FileManager: # pylint: disable=too-many-instance-attributes class WebideEventHandler(EventHandlerBase, MonitorManagerMixin): # pylint: disable=too-many-arguments + """ + Event handler implementing the backend of our browser based IDE. + By default all files in the directory specified in __init__ are displayed + on the fontend. Note that this is a stateful component. + + This EventHandler accepts messages that have a data["command"] key specifying + a command to be executed. + The API of each command is documented in their respective handlers. + """ def __init__(self, key, directory, allowed_directories, selected_file=None, exclude=None): + """ + :param key: the key this instance should listen to + :param directory: working directory which the EventHandler should serve files from + :param allowed_directories: list of directories that can be switched to using the selectdir command + :param selected_file: file that is selected by default + :param exclude: list of filenames that should not appear between files (for *.o, *.pyc, etc.) + """ super().__init__(key) self.filemanager = FileManager(allowed_directories=allowed_directories, working_directory=directory, selected_file=selected_file, exclude=exclude) @@ -106,6 +122,11 @@ class WebideEventHandler(EventHandlerBase, MonitorManagerMixin): 'exclude': self.exclude} def read(self, data): + """ + Read the currently selected file. + + :return: message with the contents of the file in data['content'] + """ try: data['content'] = self.filemanager.file_contents except PermissionError: @@ -117,6 +138,11 @@ class WebideEventHandler(EventHandlerBase, MonitorManagerMixin): return data def write(self, data): + """ + Overwrites a file with the desired string. + + :param data['content']: string containing the desired file contents + """ self.monitor.ignore = self.monitor.ignore + 1 try: self.filemanager.file_contents = data['content'] @@ -126,6 +152,11 @@ class WebideEventHandler(EventHandlerBase, MonitorManagerMixin): return data def select(self, data): + """ + Selects a file from the current directory. + + :param data['filename']: name of file to select relative to the current directory + """ try: self.filemanager.filename = data['filename'] except EnvironmentError: @@ -133,6 +164,13 @@ class WebideEventHandler(EventHandlerBase, MonitorManagerMixin): return data def select_dir(self, data): + """ + Select a new working directory to display files from. + + :param data['directory']: absolute path of diretory to select. + must be a path whitelisted in + self.allowed_directories + """ try: self.filemanager.workdir = data['directory'] self.reload_monitor() @@ -146,6 +184,11 @@ class WebideEventHandler(EventHandlerBase, MonitorManagerMixin): return data def exclude(self, data): + """ + Overwrite list of excluded files + + :param data['exclude']: list of filename patterns to be excluded, e.g.: ["*.pyc", "*.o"] + """ try: self.filemanager.exclude = list(data['exclude']) except TypeError: @@ -153,6 +196,9 @@ class WebideEventHandler(EventHandlerBase, MonitorManagerMixin): return data def attach_fileinfo(self, data): + """ + Basic information included in every response to the frontend. + """ data['filename'] = self.filemanager.filename data['files'] = self.filemanager.files data['directory'] = self.filemanager.workdir diff --git a/lib/tfw/event_handler_base.py b/lib/tfw/event_handler_base.py index 5f07965..47ba5c9 100644 --- a/lib/tfw/event_handler_base.py +++ b/lib/tfw/event_handler_base.py @@ -8,6 +8,12 @@ from tfw.networking.event_handlers import ServerConnector class EventHandlerBase(ABC): + """ + Abstract base class for all Python based EventHandlers. Useful implementation template + for other languages. + + Derived classes must implement the handle_event() method + """ def __init__(self, key): self.server_connector = ServerConnector() self.key = key @@ -60,6 +66,10 @@ class EventHandlerBase(ABC): class TriggeredEventHandler(EventHandlerBase, ABC): # pylint: disable=abstract-method + """ + Abstract base class for EventHandlers which are only triggered in case + TFWServer has successfully triggered an FSM step defined in __init__. + """ def __init__(self, key, trigger): super().__init__(key) self.trigger = trigger diff --git a/lib/tfw/fsm_base.py b/lib/tfw/fsm_base.py index 4657b36..d2e1df0 100644 --- a/lib/tfw/fsm_base.py +++ b/lib/tfw/fsm_base.py @@ -9,6 +9,13 @@ from tfw.mixins import CallbackMixin class FSMBase(CallbackMixin): + """ + A general FSM base class you can inherit from to track user progress. + See linear_fsm.py for an example use-case. + TFW the transitions library for state machines, please refer to their + documentation for more information on creating your own machines: + https://github.com/pytransitions/transitions + """ states, transitions = [], [] def __init__(self, initial: str = None, accepted_states: List[str] = None): diff --git a/lib/tfw/linear_fsm.py b/lib/tfw/linear_fsm.py index 574f5c9..ca73002 100644 --- a/lib/tfw/linear_fsm.py +++ b/lib/tfw/linear_fsm.py @@ -5,6 +5,12 @@ from .fsm_base import FSMBase class LinearFSM(FSMBase): + """ + This is a state machine for challenges with linear progression, consisting of + a number of steps specified in the constructor. It automatically sets up a single + action between states as such: + 0 ==step_1==> 1 ==step_2==> 2 ==step_3==> 3 ... and so on + """ def __init__(self, number_of_steps): self.states = list(map(str, range(number_of_steps))) self.transitions = [{'trigger': 'step_{}'.format(int(index)+1), 'source': index, 'dest': str(int(index)+1)} diff --git a/lib/tfw/mixins/callback_mixin.py b/lib/tfw/mixins/callback_mixin.py index 4f031f0..e5cd534 100644 --- a/lib/tfw/mixins/callback_mixin.py +++ b/lib/tfw/mixins/callback_mixin.py @@ -9,6 +9,12 @@ class CallbackMixin: self._callbacks = [] def subscribe_callback(self, callback, *args, **kwargs): + """ + Subscribe a callable to invoke once an event is triggered. + :param callback: callable to be executed on events + :param *args: arguments passed to callable + :param **kwargs: kwargs passed to callable + """ fun = partial(callback, *args, **kwargs) self._callbacks.append(fun) diff --git a/lib/tfw/networking/event_handlers/server_connector.py b/lib/tfw/networking/event_handlers/server_connector.py index 5613bbb..5d5f21b 100644 --- a/lib/tfw/networking/event_handlers/server_connector.py +++ b/lib/tfw/networking/event_handlers/server_connector.py @@ -24,18 +24,30 @@ class ServerDownlinkConnector(ZMQConnectorBase): class ServerUplinkConnector(ZMQConnectorBase): + """ + Class capable of sending messages to the TFW server and event handlers. + """ def __init__(self, zmq_context=None): super(ServerUplinkConnector, self).__init__(zmq_context) self._zmq_push_socket = self._zmq_context.socket(zmq.PUSH) self._zmq_push_socket.connect('tcp://localhost:{}'.format(TFWENV.RECEIVER_PORT)) def send_to_eventhandler(self, message): + """ + Send a message to an event handler. + :param message: JSON message you want to send + :param message['key']: key of event handler you want to address + """ nested_message = {'key': message['key'], 'data': message.pop('data')} message['key'] = 'mirror' message['data'] = nested_message self.send(message) def send(self, message): + """ + Send a message to the TFW server + :param message: JSON message you want to send + """ self._zmq_push_socket.send_multipart(serialize_tfw_msg(message)) diff --git a/lib/tfw/networking/message_sender.py b/lib/tfw/networking/message_sender.py index 5e73b08..d1bf0d5 100644 --- a/lib/tfw/networking/message_sender.py +++ b/lib/tfw/networking/message_sender.py @@ -7,11 +7,20 @@ from tfw.networking.event_handlers import ServerUplinkConnector class MessageSender: + """ + Provides a mechanism to send messages to our frontend messaging component which + displays messages with the key "message". + """ def __init__(self, custom_key: str = None): self.server_connector = ServerUplinkConnector() self.key = custom_key or 'message' def send(self, originator, message): + """ + Sends a message to the key specified in __init__. + :param originator: name of sender to be displayed on the frontend + :param message: message to send + """ data = { 'originator': originator, 'timestamp': datetime.now().isoformat(), diff --git a/lib/tfw/networking/server/tfw_server.py b/lib/tfw/networking/server/tfw_server.py index a39b33e..89ad3d4 100644 --- a/lib/tfw/networking/server/tfw_server.py +++ b/lib/tfw/networking/server/tfw_server.py @@ -15,7 +15,15 @@ LOG = logging.getLogger(__name__) class TFWServer: + """ + This class handles the proxying of messages between the frontend and event handers. + It proxies messages from the "/ws" route to all event handlers subscribed to a ZMQ + SUB socket. It also manages an FSM you can define as a constructor argument. + """ def __init__(self, fsm_type): + """ + :param fsm_type: the type of FSM you want TFW to use + """ self._fsm = fsm_type() self._fsm_updater = FSMUpdater(self._fsm) self._fsm_manager = FSMManager(self._fsm)