Add a huge bunch of docstrings

This commit is contained in:
Kristóf Tóth 2018-04-18 19:44:26 +02:00
parent 690f9bb190
commit addd517ba7
12 changed files with 183 additions and 0 deletions

View File

@ -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'

View File

@ -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

View File

@ -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']:]

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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)}

View File

@ -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)

View File

@ -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))

View File

@ -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(),

View File

@ -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)