Source code for pytek.util

import re
import os

[docs]class Configurator(object): """ The `Configurator` class creates helper objects that can be used to easily add methods to a class to configure and query a particular setting on the device. The easiest way to understand it is by example. First, a stripped down usage example: .. code :: python class MyDevice(object): __metaclass__ = Configurator.ConfigurableMeta @Configurator.config("FOO:BAR") def foobar(self, val): return val.lower() @foobar.setter def foobar(self, val): return val.upper() @Configurator.config def frobbed(self, val): return (val == "ON") @frobbed.setter def frobbed(self, val): return "ON" if val else "OFF" And now, a more thorough example, expanded from this: .. code :: python class MyDevice(object): #Make sure it uses the ConfigurableMeta class as its metaclass, # so Configurator objects in the class definition get replaced with # appropriate methods. __metaclass__ = Configurator.ConfigurableMeta #Just some ordinary instance attributes, which we will be the target of # our setting configuring and querying. __foobar = "TAZ" __frobbed = "OFF" #This is where the class actually implements sending command and queries. # The Configurator objects will call these methods. def send_command(self, name, arg): print "~~~> %s %s" % (name, arg) if name == "FOO:BAR": if not isinstance(arg, str): raise TypeError() if arg != arg.upper(): raise ValueError() self.__foobar = arg elif name == "FROBBED": if arg not in ("ON", "OFF"): raise ValueError() self.__frobbed = arg else: raise KeyError() def send_query(self, name): print "???? %s" % name if name == "FOO:BAR": val = self.__foobar elif name == "FROBBED": val = self.__frobbed else: raise KeyError() print " <<<< %s" % val return val #Now, define Configurators for each of our configurable settings. #First, for the FOO:BAR setting, which will be accessed through a # function called `foobar`. @Configurator.config("FOO:BAR") def foobar(self, val): #Translate a value returned by `send_query` into a value to return # to the calling code. return val.lower() @foobar.setter def foobar(self, val): #Translate a value provided by the calling code into a value that # will be passed to `send_command`. return val.upper() #Now, the FROBBED setting. We can use implicit named in the decorator # for this one. @Configurator.config def frobbed(self, val): ''' +++ Querying returns True for "ON", and False for "OFF". ''' if val == "ON": return True if val == "OFF": return False raise ValueError(val) @frobbed.setter def frobbed(self, val): ''' +++ Valid values for configuring are True and False, or synonomously "ON" and "OFF". ''' if val is True or val == "ON": return "ON" elif val is False or val == "OFF": return "OFF" raise ValueError() With the above code, you could then do the following: .. code :: pycon >>> dev = MyDevice() >>> dev.foobar() ???? FOO:BAR <<<< TAZ 'taz' >>> >>> dev.foobar('razzle-dazzle') ~~~> FOO:BAR RAZZLE-DAZZLE >>> >>> dev.foobar() ???? FOO:BAR <<<< RAZZLE-DAZZLE 'razzle-dazzle' >>> >>> >>> dev.frobbed() ???? FROBBED <<<< OFF False >>> dev.frobbed(True) ~~~> FROBBED ON >>> dev.frobbed() ???? FROBBED <<<< ON True >>> >>> dev.frobbed(False) ~~~> FROBBED OFF >>> dev.frobbed() ???? FROBBED <<<< OFF False >>> >>> dev.frobbed("ON") ~~~> FROBBED ON >>> dev.frobbed() ???? FROBBED <<<< ON True >>> >>> dev.frobbed("???") Traceback (most recent call last): File "<stdin>", line 1, in <module> File "src\pytek\util.py", line 125, in config return self(device, val) File "src\pytek\util.py", line 116, in __call__ self.configure(device, self.name, self.set(device, val)) File "temp.py", line 94, in frobbed raise ValueError() ValueError >>> >>> >>> help(dev.foobar) Help on method foobar in module pytek.util: foobar(device, val=None) method of temp.MyDevice instance Configures or queries the value of the ``FOO:BAR`` setting on the device. If a value is given, then the setting is configured to the given value. If the value is `None` (the default), then the setting is queried and the value is returned. >>> >>> help(dev.frobbed) Help on method frobbed in module pytek.util: frobbed(device, val=None) method of temp.MyDevice instance Configures or queries the value of the ``FROBBED`` setting on the device. If a value is given, then the setting is configured to the given value. If the value is `None` (the default), then the setting is queried and the value is returned. Querying returns True for "ON", and False for "OFF". Valid values for configuring are True and False, or synonomously "ON" and "OFF". >>> >>> """ #: A string used for default value of the `doc` attribute. DEFAULT_DOCTSTR = """ Configures or queries the value of the ``%(NAME)s`` setting on the device. If a value is given, then the setting is configured to the given value. If the value is `None` (the default), then the setting is queried and the value is returned. """ def __init__(self, name, get=None, set=None, doc=None): """ :param name: Specifies the name of the setting accessed by this object. Should be either a `callable` object with a ``__name__`` attribute, or a string. Strings will be used directly, callables will be filtered through `func_to_name`. :param callable get: Optional: if given, passed to `getter`. :param callable set: Optional: if given, passed to `setter`. :param callable doc: Optional: if given, used as the value of the `doc` attribute. """ if callable(name): self.name = self.func_to_name(name) else: self.name = str(name) self.doc = doc if doc is None: self.doc = self.DEFAULT_DOCTSTR % {'NAME': self.name} self.get = None if get is None: self.get = lambda device, val: val else: self.getter(get) self.set = None if set is None: self.set = lambda device, val: str(val) else: self.setter(set) @classmethod
[docs] def configure(cls, device, name, val): """ The final method in this object used to configure the setting, given the raw value to be sent to the device. This is called by the `__call__` method when appropriate. This delegates to the ``send_command`` method of the given `device`. :param device: The object on which the ``send_command`` will be invoked. :param str name: The name of the setting, usually the value of the `name` attribute. This is the first arguments passed to ``send_command``. :param str val: The raw value to configure the setting to. This is the second argument passed to ``send_command``. """ device.send_command(name, val)
@classmethod
[docs] def query(cls, device, name): """ The final method in this object used to query the setting, returning the raw value from the device. This is called by the `__call__` method when appropriate. This delegates to the ``send_query`` method of the given `device`. :param device: The object on which the ``send_query`` will be invoked. :param str name: The name of the setting, usually the value of the `name` attribute. This is the only arguments passed to ``send_query``. """ return device.send_query(name)
def __call__(self, device, val=None): """ __call__(self, device [, val]) The universal callback which filters values and delegates to `configure` or `query` as appropriate. If `val` is given (and not `None`), then the setting is configured. The given value is first filtered through the callable in the `set` attribute, and then passed to `configure`. If `val` is not given (or is `None`, the default), then the setting is queried. The setting's value is retrieved with `query`, then filtered through the callable in this object's `get` attribute before being returned. """ if val is None: #Get it return self.get(device, self.query(device, self.name)) self.configure(device, self.name, self.set(device, val))
[docs] def create_method(self, name): """ Creates a method with the given name which can be installed in a class to delegate to this object's `__call__` method. Sets the name of the method to `name`, and sets the docstr (``__doc__``) to the value of this object's `doc` attribute. This is used by `ConfigurableMeta` to replace Configurator instances in the classes dictionary with functions. """ config = self def c(self, val=None): return config(self, val) c.__name__ = name c.__doc__ = self.doc return c
__DOC_APPEND = re.compile(r'^\s*\n(\s*)\+\+\+\s*\n', re.M) __DOC_PREPEND = re.compile(r'^\s*\n(\s*)\-\-\-\s*\n', re.M)
[docs] def update_doc(self, func): """ If the given function has a docstrig (`__doc__`), then this object's `doc` attribute is updated with it. Otherwise, it does nothing. If `func`'s docstr begins with ``'+++'`` alone on a line (any amount of leading and trailing whitespace), then the remainder of the docstring is *appended* to the existing docstring, instead of replacing it. """ if func.__doc__ is not None: match = self.__DOC_APPEND.match(func.__doc__) if match: #Append indent = re.compile(r'^' + re.escape(match.group(1))) doc = self.__DOC_APPEND.sub('', func.__doc__) doc = os.linesep.join(indent.sub('', line) for line in doc.splitlines()) self.doc += '\n\n' + doc else: match = self.__DOC_PREPEND.match(func.__doc__) if match: #Prepend indent = re.compile(r'^' + re.escape(match.group(1))) doc = self.__DOC_PREPEND.sub('', func.__doc__) doc = os.linesep.join(indent.sub('', line) for line in doc.splitlines()) self.doc = doc + '\n\n' + self.doc else: #Replace self.doc = func.__doc__
@classmethod
[docs] def func_to_name(cls, func): """ Derives a setting name from a function. The implementation here just uses the `__name__` attribute of the given `func`, and then uses `str.upper()` to make it all upper case. This is used in `__init__` if the name is a `callable` object. """ return func.__name__.upper()
@classmethod
[docs] def boolean(cls, arg, **kwargs): """ A function decorator utility used to create a `Configurator` object which handles boolean settings. This ends up delegating to `set_boolean` to actually set up the `get` and `set` filters based on responses from the decorated function. All keyword arguments passed to this function are forwarded to `set_boolean`. Similar to `config`, you can invoke this with *implicit arguments* or *explicit arguments* For **implicit arguments**, you use this method as a function decorator directly, and the `name` to use is derived from the decorated function with `func_to_name`. In this mode, you can't specify any additional arguments to pass to `set_boolean`. For **explicit arguments**, you invoke this method directly, and it returns a function decorator. This allows you to pass in a string as the first argument to specify the `name` to use, as well as additional keyword arguments to be forwarded on to `set_boolean`. .. seealso: * set_boolean * config """ c = cls(arg) if callable(arg): c.set_boolean(arg, **kwargs) return c def wrapper(func): c.set_boolean(func, **kwargs) return c return wrapper
@classmethod
[docs] def config(cls, arg): """ A function decorator utility used to create a `Configurator` object and a function decorator to configure its `getter`. There are two way to invoke this, using *implicit naming* or *explicit naming*. For **implicit naming***, simply pass a function in directly, or use this function directly as a decorator. For instance: .. code:: python @Configurator.config def foobar(self, val): return val The above code will create a new instance of `cls` (i.e., a Configurator object), and will pass the given function ``foobar`` in as the ``name`` parameter to the constructor. This in turn will use `func_to_name` to derive a value for the instance's `name` attribute from the function, by default (i.e., in the base `Configurator` class), this is just the name of the function in all uppercase. The function will also be passed to the instance's `getter` method so that the ``foobar`` function becomes the instance's `get` filter. This method will then return the Configurator object itself, *not* the wrapped function. The alternative is **explicit naming**, in which this function is not used *as* a function wrapper, but invoked to *return* a function wrapper. This gives you some added flexibility such as explitictly giving the `name` to use for the Configurator object. Otherwise, the behavior is essentially the same. For instance: .. code:: python @Configurator.config('BAZ:RUFFLE') def foobar(self, val): return val In this case, even though the wrapped function has the same name, ``"foobar"``, the created Configurator object will have a ``name`` of ``"BAZ:RUFFLE"``. Other than that, the effects are the same. In either case, when code like this appears in a class definition, it means that class will have an attribute named ``foobar`` whose value is a Configurator object. If this class is using the `~Configurator.ConfigurableMeta` metaclass, then this attribute will be replaced by a proper method generated by the Configurator's `create_method` method. Also note that when the wrapped function is passed to the Configurator's `getter` method, this method will also pass it to `update_doc`, so if the wrapped function has a docstring, the Configurator object's `doc` attribute will be set accordingly. When the `~Configurator.ConfigurableMeta` gets a hold of it, the corresponding method it adds to the class will receive this docstr from the Configurator object. Note that for the remainder of the class definition, you can use the generated Configurator object. For instance, you can follow up either of the above examples with the following: .. code :: python @foobar.setter def foobar(self, val): if val is False: return "OFF" return "ON" Since at this point the ``foobar`` symbol is actually a Configurator object, you can use its other decorators such as `setter` and `getter`. """ c = cls(arg) if callable(arg): c.getter(arg) return c def wrapper(func): c.getter(func) return c return wrapper
[docs] def setter(self, func): """ A function wrapper which sets this object's `set` attribute to the given function and passes the function to `update_doc`, then returns `self`. The given function should take two arguments and return a string. The first argument will be the device on which the `send_command` method is invoked, the second argument will be the client supplied value they want to configure the setting to. The function should return a corresponding string which will actually be sent to the device. """ self.set = func self.update_doc(func) return self
[docs] def getter(self, func): """ Like `setter`, but sets the object's `get` attribute, used for querying the setting from the device. This is a function wrapper which sets this object's `get` attribute to the given function and passes the function to `update_doc`, then returns `self`. The given function should take two arguments and return a string. The first argument will be the device on which the `send_query` method is invoked, the second argument will be the value returned from the device by `send_query`. The function should return a corresponding value which will be returned to the user to reflect the string returned by the device. """ self.get = func self.update_doc(func) return self
[docs] def set_boolean(self, func, strict=False, default=False, nocase=False): """ Configures the objects `set` and `get` filters based on a boolean setting. A boolean setting means the setting has a set of possible values that are partitioned into two subsets: true values and false values. On the python side, any value in these subsets corresponds to a value of `True` or `False`, respectively. This method sets up the object to filter values accordingly, so that querying the setting always returns `True` or `False`, and configuring the setting can be done with `True` or `False`. To do so, you have to pass in a function which can be evaluated immediately to get the set of true values and the set of false values. The function should take a single boolean argument, if the argument value is `True`, return the set of true values, otherwise, return the set of false values. The method will then create appropriate set and get filters based on these values and the other parameters passed into this function (see below). The sets of true values and false values returned by `func` must be sequences. The first value in each sequence will be used as the *canonical* value, meaning the ones that will actually be passed to the device for the corresponding value. All other values in the sets will be acceptable responses from the device for queries, and will result in the corresponding boolean value being returned to the caller. .. seealso:: `boolean` :param callable func: This function will be called twice, immediately. Once with a value of `True`, which should return a sequence of true values; and once with a value of `False`, which should return a sequence of false values. :param bool strict: Optional, default is `False`. If `True`, then the generated `set` and `get` filters will be strict about values. The `set` filter will only accept boolean values, and will raise a `TypeError` otherwise. The `get` filter will only accept values from the true- and false- value sets, and will raise a `ValueError` if the device returns anything else. If the value of the parameter is `False`, the generated functions are not as strict, and will not raise exceptions for unrecognized values (the way it handles unrecognized values depends on the value of the `default` parameter). For the non-strict `set` filter, values are simply evaluated as bools to choose which value to send. :param bool default: Optional, default value is `False`. This is only used if `strict` is `False`, in which case it determines the *default* value when an unrecognized value is encountered. :param bool nocase: Optional, default value is `False`. If `True`, then values are considered case-insensitive. """ t_vals = func(True) f_vals = func(False) pretty_val = lambda val : '``"%s"``' % val val_list = lambda vals : ', '.join(pretty_val(v) for v in vals) d = dict( T_VALS = val_list(t_vals), F_VALS = val_list(f_vals), TRUE = pretty_val(t_vals[0]), FALSE = pretty_val(f_vals[0]), ) convert = lambda val : val if nocase: convert = lambda val : val.lower() t_vals = map(convert, t_vals) f_vals = map(convert, f_vals) if strict: def g(device, val): """ +++ For *queries*, return `True` or `False`: * `True` if the device replies with any of the following: %(T_VALS)s * `False` if the device replies with any of the following: %(F_VALS)s * Otherwise, raise a `ValueError`. """ val = convert(val) if val in t_vals: return True if val in f_vals: return False raise ValueError("Unexpected value returned from device: %r" % val) def s(device, val): """ +++ For *configuring*, accepts values of `True` or `False`: * `True` will cause %(TRUE)s to be sent to the device. * `False` will cause %(FALSE)s to be sent to the device. * Any other value will raise a `TypeError`. """ if val is True: return t_vals[0] if val is False: return f_vals[0] raise TypeError("Expected boolean value, received: %r" % val) else: if default: def g(device, val): """ +++ For *queries*, return `True` or `False`: * `False` if the device replies with any of the following: %(F_VALS)s * `True` otherwise. """ return not (convert(val) in f_vals) def s(device, val): """ +++ For *configuring*, if `val` evaluates as `False`, causes %(FALSE)s to be sent to the device. Any other value for `val` causes %(TRUE)s to be sent. """ if bool(val): return f_vals[0] return t_vals[0] else: def g(device, val): """ +++ For *queries*, return `True` or `False`: * `True` if the device replies with any of the following: %(T_VALS)s * `False` otherwise. """ return (convert(val) in t_vals) def s(device, val): """ +++ For *configuring*, if `val` evaluates as `True`, causes %(TRUE)s to be sent to the device. Any other value for `val` causes %(FALSE)s to be sent. """ if bool(val): return t_vals[0] return f_vals[0] g.__doc__ = g.__doc__ % d s.__doc__ = s.__doc__ % d self.getter(g) self.setter(s) self.update_doc(func)
[docs] class ConfigurableMeta(type): """ This is a meta class that can be added to classes to more easily support the use of `Configurator` objects as pseudo-methods. The meta class extends the `__new__` function to find all instances of `Configurator` in the class's dictionary, and replace it with a method created by the Configurator's `~Configurator.create_method` method. See the example code in the documentation for `Configurator` for an example. """ def __new__(meta, name, bases, dct): for attr, val in dct.items(): if isinstance(val, Configurator): method = val.create_method(attr) #Add nicer signature to doc string. method.__doc__ = ("%s([val])\n\n" % attr) + method.__doc__ dct[attr] = method return super(Configurator.ConfigurableMeta, meta).__new__(meta, name, bases, dct)
[docs]class Configurable(object): """ Just a simple base classes that uses `~Configurator.ConfigurableMeta` as the metaclass. """ __metaclass__ = Configurator.ConfigurableMeta