This made sense to me; a lot of the general work about making sure a command looks correct can probably be abstracted out such that I can define what valid parameters to a command would look like. I started by defining a set of parameters (note, I'm using the numpydoc
package style for Sphinx documentation generation throughout):
This made sense to me; a lot of the general work about making sure a command looks correct can probably be abstracted out such that I can define what valid parameters to a command would look like. I started by defining a set of parameters:
This made sense to me; a lot of the general work about making sure a command looks correct can probably be abstracted out such that I can define what valid parameters to a command would look like. I started by defining a set of parameters (note, I'm using the numpydoc
package style for Sphinx documentation generation throughout):
I asked this question a while ago, and this was one of the comments was this:
There (mostly) seems to be a one-to-one mapping of a lot of your functions to IRC command verbs. For example, leave_server
leave_server
-> QUITQUIT
, join_channeljoin_channel
-> JOINJOIN
, send_private_messagesend_private_message
-> PRIVMSGPRIVMSG
, etc. Part of me just feels that (as you say) there is just a tad too much repetition in that architecture. (Perhaps some more abstract encapsulation of a server command and arguments?
I asked this question a while ago, and one of the comments was this:
There (mostly) seems to be a one-to-one mapping of a lot of your functions to IRC command verbs. For example, leave_server -> QUIT, join_channel -> JOIN, send_private_message -> PRIVMSG, etc. Part of me just feels that (as you say) there is just a tad too much repetition in that architecture. (Perhaps some more abstract encapsulation of a server command and arguments?
I asked this question a while ago, and this was one of the comments:
There (mostly) seems to be a one-to-one mapping of a lot of your functions to IRC command verbs. For example,
leave_server
->QUIT
,join_channel
->JOIN
,send_private_message
->PRIVMSG
, etc. Part of me just feels that (as you say) there is just a tad too much repetition in that architecture. (Perhaps some more abstract encapsulation of a server command and arguments?
class InvalidCommandParametersException(Exception):
"""Raised when the parameters to a command are invalid."""
def __init__(self, command, param_problems):
"""Raise an error that a command's parameters are invalid.
Parameters
----------
command: Command
The command type that failed validation.
param_problems: iterable[string]
Collection of problems with the parameters
"""
message = '\n'.join(["Command: {}".format(command.name)] + param_problems)
super(InvalidCommandParametersException, self).__init__(message)
class ExecutableCommandMixin(object):
"""Mixin to make an enum of commands executable.
Enables parameterizing a given command and specifying how to
validate each parameter, as well as specifying what "executing" the
command means. Does so by providing decorator functions that will
register functions as parameter validation or command execution.
Notes
-----
Does not subclass from `enum.Enum` because working around the
non-extensibility of enums with defined members is pretty inelegant.
Properties
----------
parameters: CommandParameterSet
Set of parameters for this command.
execution: function(*values) -> Object
Function that takes in the parameters and returns something.
"""
@property
def parameters(self):
return self.__class__._parameters[self]
_executions = {}
@property
def execution(self):
return self.__class__._executions[self]
def execute_command(self, *values):
problems = self._validate_arguments(values)
if problems:
raise InvalidCommandParametersException(self, problems)
return self.execution(*values)
def register_execution(self, func):
"""Register a function as this command's action.
Parameters
----------
func: function
Function to execute for this parameter.
Returns
-------
func: function
The original function, unchanged.
"""
self.__class__._executions[self] = func
return func
_parameters = defaultdict(CommandParameterSet)
def register_parameter(self, name, n_th, optional=False, count=1, count_type=None):
"""Decorator to register a function to validate a given parameter.
Parameters
----------
name: string
Name of the parameter; used to describe the parameter.
n_th: indexer
Which parameter this should be in the parameter list.
optional: boolean, default=False
Whether or not the parameter is optional. If it is optional,
then `None` should be passed for validation.
count: integer, default=1
Describes how many instances of this value are allowed. If
greater than 1, then `count_type` is required. If 0 or more
are allowed, then pass `count=0` and
`count_type=CountTypes.MIN`
count_type: CountType, default=None
Required if a `count` is given; describes how to interpret
the count (as a max, min, or exact requirement).
Returns
-------
decorator: function -> function
Wrapper function that wraps its argument function and adds
it as validation for this parameter
"""
def decorator(validator):
"""Add the actual validator function for this parameter.
Parameters
----------
validator: function(value) -> string | None
Function that takes in a value and returns an error
message, or None if it was okay. If there are multiple
values allowed (i.e. `count_type != None`) then this is
called for each item in the collection, _instead_ of on
the entire collection.
Returns
-------
validator: function(value) -> string | None
The original function, unchanged.
"""
self.__class__._parameters[self].insert_parameter(
CommandParameter(name, validator, optional=optional,
count=count, count_type=count_type),
n_th)
return validator
return decorator
def _validate_arguments(self, arguments):
"""Validate the command's arguments.
Parameters
----------
arguments: collection[Object]
List of the arguments being passed.
Returns
-------
list[string]
List of all of the problems with the arguments. Will be an
empty list if no problems are present.
"""
errors = list(self.__class__._parameters[self].validate(arguments))
if not errors or all(err is None for err in errors):
return []
return errors
If you need it, my folder structure looks like this (also available on GitHub)GitHub PyIRC Repository URL:
executable_command.py
is all of the code besides the sample (IrcCommand
) and the tests, plus a handful of imports.
class ExecutableCommandMixin(object):
"""Mixin to make an enum of commands executable.
Enables parameterizing a given command and specifying how to
validate each parameter, as well as specifying what "executing" the
command means. Does so by providing decorator functions that will
register functions as parameter validation or command execution.
Notes
-----
Does not subclass from `enum.Enum` because working around the
non-extensibility of enums with defined members is pretty inelegant.
Properties
----------
parameters: CommandParameterSet
Set of parameters for this command.
execution: function(*values) -> Object
Function that takes in the parameters and returns something.
"""
@property
def parameters(self):
return self.__class__._parameters[self]
_executions = {}
@property
def execution(self):
return self.__class__._executions[self]
def execute_command(self, *values):
problems = self._validate_arguments(values)
if problems:
raise InvalidCommandParametersException(self, problems)
return self.execution(*values)
def register_execution(self, func):
"""Register a function as this command's action.
Parameters
----------
func: function
Function to execute for this parameter.
Returns
-------
func: function
The original function, unchanged.
"""
self.__class__._executions[self] = func
return func
_parameters = defaultdict(CommandParameterSet)
def register_parameter(self, name, n_th, optional=False, count=1, count_type=None):
"""Decorator to register a function to validate a given parameter.
Parameters
----------
name: string
Name of the parameter; used to describe the parameter.
n_th: indexer
Which parameter this should be in the parameter list.
optional: boolean, default=False
Whether or not the parameter is optional. If it is optional,
then `None` should be passed for validation.
count: integer, default=1
Describes how many instances of this value are allowed. If
greater than 1, then `count_type` is required. If 0 or more
are allowed, then pass `count=0` and
`count_type=CountTypes.MIN`
count_type: CountType, default=None
Required if a `count` is given; describes how to interpret
the count (as a max, min, or exact requirement).
Returns
-------
decorator: function -> function
Wrapper function that wraps its argument function and adds
it as validation for this parameter
"""
def decorator(validator):
"""Add the actual validator function for this parameter.
Parameters
----------
validator: function(value) -> string | None
Function that takes in a value and returns an error
message, or None if it was okay. If there are multiple
values allowed (i.e. `count_type != None`) then this is
called for each item in the collection, _instead_ of on
the entire collection.
Returns
-------
validator: function(value) -> string | None
The original function, unchanged.
"""
self.__class__._parameters[self].insert_parameter(
CommandParameter(name, validator, optional=optional,
count=count, count_type=count_type),
n_th)
return validator
return decorator
def _validate_arguments(self, arguments):
"""Validate the command's arguments.
Parameters
----------
arguments: collection[Object]
List of the arguments being passed.
Returns
-------
list[string]
List of all of the problems with the arguments. Will be an
empty list if no problems are present.
"""
errors = list(self.__class__._parameters[self].validate(arguments))
if not errors or all(err is None for err in errors):
return []
return errors
If you need it, my folder structure looks like this:
class InvalidCommandParametersException(Exception):
"""Raised when the parameters to a command are invalid."""
def __init__(self, command, param_problems):
"""Raise an error that a command's parameters are invalid.
Parameters
----------
command: Command
The command type that failed validation.
param_problems: iterable[string]
Collection of problems with the parameters
"""
message = '\n'.join(["Command: {}".format(command.name)] + param_problems)
super(InvalidCommandParametersException, self).__init__(message)
class ExecutableCommandMixin(object):
"""Mixin to make an enum of commands executable.
Enables parameterizing a given command and specifying how to
validate each parameter, as well as specifying what "executing" the
command means. Does so by providing decorator functions that will
register functions as parameter validation or command execution.
Notes
-----
Does not subclass from `enum.Enum` because working around the
non-extensibility of enums with defined members is pretty inelegant.
Properties
----------
parameters: CommandParameterSet
Set of parameters for this command.
execution: function(*values) -> Object
Function that takes in the parameters and returns something.
"""
@property
def parameters(self):
return self.__class__._parameters[self]
_executions = {}
@property
def execution(self):
return self.__class__._executions[self]
def execute_command(self, *values):
problems = self._validate_arguments(values)
if problems:
raise InvalidCommandParametersException(self, problems)
return self.execution(*values)
def register_execution(self, func):
"""Register a function as this command's action.
Parameters
----------
func: function
Function to execute for this parameter.
Returns
-------
func: function
The original function, unchanged.
"""
self.__class__._executions[self] = func
return func
_parameters = defaultdict(CommandParameterSet)
def register_parameter(self, name, n_th, optional=False, count=1, count_type=None):
"""Decorator to register a function to validate a given parameter.
Parameters
----------
name: string
Name of the parameter; used to describe the parameter.
n_th: indexer
Which parameter this should be in the parameter list.
optional: boolean, default=False
Whether or not the parameter is optional. If it is optional,
then `None` should be passed for validation.
count: integer, default=1
Describes how many instances of this value are allowed. If
greater than 1, then `count_type` is required. If 0 or more
are allowed, then pass `count=0` and
`count_type=CountTypes.MIN`
count_type: CountType, default=None
Required if a `count` is given; describes how to interpret
the count (as a max, min, or exact requirement).
Returns
-------
decorator: function -> function
Wrapper function that wraps its argument function and adds
it as validation for this parameter
"""
def decorator(validator):
"""Add the actual validator function for this parameter.
Parameters
----------
validator: function(value) -> string | None
Function that takes in a value and returns an error
message, or None if it was okay. If there are multiple
values allowed (i.e. `count_type != None`) then this is
called for each item in the collection, _instead_ of on
the entire collection.
Returns
-------
validator: function(value) -> string | None
The original function, unchanged.
"""
self.__class__._parameters[self].insert_parameter(
CommandParameter(name, validator, optional=optional,
count=count, count_type=count_type),
n_th)
return validator
return decorator
def _validate_arguments(self, arguments):
"""Validate the command's arguments.
Parameters
----------
arguments: collection[Object]
List of the arguments being passed.
Returns
-------
list[string]
List of all of the problems with the arguments. Will be an
empty list if no problems are present.
"""
errors = list(self.__class__._parameters[self].validate(arguments))
if not errors or all(err is None for err in errors):
return []
return errors
If you need it, my folder structure looks like this (also available on GitHub)GitHub PyIRC Repository URL:
executable_command.py
is all of the code besides the sample (IrcCommand
) and the tests, plus a handful of imports.