nutools/lib/nulib/python/nulib/config.py

877 lines
30 KiB
Python

# -*- coding: utf-8 -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
"""Fonctions utilitaires pour lire des fichiers de configuration.
Dans un fichier de configuration, l'on reconnait des lignes de la forme::
[comment][prefix]varname=value
value peut être placé entre double quotes ou simple quotes. Elle peut s'étendre sur
plusieurs lignes si elle est mise entre quotes, ou si elle se termine par \
"""
__all__ = (
'ConfigFile', 'ShConfigFile', 'PListFile',
'ShConfig',
)
import os, string, re, types, shlex
from os import path
from .base import odict, make_prop, isseq, seqof, firstof
from .uio import _s, _u
from .files import TextFile
from .formats import unicodeF
####################
# gestion des commentaires
re_comments = {
'shell': re.compile(r'[ \t]*#+'),
'conf': re.compile(r"[ \t]*;+"),
'C': re.compile(r'[ \t]*//+'),
'visual basic': re.compile(r"[ \t]*'+"),
'wincmd': re.compile(r'[ \t]*(?:r|R)(?:e|E)(?:m|M)'),
}
def is_comment(s, type=None):
"""Retourner vrai si s un commentaire (c'est à dire si la ligne commence par
un des styles de commentaires supportés)
"""
comment_types = type is None and re_comments.values() or [re_comments[type]]
for comment_type in comment_types:
if comment_type.match(s): return True
return False
####################
# gestion des fichiers de configuration
_marker = object()
class ConfigFile(TextFile):
r"""Un fichier de configuration, que l'on doit lire sous Python, et que l'on
doit partager éventuellement avec d'autres langages ou d'autres systèmes
d'exploitation. Par exemple, il peut s'agir d'un fichier de configuration
sous bash.
Une valeur non quotée est trimée à droite et à gauche. Une valeur quotée
n'est jamais trimée.
Une valeur quotée peut être suivie d'une valeur non quotée, et les deux sont
mergés. Mais une fois que l'on commence à parser une valeur non quotée, plus
aucun traitement n'est effectuée, ce qui fait qu'une valeur quotée ne peut
pas suivre une valeur non quotée (cf le "andme" ci-dessus).
Ceci diffère du comportement de parseur plus évolués comme celui de bash. On
considère néanmoins que c'est une caractéristique, non un bug. XXX corriger
ce problème, ne serait-ce que pour supporter la lecture de fichiers tels que
var='value'\''with a quote'
Tests
=====
>>> from StringIO import StringIO
>>> input = StringIO(r'''# comment
... name=value
... name2= value
... name3 = value
... qname="qvalue"
... qname2=" qvalue "
... qname3 = " qvalue "
... qname4="
... multi-line
... qvalue
... "
... fancy="\
... noNL\
... "foryou"andme"
... quote='"'
... quote2="\""
... quote3='\''
... quote4='\\'
... quote5='\\\''
... quote6='\\\'remainder'
... ''')
>>> from ulib.base.config import ConfigFile
>>> cf = ConfigFile(input)
>>> cf.get_string('name')
u'value'
>>> cf.get_string('name2')
u'value'
>>> cf.get_string('name3')
u'value'
>>> cf.get_string('qname')
u'qvalue'
>>> cf.get_string('qname2')
u' qvalue '
>>> cf.get_string('qname3')
u' qvalue '
>>> cf.get_string('qname4')
u'\n multi-line\n qvalue\n '
>>> cf.get_string('fancy')
u'noNLforyouandme'
>>> cf.get_string('quote')
u'"'
>>> cf.get_string('quote2')
u'\\"'
>>> cf.get_string('quote3')
u"\\'"
>>> cf.get_string('quote4')
u'\\\\'
>>> cf.get_string('quote5')
u"\\\\\\'"
>>> cf.get_string('quote6')
u"\\\\\\'remainder"
"""
# valeurs lues dans le fichier de configuration
_items, items = make_prop('_items')[:2]
# valeurs par défaut
_defaults, defaults = make_prop('_defaults')[:2]
# expression régulière identifiant le préfixe des variables
_prefix, prefix = make_prop('_prefix', '')[:2]
# expression régulière identifiant pour le séparateur entre le nom de la
# variable et sa valeur.
_equals, equals = make_prop('_equals', r'\s*=')[:2]
# faut-il considérer les variables en commentaires?
_comment, comment = make_prop('_comment')[:2]
############################################################################
# interface publique
def __init__(self, file=None, defaults=None,
prefix=None, equals=None, comment=False,
raise_exception=True, lines=None):
"""
@param prefix une expression régulière identifiant un préfixe mentionné
avant chaque variable. par exemple, si prefix=='##@' et qu'on
cherche la variable value, alors la ligne ##@value est cherchée.
@param comment faut-il considérer les valeurs qui sont en commentaires?
Si oui, tout se passe comme si le commentaire n'existe pas.
@param defaults un ensemble de valeurs par défaut qui sont retournées si la
variable n'existe pas dans le fichier.
@param lines instance de Lines ou BLines permettant de décoder le contenu du
fichier.
"""
super(ConfigFile, self).__init__(file, raise_exception=raise_exception, lines=lines)
self._items = {}
self._defaults = defaults or {}
if prefix is not None: self._prefix = prefix
if equals is not None: self._equals = equals
self._comment = comment
def __getitem__(self, name, default=_marker):
"""Obtenir la valeur de la variable name, telle qu'elle a été lue.
Si c'est un tableau, retourner une liste. Sinon retourner une chaine.
Si la variable n'est pas définie, retourner default.
"""
if not self._items.has_key(name): self._load_value(name)
if default is _marker:
if not self._items.has_key(name) and self._defaults.has_key(name):
return self._defaults[name]
return self._items[name]
return self._items.get(name, default)
get = __getitem__
def __setitem__(self, name, value):
self._items[name] = value
def __delitem__(self, name):
del self._items[name]
def has_key(self, name):
try: self.__getitem__(name)
except KeyError: return False
else: return True
def get_string(self, name, default=_marker):
"""Obtenir la valeur de la variable name. Si la variable est un tableau,
retourner la première valeur de ce tableau. Retourner None si le tableau
est vide.
"""
value = self.__getitem__(name, default)
if isseq(value): return firstof(value)
else: return value
def get_lines(self, name, strip=False, default=_marker):
"""Obtenir une valeur avec get_string(), et la spliter sur le caractère
de fin de ligne. Retourner la liste des lignes.
si strip est vrai, on strip toutes les lignes puis on enlève les
lignes vides.
"""
lines = self.get_string(name, default)
if not isseq(lines): lines = re.split(r'(?:\r?)\n', lines)
if strip: lines = filter(None, map(string.strip, lines))
return lines
def get_paths(self, name, strip=False, default=_marker):
"""Obtenir une valeur avec get_string(), la splitter sur le caractère
'os.path.pathsep'. Retourner la liste des chemins.
si strip est vrai, on strip toutes les valeurs puis on enlève les
valeurs vide.
"""
paths = self.get_string(name, default)
if not isseq(paths): paths = paths.split(path.pathsep)
if strip: paths = filter(None, map(string.strip, paths))
return paths
def get_array(self, name, default=_marker):
"""Obtenir la liste des valeurs de la variable name. Si name est une
valeur scalaire, retourner une liste d'un seul élément.
"""
return list(seqof(self.__getitem__(name, default)))
############################################################################
# partie privée
RE_ANTISLASHES = re.compile(r'\\+$')
def _is_cont(self, value):
"""Tester si value doit être fusionné avec la ligne suivante à cause de
la présence d'un caractère de continuation de ligne.
Par défaut, on teste si value se termine par un nombre impair de '\\'
"""
mo = self.RE_ANTISLASHES.search(value)
if mo is None: return False
return len(mo.group()) % 2 == 1
def _strip_cont(self, value):
"""Enlever le caractère de continuation de ligne de value. On assume que
self._is_cont(value) est vrai.
"""
return value[:-1]
def _merge_cont(self, index, value, sep=''):
"""Merger value située à la ligne index, et la ligne suivante, en les
séparant par sep. On assume que self._is_cont(value) est vrai, et que le
caractère de continuation a été enlevé avec self._strip_cont(value)
Dans la valeur de retour, eof vaut True si la fin de fichier est
rencontrée.
@return (index+1, merged_value, eof)
"""
if index + 1 < len(self.lines):
index += 1
value = value + sep + self.lines[index]
eof = False
else:
eof = True
return index, value, eof
def _unescape(self, value, quote=''):
"""Traiter les séquences d'échappement dans une valeur scalaire. Si la
valeur était quotée, quote contient la valeur du caractère ("'", '"' ou
''). Par défaut, ne rien faire.
Cette fonction doit être surchargée en fonction du type de fichier de
configuration que l'on lit.
La valeur quote=='' signifie que la valeur n'était pas quotée, mais il
peut quand même y avoir des séquences d'échappement à traiter.
"""
return value
def _load_value(self, name):
"""charger la valeur d'une variable depuis le fichier.
XXX rendre le parcours plus robuste: faire attention à ne pas lire une
valeur à l'intérieur d'une autre valeur. Par exemple:
var1="\
var2=bad
"
var2=good
Avec l'implémentaion actuelle, si on demande la valeur de var2, on
obtient bad. Une façon de corriger cela de parcourir *tout* le fichier,
de lire les valeurs non analysées de chaque variable au fur et à mesure,
puis de les placer en cache. ensuite, _load_value() se contenterai
d'analyser les valeurs dans le cache.
@return None si la valeur n'est pas trouvée dans le fichier. Sinon,
retourner une valeur scalaire ou une séquence en fonction du type de la
valeur.
"""
# le groupe 1 sera testé pour voir si c'est un commentaire
re_varname = re.compile(r'(.*)%s%s%s' % (self._prefix, name, self._equals))
re_value = re.compile(r'.*%s%s%s(.*)' % (self._prefix, name, self._equals))
indexes = self.grepi(re_varname)
if not indexes: return None
# trouver d'abord la ligne appropriée
comment = ''
for index in indexes:
comment = re_varname.match(self.lines[index]).group(1)
if is_comment(comment):
# si la valeur est en commentaire, ne l'accepter que si
# self._comment est vrai
if not self._comment:
continue
# nous avons trouvé l'index de la ligne
break
else:
# aucune ligne n'a été trouvée
return
# ensuite lire la valeur
value = re_value.match(self.lines[index]).group(1)
value = self._parse_logic(index, value)
self._items[name] = value
def _parse_logic(self, index, value):
"""Implémenter la logique d'analyse de la valeur d'une variable.
Il faut reimplémenter cette méthode si on veut modifier le type de
valeurs supportées. _parse_scalar() permet d'analyser une valeur simple,
_parse_array() permet d'analyser un tableau de valeurs.
Par défaut, on ne supporte que les valeurs scalaire. Utiliser
ShConfigFile pour supporter les tableaux.
"""
value = value.lstrip() # ignorer les espaces avant la valeur
return self._parse_scalar(index, value)
## valeurs scalaires simples
RE_SPACES = re.compile(r'\s+')
def _parse_scalar(self, index, value):
remainder = value
value = ''
lstrip = None
rstrip = None
while remainder:
mo = self.RE_SPACES.match(remainder)
if mo is not None:
# ne pas supprimer les espaces entre les valeurs
remainder = remainder[mo.end():]
value += mo.group()
# XXX supporter de spécifier le type de commentaires valides dans ce
# fichier de configuration. A cet endroit, il faudrait pouvoir
# éliminer les commentaires qui sont sur la ligne. évidemment, ce ne
# serait pas forcément approprié suivant la configuration. exemple:
# REM pour un fichier cmd n'est valide qu'en début de ligne.
elif self._is_quoted(remainder):
# valeur quotée. pas de strip
if lstrip is None: lstrip = False
rstrip = False
index, next_value, remainder = self._parse_quoted(index, remainder)
value += self._unescape(next_value)
else:
# valeur non quotée. lstrip si en premier. rstrip si en dernier
if lstrip is None: lstrip = True
rstrip = True
index, next_value, remainder = self._parse_value(index, remainder)
value += self._unescape(next_value)
if lstrip: value = value.lstrip()
if rstrip: value = value.rstrip()
return value
RE_VALUE = re.compile('[^\\s\'"]*')
def _parse_value(self, index, value, pattern=None):
"""Parser une valeur simple non quotée à partir de value (qui se trouve
à la position index) et des lignes suivant index si la ligne se termine
par '\\'.
@return index, value, remainder
"""
while self._is_cont(value):
value = self._strip_cont(value)
index, value, eof = self._merge_cont(index, value)
if eof: break
if pattern is None: pattern = self.RE_VALUE
mo = pattern.match(value)
if mo is None:
return index, '', value
else:
remainder = value[mo.end():]
value = value[:mo.end()]
return index, value, remainder
## valeurs scalaires quotées
def _is_quoted(self, value):
"""Tester si value est le début d'une valeur quotée. Ignorer les espaces
avant la quote.
"""
return value.lstrip()[:1] in ('"', "'")
def _search_next_quote(self, value, re_quote):
"""Chercher un match de re_quote dans value, qui ne soit pas précédé par
un nombre impair de '\\'.
"""
pos = 0
while True:
mo = re_quote.search(value, pos)
if mo is None: return None
if self._is_cont(value[:mo.start()]):
# nombre impair de '\\', la quote est mise en échappement
pos = mo.end()
else:
return mo
RE_QUOTE = re.compile(r'[\'"]')
def _parse_quoted(self, index, value):
"""Parser une valeur quotée à partir de value (qui se trouve à la
position index) et des lignes suivant index.
value *doit* commencer par la quote. si _is_quoted(value) est vrai, il
faut enlever les espaces éventuels au début de value avant de la passer
à cette méthode.
@return index, value, remainder
"""
if self.RE_QUOTE.match(value) is None:
raise ValueError("value must start with a quote, got %s" % repr(_s(value)))
quote, value = value[:1], value[1:]
re_quote = re.compile(quote)
mo = self._search_next_quote(value, re_quote)
while mo is None:
if self._is_cont(value):
value = self._strip_cont(value)
index, value, eof = self._merge_cont(index, value)
else:
index, value, eof = self._merge_cont(index, value, self.nl)
mo = self._search_next_quote(value, re_quote)
if eof: break
if mo is None:
# valeur quotée, mais mal terminée. on fait comme si on a rien vu
return index, value, ''
else:
remainder = value[mo.end():]
value = value[:mo.start()]
return index, value, remainder
## tableaux
def _is_array(self, value):
"""Tester si value est le début d'un tableau. Ignorer les espaces avant
le tableau.
"""
return False
def _parse_array(self, index, value):
"""Parser un tableau à partir de value (qui se trouve à la position
index) et des lignes suivant index.
value *doit* commencer par le tableau. si _is_array(value) est vrai, il
faut enlever les espaces éventuels au début de value avant de la passer
à cette méthode.
"""
return []
class ShConfigFile(ConfigFile):
r"""Un fichier de configuration qui est susceptible d'être lu aussi par bash
(ou tout autre shell sh-like). On supporte l'évaluation de variables, et
certaines séquences d'échappement pour des valeurs quotées.
Il y a certaines limitations: lors de la lecture des valeurs des variables,
les caractères sont traduits suivant la correspondance suivante:
\ en fin de ligne: continuer sur la ligne suivante
\" "
\\ \
\$ $
La séquence \` n'est pas traduite. En effet, pour que cela aie un sens, il
faudrait que l'on traduise aussi `cmd`
De plus, on ne supporte que les variables de la forme $var et ${var}
Tests
=====
>>> from StringIO import StringIO
>>> input = StringIO(r'''# comment
... var1=value
... var2="value"
... var3='value'
... var4=(value1 "value2" 'value3')
... var5=(
... value1
... "value2\
... " 'value3'
... )
... var6=()
... var7=( )
... var8=(
... )
... ''')
>>> from ulib.base.config import ShConfigFile
>>> cf = ShConfigFile(input)
>>> cf.get_string('var1')
u'value'
>>> cf.get_string('var2')
u'value'
>>> cf.get_string('var3')
u'value'
>>> cf.get_string('var4')
u'value1'
>>> cf.get_array('var4')
[u'value1', u'value2', u'value3']
>>> cf.get_array('var5')
[u'value1', u'value2', u'value3']
>>> [cf.get_array(name) for name in ('var6', 'var7', 'var8')]
[[], [], []]
>>> cf.get_array('var1')
[u'value']
>>> cf.get_string('var4')
u'value1'
>>> cf.get_string('var6') is None
True
"""
RE_VAR = re.compile(r'\$(?:\{([^}]+)\}|(\w+))')
TRANS_MAP = {r'\"': '"', r'\\': '\\', r'\$': '$'}
def __convert(self, value):
# XXX rendre la conversion plus robuste: veiller à l'ordre ('\\\\' en
# dernier...), et ne faire la conversion que pour un nombre impaire de
# '\\'.
for s, r in self.TRANS_MAP.items():
value = value.replace(s, r)
return value
def _unescape(self, value, quote=''):
"""convertir une valeur quotée, suivant les règles de bash.
quote peut valoir "'", '"', ''
"""
# aucune traduction entre ''
if quote == "'": return value
# sinon appliquer les règles standards. notamment, remplacer $var et
# ${var} par self._items["var"] ou os.environ["var"]
splited = self.RE_VAR.split(value)
value = self.__convert(splited[0])
splited = splited[1:]
while splited:
var0 = splited[0]
var1 = splited[1]
text = splited[2]
splited = splited[3:]
var = var0 or var1
if self.has_key(var): value = value + self.get_string(var)
else: value = value + os.environ.get(var, "")
value = value + self.__convert(text)
return value
def _parse_logic(self, index, value):
value = value.lstrip() # ignorer les espaces avant la valeur
if self._is_array(value): return self._parse_array(index, value)
else: return self._parse_scalar(index, value)
## tableaux
def _is_array(self, value):
"""Tester si value est le début d'un tableau.
"""
return value.strip()[:1] == '('
RE_ARRAY_VALUE = re.compile('[^\\s\'")]*')
def _parse_next_scalar(self, index, value):
"""Parser la prochaine valeur scalaire
XXX à faire
@return index, value, remainder
"""
remainder = value
value = ''
lstrip = None
rstrip = None
while remainder:
if self.RE_SPACES.match(remainder) is not None:
# les valeurs sont séparées par des espaces
break
# XXX cf ConfigFile._parse_scalar pour la gestion des commentaires
elif self.RE_EOA.match(remainder) is not None:
# fin de tableau
break
elif self._is_quoted(remainder):
# valeur quotée. pas de strip
if lstrip is None: lstrip = False
rstrip = False
index, next_value, remainder = self._parse_quoted(index, remainder)
value += self._unescape(next_value)
else:
# valeur non quotée. lstrip si en premier. rstrip si en dernier
if lstrip is None: lstrip = True
rstrip = True
index, next_value, remainder = self._parse_value(index, remainder, self.RE_ARRAY_VALUE)
value += self._unescape(next_value)
if lstrip: value = value.lstrip()
if rstrip: value = value.rstrip()
return index, value, remainder
RE_SOA = re.compile(r'\(')
RE_EOA = re.compile(r'\)')
def _parse_array(self, index, value):
"""Parser un tableau à partir de value (qui se trouve à la position
index) et des lignes suivant index.
@return index, values, remaining
"""
if self.RE_SOA.match(value) is None:
raise ValueError("value must start with '(', got %s" % repr(_s(value)))
remainder = value[1:]
values = []
eoa = False # end of array
while True:
if not remainder:
# nous n'avons pas encore rencontré la fin du tableau. Lire les
# lignes jusqu'à ce que nous trouvions ce qui est nécessaire
index, remainder, eof = self._merge_cont(index, remainder)
if eof: break
# ignorer les espaces entre les valeurs
mo = self.RE_SPACES.match(remainder)
if mo is not None:
remainder = remainder[mo.end():]
continue
# tester si on arrive à la fin du tableau
if self.RE_EOA.match(remainder) is not None:
remainder = remainder[1:]
eoa = True
break
# parser une valeur scalaire
index, next_value, remainder = self._parse_next_scalar(index, remainder)
values.append(next_value)
# ici, eoa vaut True si le tableau a été terminé proprement.
# sinon, on fait comme si on a rien vu.
return values
_debug = False
def _print_debug(s):
if _debug: print s
class PListFile(TextFile):
def readlines(self, raise_exception=True, close=True):
TextFile.readlines(self, raise_exception, close)
self.items = None
self.list = None
self.value = None
if self.is_valid():
if self.lines and self.lines[0][:5] == '<?xml':
self.__read_xml()
else:
self.__read_plist()
def is_dict(self):
return self.items is not None
def is_list(self):
return self.list is not None
def is_scalar(self):
return self.value is not None
def __getitem__(self, name, default=_marker):
if self.is_dict():
if default is _marker:
return self.items[name]
return self.items.get(name, default)
if self.is_list():
return self.list[name]
raise IndexError("This object contains a scalar value. use the value attribute instead")
def __setitem__(self, name, value):
self.items[name] = value
def get(self, name, default=_marker):
return self.__getitem__(name, default)
def __read_xml(self):
"""charger un fichier au format plist xml
XXX à faire
"""
raise NotImplementedError
def __read_plist(self):
"""charger un fichier au format plist natif
"""
self.data = self.get_nl().join(self.grep(r'[ \t]*//', inverse=True))
value = self.__parse_value()
if type(value) is types.DictType:
self.items = value
elif type(value) is types.ListType:
self.list = value
else:
self.value = value
re_blank = re.compile(r'[ \t\r\n]+')
def __skip_blank(self):
mo = self.re_blank.match(self.data)
if mo is not None:
self.data = self.data[mo.end(0):]
def __parse_chars(self, *cs, **kw):
if kw.get('skip_blank', False):
self.__skip_blank()
if self.data[:1] in cs:
c, self.data = self.data[:1], self.data[1:]
return c
else:
if kw.get('optional', False): return None
raise ValueError("Unable to find '%s'" % _s(''.join(cs)))
re_name = re.compile(r'[a-zA-Z0-9_]+')
def __parse_name(self, optional=False):
"""chercher un nom, retourner None si pas trouvé
"""
self.__skip_blank()
c = self.data[:1]
if c == '"':
name = self.__parse_string()
else:
mo = self.re_name.match(self.data)
if mo is None:
if optional: return None
raise ValueError("Expected an unquoted name")
name = mo.group(0)
self.data = self.data[mo.end(0):]
_print_debug("XXX name=%s" % name)
return name
re_value = re.compile(r'[a-zA-Z0-9_/.$]+')
def __parse_value(self, optional=False):
_print_debug("XXX parse_value, data=\n %s" % self.data[:70])
value = None
self.__skip_blank()
c = self.data[:1]
if c == '{':
value = self.__parse_dict()
elif c == '(':
value = self.__parse_list()
elif c == '"':
value = self.__parse_string()
else:
mo = self.re_value.match(self.data)
if mo is None:
if optional: return None
raise ValueError("Expected a quoted name")
value = mo.group(0)
self.data = self.data[mo.end(0):]
_print_debug("XXX value=%s" % value)
return value
def __parse_dict(self):
dict = {}
self.__parse_chars('{')
while True:
name = self.__parse_name(optional=True)
if name is None: break
self.__parse_chars('=', skip_blank=True)
value = self.__parse_value()
self.__parse_chars(';', skip_blank=True)
dict[name] = value
self.__parse_chars('}', skip_blank=True)
_print_debug("XXX dict=%s" % dict)
return dict
def __parse_list(self):
list = []
first = True
self.__parse_chars('(')
while True:
if first:
value = self.__parse_value(optional=True)
if value is None: break
first = False
else:
c = self.__parse_chars(',', skip_blank=True, optional=True)
if c is None: break
value = self.__parse_value(optional=True)
if value is None: break
list.append(value)
self.__parse_chars(')', skip_blank=True)
_print_debug("XXX list=%s" % list)
return list
re_string = re.compile(r'"((?:\\"|[^"])*)"')
def __parse_string(self):
mo = self.re_string.match(self.data)
if mo is None:
raise ValueError("Expected a quoted string")
string = mo.group(1)
self.data = self.data[mo.end(0):]
_print_debug("XXX string=%s" % string)
return string
################################################################################
# classes utilisant shlex et l'interface odict
class ShConfig(odict):
_formats = None
def __init__(self, config, formats=None):
super(ShConfig, self).__init__()
if formats is None: formats = {}
else: formats = dict(formats)
self.__dict__['_formats'] = formats
for name in self._formats.keys():
self[name] = None
inf = open(config, 'rb')
try: s = inf.read()
finally: inf.close()
parts = shlex.split(s, True)
self.parse(parts)
def get_format(self, name):
format = None
if format is None: format = self._formats.get(name, None)
if format is None: format = self._formats.get(None, None)
if format is None: format = unicodeF
return format
RE_ARRAY = re.compile(r'([^=]+)=\((.*)')
RE_ARRAY_LAST = re.compile(r'(.*)\)$')
RE_SCALAR = re.compile(r'([^=]+)=(.*)')
def parse(self, parts):
i = 0
while i < len(parts):
part = parts[i]
i += 1
amo = self.RE_ARRAY.match(part)
smo = self.RE_SCALAR.match(part)
if amo is not None:
array = []
name, value = amo.groups()
format = self.get_format(name)
if value != '': array.append(format.parse(value))
while i < len(parts):
value = parts[i]
i += 1
mo = self.RE_ARRAY_LAST.match(value)
if mo is not None:
value = mo.group(1)
if value != '': array.append(format.parse(value))
break
else:
array.append(format.parse(value))
self[name] = array
elif smo is not None:
name, value = smo.groups()
format = self.get_format(name)
self[name] = format.parse(value)
else:
continue # ignorer l'erreur pour le moment
raise ValueError("%s: not a variable" % part)
################################################################################
if __name__ == '__main__':
import doctest
doctest.testmod()