# -*- coding: utf-8 -*-
################################################################################
# #
# This file is part of the Bibolamazi Project. #
# Copyright (C) 2014 by Philippe Faist #
# philippe.faist@bluewin.ch #
# #
# Bibolamazi is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# Bibolamazi is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with Bibolamazi. If not, see <http://www.gnu.org/licenses/>. #
# #
################################################################################
import re
import bibolamazi.init
from .. import butils
# for meta-typing. This is used by the graphical interface.
[docs]class EnumArgType:
def __init__(self, listofvalues):
self.listofvalues = listofvalues
# magic method that produces a representation of the option value for the
# command-line string. Must return a bool (for -d...) or a string (for
# -s...).
[docs] def option_val_repr(self, x):
return str(x)
[docs]def enum_class(class_name, values, default_value=0, value_attr_name='value'):
"""
Define a class with the given name `class_name`, which can store one of
several choices. The possible values are fixed. Each choice has an integer
and a string representation.
`class_name` is the class name.
`values` should be a list of tuples `(string_key, numeric_value)` of all the
expected string names and of their corresponding numeric values.
`default_value` should be the value that would be taken by default, e.g. by
using the default constructor.
`value_attr_name` the name of the attribute in the class that should store the
value. For example, the `arxiv` module defines the enum class `Mode` this way
with the attribute `mode`, so that the numerical mode can be obtained with
`enumobject.mode`.
"""
class ThisEnumArgClass:
_values = values
_values_list = [x[0] for x in _values]
_values_dict = dict(_values)
def __init__(self, val=None):
# this will call _parse_value(), and also assign the default_value if
# val is None.
self._value = val
def __setattr__(self, attname, val):
if (attname == value_attr_name or attname == '_value'):
theval = self._parse_value(val)
self.__dict__[value_attr_name] = theval
self.__dict__['_value'] = theval
return
self.__dict__[attname] = val
def _parse_value(self, value):
# easy cases.
if (isinstance(value, int)):
return value
if (isinstance(value, self.__class__)):
return value._value
# request for default value
if (value is None):
# just make sure default_value is not None, to avoid infinite recursion.
return (0 if default_value is None else self._parse_value(default_value))
# value given by key
svalue = str(value)
if (svalue in self._values_dict):
return self._values_dict.get(svalue)
try:
return int(value)
except ValueError:
pass
raise ValueError("%s: Invalid value: %r" %(self.__class__.__name__, value))
# so that we can use this object like an int, at least compare it to an int or to the
# string corresponding to the other mode
def __eq__(self, other):
if (isinstance(other, int)):
return self._value == other
if (isinstance(other, self.__class__)):
return self._value == other._value
return self._value == self._parse_value(other)
def __str__(self):
ok = [x for (x,v) in self._values if v == self._value]
if not len(ok):
# this doesn't correspond to a valid value... return the integer value directly
return str(self._value)
# the corresponding string key for this value
return ok[0]
def __repr__(self):
return "%s(%s)"%(self.__class__.__name__, repr(self.__str__()))
def __hash__(self):
return hash(self.value)
thecls = ThisEnumArgClass
thecls.__name__ = str(class_name)
# add docstring
mapped_vals_list = [ "‘%s’"%(x) for x in thecls._values_list ]
if len(mapped_vals_list) > 1:
show_vals_list = ", ".join(mapped_vals_list[:-1]) + ", or " + mapped_vals_list[-1]
elif len(mapped_vals_list) == 1:
show_vals_list = mapped_vals_list[0]
else:
show_vals_list = '<no values>'
thecls.__doc__ = "One of %s."%(
show_vals_list
)
# for the gui
thecls.type_arg_input = EnumArgType(thecls._values_list)
# provide e.g. 'Mode.modes', 'Mode.modes_list', 'Mode.modes_dict' (or 'values_list' etc.)
setattr(thecls, value_attr_name+'s', thecls._values)
setattr(thecls, value_attr_name+'s_list', thecls._values_list)
setattr(thecls, value_attr_name+'s_dict', thecls._values_dict)
return thecls
# ------------------------------------------------------------------------------
# for meta-typing. This is particularly used by the graphical interface.
[docs]class MultiTypeArgType:
def __init__(self, typelist, parse_value_fn):
self.typelist = typelist
self.parse_value_fn = parse_value_fn
# magic method that produces a representation of the option value for the
# command-line string. Must return a bool (for -d...) or a string (for
# -s...).
[docs] def option_val_repr(self, x):
if isinstance(x.value, bool):
return bool(x.value)
return str(x.value)
multi_type_class_default_convert_functions = [
(bool, butils.getbool),
]
[docs]def multi_type_class(class_name, typelist,
value_attr_name='value', valuetype_attr_name='valuetype',
convert_functions=multi_type_class_default_convert_functions,
parse_value_fn=None, doc=None):
"""
Define a class with the given name `class_name`, which can store a value of
one of several fixed types. This is used, for instance, in the `fixes`
filter where for some options one can specify either "True/False" (`bool`
type) or a list of fields (`CommaStrList` type).
`class_name` is the class name.
`typelist` should be a list of tuples `(typeobject, description)` of type
objects that can be stored by this object and a corresponding very short
description of what is stored with that type
`default_value` should be the value that would be taken by default, e.g. by
using the default constructor.
`value_attr_name` the name of the attribute in the class that should store the
value.
`valuetype_attr_name` the name of the attribute in the class that should store the
type object that is currently stored.
Optionally, you can also specify a list of helper functions that can convert
stuff into a given type: `convert_functions` is a list of tuples
`(type_object, function)` that specifies this.
If `parse_value_fn` is not None, then it should be set to a callable that
parses a value and returns a tuple `(typeobject, value)`. It can raise
`ValueError`.
"""
def parse_value_impl(value,
typelist=typelist,
convert_functions=convert_functions,
parse_value_fn=parse_value_fn):
if parse_value_fn is not None:
return parse_value_fn(value)
if value is None:
t = typelist[0][0]
return (t, t()) # instantiate first type
# try to convert in order of types
for t,s in typelist:
cfn = t
cfnlst = [cfn2 for t2,cfn2 in convert_functions if t is t2]
if len(cfnlst):
cfn = cfnlst[0]
try:
# try to convert to this type
theval = cfn(value)
return t, theval # return if successful
except (TypeError,ValueError):
continue
# none of that worked
raise ValueError("Invalid value: %r" %(value,))
class ThisMultiTypeArgClass:
_typelist = typelist
def __init__(self, *args):
# X() or X(value) or X(type, value)
if len(args) == 0: # X()
t, v = self.parse_value(None)
elif len(args) == 1: # X(x) or X(value)
if isinstance(args[0], self.__class__):
t, v = args[0]._valuetype, args[0]._value
else:
t, v = self.parse_value(args[0])
elif len(args) == 2: # X(type, value)
t, v = args
else:
raise TypeError("Wrong number of arguments: %d"%(len(args)))
self.set_type_value(t, v)
def __setattr__(self, attname, val):
if (attname == value_attr_name):
thetyp, theval = self.parse_value(val)
self.set_type_value(thetyp, theval)
return
self.__dict__[attname] = val
def set_type_value(self, thetyp, theval):
#print("set_type_value(%r,%r)"%(thetyp,theval))
if len([t for t,s in self._typelist if t is thetyp]) != 1:
raise ValueError("Invalid type: %r"%(thetyp,))
# ### Don't be this strict -- for instance, we should accept int for bool ...
#if not isinstance(theval, thetyp):
# raise ValueError("Value is not of given type: %r (expected type %s)"
# %(theval, thetyp.__name__))
self.__dict__[valuetype_attr_name] = thetyp
self.__dict__[value_attr_name] = theval
self.__dict__['_valuetype'] = thetyp
self.__dict__['_value'] = theval
@staticmethod
def parse_value(value):
return parse_value_impl(value)
# so that we can use this object like an int, at least compare it to an int or to the
# string corresponding to the other mode
def __eq__(self, other):
if (isinstance(other, self.__class__)):
return self._valuetype is other._valuetype and self._value == other._value
return self == self.__class__(other)
def __str__(self):
return str(self._value)
def __repr__(self):
return "%s(%r)"%(self.__class__.__name__, self._value)
def __hash__(self):
return hash(self._value)
thecls = ThisMultiTypeArgClass
thecls.__name__ = str(class_name)
# add docstring
if doc is None:
mapped_vals_list = [ "`%s` (%s)"%(t.__name__, s) for t,s in thecls._typelist ]
if len(mapped_vals_list) > 1:
show_vals_list = ", ".join(mapped_vals_list[:-1]) + ", or "+mapped_vals_list[-1]
elif len(mapped_vals_list) == 1:
show_vals_list = mapped_vals_list[0]
else:
show_vals_list = '<no types>'
doc = "A class which can store a value of one of the following types: %s."%(
show_vals_list,
)
for t, s in thecls._typelist:
if hasattr(t, '__doc__'):
doc += "\n\n" + str(t.__name__) + ": " + t.__doc__
thecls.__doc__ = doc
# for the gui
thecls.type_arg_input = MultiTypeArgType(thecls._typelist, parse_value_impl)
return thecls
# ------------------------------------------------------------------------------
# meta type for GUI which can be used for any type (like CommaStrList) that is
# preferrably edited directly as a string.
[docs]class StrEditableArgType:
def __init__(self):
pass
# magic method that produces a representation of the option value for the
# command-line string. Must return a bool (for -d...) or a string (for
# -s...).
[docs] def option_val_repr(self, x):
return str(x)
# ------------------------------------------------------------------------------
#_rx_escape_lst = re.compile(r'(\\|,)')
#def _escape_lst(x):
# return _rx_escape_lst.sub(lambda m: '\\'+m.group(1), x)
#_rx_unescape_lst = re.compile(r'\\(?P<char>.)|\s*(?P<sep>,)\s*')
[docs]class CommaStrList(list):
"""
A list of values, specified as a comma-separated string.
"""
def __init__(self, iterable=[]):
# if (isinstance(iterable, str)):
# fullstr = iterable
# lastpos = 0
# strlist = []
# laststr = ""
# for m in _rx_unescape_lst.finditer(iterable):
# laststr += fullstr[lastpos:m.start()]
# if (m.group('sep') == ','):
# strlist.append(laststr)
# laststr = ""
# # ### DON'T ALLOW ESCAPES, THIS MESSES BADLY WITH LATEX STUFF
# #elif (m.group() and m.group()[0] == '\\'):
# # # escaped char
# # laststr += m.group('char')
# else:
# raise RuntimeError("Unexpected match!?: %r", m)
# lastpos = m.end()
# # include the last bit of string
# laststr += fullstr[lastpos:]
# strlist.append(laststr)
# # now we've got our decoded string list.
# iterable = strlist
if isinstance(iterable, str):
iterable = iterable.split(',')
super().__init__(iterable)
type_arg_input = StrEditableArgType()
def __str__(self):
return ",".join([str(x) for x in self])
# ------------------------------------------------------------------------------
# _rx_escape_dic = re.compile(r'(\\|,|:)')
# def _escape_dic(x):
# return _rx_escape_dic.sub(lambda m: '\\'+m.group(1), x)
#
# _rx_unescape_val = re.compile(r'\\(?P<char>.)')
# _rx_unescape_keyvalsep = re.compile(r'\s*(?P<sep>:)\s*')
_rx_keyvalsep = re.compile(r'\s*(?P<sep>:)\s*')
[docs]class ColonCommaStrDict(dict):
"""
A dictionary of values, specified as a comma-separated string of pairs
``'key:value'``. If no value is given (no colon), then the value is `None`.
"""
def __init__(self, *args, **kwargs):
if len(args) == 1:
iterable = args[0]
elif len(args) == 0:
iterable = None
else:
raise ValueError('ColonCommaStrDict accepts at most one *arg, an iterable')
if isinstance(iterable, str):
pairlist = CommaStrList(iterable)
d = {}
# now, read each key/value pair
for pairstr in pairlist:
#m = _rx_unescape_keyvalsep.search(pairstr)
m = _rx_keyvalsep.search(pairstr)
if m:
key = pairstr[:m.start()]
val = pairstr[m.end():]
else:
key = pairstr
val = None
# key = _rx_unescape_val.sub(lambda m: m.group('char'), key)
# if val:
# val = _rx_unescape_val.sub(lambda m: m.group('char'), val)
if key in d:
raise ValueError("Repeated key in input: %s"%(key,))
d[key] = val
super().__init__(d)
else:
super().__init__(*args, **kwargs)
type_arg_input = StrEditableArgType()
def __str__(self):
return ",".join([str(k)+(':'+str(v) if v is not None else '')
for k,v in self.items()])
#
# A special type for a Logging Level
#
import logging
from bibolamazi.core.blogger import LONGDEBUG
LogLevel = enum_class('LogLevel',
[('CRITICAL', logging.CRITICAL),
('ERROR', logging.ERROR),
('WARNING', logging.WARNING),
('INFO', logging.INFO),
('DEBUG', logging.DEBUG),
('LONGDEBUG', LONGDEBUG)],
default_value='INFO',
value_attr_name='levelno')